第十二章 接口技术

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的程序调用实现一个通信协议:

  1. 数据包前两个字节表示数据的长度Len, 之后跟上长度为Len的数据
  2. 调用第一个函数twice(需要一个参数x), 则数据格式为[1, N];
  3. 调用第二个函数sum(需要两个参数x, y),则数据格式为[2, M, N]
  4. 返回值规定为一个字节

12.2.1 C程序

  1. example1.c
    包含具体调用的函数
int twice(int x) {
    return 2*x;
}
int sum(int x, int y) {
    return x+y;
}
  1. 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);
    }
}
  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
可以是如下形式:

  1. {fd, In, Out}
    Opt
  2. {packet, N}
    以N字节长度的数据作为包的长度计数
  3. stream
    不包含数据包长度
  4. {line, Max}
    基于行发送消息, 超过Max字节则折行
  5. {cd, Dir}
    只针对当PortName为{spawn, Command}方式时有效, 即使外部程序在Dir目录中启动
  6. {env, Env}
    只针对当PortName为{spawn, Command}方式时有效, 即为外部程序配置运行参数

12.4 内联驱动

把第三方程序编译成共享库, 然后动态的内联到Erlang的运行时系统中称为内联驱动方式。创建内联驱动程序是Erlang与其它语言对接的最有效方式, 但因为内联驱动程序的任何错误都会导致Erlang系统崩溃, 因此这种方式比较危险。

  1. 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).
  1. 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

posted @ 2020-08-20 17:50  养诚  阅读(161)  评论(0编辑  收藏  举报