檢視原始碼 NIFs

本節概述如何使用原生實作函式(NIFs)解決範例問題中的範例問題。

與使用埠驅動程式相比,NIFs 是呼叫 C 程式碼更簡單且更有效率的方式。NIFs 最適合用於同步函式,例如範例中的 foobar,這些函式會執行一些相對較短的計算而沒有副作用,並傳回結果。

NIF 是在 C 中實作而不是在 Erlang 中實作的函式。對於呼叫者而言,NIF 看起來就像任何其他函式。它們屬於一個模組,並且像任何其他 Erlang 函式一樣被呼叫。模組的 NIF 會被編譯並連結到一個動態可載入的共享程式庫(在 UNIX 中為 SO,在 Windows 中為 DLL)。NIF 程式庫必須在執行時由模組的 Erlang 程式碼載入。

由於 NIF 程式庫是動態連結到模擬器程序中的,因此這是從 Erlang 呼叫 C 程式碼最快的方式(與埠驅動程式一起)。呼叫 NIF 不需要上下文切換。但它也是最不安全的,因為 NIF 中的崩潰也會使模擬器當機。

Erlang 程式

即使模組的所有函式都是 NIF,仍然需要 Erlang 模組,原因有二

  • NIF 程式庫必須由同一個模組中的 Erlang 程式碼明確載入。
  • 模組的所有 NIF 也必須具有 Erlang 實作。

通常,這些是拋出例外狀況的最小存根實作。但是,它們也可以用作在某些架構上沒有原生實作的函式的回退實作。

NIF 程式庫透過呼叫 erlang:load_nif/2 載入,並以共享程式庫的名稱作為參數。第二個參數可以是任何將傳遞到程式庫並用於初始化的項。

-module(complex6).
-export([foo/1, bar/1]).
-nifs([foo/1, bar/1]).
-on_load(init/0).

init() ->
    ok = erlang:load_nif("./complex6_nif", 0).

foo(_X) ->
    erlang:nif_error(nif_library_not_loaded).
bar(_Y) ->
    erlang:nif_error(nif_library_not_loaded).

在這裡,指令 on_load 用於在載入模組時自動呼叫函式 init。如果 init 傳回除了 ok 之外的任何值,例如在此範例中載入 NIF 程式庫失敗時,模組將被卸載,並且對其中的函式的呼叫將失敗。

載入 NIF 程式庫會覆蓋存根實作,並導致對 foobar 的呼叫改為分派到 NIF 實作。

NIF 程式庫程式碼

模組的 NIF 會被編譯並連結到一個共享程式庫中。每個 NIF 都實作為一個普通的 C 函式。巨集 ERL_NIF_INIT 與結構陣列一起定義了模組中所有 NIF 的名稱、arity 和函式指標。必須包含標頭檔 erl_nif.h。由於程式庫是共享模組,而不是程式,因此不存在 main 函式。

傳遞給 NIF 的函式參數會出現在陣列 argv 中,argc 作為陣列的長度,因此也是函式的 arity。函式的第 N 個參數可以透過 argv[N-1] 存取。NIF 還會接受一個環境參數,該參數充當一個不透明的控制代碼,需要將其傳遞給大多數 API 函式。環境包含有關呼叫 Erlang 程序的信息

#include <erl_nif.h>

extern int foo(int x);
extern int bar(int y);

static ERL_NIF_TERM foo_nif(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[])
{
    int x, ret;
    if (!enif_get_int(env, argv[0], &x)) {
	return enif_make_badarg(env);
    }
    ret = foo(x);
    return enif_make_int(env, ret);
}

static ERL_NIF_TERM bar_nif(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[])
{
    int y, ret;
    if (!enif_get_int(env, argv[0], &y)) {
	return enif_make_badarg(env);
    }
    ret = bar(y);
    return enif_make_int(env, ret);
}

static ErlNifFunc nif_funcs[] = {
    {"foo", 1, foo_nif},
    {"bar", 1, bar_nif}
};

ERL_NIF_INIT(complex6, nif_funcs, NULL, NULL, NULL, NULL)

在此,ERL_NIF_INIT 具有以下參數

  • 第一個參數必須是 Erlang 模組的名稱,作為 C 識別符。它將由巨集字串化。
  • 第二個參數是包含每個 NIF 的名稱、arity 和函式指標的 ErlNifFunc 結構陣列。
  • 其餘參數是指向可用於初始化程式庫的回呼函式的指標。在此簡單範例中未使用它們,因此它們都設定為 NULL

函式參數和傳回值表示為 ERL_NIF_TERM 類型的值。在這裡,像 enif_get_intenif_make_int 這樣的函式用於在 Erlang 項和 C 類型之間進行轉換。如果函式參數 argv[0] 不是整數,enif_get_int 會傳回 false,在這種情況下,它會透過 enif_make_badarg 拋出 badarg 例外狀況來傳回。

執行範例

步驟 1. 編譯 C 程式碼

unix> gcc -o complex6_nif.so -fpic -shared complex.c complex6_nif.c
windows> cl -LD -MD -Fe complex6_nif.dll complex.c complex6_nif.c

步驟 2:啟動 Erlang 並編譯 Erlang 程式碼

> erl
Erlang R13B04 (erts-5.7.5) [64-bit] [smp:4:4] [rq:4] [async-threads:0] [kernel-poll:false]

Eshell V5.7.5  (abort with ^G)
1> c(complex6).
{ok,complex6}

步驟 3:執行範例

3> complex6:foo(3).
4
4> complex6:bar(5).
10
5> complex6:foo("not an integer").
** exception error: bad argument
     in function  complex6:foo/1
        called as comlpex6:foo("not an integer")