6 端口驱动程序
本节概述了如何使用链接的端口驱动程序来解决 问题示例 中的问题示例。
端口驱动程序是链接的驱动程序,可以从 Erlang 程序中作为端口访问。它是一个共享库(在 UNIX 中为 SO,在 Windows 中为 DLL),具有特殊的入口点。当驱动程序启动和数据发送到端口时,Erlang 运行时系统会调用这些入口点。端口驱动程序也可以将数据发送到 Erlang。
由于端口驱动程序是动态链接到仿真器进程中的,因此这是从 Erlang 调用 C 代码的最快方法。调用端口驱动程序中的函数不需要上下文切换。但这也是最不安全的方式,因为端口驱动程序中的崩溃也会导致仿真器崩溃。
以下图示说明了这种情况
6.1 Erlang 程序
与端口程序一样,端口与一个 Erlang 进程通信。所有通信都通过一个 Erlang 进程进行,该进程是端口驱动程序的 **连接进程**。终止此进程会关闭端口驱动程序。
在创建端口之前,必须加载驱动程序。这是通过使用函数 erl_ddll:load_driver/1 完成的,并将共享库的名称作为参数。
然后使用 BIF open_port/2 创建端口,将元组 {spawn, DriverName} 作为第一个参数。字符串 SharedLib 是端口驱动程序的名称。第二个参数是选项列表,本例中没有选项。
-module(complex5). -export([start/1, init/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).
现在可以实现 complex5:foo/1 和 complex5:bar/1。两者都向 complex 进程发送消息,并接收以下回复
foo(X) -> call_port({foo, X}). bar(Y) -> call_port({bar, Y}). call_port(Msg) -> complex ! {call, self(), Msg}, receive {complex, Result} -> Result end.
complex 进程执行以下操作
- 将消息编码为字节序列。
- 将其发送到端口。
- 等待回复。
- 解码回复。
- 将其发送回调用方
loop(Port) -> receive {call, Caller, Msg} -> Port ! {self(), {command, encode(Msg)}}, receive {Port, {data, Data}} -> Caller ! {complex, decode(Data)} end, loop(Port) end.
假设 C 函数的参数和结果都小于 256,则使用简单的编码/解码方案。在这个方案中,foo 由字节 1 表示,bar 由 2 表示,参数/结果也由单个字节表示
encode({foo, X}) -> [1, X]; encode({bar, Y}) -> [2, Y]. decode([Int]) -> Int.
生成的 Erlang 程序,包括用于停止端口和检测端口故障的函数,如下所示
-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.
6.2 C 驱动程序
C 驱动程序是一个模块,它被编译并链接到一个共享库中。它使用驱动程序结构,并包含头文件 erl_driver.h。
驱动程序结构中填充了驱动程序名称和函数指针。它从特殊入口点返回,使用宏 DRIVER_INIT(<driver_name>) 声明。
用于接收和发送数据的函数被合并到一个函数中,该函数由驱动程序结构指向。发送到端口的数据作为参数给出,回复数据通过 C 函数 driver_output 发送。
由于驱动程序是共享模块,而不是程序,因此没有 main 函数。本示例中未使用所有函数指针,驱动程序结构中的相应字段设置为 NULL。
驱动程序中的所有函数都接受一个句柄(从 start 返回),该句柄只是由 Erlang 进程传递的。这必须以某种方式引用端口驱动程序实例。
example_drv_start 是唯一一个使用端口实例句柄调用的函数,因此必须保存此句柄。习惯上使用分配的驱动程序定义的结构来完成此操作,并将指针作为引用传递回去。
不建议使用全局变量,因为端口驱动程序可以由多个 Erlang 进程生成。此驱动程序结构将被实例化多次
/* 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; 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, /* unused */ 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; }
6.3 运行示例
**步骤 1。**编译 C 代码
unix> gcc -o example_drv.so -fpic -shared complex.c port_driver.c windows> cl -LD -MD -Fe example_drv.dll complex.c port_driver.c
**步骤 2。**启动 Erlang 并编译 Erlang 代码
> erl Erlang (BEAM) emulator version 5.1 Eshell V5.1 (abort with ^G) 1> c(complex5). {ok,complex5}
**步骤 3。**运行示例
2> complex5:start("example_drv"). <0.34.0> 3> complex5:foo(3). 4 4> complex5:bar(5). 10 5> complex5:stop(). stop