檢視原始碼 如何實作驅動程式

注意

本節撰寫於很久以前。雖然大部分內容仍然有效,因為它解釋了重要的概念,但這是為較舊的驅動程式介面撰寫的,因此範例不再適用。建議讀者同時閱讀 erl_driverdriver_entry 文件。

簡介

本節描述如何為 Erlang 建構您自己的驅動程式。

Erlang 中的驅動程式是一個以 C 語言撰寫的程式庫,它連結到 Erlang 模擬器並從 Erlang 呼叫。當 C 語言比 Erlang 更適合、用於加速或提供對 Erlang 無法直接存取的 OS 資源的存取時,可以使用驅動程式。

驅動程式可以動態載入,作為共享程式庫(在 Windows 上稱為 DLL),或靜態載入,在編譯和連結時與模擬器連結。此處僅描述動態載入的驅動程式,靜態連結的驅動程式超出本節的範圍。

警告

當驅動程式載入時,它會在模擬器的上下文中執行,共享相同的記憶體和相同的執行緒。這表示驅動程式中的所有操作都必須是非阻塞的,並且驅動程式中的任何崩潰都會導致整個模擬器崩潰。簡而言之,請小心。

範例驅動程式

本節描述一個簡單的驅動程式,用於使用 libpq C 用戶端程式庫存取 postgres 資料庫。使用 Postgres 是因為它是免費且開源的。有關 postgres 的資訊,請參閱 www.postgres.org

驅動程式是同步的,它使用用戶端程式庫的同步呼叫。這僅為了簡單起見,但不好,因為它會在等待資料庫時停止模擬器。下面將使用異步範例驅動程式來改進此問題。

程式碼很簡單:Erlang 和驅動程式之間的所有通訊都使用 port_control/3 完成,驅動程式使用 rbuf 返回資料。

Erlang 驅動程式僅匯出一個函式:驅動程式進入函式。這是使用巨集 DRIVER_INIT 定義的,它會傳回指向 C struct 的指標,其中包含從模擬器呼叫的進入點。struct 定義模擬器呼叫以呼叫驅動程式的進入點,對於驅動程式未定義和使用的進入點則使用 NULL 指標。

當驅動程式使用 open_port/2 作為埠開啟時,會呼叫 start 進入點。在這裡,我們為使用者資料結構配置記憶體。每次模擬器呼叫我們時,都會傳遞此使用者資料。首先,我們儲存驅動程式控制代碼,因為稍後的呼叫會需要它。我們為 LibPQ 使用的連線控制代碼配置記憶體。我們還透過呼叫 set_port_control_flags,設定旗標 PORT_CONTROL_FLAG_BINARY,來設定埠以傳回配置的驅動程式二進位檔。(這是因為我們不知道我們的資料是否適合 control 的結果緩衝區,該緩衝區具有模擬器設定的預設大小 64 位元組。)

當驅動程式載入時,會呼叫進入點 init。但是,我們不使用它,因為它只執行一次,而且我們希望能夠有多個驅動程式實例。

當埠關閉時,會呼叫 stop 進入點。

當 Erlang 程式碼呼叫 port_control/3 時,會從模擬器呼叫 control 進入點來執行實際工作。我們定義了一組簡單的命令:connect 登入資料庫、disconnect 登出,以及 select 發送 SQL 查詢並取得結果。所有結果都透過 rbuf 返回。 ei 程式庫位於 erl_interface 中,用於以二進位項格式編碼資料。結果以二進位項的形式返回給模擬器,因此在 Erlang 中呼叫 binary_to_term 以將結果轉換為項形式。

該程式碼可在 ertssample 目錄中的 pg_sync.c 中取得。

驅動程式進入點包含模擬器將呼叫的函式。在此範例中,僅提供 startstopcontrol

/* Driver interface declarations */
static ErlDrvData start(ErlDrvPort port, char *command);
static void stop(ErlDrvData drv_data);
static int control(ErlDrvData drv_data, unsigned int command, char *buf,
                   int len, char **rbuf, int rlen);

