强曰为道

与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

22 - NIF 与 C 集成

第 22 章:NIF 与 C 集成

NIF(Native Implemented Functions)允许你用 C/C++ 编写 Erlang 函数,用于 CPU 密集型任务或访问系统 API。


22.1 NIF 基础

22.1.1 什么是 NIF?

NIF 是用 C/C++ 编写的函数,可以直接从 Erlang 调用,性能接近原生代码。

方面ErlangNIF (C)
执行速度中等
并发安全天然安全需要自行保证
内存管理自动 GC手动管理
调度抢占式占用调度器
开发难度

22.1.2 NIF 生命周期

Erlang 调用 my_nif:func(Args)
    ↓
BEAM 查找已加载的 NIF 实现
    ↓
调用 C 函数 nif_func(env, argc, argv)
    ↓
C 函数执行并返回 ERL_NIF_TERM
    ↓
Erlang 接收返回值

22.2 编写第一个 NIF

22.2.1 C 源文件

/* c_src/my_nif.c */
#include <erl_nif.h>

/* NIF 函数:加法 */
static ERL_NIF_TERM add(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[])
{
    int a, b;
    
    /* 从 Erlang term 提取参数 */
    if (!enif_get_int(env, argv[0], &a) ||
        !enif_get_int(env, argv[1], &b)) {
        return enif_make_badarg(env);
    }
    
    /* 返回结果 */
    return enif_make_int(env, a + b);
}

/* NIF 函数:字符串处理 */
static ERL_NIF_TERM string_length(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[])
{
    ErlNifBinary bin;
    
    if (!enif_inspect_binary(env, argv[0], &bin)) {
        return enif_make_badarg(env);
    }
    
    return enif_make_int(env, bin.size);
}

/* NIF 函数数组 */
static ErlNifFunc nif_funcs[] = {
    {"add", 2, add},
    {"string_length", 1, string_length}
};

/* 初始化模块 */
ERL_NIF_INIT(my_nif, nif_funcs, NULL, NULL, NULL, NULL)

22.2.2 Erlang 模块

%% src/my_nif.erl
-module(my_nif).
-export([add/2, string_length/1]).

-on_load(load_nif/0).

load_nif() ->
    SoName = filename:join(code:priv_dir(my_nif), "my_nif"),
    erlang:load_nif(SoName, 0).

%% NIF 实现(如果加载失败,会调用这些后备实现)
add(_A, _B) ->
    erlang:nif_error("NIF library not loaded").

string_length(_Str) ->
    erlang:nif_error("NIF library not loaded").

22.2.3 rebar.config 配置

%% rebar.config
{plugins, [rebar3_hex]}.

{port_specs, [
    {"priv/my_nif.so", ["c_src/*.c"]}
]}.

{provider_hooks, [
    {pre, [{compile, {pc, compile}}]},
    {clean, {pc, clean}}
]}.

{deps, [
    {pc, "1.15.0"}  %% rebar3 port compiler plugin
]}.

22.3 数据类型映射

22.3.1 基本类型

Erlang 类型C 函数(读取)C 函数(创建)
integer()enif_get_int/3enif_make_int/2
float()enif_get_double/3enif_make_double/2
atom()enif_get_atom/4enif_make_atom/2
binary()enif_inspect_binary/3enif_make_binary/3
list()enif_get_list_cell/3enif_make_list_cell/3
tuple()enif_get_tuple/4enif_make_tuple/3
pid()enif_get_local_pid/3enif_make_pid/2
ref()enif_get_local_ref/3enif_make_ref/2

22.3.2 示例:类型转换

/* 创建元组 {ok, Result} */
static ERL_NIF_TERM make_ok(ErlNifEnv* env, ERL_NIF_TERM result)
{
    return enif_make_tuple2(env, 
        enif_make_atom(env, "ok"),
        result);
}

/* 创建错误元组 {error, Reason} */
static ERL_NIF_TERM make_error(ErlNifEnv* env, const char* reason)
{
    return enif_make_tuple2(env,
        enif_make_atom(env, "error"),
        enif_make_atom(env, reason));
}

/* 从列表创建 Erlang binary */
static ERL_NIF_TERM make_binary_string(ErlNifEnv* env, const char* str)
{
    ErlNifBinary bin;
    size_t len = strlen(str);
    enif_alloc_binary(len, &bin);
    memcpy(bin.data, str, len);
    return enif_make_binary(env, &bin);
}

22.4 内存管理

22.4.1 ErlNifEnv 环境

/* 每个 NIF 调用都有一个环境 */
static ERL_NIF_TERM my_nif(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[])
{
    /* 在这个环境中分配的内存会在函数返回时自动释放 */
    /* 所以不需要手动释放 enif_alloc 分配的内存 */
    
    ERL_NIF_TERM result = enif_make_int(env, 42);
    return result;
}

