8  NIFs

8 NIFs

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

NIFs 是比使用端口驱动程序更简单、更高效的调用 C 代码的方式。 NIFs 最适合用于同步函数,例如示例中的 foobar,这些函数执行一些相对较短的计算,没有副作用并返回结果。

NIF 是用 C 而不是 Erlang 实现的函数。 NIF 对调用者来说与其他任何函数一样。 它们属于一个模块,并且像任何其他 Erlang 函数一样被调用。 模块的 NIFs 被编译并链接到一个动态可加载的共享库(在 UNIX 中为 SO,在 Windows 中为 DLL)。 NIF 库必须在运行时由模块的 Erlang 代码加载。

由于 NIF 库是动态链接到模拟器进程中的,因此这是从 Erlang 调用 C 代码的最快方式(与端口驱动程序并列)。 调用 NIFs 不需要任何上下文切换。 但它也是最不安全的,因为 NIF 中的崩溃也会导致模拟器崩溃。

即使模块的所有函数都是 NIFs,仍然需要 Erlang 模块,原因有两个:

  • NIF 库必须由同一模块中的 Erlang 代码显式加载。
  • 模块的所有 NIFs 也必须具有 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 实现。

模块的 NIFs 被编译并链接到一个共享库中。 每个 NIF 都实现为一个普通的 C 函数。 宏 ERL_NIF_INIT 与结构数组一起定义了模块中所有 NIFs 的名称、元数和函数指针。 必须包含头文件 erl_nif.h。 由于库是一个共享模块,而不是一个程序,因此不应该存在任何主函数。

传递给 NIF 的函数参数出现在数组 argv 中,argc 作为数组的长度,因此是函数的元数。 函数的第 N 个参数可以访问为 argv[N-1]。 NIFs 还接受一个环境参数,该参数充当不透明句柄,需要传递给大多数 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 的名称、元数和函数指针。
  • 其余参数是指向回调函数的指针,这些回调函数可用于初始化库。 它们没有在本例中使用,因此它们都被设置为 NULL

函数参数和返回值表示为类型 ERL_NIF_TERM 的值。 这里,函数如 enif_get_intenif_make_int 用于在 Erlang 项和 C 类型之间进行转换。 如果函数参数 argv[0] 不是整数,enif_get_int 返回 false,在这种情况下,它通过抛出 badarg 异常并使用 enif_make_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")