static ErlDrvEntry pq_driver_entry = {
    NULL,                        /* init */
    start,
    stop,
    NULL,                        /* output */
    NULL,                        /* ready_input */
    NULL,                        /* ready_output */
    "pg_sync",                   /* the name of the driver */
    NULL,                        /* finish */
    NULL,                        /* handle */
    control,
    NULL,                        /* timeout */
    NULL,                        /* outputv */
    NULL,                        /* ready_async */
    NULL,                        /* flush */
    NULL,                        /* call */
    NULL                         /* event */
};

我們有一個結構來儲存驅動程式所需的狀態,在此案例中,我們只需要保留資料庫連線

typedef struct our_data_s {
    PGconn* conn;
} our_data_t;

我們定義的控制碼如下

/* Keep the following definitions in alignment with the
 * defines in erl_pq_sync.erl
 */

#define DRV_CONNECT             'C'
#define DRV_DISCONNECT          'D'
#define DRV_SELECT              'S'

這會傳回驅動程式結構。巨集 DRIVER_INIT 定義唯一匯出的函式。所有其他函式都是靜態的,不會從程式庫匯出。

/* INITIALIZATION AFTER LOADING */

/*
 * This is the init function called after this driver has been loaded.
 * It must *not* be declared static. Must return the address to
 * the driver entry.
 */

DRIVER_INIT(pq_drv)
{
    return &pq_driver_entry;
}

在此處完成一些初始化,start 是從 open_port/2 呼叫的。資料將傳遞給 controlstop

/* DRIVER INTERFACE */
static ErlDrvData start(ErlDrvPort port, char *command)
{
    our_data_t* data;

    data = (our_data_t*)driver_alloc(sizeof(our_data_t));
    data->conn = NULL;
    set_port_control_flags(port, PORT_CONTROL_FLAG_BINARY);
    return (ErlDrvData)data;
}

我們呼叫 disconnect 以從資料庫登出。(這應該從 Erlang 完成,但以防萬一。)

static int do_disconnect(our_data_t* data, ei_x_buff* x);

static void stop(ErlDrvData drv_data)
{
    our_data_t* data = (our_data_t*)drv_data;

    do_disconnect(data, NULL);
    driver_free(data);
}

我們僅使用二進位格式將資料返回給模擬器;輸入資料是 connectselect 的字串參數。傳回的資料由 Erlang 項組成。

函式 get_sei_x_to_new_binary 是用於縮短程式碼的工具。get_s 會複製字串並以零結尾,因為 postgres 用戶端程式庫需要這樣。ei_x_to_new_binary 接受 ei_x_buff 緩衝區,配置二進位檔,並將資料複製到該處。此二進位檔在 *rbuf 中傳回。(請注意,此二進位檔由模擬器釋放,而不是由我們釋放。)

static char* get_s(const char* buf, int len);
static int do_connect(const char *s, our_data_t* data, ei_x_buff* x);
static int do_select(const char* s, our_data_t* data, ei_x_buff* x);

/* As we are operating in binary mode, the return value from control
 * is irrelevant, as long as it is not negative.
 */
static int control(ErlDrvData drv_data, unsigned int command, char *buf,
                   int len, char **rbuf, int rlen)
{
    int r;
    ei_x_buff x;
    our_data_t* data = (our_data_t*)drv_data;
    char* s = get_s(buf, len);
    ei_x_new_with_version(&x);
    switch (command) {
        case DRV_CONNECT:    r = do_connect(s, data, &x);  break;
        case DRV_DISCONNECT: r = do_disconnect(data, &x);  break;
        case DRV_SELECT:     r = do_select(s, data, &x);   break;
        default:             r = -1;        break;
    }
    *rbuf = (char*)ei_x_to_new_binary(&x);
    ei_x_free(&x);
    driver_free(s);
    return r;
}

