小试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)表示,如下:
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的初始化:
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)用于在模块装载时自动调用某个函数:
-on_load(FunName/0).

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


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下:
gcc -fPIC -shared -o hello.so hello.c -I$ERL_ROOT/usr/include/


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函数就可以统一用
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实现:
#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=0x863548"
4> Bin.
<<2,3,4,5,6>>
5> niftest:change_bin(Bin).
"change_bin: size=5, ptr=0x863548"
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在不同环境间传递拷贝。

你可能感兴趣的:(数据结构,c,erlang,NIF)