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 调用,性能接近原生代码。
| 方面 | Erlang | NIF (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/3 | enif_make_int/2 |
float() | enif_get_double/3 | enif_make_double/2 |
atom() | enif_get_atom/4 | enif_make_atom/2 |
binary() | enif_inspect_binary/3 | enif_make_binary/3 |
list() | enif_get_list_cell/3 | enif_make_list_cell/3 |
tuple() | enif_get_tuple/4 | enif_make_tuple/3 |
pid() | enif_get_local_pid/3 | enif_make_pid/2 |
ref() | enif_get_local_ref/3 | enif_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_BOUND | CPU 密集型任务 |
ERL_NIF_DIRTY_JOB_IO_BOUND | IO 密集型任务 |
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 进程崩溃 | 充分测试 |
| 内存泄漏 | 系统内存耗尽 | 使用资源对象 |
| 线程安全 | 数据竞争 | 使用锁或原子操作 |
💡 最佳实践
- CPU 密集任务使用 dirty scheduler
- 使用 rebar3 port compiler 管理编译
- 尽量减少 Erlang/C 边界调用次数
- 使用
enif_alloc_binary管理 binary 内存 - 充分测试 NIF 的错误处理路径
22.8 扩展阅读
上一章:21 - Docker 容器化 下一章:23 - Web 开发