do_connect 是我們登入資料庫的地方。如果連線成功,我們將連線控制代碼儲存在驅動程式資料中,並傳回 'ok'。否則,我們會傳回來自 postgres 的錯誤訊息,並在驅動程式資料中儲存 NULL

static int do_connect(const char *s, our_data_t* data, ei_x_buff* x)
{
    PGconn* conn = PQconnectdb(s);
    if (PQstatus(conn) != CONNECTION_OK) {
        encode_error(x, conn);
        PQfinish(conn);
        conn = NULL;
    } else {
        encode_ok(x);
    }
    data->conn = conn;
    return 0;
}

如果我們已連線(且連線控制代碼不是 NULL),我們會從資料庫登出。我們需要檢查是否應編碼 'ok',因為我們可以從函式 stop 取得此處,而該函式不會將資料傳回給模擬器

static int do_disconnect(our_data_t* data, ei_x_buff* x)
{
    if (data->conn == NULL)
        return 0;
    PQfinish(data->conn);
    data->conn = NULL;
    if (x != NULL)
        encode_ok(x);
    return 0;
}

我們執行查詢並編碼結果。編碼在另一個 C 模組 pg_encode.c 中完成,該模組也作為範例程式碼提供。

static int do_select(const char* s, our_data_t* data, ei_x_buff* x)
{
   PGresult* res = PQexec(data->conn, s);
    encode_result(x, res, data->conn);
    PQclear(res);
    return 0;
}

在這裡,我們檢查來自 postgres 的結果。如果是資料,我們會將其編碼為具有欄資料的清單清單。來自 postgres 的所有內容都是 C 字串,因此我們使用 ei_x_encode_string 將結果作為字串傳送到 Erlang。(清單的開頭包含欄名稱。)

void encode_result(ei_x_buff* x, PGresult* res, PGconn* conn)
{
    int row, n_rows, col, n_cols;
    switch (PQresultStatus(res)) {
    case PGRES_TUPLES_OK:
        n_rows = PQntuples(res);
        n_cols = PQnfields(res);
        ei_x_encode_tuple_header(x, 2);
        encode_ok(x);
        ei_x_encode_list_header(x, n_rows+1);
        ei_x_encode_list_header(x, n_cols);
        for (col = 0; col < n_cols; ++col) {
            ei_x_encode_string(x, PQfname(res, col));
        }
        ei_x_encode_empty_list(x);
        for (row = 0; row < n_rows; ++row) {
            ei_x_encode_list_header(x, n_cols);
            for (col = 0; col < n_cols; ++col) {
                ei_x_encode_string(x, PQgetvalue(res, row, col));
            }
            ei_x_encode_empty_list(x);
        }
        ei_x_encode_empty_list(x);
        break;
    case PGRES_COMMAND_OK:
        ei_x_encode_tuple_header(x, 2);
        encode_ok(x);
        ei_x_encode_string(x, PQcmdTuples(res));
        break;
    default:
        encode_error(x, conn);
        break;
    }
}

編譯和連結範例驅動程式

驅動程式將被編譯並連結到共享程式庫(Windows 上的 DLL)。使用 gcc 時,這是使用連結旗標 -shared-fpic 完成的。由於我們使用 ei 程式庫,我們也應該將其包含在內。ei 有多個版本,針對偵錯或非偵錯以及多執行緒或單執行緒進行編譯。在範例的 makefile 中,obj 目錄用於 ei 程式庫,這表示我們使用非偵錯的單執行緒版本。

在 Erlang 中以埠的形式呼叫驅動程式

在可以從 Erlang 呼叫驅動程式之前,必須先載入並開啟它。載入是使用 erl_ddll 模組完成的(載入動態驅動程式的 erl_ddll 驅動程式實際上本身就是一個驅動程式)。如果載入成功,可以使用 open_port/2 開啟埠。埠名稱必須與共享程式庫的名稱和驅動程式進入結構中的名稱相符。

