erlang-nif

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)表示,如下:

C代码  收藏代码
  1. typedef struct {
  2.     const char* name;
  3.     unsigned arity;
  4.     ERL_NIF_TERM (*fptr)(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]);
  5. } 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的初始化:

Erlang代码  收藏代码
  1. ERL_NIF_INIT(MODULE, ErlNifFunc funcs[], load, reload, upgrade, unload)

MODULE是对应的erlang模块名字,直接用模块名(不要字符串),funcs是NIF中用C实现的相关函数。 load, reload, upgrade, unload是在NIF相关生命周期中调用的C语言的回调函数。

新版本的erlang还提供了一个新的on_load指令(directive)用于在模块装载时自动调用某个函数:

Erlang代码  收藏代码
  1. -on_load(FunName/0).

该函数如果调用成功必须返回ok(表示模块正确装载),否则返回其它。一般通过on_load指定的函数在启动时自动调用erlang:load_nif(Path, LoadInfo)装载NIF模块实现。一个例子:

Erlang代码  收藏代码
  1. -on_load(init/0).
  2. init() ->
  3.     erlang:load_nif(“./hello_nif”, 0).

3. hello nif
3.1 一个hello world的例子
erlang模块代码:

Erlang代码  收藏代码
  1. -module(hello).
  2. -export([say/0, on_load/0]).
  3. -on_load(on_load/0).
  4. on_load() ->
  5.     erlang:load_nif(“./hello”, 0).
  6. say() ->
  7.     ”hello, i’m from erlang”.

NIF实现代码:

C代码  收藏代码
  1. #include ”erl_nif.h”
  2. static ERL_NIF_TERM say(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) {
  3.     return enif_make_string(env, ”hello, i’m from C NIF”, ERL_NIF_LATIN1);
  4. }
  5. static ErlNifFunc nif_funcs[] =
  6. {
  7.     {“say”, 0, say}
  8. };
  9. ERL_NIF_INIT(hello, nif_funcs, NULL, NULL, NULL, NULL);

3.2 编译
Linux下:

Bash代码  收藏代码
  1. gcc -fPIC -shared -o hello.so hello.c -I$ERL_ROOT/usr/include/

Mac OS下

Bash代码  收藏代码
  1. 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函数就可以统一用

Erlang代码  收藏代码
  1. ERL_NIF_TERM func(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[])

这样的形式定义了。其中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实现:

C代码  收藏代码
  1. #include ”erl_nif.h”
  2. #include <stdio.h>
  3. static ERL_NIF_TERM change_bin(ErlNifEnv *env, int argc, const ERL_NIF_TERM argv[]) {
  4.     ErlNifBinary bin;
  5.     enif_inspect_binary(env, argv[0], &bin);
  6.     for (int i=0; i<bin.size; ++i) {
  7.         ++bin.data[i];
  8.     }
  9.     char buf[256];
  10.     sprintf(buf, ”change_bin: size=%zu, ptr=%p”, bin.size, bin.data);
  11.     return enif_make_string(env, buf, ERL_NIF_LATIN1);
  12. }
  13. static ErlNifFunc nif_funcs[] =
  14. {
  15.     {“change_bin”, 1, change_bin}
  16. };
  17. ERL_NIF_INIT(niftest,nif_funcs,NULL,NULL,NULL,NULL);

对应的erlang模块:

Erlang代码  收藏代码
  1. -module(niftest).
  2. -export([change_bin/1]).
  3. -on_load(init/0).
  4. init() ->
  5.     erlang:load_nif(“./niftest”, 0).
  6. change_bin(_Bin) ->
  7.     erlang:error({“NIF not implemented in nif_test at line”, ?LINE}).

运行测试:

Eshell代码  收藏代码
  1. 2> Bin = <<1, 2, 3, 4, 5>>.
  2. <<1,2,3,4,5>>
  3. 3> niftest:change_bin(Bin).
  4. “change_bin: size=5, ptr=0×863548″
  5. 4> Bin.
  6. <<2,3,4,5,6>>
  7. 5> niftest:change_bin(Bin).
  8. “change_bin: size=5, ptr=0×863548″
  9. 6> Bin.
  10. <<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

posted on 2012-12-21 11:28  应无所住而生其心  阅读(790)  评论(0编辑  收藏  举报

导航