技术头条 - 一个快速在微博传播文章的方式     搜索本站
您现在的位置首页 --> 其他 --> Erlang linkin driver用port_control方式时的一些经验分享

Erlang linkin driver用port_control方式时的一些经验分享

浏览:2958次  出处信息

    最近由于需要Erlang与C交互,采用了linkin driver的方式。利用port_control以及driver_entry中的control回调,调用C函数。在传递复杂的数据结构,序列化和反序列化数据时遇到了一些问题,与大家分享一下。先简单介绍一下eralng driver。

     首先,Erlang与外部程序交互的方式主要有两种:

  • Port方式,Erlang利用标准输入和输出与外部的程序进行交互。此种方式下,外部程序作为一个外部的进程运行。
  • 内联驱动(linkin driver)方式,Erlang动态载入so,并直接调用so中的函数。此种方式,效率高于前一种,但比较危险,会导致erlang 系统崩溃。
  •     另外,还有一种NIF方式,暂时没有考虑。这里我采用了linkin driver的方式。

        Erlang调用driver也有两种方式:

  • 通过Port ! Message发送消息,然后用recevie接收反馈。
  • port_control或者port_call直接调用,可以直接返回值。此种方式效率较高,属于同步调用。port_control与port_call用法差不多,只是port_contorl最后一个参数Data是binary,返回也是binary。而port_call最后一个参数是term,返回也是term。
  •     这里选用port_control的方式。

        为了方便encode和decode比较复杂的数据结构,采用ei这个库来序列化和反序列化参数。

         首先,open一个port, 采用binary方式:

    case erl_ddll:load_driver(".", SharedLib) of
         ok -> ok;
         {error, alread_loaded} -> ok;
         Ret ->
            error_logger:error_msg("Could not load driver ~p~n", [Ret]),
            exit({error, could_not_load_driver})
    end,
    Port = open_port({spawn, SharedLib}, [binary]).

        打开port之后,就能直接用port_control来调用driver里面的函数了

      Resp=erlang:port_control(Port, Cmd, term_to_binary(Msg)),
       io:format("recv ctl data ~p~n",[binary_to_term(Resp)]).

        Port即先前打开的Port, Cmd是一个int,你可以对你的函数编号,1代表调用sum,2代表调用twice。。。最后Msg即参数,注意需要传入binary(或者字符串),返回结果Resp也是binary(或者字符串)。

        对应C程序这里,需要定义一个回调函数port_ctl, 并把它添加到driver_entry里面注册,如下:

    ErlDrvEntry example_driver_entry = {
     NULL,
     example_drv_start, //driver启动时回调函数
     example_drv_stop, //driver停止时回调函数
     NULL,
     NULL,
     NULL,
     "example1_drv", //driver名称
     NULL,
     NULL,
     port_ctl,   //在这里注册
     NULL,
     NULL,
     NULL,
     NULL,
     NULL,
     NULL
    };
    
    DRIVER_INIT(example1_drv)
    {
        return &example_driver_entry;
    }
    
    static ErlDrvData example_drv_start(ErlDrvPort port, char* buff)
    {
        example_data * d = (example_data*) driver_alloc(sizeof(example_data));
        d->port = port;
        set_port_control_flags(port, PORT_CONTROL_FLAG_BINARY);
        return (ErlDrvData)d;
    }
    
    static void example_drv_stop(ErlDrvData handle)
    {
        driver_free((char*) handle);
    }

        Erlang调用port_control 会使得driver调用刚刚注册的port_ctl回调函数。

         下面是最重要的port_ctl函数. cmd即port_control的Cmd, buf和len为接受到的函数参数数据的buffer和长度, rbuf是返回值buffer。 这里假设port_control传入Msg的参数是将一个tuple {a,b}序列化后的结果:

     static int port_ctl(ErlDrvData handle, unsigned int cmd, char* buf, int len,
                            char** rbuf, int rsize)
    {
    	ei_x_buff x;
    
    	if(1 == cmd) { //1号命令,调用 sum(a,b)
    
    	   char *p;
               int i;
    	   int arity = 0;
    	   int version = 0;
    	   long a, b;
    	   int index = 0;
    	   int res = 0;
    
    	   ei_decode_version(buf, &index, &version); //必须有
    
               ei_decode_tuple_header(buf, &index, &arity); //decode出tuple头,
                                                            //得到tuple的长度arity,这里为2
    
    	   ei_decode_long(buf, &index, &a); //tuple第一个元素
    	   ei_decode_long(buf, &index, &b); //tuple第二个元素
    
    	   res = sum(a, b); //这里调用目标函数,这里是一个简单的求和
    
               //encode返回数据阶段
    	   ei_x_new_with_version(&x);
    
    	   ei_x_encode_long(&x, res);
    
               if(x.index > rsize){
                  ErlDrvBinary *ptr = driver_alloc_binary(x.index);
                  if(NULL == ptr)
                      erl_exit(1,"Out of virtual memory in malloc (%s)", __FILE__);
    
                  memcpy(ptr->orig_bytes, x.buff, x.index);
                  *rbuf = (char *)ptr;
               } else
                   memcpy(*rbuf, x.buff, x.index);
    
    	    ei_x_free(&x);
    
    	    return x.index;
    
    	} else
    		if(2 == cmd) {...... }
    
    	return -1; //返回<0会导致elang:port_control抛出badarg异常
    }

        注意,ei_decode_version(buf, &index, &version);必须有,我在开始没有这一行时,总是无法decode成功。

         后来经过阅读erlang源代码发现,传入的数据是这样序列化的:

        序列化的数据都会有生成个magic number作为version, 放在第一个字节。比如一个tuple {1,2}, 假设他的version为131, tuple类型用’i\'表示, 1,2属于ERL_SMALL_INTEGER_EXT,用‘a’表示该类型,则序列化后如下:

         131 i 2 a 1 a 2

         对应的131为version, i代表是一个tuple,2代表长度为2, 后面是tuple的内容,第一个a代表是一个小整数,1是这个整数的值;接着第二个a代表第二个元素为小整数,2代表这个整数值为2.

         所以,必须先把version 131 decode出来,然后调用ei_decode_tuple接着解析131之后的数据才能正常。 这里,ei_decode_xxx函数中 index这个参数随着decode的调用,会指向下一个需要decode的位置。

        另外,example_drv_start回调函数中set_port_control_flags(port, PORT_CONTROL_FLAG_BINARY);也必须有。

        如果没有这一行, port_control返回值Reply不会是binary,而是一个list,[131, $i, 2, $a, 1, $a, 2], 这个list无法用binary_to_term还原,而且自己处理也比较麻烦。

         相反,如果设置后,则会返回一个binary 《131,$i, 2, $a, 1, $a, 2》,这个格式可以用binary_to_term转化为term。相应的,如果设置PORT_CONTROL_FLAG_BINARY, 当返回缓冲区大小不够时,一定要用driver_alloc_binary来为*rbuf分配缓冲区。

        如果大家觉得binary与term转化比较麻烦,可以考虑用erlang:port_call函数, c程序不变,只是erlang程序不需要term_to_binary以及binary_to_term.

        欢迎大家指出本文的不足,以及与我交流。

    建议继续学习:

    1. whatsapp深度使用Erlang有感    (阅读:4543)
    2. Erlang match_spec引擎介绍和应用    (阅读:4509)
    3. php-erlang    (阅读:4300)
    4. gen_tcp调用进程收到{empty_out_q, Port}消息奇怪行为分析    (阅读:3530)
    5. hibernate使用注意事项    (阅读:3207)
    6. Erlang如何限制节点对集群的访问之net_kernel:allow    (阅读:2944)
    7. ERLANG OTP源码分析 – gen_server    (阅读:2846)
    8. erlang学习手记    (阅读:2689)
    9. gen_tcp容易误用的一点解释    (阅读:2625)
    10. Erlang虚拟机内存使用问题以及监控    (阅读:2408)
    QQ技术交流群:445447336,欢迎加入!
    扫一扫订阅我的微信号:IT技术博客大学习
    © 2009 - 2024 by blogread.cn 微博:@IT技术博客大学习

    京ICP备15002552号-1