開啟埠後,即可呼叫驅動程式。在 pg_sync 範例中,我們沒有來自埠的任何資料,只有來自 port_control/3 的傳回值。

以下程式碼是同步 postgres 驅動程式的 Erlang 部分,pg_sync.erl

-module(pg_sync).

-define(DRV_CONNECT, 1).
-define(DRV_DISCONNECT, 2).
-define(DRV_SELECT, 3).

-export([connect/1, disconnect/1, select/2]).

connect(ConnectStr) ->
    case erl_ddll:load_driver(".", "pg_sync") of
        ok -> ok;
        {error, already_loaded} -> ok;
        E -> exit({error, E})
    end,
    Port = open_port({spawn, ?MODULE}, []),
    case binary_to_term(port_control(Port, ?DRV_CONNECT, ConnectStr)) of
        ok -> {ok, Port};
        Error -> Error
    end.

disconnect(Port) ->
    R = binary_to_term(port_control(Port, ?DRV_DISCONNECT, "")),
    port_close(Port),
    R.

select(Port, Query) ->
    binary_to_term(port_control(Port, ?DRV_SELECT, Query)).

API 很簡單

  • connect/1 載入驅動程式、開啟它並登入資料庫,如果成功,則傳回 Erlang 埠。
  • select/2 將查詢傳送到驅動程式並傳回結果。
  • disconnect/1 關閉資料庫連線和驅動程式。(但是,它不會卸載它。)

連線字串應為 postgres 的連線字串。

使用 erl_ddll:load_driver/2 載入驅動程式。如果此操作成功,或者如果已載入,則會開啟它。這將呼叫驅動程式中的 start 函式。

我們使用 port_control/3 函式來進行所有對驅動程式的呼叫。驅動程式的結果會立即返回,並透過呼叫 binary_to_term/1 轉換為 term。(我們信任從驅動程式返回的 term 格式正確,否則 binary_to_term/1 的呼叫可能會被包含在 catch 內。)

非同步驅動程式範例

有時資料庫查詢可能需要很長時間才能完成,在我們的 pg_sync 驅動程式中,當驅動程式執行其工作時,模擬器會暫停。這通常是無法接受的,因為沒有其他 Erlang 程序有機會執行任何操作。為了改進我們的 postgres 驅動程式,我們使用 LibPQ 中的非同步呼叫重新實作它。

驅動程式的非同步版本位於範例檔案 pg_async.cpg_asyng.erl 中。

/* Driver interface declarations */
static ErlDrvData start(ErlDrvPort port, char *command);
static void stop(ErlDrvData drv_data);
static int control(ErlDrvData drv_data, unsigned int command, char *buf,
                   int len, char **rbuf, int rlen);
static void ready_io(ErlDrvData drv_data, ErlDrvEvent event);

static ErlDrvEntry pq_driver_entry = {
    NULL,                     /* init */
    start,
    stop,
    NULL,                     /* output */
    ready_io,                 /* ready_input */
    ready_io,                 /* ready_output */
    "pg_async",               /* the name of the driver */
    NULL,                     /* finish */
    NULL,                     /* handle */
    control,
    NULL,                     /* timeout */
    NULL,                     /* outputv */
    NULL,                     /* ready_async */
    NULL,                     /* flush */
    NULL,                     /* call */
    NULL                      /* event */
};

typedef struct our_data_t {
    PGconn* conn;
    ErlDrvPort port;
    int socket;
    int connecting;
} our_data_t;

相較於 pg_sync.c,有些地方發生了變化:我們使用 ready_io 作為 ready_inputready_output 的入口點,只有當有資料從 socket 讀取時,才會從模擬器呼叫它。(實際上,socket 是在模擬器內的 select 函式中使用,當 socket 發出訊號,表示有資料要讀取時,就會呼叫 ready_input 入口點。更多相關資訊如下。)

