查看源码 NIF

本节概述如何使用本机实现函数 (NIF) 解决问题示例中的示例问题。

与使用端口驱动程序相比,NIF 是一种更简单、更高效的调用 C 代码的方式。NIF 最适合同步函数,例如示例中的 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 标识符。它将被宏字符串化。
  • 第二个参数是 ErlNifFunc 结构数组,其中包含每个 NIF 的名称、arity 和函数指针。
  • 其余参数是指向可用于初始化库的回调函数的指针。在这个简单的示例中它们未使用,因此它们都设置为 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")