檢視原始碼 Port 驅動程式
本節概述如何使用連結式 Port 驅動程式來解決問題範例中的範例問題。
Port 驅動程式是一種連結式驅動程式,可從 Erlang 程式以 Port 的形式存取。它是一個共享函式庫 (在 UNIX 中為 SO,在 Windows 中為 DLL),具有特殊的進入點。當驅動程式啟動且資料傳送到 Port 時,Erlang 執行時系統會呼叫這些進入點。Port 驅動程式也可以將資料傳送到 Erlang。
由於 Port 驅動程式是動態連結到模擬器進程中的,因此這是從 Erlang 呼叫 C 程式碼的最快方式。呼叫 Port 驅動程式中的函式不需要內容切換。但這也是最不安全的方式,因為 Port 驅動程式中的當機也會導致模擬器當機。
此情境如下圖所示
---
title: Port Driver Communication
---
flowchart
subgraph Legend
direction LR
os[OS Process]
erl([Erlang Process])
end
subgraph emulator
direction LR
port{Port} --> erlProc
erlProc([Connected process]) --> port
port --> proc[Port Driver Shared Library]
proc --> port
end
Erlang 程式
與 Port 程式一樣,Port 會與 Erlang 進程通訊。所有通訊都透過一個 Erlang 進程,該進程是 Port 驅動程式的連接進程。終止此進程會關閉 Port 驅動程式。
在建立 Port 之前,必須先載入驅動程式。這是使用函式 erl_ddll:load_driver/2
完成的,並以共享函式庫的名稱作為引數。
然後使用 BIF open_port/2
建立 Port,其中 tuple {spawn, DriverName}
作為第一個引數。字串 SharedLib
是 Port 驅動程式的名稱。第二個引數是選項清單,在此例中沒有任何選項
-module(complex5).
-export([start/1, init/1]).
start(SharedLib) ->
case erl_ddll:load_driver(".", SharedLib) of
ok -> ok;
{error, already_loaded} -> ok;
_ -> exit({error, could_not_load_driver})
end,
spawn(?MODULE, init, [SharedLib]).
init(SharedLib) ->
register(complex, self()),
Port = open_port({spawn, SharedLib}, []),
loop(Port).
現在可以實作 complex5:foo/1
和 complex5: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
進程執行以下操作
- 將訊息編碼為位元組序列。
- 將其傳送到 Port。
- 等待回覆。
- 解碼回覆。
- 將其傳送回呼叫者
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 程式(包括停止 Port 和偵測 Port 失敗的函式)如下所示
-module(complex5).
-export([start/1, stop/0, init/1]).
-export([foo/1, bar/1]).
start(SharedLib) ->
case erl_ddll:load_driver(".", SharedLib) of
ok -> ok;
{error, already_loaded} -> ok;
_ -> exit({error, could_not_load_driver})
end,
spawn(?MODULE, init, [SharedLib]).
init(SharedLib) ->
register(complex, self()),
Port = open_port({spawn, SharedLib}, []),
loop(Port).
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.
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} ->
io:format("~p ~n", [Reason]),
exit(port_terminated)
end.
encode({foo, X}) -> [1, X];
encode({bar, Y}) -> [2, Y].
decode([Int]) -> Int.
C 驅動程式
C 驅動程式是一個模組,會編譯並連結到共享函式庫中。它使用驅動程式結構並包含標頭檔 erl_driver.h
。
驅動程式結構會填入驅動程式名稱和函式指標。它會從特殊的進入點傳回,並使用巨集 DRIVER_INIT(<driver_name>)
宣告。
接收和傳送資料的函式會組合到一個函式中,該函式由驅動程式結構指出。傳送到 Port 的資料會作為引數給定,而回覆的資料會使用 C 函式 driver_output
傳送。
由於驅動程式是一個共享模組,而不是程式,因此不存在 main 函式。此範例中不使用所有函式指標,並且 driver_entry
結構中的相應欄位會設定為 NULL。
驅動程式中的所有函式都會取得一個句柄(從 start
傳回),該句柄僅由 Erlang 進程傳遞。這必須以某種方式引用 Port 驅動程式實例。
example_drv_start
是唯一一個使用 Port 實例句柄呼叫的函式,因此必須儲存此句柄。習慣上使用已配置的驅動程式定義結構來執行此操作,並將指標傳回作為參考。
不宜使用全域變數,因為 Port 驅動程式可以由多個 Erlang 進程產生。此驅動程式結構將被多次實例化
/* port_driver.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,
ErlDrvSizeT bufflen)
{
example_data* d = (example_data*)handle;
char fn = buff[0], arg = buff[1], res;
if (fn == 1) {
res = foo(arg);
} else if (fn == 2) {
res = bar(arg);
}
driver_output(d->port, &res, 1);
}
ErlDrvEntry example_driver_entry = {
NULL, /* F_PTR init, called when driver is loaded */
example_drv_start, /* L_PTR start, called when port is opened */
example_drv_stop, /* F_PTR stop, called when port is closed */
example_drv_output, /* F_PTR output, called when erlang has sent */
NULL, /* F_PTR ready_input, called when input descriptor ready */
NULL, /* F_PTR ready_output, called when output descriptor ready */
"example_drv", /* char *driver_name, the argument to open_port */
NULL, /* F_PTR finish, called when unloaded */
NULL, /* void *handle, Reserved by VM */
NULL, /* F_PTR control, port_command callback */
NULL, /* F_PTR timeout, reserved */
NULL, /* F_PTR outputv, reserved */
NULL, /* F_PTR ready_async, only for async drivers */
NULL, /* F_PTR flush, called when port is about
to be closed, but there is data in driver
queue */
NULL, /* F_PTR call, much like control, sync call
to driver */
NULL, /* unused */
ERL_DRV_EXTENDED_MARKER, /* int extended marker, Should always be
set to indicate driver versioning */
ERL_DRV_EXTENDED_MAJOR_VERSION, /* int major_version, should always be
set to this value */
ERL_DRV_EXTENDED_MINOR_VERSION, /* int minor_version, should always be
set to this value */
0, /* int driver_flags, see documentation */
NULL, /* void *handle2, reserved for VM use */
NULL, /* F_PTR process_exit, called when a
monitored process dies */
NULL /* F_PTR stop_select, called to close an
event object */
};
DRIVER_INIT(example_drv) /* must match name in driver_entry */
{
return &example_driver_entry;
}
執行範例
步驟 1. 編譯 C 程式碼
unix> gcc -o example_drv.so -fpic -shared complex.c port_driver.c
windows> cl -LD -MD -Fe example_drv.dll complex.c port_driver.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(complex5).
{ok,complex5}
步驟 3. 執行範例
2> complex5:start("example_drv").
<0.34.0>
3> complex5:foo(3).
4
4> complex5:bar(5).
10
5> complex5:stop().
stop