我們的驅動程式資料也擴展了,我們追蹤用於與 postgres 通訊的 socket,以及當我們使用 driver_output 向 port 發送資料時所需的 port。我們有一個旗標 connecting,用來判斷驅動程式是在等待連線還是等待查詢結果。(這是必要的,因為當連線和有查詢結果時,都會呼叫 ready_io 入口點。)

static int do_connect(const char *s, our_data_t* data)
{
    PGconn* conn = PQconnectStart(s);
    if (PQstatus(conn) == CONNECTION_BAD) {
        ei_x_buff x;
        ei_x_new_with_version(&x);
        encode_error(&x, conn);
        PQfinish(conn);
        conn = NULL;
        driver_output(data->port, x.buff, x.index);
        ei_x_free(&x);
    }
    PQconnectPoll(conn);
    int socket = PQsocket(conn);
    data->socket = socket;
    driver_select(data->port, (ErlDrvEvent)socket, DO_READ, 1);
    driver_select(data->port, (ErlDrvEvent)socket, DO_WRITE, 1);
    data->conn = conn;
    data->connecting = 1;
    return 0;
}

connect 函式的外觀也有些不同。我們使用非同步的 PQconnectStart 函式進行連線。連線開始後,我們使用 PQsocket 取得連線的 socket。這個 socket 與 driver_select 函式一起使用,以等待連線。當 socket 準備好輸入或輸出時,就會呼叫 ready_io 函式。

請注意,只有當這裡發生錯誤時,我們才會返回資料(使用 driver_output),否則我們會等待連線完成,在這種情況下,會呼叫我們的 ready_io 函式。

static int do_select(const char* s, our_data_t* data)
{
    data->connecting = 0;
    PGconn* conn = data->conn;
    /* if there's an error return it now */
    if (PQsendQuery(conn, s) == 0) {
        ei_x_buff x;
        ei_x_new_with_version(&x);
        encode_error(&x, conn);
        driver_output(data->port, x.buff, x.index);
        ei_x_free(&x);
    }
    /* else wait for ready_output to get results */
    return 0;
}

do_select 函式會啟動 select,如果沒有立即發生的錯誤,則會返回。當呼叫 ready_io 時,會返回結果。

static void ready_io(ErlDrvData drv_data, ErlDrvEvent event)
{
    PGresult* res = NULL;
    our_data_t* data = (our_data_t*)drv_data;
    PGconn* conn = data->conn;
    ei_x_buff x;
    ei_x_new_with_version(&x);
    if (data->connecting) {
        ConnStatusType status;
        PQconnectPoll(conn);
        status = PQstatus(conn);
        if (status == CONNECTION_OK)
            encode_ok(&x);
        else if (status == CONNECTION_BAD)
            encode_error(&x, conn);
    } else {
        PQconsumeInput(conn);
        if (PQisBusy(conn))
            return;
        res = PQgetResult(conn);
        encode_result(&x, res, conn);
        PQclear(res);
        for (;;) {
            res = PQgetResult(conn);
            if (res == NULL)
                break;
            PQclear(res);
        }
    }
    if (x.index > 1) {
        driver_output(data->port, x.buff, x.index);
        if (data->connecting)
            driver_select(data->port, (ErlDrvEvent)data->socket, DO_WRITE, 0);
    }
    ei_x_free(&x);
}

當我們從 postgres 取得的 socket 準備好輸入或輸出時,就會呼叫 ready_io 函式。在這裡,我們先檢查是否正在連線到資料庫。在這種情況下,我們會檢查連線狀態,如果連線成功則返回 OK,如果連線失敗則返回錯誤。如果連線尚未建立,我們就直接返回;會再次呼叫 ready_io

如果我們有來自連線的結果,這表示 x 緩衝區中有資料,我們就不再需要在輸出上進行 select(ready_output),因此我們透過呼叫 driver_select 來移除它。

如果我們沒有進行連線,我們會等待 PQsendQuery 的結果,因此我們取得結果並返回它。編碼是使用與先前範例中相同的函式完成的。

