檢視原始碼 Ports(埠)

本節概述如何使用埠來解決上一節中的範例問題。

情境如下圖所示

---
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/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 程式

在 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 程式碼

$ 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