查看源码 端口
本节概述了如何使用端口来解决上一节中的示例问题。
场景如下图所示
---
title: Port Communication
---
flowchart LR
subgraph Legend
direction LR
os[OS Process]
erl([Erlang Process])
end
subgraph ERTS
direction LR
port{Port} --> erlProc
erlProc([Connected process]) --> port
end
port --> proc[External Program]
proc --> port
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.
C 程序
在 C 端,有必要编写函数来从/向 Erlang 接收和发送带有 2 字节长度指示符的数据。默认情况下,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 程序必须检测到端口何时关闭并终止。
运行示例
步骤 1. 编译 C 代码
$ gcc -o extprg complex.c erl_comm.c port.c
步骤 2. 启动 Erlang 并编译 Erlang 代码
$ erl
Erlang/OTP 26 [erts-14.2] [source] [64-bit] [smp:8:8] [ds:8:8:10] [async-threads:1] [jit:ns]
Eshell V14.2 (press Ctrl+G to abort, type help(). for help)
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