這裡應該新增錯誤處理,例如,檢查 socket 是否仍然開啟,但這只是一個簡單的範例。

非同步驅動程式的 Erlang 部分包含在範例檔案 pg_async.erl 中。

-module(pg_async).

-define(DRV_CONNECT, $C).
-define(DRV_DISCONNECT, $D).
-define(DRV_SELECT, $S).

-export([connect/1, disconnect/1, select/2]).

connect(ConnectStr) ->
    case erl_ddll:load_driver(".", "pg_async") of
        ok -> ok;
        {error, already_loaded} -> ok;
        _ -> exit({error, could_not_load_driver})
    end,
    Port = open_port({spawn, ?MODULE}, [binary]),
    port_control(Port, ?DRV_CONNECT, ConnectStr),
    case return_port_data(Port) of
        ok ->
            {ok, Port};
        Error ->
            Error
    end.

disconnect(Port) ->
    port_control(Port, ?DRV_DISCONNECT, ""),
    R = return_port_data(Port),
    port_close(Port),
    R.

select(Port, Query) ->
    port_control(Port, ?DRV_SELECT, Query),
    return_port_data(Port).

return_port_data(Port) ->
    receive
        {Port, {data, Data}} ->
            binary_to_term(Data)
    end.

Erlang 程式碼略有不同,因為我們不會從 port_control/3 同步返回結果,而是從訊息佇列中的 driver_output 取得資料。上面的函式 return_port_data 會從 port 接收資料。由於資料是二進制格式,我們使用 binary_to_term/1 將其轉換為 Erlang term。請注意,驅動程式是在二進制模式下開啟的(open_port/2 是使用選項 [binary] 呼叫的)。這表示從驅動程式傳送到模擬器的資料會以二進制形式傳送。如果沒有選項 binary,它們將會是整數列表。

使用 driver_async 的非同步驅動程式

作為最後一個範例,我們示範如何使用 driver_async。我們還使用驅動程式 term 介面。驅動程式是以 C++ 編寫的。這使我們可以使用 STL 中的演算法。我們使用 next_permutation 演算法來取得整數列表的下一個排列。對於大型列表(> 100,000 個元素),這需要一些時間,因此我們將其作為非同步任務執行。

驅動程式的非同步 API 很複雜。首先,必須準備工作。在範例中,這是透過 output 完成的。我們可以使用 control,但我們希望在範例中有些變化。在我們的驅動程式中,我們會配置一個結構,其中包含非同步任務執行工作所需的任何內容。這是在主模擬器執行緒中完成的。然後,從與主模擬器執行緒分離的驅動程式執行緒呼叫非同步函式。請注意,驅動程式函式不是可重入的,因此不能使用。最後,在函式完成後,會從主模擬器執行緒呼叫驅動程式回呼 ready_async,這就是我們將結果返回 Erlang 的地方。(我們無法從非同步函式內返回結果,因為我們無法呼叫驅動程式函式。)

以下程式碼來自範例檔案 next_perm.cc。驅動程式入口點看起來與之前相同,但也包含回呼 ready_async

static ErlDrvEntry next_perm_driver_entry = {
    NULL,                        /* init */
    start,
    NULL,                        /* stop */
    output,
    NULL,                        /* ready_input */
    NULL,                        /* ready_output */
    "next_perm",                 /* the name of the driver */
    NULL,                        /* finish */
    NULL,                        /* handle */
    NULL,                        /* control */
    NULL,                        /* timeout */
    NULL,                        /* outputv */
    ready_async,
    NULL,                        /* flush */
    NULL,                        /* call */
    NULL                         /* event */
};

output 函式會配置非同步函式的工作區域。由於我們使用 C++,因此我們使用 struct,並將資料塞入其中。我們必須複製原始資料,在我們從 output 函式返回之後,該資料將不再有效,而 do_perm 函式稍後會從另一個執行緒呼叫。我們在這裡不返回任何資料,而是稍後從 ready_async 回呼傳送資料。

