Erlang运行第三方代码时需要一个与Erlang运行时系统相互独立的外部程序, 两者通过二进制通道进行通信。在Erlang中是通过端口连接进程来作为中间人管理两者之间的通信。
12.1 端口
创建端口
Port = open_port(PortName, PortSettings)
发送数据
Port ! {PidC, {command, Data}}
改变连接进程的PID
Port ! {PidC, connect, Pid1}
关闭端口
Port ! {PidC, close}
为一个外部C程序添加接口
为Erlang与C的程序调用实现一个通信协议:
- 数据包前两个字节表示数据的长度Len, 之后跟上长度为Len的数据
- 调用第一个函数twice(需要一个参数x), 则数据格式为[1, N];
- 调用第二个函数sum(需要两个参数x, y),则数据格式为[2, M, N]
- 返回值规定为一个字节
12.2.1 C程序
- example1.c
包含具体调用的函数
int twice(int x) {
return 2*x;
}
int sum(int x, int y) {
return x+y;
}
- example_driver.c
实现字节流协议并调用函数
#include <stdio.h>
typedef unsigned char byte;
int read_cmd(byte *buff);
int write_cmd(byte *buff, int len);
int main() {
int fn, arg1, arg2, result;
byte buff[100];
/* 在无限循环中读取输入, 按照通信协议进行解析处理 */
while (read_cmd(buff) > 0) {
fn = buff[0];
if (fn == 1) {
arg1 = buff[1];
result = twice(arg1);
} else if (fn == 2) {
arg1 = buff[1];
arg2 = buff[2];
result = sum(arg1, arg2);
}
buff[0] = result;
write_cmd(buff, 1);
}
}
- erl_comm.c
读写内存缓冲中的数据
#include <unistd.h>
typedef unsigned char byte;
int read_cmd(byte *buff);
int write_cmd(byte *buff, int len);
int read_exact(byte *buff, int len);
int write_exact(byte *buff, int len);
/* 首先读取两个字节的包头, 然后根据其值(数据包长度)继续读取 */
int read_cmd(byte *buff) {
int len;
if (read_exact(buff, 2) != 2) {
return -1;
}
len = (buff[0] << 8) | buff[1];
return read_exact(buff, len);
}
/* 依次写入运行结果:两字节的包头+一字节计算结果 */
int write_cmd(byte *buff, int len) {
byte li;
li = (len >> 8) & 0xff;
write_exact(&li, 1);
li = len & 0xff;
write_exact(&li, 1);
return write_exact(buff, len);
}
/* 从标准输入中读取指定长度的数据 */
int read_exact(byte *buff, int len) {
int i, got = 0;
do {
if ((i == read(0, buff+got, len-got)) <= 0) {
return i;
}
got += i;
} while (got < len);
return len;
}
/* 向标准输出中写入指定长度的数据 */
int write_exact(byte *buff, int len) {
int i, wrote = 0;
do {
if ((i == write(1, buff+wrote, len-wrote)) <= 0) {
return i;
}
wrote += 1;
} while (wrote < len);
return len;
}
12.2.2 Erlang程序
-module(example1).
-export([start/0, stop/0]).
-export([twice/1, sum/2]).
%% 创建一个注册名为example1的进程
%% 使用open_port创建端口, 指明数据包需要两字节长的包头
%% 使用loop等待消息处理
start() ->
spawn(fun() ->
register(example1, self()),
process_flag(trap_exit, true),
Port = open_port({spawn, "./example1"}, [{packet, 2}]),
loop(Port)
end).
stop() ->
example1 ! stop.
twice(X) ->call_port({twice, X}).
sum(X, Y) ->call_port({sum, X, Y}).
call_port(Msg) ->
example1 ! {call, self(), Msg},
receive
{example1, Result} ->
Result
end.
loop(Port) ->
receive
{call, Caller, Msg} ->
Port ! {self(), {command, encode(Msg)}},
receive
{Port, {data, Data}} ->
Caller ! {example1, decode(Data)}
end,
loop(Port);
stop ->
Port ! {self(), close},
receive
{Port, closed} ->
exit(normal)
end;
{'EXIT', Port, Reason} ->
exit({port_terminated, Reason})
end.
encode({twice, X}) ->{1, X};
encode({sum, X, Y}) ->{2, X, Y}.
decode([Int]) ->Int.
运行结果:
1> example1:start().
<0.34.0>
2> example1:sum(45, 32).
77
3> example1:twice(10).
20
4> example1:stop().
stop
12.3 open_port
open_port函数的标准形式:
open_port(PortName, [Opt]) -> Port
其中
PortName
可以是如下形式:
- {spawn, Command}
- {fd, In, Out}
Opt - {packet, N}
以N字节长度的数据作为包的长度计数 - stream
不包含数据包长度 - {line, Max}
基于行发送消息, 超过Max字节则折行 - {cd, Dir}
只针对当PortName为{spawn, Command}方式时有效, 即使外部程序在Dir目录中启动 - {env, Env}
只针对当PortName为{spawn, Command}方式时有效, 即为外部程序配置运行参数
12.4 内联驱动
把第三方程序编译成共享库, 然后动态的内联到Erlang的运行时系统中称为内联驱动方式。创建内联驱动程序是Erlang与其它语言对接的最有效方式, 但因为内联驱动程序的任何错误都会导致Erlang系统崩溃, 因此这种方式比较危险。
- example1_lid.erl
%% 相比于example1.erl只是多了加载共享库的过程
start() ->
start("example1_drv").
start(SharedLib) ->
case erl_ddll:load_driver(".", SharedLib) of
ok ->ok;
{error, already_loaded} ->ok;
_ ->exit({error, could_not_load_driver})
end,
spawn(fun() ->init(SharedLib) end).
init(SharedLib) ->
register(example1_lid, self()),
Port = open_port({spawn, SharedLib}, []),
loop(Port).
- example1_lid.c
/* example1_lid.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, int bufflen)
{
example_data* d = (example_data*)handle;
char fn = buff[0], arg = buff[1], res;
if (fn == 1) {
res = twice(arg);
} else if (fn == 2) {
res = sum(buff[1], buff[2]);
}
driver_output(d->port, &res, 1);
}
ErlDrvEntry example_driver_entry = {
NULL, /* F_PTR init, N/A */
example_drv_start, /* L_PTR start, 端口打开时调用 */
example_drv_stop, /* F_PTR stop, 端口关闭时调用 */
example_drv_output, /* F_PTR output, 发送数据时调用 */
NULL, /* F_PTR ready_input,
输入设备就绪时调用 */
NULL, /* F_PTR ready_output,
输出设备就绪时调用 */
"example1_drv", /* char *driver_name, 驱动名称, 调用open_port函数时使用 */
NULL, /* F_PTR finish, 卸载驱动时调用 */
NULL, /* F_PTR control, 端口回调 */
NULL, /* F_PTR timeout, 保留 */
NULL /* F_PTR outputv, 保留 */
};
DRIVER_INIT(example_drv) /* must match name in driver_entry */
{
return &example_driver_entry;
}
运行结果:
5> c(example1_lid).
{ok,example1_lid}
6> example1_lid:start().
<0.35.0>
7> example1_lid:sum(45, 32).
77
8> example1_lid:twice(10).
20
9> example1_lid:stop().
stop
12.5 注意
Erlang提供了几个与第三方语言通信的库。
Erl接口(ei), 针对C, 文档链接更新至:
http://www.erlang.org/doc/apps/erl_interface/erl_interface.pdf
Jinteface, 针对Java, 文档链接更新至:
http://www.erlang.org/doc/apps/jinterface/jinterface.pdf