檢視原始碼 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/1
和 complex1: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/1
和 write_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);
}
請注意,stdin
和 stdout
用於緩衝的輸入/輸出,不得 用於與 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