async_data 會傳遞給 do_perm 函式。我們不使用 async_free 函式(driver_async 的最後一個引數),它僅在程式以程式設計方式取消任務時使用。

struct our_async_data {
    bool prev;
    vector<int> data;
    our_async_data(ErlDrvPort p, int command, const char* buf, int len);
};

our_async_data::our_async_data(ErlDrvPort p, int command,
                               const char* buf, int len)
    : prev(command == 2),
      data((int*)buf, (int*)buf + len / sizeof(int))
{
}

static void do_perm(void* async_data);

static void output(ErlDrvData drv_data, char *buf, int len)
{
    if (*buf < 1 || *buf > 2) return;
    ErlDrvPort port = reinterpret_cast<ErlDrvPort>(drv_data);
    void* async_data = new our_async_data(port, *buf, buf+1, len);
    driver_async(port, NULL, do_perm, async_data, do_free);
}

do_perm 中,我們執行工作,並對在 output 中配置的結構進行操作。

static void do_perm(void* async_data)
{
    our_async_data* d = reinterpret_cast<our_async_data*>(async_data);
    if (d->prev)
        prev_permutation(d->data.begin(), d->data.end());
    else
        next_permutation(d->data.begin(), d->data.end());
}

ready_async 函式中,會將輸出送回模擬器。我們使用驅動程式 term 格式,而不是 ei。這是將 Erlang term 直接傳送到驅動程式的唯一方法,而無需 Erlang 程式碼呼叫 binary_to_term/1。在簡單的範例中,這效果很好,我們不需要使用 ei 來處理二進制 term 格式。

當資料返回時,我們會解除配置我們的資料。

static void ready_async(ErlDrvData drv_data, ErlDrvThreadData async_data)
{
    ErlDrvPort port = reinterpret_cast<ErlDrvPort>(drv_data);
    our_async_data* d = reinterpret_cast<our_async_data*>(async_data);
    int n = d->data.size(), result_n = n*2 + 3;
    ErlDrvTermData *result = new ErlDrvTermData[result_n], *rp = result;
    for (vector<int>::iterator i = d->data.begin();
         i != d->data.end(); ++i) {
        *rp++ = ERL_DRV_INT;
        *rp++ = *i;
    }
    *rp++ = ERL_DRV_NIL;
    *rp++ = ERL_DRV_LIST;
    *rp++ = n+1;
    driver_output_term(port, result, result_n);
    delete[] result;
    delete d;
}

此驅動程式的呼叫方式與 Erlang 中的其他驅動程式相同。但是,由於我們使用 driver_output_term,因此無需呼叫 binary_to_term/1。Erlang 程式碼位於範例檔案 next_perm.erl 中。

輸入會變更為整數列表並傳送到驅動程式。

-module(next_perm).

-export([next_perm/1, prev_perm/1, load/0, all_perm/1]).

load() ->
    case whereis(next_perm) of
        undefined ->
            case erl_ddll:load_driver(".", "next_perm") of
                ok -> ok;
                {error, already_loaded} -> ok;
                E -> exit(E)
            end,
            Port = open_port({spawn, "next_perm"}, []),
            register(next_perm, Port);
        _ ->
            ok
    end.

list_to_integer_binaries(L) ->
    [<<I:32/integer-native>> || I <- L].

next_perm(L) ->
    next_perm(L, 1).

prev_perm(L) ->
    next_perm(L, 2).

next_perm(L, Nxt) ->
    load(),
    B = list_to_integer_binaries(L),
    port_control(next_perm, Nxt, B),
    receive
        Result ->
            Result
    end.

all_perm(L) ->
    New = prev_perm(L),
    all_perm(New, L, [New]).

all_perm(L, L, Acc) ->
    Acc;
all_perm(L, Orig, Acc) ->
    New = prev_perm(L),
    all_perm(New, Orig, [New | Acc]).