minzhan's blog

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

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

  1. Port方式,Erlang利用标准输入和输出与外部的程序进行交互。此种方式下,外部程序作为一个外部的进程运行。
  2. 内联驱动(linkin driver)方式,Erlang动态载入so,并直接调用so中的函数。此种方式,效率高于前一种,但比较危险,会导致erlang 系统崩溃。

另外,还有一种NIF方式,暂时没有考虑。这里我采用了linkin driver的方式。

Erlang调用driver也有两种方式:

  1. 通过Port ! Message发送消息,然后用recevie接收反馈。
  2. 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方式:

1
2
3
4
5
6
7
8
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里面的函数了

1
2
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里面注册,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
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}序列化后的结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
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’表示该类型,则序列化后如下:

1
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.