22.4.2 持久资源

/* 需要在多次 NIF 调用间保持的数据,使用资源对象 */
typedef struct {
    int fd;
    char buffer[1024];
} my_resource;

static ErlNifResourceType* my_resource_type;

/* 初始化时创建资源类型 */
static int load(ErlNifEnv* env, void** priv_data, ERL_NIF_TERM load_info)
{
    my_resource_type = enif_open_resource_type(env, NULL, "my_resource",
        NULL, ERL_NIF_RT_CREATE | ERL_NIF_RT_TAKEOVER, NULL);
    return 0;
}

/* 创建资源对象 */
static ERL_NIF_TERM create_resource(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[])
{
    my_resource* res = (my_resource*)enif_alloc_resource(my_resource_type, sizeof(my_resource));
    res->fd = -1;
    memset(res->buffer, 0, sizeof(res->buffer));
    
    ERL_NIF_TERM term = enif_make_resource(env, res);
    enif_release_resource(res);
    
    return term;
}

22.5 Dirty Scheduler

22.5.1 问题

NIF 默认在正常调度器上执行,如果执行时间太长,会阻塞其他进程。

22.5.2 Dirty Scheduler 解决方案

/* 声明为 dirty NIF(在 dirty scheduler 上执行) */
static ErlNifFunc nif_funcs[] = {
    {"add", 2, add, 0},                    /* 正常 scheduler */
    {"heavy_compute", 1, heavy_compute, ERL_NIF_DIRTY_JOB_CPU_BOUND},  /* CPU 密集 */
    {"io_operation", 1, io_operation, ERL_NIF_DIRTY_JOB_IO_BOUND}     /* IO 密集 */
};
Flag说明
0正常 scheduler
ERL_NIF_DIRTY_JOB_CPU_BOUNDCPU 密集型任务
ERL_NIF_DIRTY_JOB_IO_BOUNDIO 密集型任务

22.5.3 启动参数

%% config/vm.args
%% 必须有足够的 dirty scheduler
+SDcpu 4   %% 4 个 CPU-bound dirty scheduler
+SDio 4    %% 4 个 IO-bound dirty scheduler

22.6 实战:Zlib 压缩 NIF

/* c_src/zlib_nif.c */
#include <erl_nif.h>
#include <zlib.h>

static ERL_NIF_TERM compress_nif(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[])
{
    ErlNifBinary input;
    ErlNifBinary output;
    uLongf dest_len;
    
    if (!enif_inspect_binary(env, argv[0], &input)) {
        return enif_make_badarg(env);
    }
    
    dest_len = compressBound(input.size);
    enif_alloc_binary(dest_len, &output);
    
    if (compress(output.data, &dest_len, input.data, input.size) != Z_OK) {
        enif_release_binary(&output);
        return enif_make_atom(env, "error");
    }
    
    output.size = dest_len;
    return enif_make_binary(env, &output);
}

static ERL_NIF_TERM decompress_nif(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[])
{
    ErlNifBinary input, output;
    uLongf dest_len = 1024 * 1024; /* 1MB 初始缓冲区 */
    
    if (!enif_inspect_binary(env, argv[0], &input)) {
        return enif_make_badarg(env);
    }
    
    enif_alloc_binary(dest_len, &output);
    
    if (uncompress(output.data, &dest_len, input.data, input.size) != Z_OK) {
        enif_release_binary(&output);
        return enif_make_atom(env, "error");
    }
    
    output.size = dest_len;
    return enif_make_binary(env, &output);
}

static ErlNifFunc nif_funcs[] = {
    {"compress", 1, compress_nif, ERL_NIF_DIRTY_JOB_CPU_BOUND},
    {"decompress", 1, decompress_nif, ERL_NIF_DIRTY_JOB_CPU_BOUND}
};

ERL_NIF_INIT(zlib_nif, nif_funcs, NULL, NULL, NULL, NULL)

22.7 注意事项

⚠️ NIF 陷阱

陷阱后果解决
NIF 执行超过 1ms调度器被占用使用 dirty scheduler
NIF 崩溃BEAM 进程崩溃充分测试
内存泄漏系统内存耗尽使用资源对象
线程安全数据竞争使用锁或原子操作

💡 最佳实践

  1. CPU 密集任务使用 dirty scheduler
  2. 使用 rebar3 port compiler 管理编译
  3. 尽量减少 Erlang/C 边界调用次数
  4. 使用 enif_alloc_binary 管理 binary 内存
  5. 充分测试 NIF 的错误处理路径

22.8 扩展阅读


上一章:21 - Docker 容器化 下一章:23 - Web 开发