檢視原始碼 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/1complex5: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