Programming Erlang 第12章 接口技术 (完整)
接口技术
译者: | gashero |
---|
假设我们需要以Erlang接口运行以C/Python/Shell编写的程序。想要实现这些,我们需要在一个单独的操作系统进程中运行这些程序,而不是在Erlang运行时系统中,他们之间以面向字节的通道完成通信。Erlang端通过 Port 控制。创建端口的进程成为端口的连接进程。连接进程拥有特殊意义:让所有来自扩展程序的消息都会以连接进程的PID来标志。所有的扩展程序消息都会发送到连接进程。
我们可以看看连接进程(C)与端口(P)和扩展操作系统进程的关系。
从程序员的角度,端口就像是Erlang进程。你可以发送消息给它,你可以注册(register)它,等等。如果扩展程序crash了,那么连接程序就会收到退出信号,如果连接进程死掉了,扩展进程也会被kill掉。
你可能会好奇与为什么要这么做。很多编程语言允许其他语言编写的程序连接到可执行文件。而在Erlang,我们为了安全性不允许这么做。如果我们连 接一个扩展程序到Erlang可执行程序,那么扩展程序的错误将会轻易的干掉Erlang。所以,其他语言编写的程序必须以单独的操作系统进程来运行。 Erlang运行时系统和扩展进程通过字节流通信。
1 端口
创建端口使用如下命令:
Port=open_port(PortName,PortSettings)
这会返回端口,而如下消息是会被发送到端口的(这些消息中PidC是连接进程的PID):
Port ! {PidC,{command,Data}} :发送数据到端口
Port ! {PidC,{connect,Pid1}} :改变控制进程的PID,从PidC到Pid1
Port ! {PidC,close} :关闭端口
连接进程会从扩展程序收到如下消息:
receive {Port,{data,Data}} -> ... 数据处理 ...
下面的节,我们会编写Erlang与C结合的简单例子。C程序尽可能的简单以避开细节,直接谈接口技术。
注意,下面的例子对接口机制和协议做了加亮。编码和解码复杂的数据结构是个困难的问题,这里没有谈及。在本章末尾,我们会指出一些用于其他编程语言的接口库。
2 扩展C程序的接口
我们先从C程序开始:
int twice(int x) { return 2*x; } int sum(int x, int y) { return x+y; }
我们最终目标是从Erlang调用这些例程,我们希望看到的是这个样子(Erlang中):
X1=example1:twice(23), Y1=example1:sum(45,32),
与用户的设置有关,example1是一个Erlang模块,所有与C接口的细节都被隐藏在了模块example1中。
我们的接口需要一个主程序,用来解码Erlang程序发来的数据。在我们的例子,我们首先定义端口和扩展C程序的协议。我们使用一个超级简单的协议,并展示如何在Erlang和C中实现。协议定义如下:
- 所有包都以2字节的长度代码开头,后面跟着这些字节的数据。
- 想要调用 twice(N) ,Erlang程序必须以特定形式编码函数调用。我们假设编码是2字节序列 [1,N] ;参数1表示调用函数 twice ,后面的N代表一个1字节的参数。
- 调用 sum(N,M) ,我们编码请求到字节序列 [2,N,M] 。
- 假设返回值都是单一的字节长度的。
扩展C程序和Erlang程序都必须遵守这个协议。作为例子,我们通过 sum(45,32) 来看看工作流程:
- 端口发送字节序列 0,3,2,45,32 到扩展程序。头两个字节0,3,表示包的长度是3,代码2表示调用扩展的 sum 函数,而 45 和 32 表示调用函数的参数。
- 扩展程序从标准输入(stdin)读取这5个字节,调用 sum 函数,然后把字节序列 0,2,77 写到标准输出(stdout)。前两个字节表示包长度,后面的77是结果(仍然是1字节长度)。
我们现在写在两端遵守协议的接口,先以C程序开始。
2.1 C程序
扩展的C程序需要编写3个文件:
- example1.c :这个包含了我们需要调用的原始函数。
- example1_driver.c :实现了字节流协议和对 example1.c 的调用例程。
- erl_comm.c :包含读写内存缓冲区的例程。
example1_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], //如果希望调试,可以写到stderr //fprintf(stderr,"call sum %i %i\n",arg1,arg2); result=sum(arg1,arg2); } buff[0]=result; write_cmd(buff,1); } }
这段代码运行一个无限循环,从标准输入stdin读取调用命令,并且将结果写入到标准输出。
如果希望调试,可以写到stderr,上面的例子里面就有相关的输出语句。
erl_comm.c 是对stdin和stdout读写带有2字节包头的数据包的代码。这么写是允许有分片的。
#include <unistd.h> typedef unsigned char byte; int read_cmd(byte* buf); int write_cmd(byte* buf, int len); int read_exact(byte* buf, int len); int write_exact(byte* buf, int 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); } 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) { itn i,wrote=0; do { if ((i=write(1,buf+wrote,len-wrote)<=0) return i; wrote+=i; } while(wrote<len); return len; }
这段代码处理带有2字节长度包头的数据包,所以用于匹配 {packet,2} 选项的端口驱动程序。
2.2 Erlang程序
Erlang端的驱动程序参考如下:
-module(example1). -export([start/0,stop/0]). -export([twice/1,sum/2]). 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.
端口通过如下方式打开:
Port=open_port({spawn,"./example1"},[{packet,2}])
选项 {packet,2} 告诉系统自动在发送数据包时添加一个2字节的包头。所以,会在发送消息 {PidC,{command,[2,45,32]}} 自动加上两字节的长度包头,也就是发送 0,3,2,45,32 到扩展程序。
而输入端口会假设输入数据包也有2字节的长度包头,获取指定长度的数据包之后就会去掉长度包头。
完善程序还需要一个makefile来构建。命令 make example1 用于构建 open_port 调用的扩展程序。注意makefile同时也链接了本章稍后会用到的内联驱动。
makefile
.SUFFIXES: .erl .beam .yrl .erl.beam: erlc -W $< MODS=example1 example1_lid.c all: ${MODS:%=%.beam} example1 example1_drv.so example1: example1.c erl_comm.c example1_driver.c gcc -o example1 example1.c erl_comm.c example1_driver.c example1_drv.so: example1_lid.c example.c gcc -o example1_drv.so -fpic -shared example1.c example1_lid.c clean: rm example example1_drv.so *.beam
2.3 运行程序
现在运行程序:
1> example1:start(). <0.32.0> 2> example1:sum(45,32). 77 4> example1:twice(10). 20 ...
这就完成了我们的第一个例子。
进入下个主题之前,必须注意的是:
- 例子程序没有尝试统一Erlang和C程序的整数。这里我们假设整数都是单字节的,并且忽略了精度和符号的问题。在现实的应用中必须注意类型和精度的问题。这是个困难的问题,因为erlang管理着不限制大小的整数,而像C一类的语言却必须管理整数的精度等等。
- 我们无法在运行扩展程序之前调用对应的erlang的函数(也就是运行 example1:start() 之前不能调用对应函数)。我们更希望可以自动的启动。这在18.7节会详细讲解。
3 open_port
前一节使用了 open_port 却没有介绍具体的参数。只是使用了增加了 {packet,2} 选项的 open_port 而已。 open_port 包含很多参数。
一些常用参数如下:
@spec open_port(PortName,[Opt]) -> Port
PortName是如下之一:
{spawn,Command}
启动一个扩展程序。Command是扩展程序的名字。Command会在Erlang的工作空间以外工作,除非找到了叫做Command的内联驱动。
{fd,In,Out}
允许Erlang进程存取一个已经打开的文件描述符。文件描述符 “In” 用作stdin,而文件描述符 “Out” 用作stdout。查看例子 http://www.erlang.org/examples/examples-2.0.html 。
Opt是如下之一:
{packet,N}
包前面加上N(1,2,4)字节长度的长度包头。
stream
消息不是按照包长度发送的。应用自己知道如何处理这些包。
{line,Max}
以行为单位传递消息。如果一行大于Max字节,则会切割为只有Max字节。
{cd,Dir}
仅用于 {spawn,Command} 选项,扩展程序的初始目录为Dir。
{env,Env}
仅用于 {spawn,Command} 选项。指定扩展程序可用的环境变量为Env。Env是一个列表的 {VarName,Value} 对。两个变量都是字符串。
这并不是完整的 open_port 参数列表。我们可以在参考手册的erlang模块里找到详细的描述。
4 内联驱动
有时候我们希望在Erlang运行时系统内运行一个外语程序。在这种情况下,程序以共享库的方式编写并动态链接到Erlang运行时系统。内联驱动对程序员来说就是一个端口程序,而且与端口程序使用相同的协议。
创建内联驱动是最有效率的工作方式,不过却很危险。任何致命错误都会干掉整个Erlang系统,并且影响到正在工作的所有进程。因为这个原因,非常不推荐内联驱动。
通过刚才的例子讲讲内联驱动。你需要三个文件:
- example1_lid.erl :这个是erlang的服务器。
- example1.c :包含我们需要调用的C函数,跟上一节的一样。
- example1_lid.c :调用 example1.c 中好函数的函数。
Erlang管理接口的代码如下:
-module(example1_lid). -export([start/0,stop/0]). -export([twice/1,sum/2]). start() -> start("example1_drv"). start(SharedLib) -> case erl_ddl1: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). stop() -> example1_lid ! stop. twice(X) -> call_port({twice,X}). sum(X,Y) -> call_port({sum,X,Y}). call_port(Msg) -> example1_lid ! {call,self(),Msg}, receive {example1_lid,Result} -> Result end. loop(Port) -> receive {call,Caller,Msg} -> Port ! {self(),{command,encode(Msg)}}, receive {Port,{data,Data}} -> Caller ! {example1_lid,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({twice,X}) -> [1,X]; encode({sum,X,Y}) -> [2,X,Y]. decode([Int]) -> Int.
对比该程序与较早的版本,看起来基本相同。
驱动程序由 driver 结构体组成。命令 make example1_drv.so 用于构建动态链接库。
// 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, 当erlang发送数据到端口时 NULL, // F_PTR ready_input NULL, // F_PTR ready_output "example1_drv", // 驱动的名字 NULL, // F_PTR finish, 卸载(unload)时 NULL, // F_PTR control, port_command回调 NULL, // F_PTR timeout, 保留 NULL // F_PTR outputv, 保留 }; DRIVER_INIT(example_drv) { //必须与driver_entry中的名字匹配 return &example_driver_entry; }
这里时运行程序:
1> c(example_lid). {ok,example1_lid} 2> example1_lid:start(). <0.41.0> 3> example1_lid:twice(50). 100 4> example1_lid:sum(10,20). 30
5 注意
本章研究了如何以端口来调用扩展程序。除了使用端口协议之外,还可以使用一些其他的BIF来操作端口。这些都在erlang模块的手册中。
在这一点上,你可能会关心如何方便的发送复杂的数据结构,如字符串、元组等等到扩展程序?很不幸,答案是还没有比较简单的方法来实现。所有的端口只 是提供了很底层的机制来传输一系列字节而已。同样的问题也发生在Socket编程中。一个Socket提供了数据流与应用交互,对于数据流的解释还需要应 用自己做。
不过一些库提供了简单的交互方法,可以查看:
http://www.erlang.org/doc/pdf/erl_interface.pdf
Erl interface (ei) 是一系列的C例程和宏,用于编码和解码Erlang的扩展格式。在Erlang端,一个程序使用 term_to_binary 来将Erlang术语串行化,而在C端ei的例程可以把binary解包。ei也可以用于构造binary,而在Erlang使用 binary_to_term 来解包。
http://www.erlang.org/doc/pdf/ic.pdf
Erlang IDL Compiler (ic) 。这是一个Erlang的OMG IDL编译器实现。
http://www.erlang.org/doc/pdf/jinterface.pdf
Jinterface 是一系列Java到Erlang的接口工具。它提供了Erlang类型到Java类型的完全映射,编码和解码Erlang术语,连接Erlang进程等等。还有一大堆的扩展功能。