Erlang下与其他程序和语言的通信机制(2)
前面聊了普通端口,今天聊下链入式驱动端口,以及NIFs。
-
链入式驱动端口
如上图所示,链入式驱动端口与Erlang虚拟机存在于同一个OS进程中。
在Erlang这边与普通端口类似,所有与链入式驱动端口的通信,也是通过一个Erlang的连接进程进行的,终止这个进程将同时终止端口。在端口创建之前,驱动需要先载入运行时(驱动以动态库方式提供),可以通过erl_dll:load_driver/1函数。然后就与普通端口一样,使用open_port/2函数打开。
-module(complex5). -export([start/1, stop/0, init/1]). -export([foo/1, bar/1]). start(SharedLib) -> case erl_ddll:load_driver(".", SharedLib) of %% 载入驱动 ok -> ok; {error, already_loaded} -> ok; _ -> exit({error, could_not_load_driver}) end, spawn(?MODULE, init, [SharedLib]). init(SharedLib) -> register(complex, self()), Port = open_port({spawn, SharedLib}, []), loop(Port). stop() -> complex ! stop. foo(X) -> call_port({foo, X}). bar(Y) -> call_port({bar, Y}). call_port(Msg) -> complex ! {call, self(), Msg}, receive {complex, Result} -> Result end. loop(Port) -> receive {call, Caller, Msg} -> Port ! {self(), {command, encode(Msg)}}, receive {Port, {data, Data}} -> Caller ! {complex, decode(Data)} end, loop(Port); stop -> Port ! {self(), close}, receive {Port, closed} -> exit(normal) end; {'EXIT', Port, Reason} -> io:format("~p ~n", [Reason]), exit(port_terminated) end. encode({foo, X}) -> [1, X]; encode({bar, Y}) -> [2, Y]. decode([Int]) -> Int.
在C这边,首先包含erl_driver.h,需要使用里面的driver structure将C函数暴露给端口,这些函数用于接收和发送数据。端口发送过来的数据以函数参数的形式提供,而从C发送数据回端口则需要使用driver_output函数。端口由于可能被多个Erlang进程打开,所以最好不要使用全局变量,不然函数不可重入。应当使用driver_alloc,driver_free来分配每个端口独有的数据。最后我们需要把模块编译成动态库。
/* port_driver.c */ #include <stdio.h> #include "erl_driver.h" typedef struct { ErlDrvPort port; } example_data; static ErlDrvData example_drv_start(ErlDrvPort port, char *buff) { example_data* d = (example_data*)driver_alloc(sizeof(example_data)); d->port = port; //start 回调,保存端口在独立的数据中,以后的回调可以访问 return (ErlDrvData)d; } static void example_drv_stop(ErlDrvData handle) { driver_free((char*)handle); } static void example_drv_output(ErlDrvData handle, char *buff, ErlDrvSizeT bufflen) { example_data* d = (example_data*)handle; char fn = buff[0], arg = buff[1], res; if (fn == 1) { res = foo(arg); } else if (fn == 2) { res = bar(arg); } driver_output(d->port, &res, 1);//输出结果 } ErlDrvEntry example_driver_entry = { //注册端口回调 NULL, /* F_PTR init, called when driver is loaded */ example_drv_start, /* L_PTR start, called when port is opened */ example_drv_stop, /* F_PTR stop, called when port is closed */ example_drv_output, /* F_PTR output, called when erlang has sent */ NULL, /* F_PTR ready_input, called when input descriptor ready */ NULL, /* F_PTR ready_output, called when output descriptor ready */ "example_drv", /* char *driver_name, the argument to open_port */ NULL, /* F_PTR finish, called when unloaded */ NULL, /* void *handle, Reserved by VM */ NULL, /* F_PTR control, port_command callback */ NULL, /* F_PTR timeout, reserved */ NULL, /* F_PTR outputv, reserved */ NULL, /* F_PTR ready_async, only for async drivers */ NULL, /* F_PTR flush, called when port is about to be closed, but there is data in driver queue */ NULL, /* F_PTR call, much like control, sync call to driver */ NULL, /* F_PTR event, called when an event selected by driver_event() occurs. */ ERL_DRV_EXTENDED_MARKER, /* int extended marker, Should always be set to indicate driver versioning */ ERL_DRV_EXTENDED_MAJOR_VERSION, /* int major_version, should always be set to this value */ ERL_DRV_EXTENDED_MINOR_VERSION, /* int minor_version, should always be set to this value */ 0, /* int driver_flags, see documentation */ NULL, /* void *handle2, reserved for VM use */ NULL, /* F_PTR process_exit, called when a monitored process dies */ NULL /* F_PTR stop_select, called to close an event object */ }; DRIVER_INIT(example_drv) /* must match name in driver_entry */ { return &example_driver_entry; //初始化端口回调 }
-
NIFs
NIFs是一种更简单,高效的通信方法,特别适合一些同步调用的函数,这些函数只是简单的计算并返回结果,并且没有副作用。它也需要编译成动态库,加载入运行时中。在Erlang这边,我们也需要一个Erlang模块来引用它,首先通过这个模块加载NIFs库(erlang:load_nif/2),然后在Erlang这边实现NIFs里同名的函数作为stub implementations,这些函数实现很简单,只是抛出异常。当NIFs中没有这个函数时,这些函数将作为替代被调用。
-module(complex6). -export([foo/1, bar/1]). -on_load(init/0). %%在模块被加载时调用init/0函数,如果过init不是返回的ok,模块将会加载失败 init() -> ok = erlang:load_nif("./complex6_nif", 0). foo(_X) -> exit(nif_library_not_loaded). bar(_Y) -> exit(nif_library_not_loaded).
在C这边,我们通过ERL_NIF_INIT宏将函数暴露出去。函数的参数在C这边通过一个ERL_NIF_TERM类型的数组表示。第Nth参数为数组里Nth-1元素。ErlNifEnv指针里包含了许多Erlang进程的信息,我们通过它调用enif_xx函数与Erlang通信。
#include "erl_nif.h" extern int foo(int x); extern int bar(int y); static ERL_NIF_TERM foo_nif(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) { int x, ret; if (!enif_get_int(env, argv[0], &x)) { return enif_make_badarg(env); } ret = foo(x); return enif_make_int(env, ret); } static ERL_NIF_TERM bar_nif(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) { int y, ret; if (!enif_get_int(env, argv[0], &y)) { return enif_make_badarg(env); } ret = bar(y); return enif_make_int(env, ret); } static ErlNifFunc nif_funcs[] = { {"foo", 1, foo_nif}, {"bar", 1, bar_nif} }; ERL_NIF_INIT(complex6, nif_funcs, NULL, NULL, NULL, NULL)
链入式驱动端口,和NIFs就聊到这,后面再写下C Nodes。