4  端口

4 端口

本节概述了如何使用端口解决上一节中示例问题的示例。

以下图示说明了该场景

IMAGE MISSING

图 4.1:   端口通信

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/1complex1: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 方面,有必要编写用于接收和发送带 2 字节长度指示符的数据的函数,这些数据来自/发往 Erlang。默认情况下,C 程序应从标准输入(文件描述符 0)读取数据并写入标准输出(文件描述符 1)。此类函数的示例,read_cmd/1write_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);
}

请注意,stdinstdout 用于缓冲的输入/输出,不能用于与 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 代码

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