NIF是Erlang OTP R13B03版引入的,在这一版中还只是一个实验特性,按照原计划,NIF在R14B版成为正式特性,相应的API也将在该版之后稳定下来。等不及了,先试试再说。
1. 基本原理
最大的好处是速度。Erlang程序的逻辑当然是用Erlang写的,速度上不能和C比。NIF使我们可以用C实现相同的程序逻辑, 而速度则是C的速度。
简单的说就是将C实现的程序编译成动态共享对象(shared object)后动态加载到Erlang节点中,与Erlang共享内存空间,这与内联驱动(linked driver)有点类似,因此也就同样危险:有缺陷的代码会使整个Erlang节点当掉。
此外,在NIF函数中也不适合做那种太耗时的计算,不然会影响Erlang虚拟机的响应。
2. NIF编程模式
业务逻辑代码一般是在erlang函数中实现,这些函数一般是erlang写的(听上去像废话),作为一门高级的函数语言,Erlang在运行效率上是不能与C比。不过,有了NIF,如果我们对某些erlang函数的效率不满意可以用C的实现替代Erlang实现。
我的理解是:在实现上,某个Erlang模块的某些逻辑功能可以由一个基于NIF的C模块实现,具体来讲就是erlang模块中的某个或某些 erlang函数可以对应C模块中一个或多个C函数。这些erlang函数不一定非得export给外界,也可以是模块私有的(但是如果该模块的私有函数 没有被其它函数调用则在编译时可能会被编译器优化掉,这种情况下会导致装载NIF库失败)。
这需要告诉Erlang,哪些erlang函数有C版本的NIF实现,在NIF中,每个这样的erlang函数-c函数映射关系由一个C的数据结构(ErlNifFunc)表示,如下:
- typedef struct {
- const char* name;
- unsigned arity;
- ERL_NIF_TERM (*fptr)(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]);
- } ErlNifFunc;
第一个结构成员name表示对应的要替换掉的erlang函数名,第二个结构成员arity是此erlang函数的参数个数,这两个结构成员就确定了要替换掉的erlang函数;第三个结构成员是进行替换的C实现函数(NIF)。
可以看到,所有进行替换的C函数有着特定、统一的定义格式:
- C函数名字当然可以随便取,不过最好与对应的erlang函数相关;
- C函数第一个参数总是ErlNifEnv,代表着函数调用的上下文环境, 可以通过它得到对应的NIF模块的某些特定数据;
- C函数第二个参数argc对应着Erlang函数的参数个数(erlang中是通过函数参数数量的不同来区分同名函数的);
- C函数的第三个参数argv,按顺序一一对应着erlang函数的参数,参数类型都是统一的ERL_NIF_TERM数据结构的数组。C的数据结构 ERL_NIF_TERM对应着erlang中的term,而所有的Erlang数据类型,无论是atom,整型,浮点数,tuple还是 list,binary都统一叫term。,数组大小由钱一个参数argc决定,注意数组元素是const的;
- C函数的返回值类型都是ERL_NIF_TERM。
当然,也可以在一个C函数中可以实现多个不同arity大小的erlang函数的业务逻辑。例如根据argc的个数做switch逻辑分支。
最终,通过ERL_NIF_INIT宏将C实现和对应的erlang模块绑定起来,实现NIF的初始化:
MODULE是对应的erlang模块名字,直接用模块名(不要字符串),funcs是NIF中用C实现的相关函数。 load, reload, upgrade, unload是在NIF相关生命周期中调用的C语言的回调函数。
新版本的erlang还提供了一个新的on_load指令(directive)用于在模块装载时自动调用某个函数:
该函数如果调用成功必须返回ok(表示模块正确装载),否则返回其它。一般通过on_load指定的函数在启动时自动调用erlang:load_nif(Path, LoadInfo)装载NIF模块实现。一个例子:
3. hello nif
3.1 一个hello world的例子
erlang模块代码:
- -module(hello).
- -export([say/0, on_load/0]).
- -on_load(on_load/0).
- on_load() ->
- erlang:load_nif(“./hello”, 0).
- say() ->
- ”hello, i’m from erlang”.
NIF实现代码:
- #include ”erl_nif.h”
- static ERL_NIF_TERM say(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) {
- return enif_make_string(env, ”hello, i’m from C NIF”, ERL_NIF_LATIN1);
- }
- static ErlNifFunc nif_funcs[] =
- {
- {“say”, 0, say}
- };
- ERL_NIF_INIT(hello, nif_funcs, NULL, NULL, NULL, NULL);
3.2 编译
Linux下:
Mac OS下
- gcc -fPIC -bundle -flat_namespace -undefined suppress -o hello.so hello.c -I$ERL_ROOT/usr/include
环境变量ERL_ROOT为erlang-otp的安装路径
4. erlang-c数据交换
如何在两种语言中表示逻辑上相同的数据是写NIF程序关键。对NIF来说,数据的交换分输入和输出两种。
4.1 基本数据的交换
这里涉及的一个主要问题是函数参数的传递和计算结果的返回:即函数调用时将Erlang传来的数据转换成C的,函数计算的结果返回时将C的数据转换成Erlang的。
在erlang中,无论是基本数据类型atom、浮点数、整数,还是复合数据类型tuple, list,都统一被称为term。在NIF的C实现函数中,数据类型ERL_NIF_TERM对应Erlang中的这些term数据。
因此,所有的输入和输出都由统一的ERL_NIF_TERM类型表示,最后所有的NIF的C函数就可以统一用
这样的形式定义了。其中argc表示输入参数的个数,argv数组表示对应的输入参数数据;函数返回值也是ERL_NIF_TERM类型的数据。
- 对输入参数的处理,例如第一个输入到底是int的还是double的,这取决于程序逻辑的约定。虽然NIF也提供了一系列的enif_is_*函数进行判 断,但主要靠程序员自己根据约定转换成C中具体的数据类型。这个转换过程是通过一系列enif_get_*函数完成的。早期版本(R13B)的NIF API还很简陋, 从Erlang term到C的数据转换所支持的基本数据类型只有int, unsigned long和char数组, binary,不过还支持list类型的复合数据。后续版本的NIF开始支持更多数据类型了,例如double;
- 对输出(函数返回)的出来,要将C的数据类型转换成ERL_NIF_TERM,这是通过一系列enif_make_*函数完成的,这组API生产的 ERL_NIF_TERM数据最好视为只读的(想想erlang的不变的变量)。从NIF返回给erlang的这些ERL_NIF_TERM数据将由 erlang节点管理并负责垃圾回收;
- 所有ERL_NIF_TERM数据的属于某个ErlNifEnv数据,这些ERL_NIF_TERM数据的生命周期都与某个ErlNifEnv数据对象的生命周期有关。
4.2 binary数据的交换
erlang和nif实现中最有趣的是binary数据的交换了。这种交换甚至能使erlang变量成为真的“变”量。
NIF实现:
- #include ”erl_nif.h”
- #include <stdio.h>
- static ERL_NIF_TERM change_bin(ErlNifEnv *env, int argc, const ERL_NIF_TERM argv[]) {
- ErlNifBinary bin;
- enif_inspect_binary(env, argv[0], &bin);
- for (int i=0; i<bin.size; ++i) {
- ++bin.data[i];
- }
- char buf[256];
- sprintf(buf, ”change_bin: size=%zu, ptr=%p”, bin.size, bin.data);
- return enif_make_string(env, buf, ERL_NIF_LATIN1);
- }
- static ErlNifFunc nif_funcs[] =
- {
- {“change_bin”, 1, change_bin}
- };
- ERL_NIF_INIT(niftest,nif_funcs,NULL,NULL,NULL,NULL);
对应的erlang模块:
- -module(niftest).
- -export([change_bin/1]).
- -on_load(init/0).
- init() ->
- erlang:load_nif(“./niftest”, 0).
- change_bin(_Bin) ->
- erlang:error({“NIF not implemented in nif_test at line”, ?LINE}).
运行测试:
- 2> Bin = <<1, 2, 3, 4, 5>>.
- <<1,2,3,4,5>>
- 3> niftest:change_bin(Bin).
- “change_bin: size=5, ptr=0×863548″
- 4> Bin.
- <<2,3,4,5,6>>
- 5> niftest:change_bin(Bin).
- “change_bin: size=5, ptr=0×863548″
- 6> Bin.
- <<3,4,5,6,7>>
这段hack代码说明了在Erlang中的binary数据与NIF C中操作的是同一块内存的数据。
这种用法可能什么实际价值,因为无法改变Bin的大小。实际应用中不要这样用,应将ErlNifBinary数据视为只读的。手册说只有enif_alloc_binary或enif_realloc_binary分配的ErlNifBinary才能做修改,一般情况下ErlNifBinary都被nif函数(NIF API)视为只读数据。
4.3 ErlNifEnv环境对象
所有的ERL_NIF_TERM数据都由某个ErlNifEnv管理,后者代表一种环境,一种能持有(英文是host)Erlang term的环境。ERL_NIF_TERM的有效期取决于ErlNifEnv环境的有效期,环境不存在了ERL_NIF_TERM数据也就无效了。
ErlNifEnv环境对象的指针在很多NIF API中做为第一个参数传递进来.
有两种ErlNifEnv环境对象:进程绑定的环境和进程独立的环境。
- 进程绑定环境:所有NIF实现函数的第一个参数传递的都是此类环境,所有NIF函数调用参数(其它参数)都将属于此环境对象。进程绑定环境对象还提供了相 关Erlang调用进程的信息。进程绑定环境对象只在NIF调用时有效,也就是说在不同NIF执行过程中保存并传递进程绑定对象的指针是无意义的(而且很 危险);
- 进程独立环境:此类环境对象由API函数enif_alloc_env创建,可用于在不同NIF执行过程中存储term,也可以通过enif_send发 送term。进程独立环境对象及其包含的term数据总是有效的,可以通过调用API函数enif_free_env显式的摧毁它。
term数据可以通过enif_make_copy在不同环境间传递拷贝。
原文地址:http://x3ge.com/?p=200