4 端口
本节概述了如何使用端口解决上一节中示例问题的示例。
以下图示说明了该场景
4.1 Erlang 程序
Erlang 和 C 之间的所有通信都必须通过创建端口来建立。创建端口的 Erlang 进程被称为连接进程。所有进出端口的通信都必须通过连接进程进行。如果连接进程终止,端口也会终止(如果外部程序编写正确,则外部程序也会终止)。
使用 BIF open_port/2 创建端口,其中 {spawn,ExtPrg} 作为第一个参数。字符串 ExtPrg 是外部程序的名称,包括任何命令行参数。第二个参数是选项列表,在本例中仅为 {packet,2}。此选项表示将使用 2 字节长度指示符来简化 C 和 Erlang 之间的通信。Erlang 端口会自动添加长度指示符,但这必须在外部 C 程序中显式完成。
该进程还设置为捕获退出,这使能够检测外部程序的失败
-module(complex1). -export([start/1, init/1]). start(ExtPrg) -> spawn(?MODULE, init, [ExtPrg]). init(ExtPrg) -> register(complex, self()), process_flag(trap_exit, true), Port = open_port({spawn, ExtPrg}, [{packet, 2}]), loop(Port).
现在可以实现 complex1:foo/1 和 complex1: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(complex1). -export([start/1, stop/0, init/1]). -export([foo/1, bar/1]). start(ExtPrg) -> spawn(?MODULE, init, [ExtPrg]). 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. init(ExtPrg) -> register(complex, self()), process_flag(trap_exit, true), Port = open_port({spawn, ExtPrg}, [{packet, 2}]), loop(Port). 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} -> exit(port_terminated) end. encode({foo, X}) -> [1, X]; encode({bar, Y}) -> [2, Y]. decode([Int]) -> Int.
4.2 C 程序
在 C 方面,有必要编写用于接收和发送带 2 字节长度指示符的数据的函数,这些数据来自/发往 Erlang。默认情况下,C 程序应从标准输入(文件描述符 0)读取数据并写入标准输出(文件描述符 1)。此类函数的示例,read_cmd/1 和 write_cmd/2,如下所示
/* erl_comm.c */ #include <stdio.h> #include <unistd.h> typedef unsigned char byte; int read_exact(byte *buf, int len) { int i, got=0; do { if ((i = read(0, buf+got, len-got)) <= 0){ return(i); } got += i; } while (got<len); return(len); } int write_exact(byte *buf, int len) { int i, wrote = 0; do { if ((i = write(1, buf+wrote, len-wrote)) <= 0) return (i); wrote += i; } while (wrote<len); return (len); } int read_cmd(byte *buf) { int len; if (read_exact(buf, 2) != 2) return(-1); len = (buf[0] << 8) | buf[1]; return read_exact(buf, len); } int write_cmd(byte *buf, int len) { byte li; li = (len >> 8) & 0xff; write_exact(&li, 1); li = len & 0xff; write_exact(&li, 1); return write_exact(buf, len); }
请注意,stdin 和 stdout 用于缓冲的输入/输出,不能用于与 Erlang 的通信。
在 main 函数中,C 程序应侦听来自 Erlang 的消息,并根据所选的编码/解码方案,使用第一个字节确定要调用的函数,并将第二个字节用作该函数的参数。然后将调用函数的结果发送回 Erlang
/* port.c */ typedef unsigned char byte; int main() { int fn, arg, res; byte buf[100]; while (read_cmd(buf) > 0) { fn = buf[0]; arg = buf[1]; if (fn == 1) { res = foo(arg); } else if (fn == 2) { res = bar(arg); } buf[0] = res; write_cmd(buf, 1); } }
请注意,C 程序处于 while 循环中,检查 read_cmd/1 的返回值。这是因为 C 程序必须检测端口何时关闭并终止。
4.3 运行示例
步骤 1. 编译 C 代码
unix> gcc -o extprg complex.c erl_comm.c port.c
步骤 2. 启动 Erlang 并编译 Erlang 代码
unix> erl Erlang (BEAM) emulator version 4.9.1.2 Eshell V4.9.1.2 (abort with ^G) 1> c(complex1). {ok,complex1}
步骤 3. 运行示例
2> complex1:start("./extprg"). <0.34.0> 3> complex1:foo(3). 4 4> complex1:bar(5). 10 5> complex1:stop(). stop