最近在项目中频繁用到erlang的NIF接口,以扩展erlang虚拟机的功能,同时又能提供较高的性能。

NIF(native implemented functions)从R14B开始支持,其功能在于,能够使得erlang module的功能通过c/c++实现。

erlang虚拟机有很多与外部进行功能交互的方式,如通过spawn_executable类型的port调用其它程序,port_driver,nif等,它们各有适应的场合:

spawn_executable类型的port会产生一个外部进程执行命令,不会干扰到erlang虚拟机本身,但是性能较低;

port_driver遵循erlang虚拟机的port机制,功能强大,但需要对虚拟机有较多的了解,编程门槛较高,内嵌入虚拟机,会影响到虚拟机执行,执行结果异步地通过消息队列返回,一些同步的环境下不适合;

nif功能强大但编程接口相对简单,不需要对虚拟机了解太多即可编写,调用一个nif实现的erlang接口就如同调用一个c函数一般,同时也有异步向进程投递消息的能力,极大的提升了erlang虚拟机的扩展能力,缺点也是需要内嵌入虚拟机,会影响到虚拟机执行。

如何利用NIF编写接口,初学者可以看看《erlang otp in action》上的例子,进阶者可以看看一些开源项目,如riak依赖的bitcask、eleveldb、ebloom等等,都是非常好的范例。

本次将分析NIF的部分重要接口的实现,其中涉及到erlang虚拟机的部分仅介绍基本原理而不会深入分析。

照抄官方文档上给出的例子:

NIF实现:

niftest.c

#include "erl_nif.h"

static ERL_NIF_TERM hello(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[])

{

return enif_make_string(env, "Hello world!", ERL_NIF_LATIN1);

}

static ErlNifFunc nif_funcs[] =

{

{"hello", 0, hello}

};

ERL_NIF_INIT(niftest,nif_funcs,NULL,NULL,NULL,NULL)

ERL_NIF_INIT的第一个参数是该NIF的模块名,第二个参数是该模块包含的所有可供外部调用的函数定义数组,本例中,niftest即为NIF的模块名,而nif_funcs即为函数定义数组,nif_funcs包含了一个函数定义hello,其参数个数为0,具体实现为c函数static ERL_NIF_TERM hello(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[])。

NIF的模块名可以将其与同名的erlang module对应起来。

对于linux平台,按照官方文档给出的编译方法,

gcc -fPIC -shared -o niftest.so niftest.c -I $ERL_ROOT/usr/include/

niftest.c将被编译为共享库niftest.so,以供虚拟机在需要的时候加载。

相应的erlang模块实现:

niftest.erl

-module(niftest).

-export([init/0, hello/0]).

init() ->

erlang:load_nif("./niftest", 0).

hello() ->

"NIF library not loaded".

niftest.erl定义了一个erlang模块niftest,包含一个init函数和一个hello函数,init函数用于初始化模块,通过load_nif函数加载niftest.so,这个定义与NIF对模块和函数的定义是一致的。

官方文档介绍时,明确的给出了调用结果:

1> c(niftest).

{ok,niftest}

2> niftest:hello().

"NIF library not loaded"

3> niftest:init().

ok

4> niftest:hello().

"Hello world!"

这是为什么呢?

首先来看加载时做了什么。

erlang:load_nif/2是一个bif,也即erlang的内建函数,要求必须由NIF对应的erlang module的某个函数直接调用。

erl_nif.c

BIF_RETTYPE load_nif_2(BIF_ALIST_2)

/*该函数的参数BIF_ALIST_2是一个数组,包含两个元素,对应了erlang:load_nif/2两个参数,第一个为共享库文件名,第二个为加载时用户传入的一些私有参数。*/

{

len = list_length(BIF_ARG_1);

if (len < 0) {

BIF_ERROR(BIF_P, BADARG);

}

lib_name = (char *) erts_alloc(ERTS_ALC_T_TMP, len + 1);

if (intlist_to_buf(BIF_ARG_1, lib_name, len) != len) {

erts_free(ERTS_ALC_T_TMP, lib_name);

BIF_ERROR(BIF_P, BADARG);

}

lib_name[len] = '\0';

/* 首先,为待加载nif的共享库文件名单独分配一个缓冲区lib_name,并将文件名拷入这个缓冲区中 */

caller = find_function_from_pc(BIF_P->cp);

mod_atom = caller[0];

mod=erts_get_module(mod_atom);

/* 接着,从进程程序计数器中取得当前指令所在的模块的描述符mod,由于进程当前调用的函数是erlang:load_nif/2,该函数必须由NIF对应的erlang module调用,否则将会在之后的检查过程中报错 */

erts_sys_ddll_open2(lib_name, &handle, &errdesc);

/* 使用dlopen打开NIF的共享库文件 */

erts_sys_ddll_load_nif_init(handle, &init_func, &errdesc);

/* 找打共享库文件中一个名为"nif_init"的函数,记录到init_func中 */

entry = erts_sys_ddll_call_nif_init(init_func);

/* 调用init_func进行初始化工作,该函数会返回一个ErlNifEntry结构,相当于NIF的描述符,正是由宏ERL_NIF_INIT构造的,它的作用将在稍后介绍 */

for (i=0; i < entry->num_of_funcs && ret==am_ok; i++) {

BeamInstr** code_pp;

ErlNifFunc* f = &entry->funcs[i];

if (!erts_atom_get(f->name, sys_strlen(f->name), &f_atom)

|| (code_pp = get_func_pp(mod->code, f_atom, f->arity))==NULL) {

ret = load_nif_error(BIF_P,bad_lib,"Function not found %T:%s/%u",

mod_atom, f->name, f->arity);

}

/* 检查NIF描述符的每一个函数,这些函数必须要在NIF对应的erlang module中有同名构造,否则将会报错,这也就是为什么要求load_nif仅能由NIF对应的erlang module调用的原因了,若NIF出现任何一个没有在erlang module中定义的函数,则会影响到erlang的模块定义 */

ErlNifEnv env;

struct erl_module_nif* lib = erts_alloc(ERTS_ALC_T_NIF, sizeof(struct erl_module_nif));

lib->handle = handle;

lib->entry = entry;

erts_refc_init(&lib->rt_cnt, 0);

erts_refc_init(&lib->rt_dtor_cnt, 0);

lib->mod = mod;

env.mod_nif = lib;

/* 为NIF构建一个模块描述符,分别记录了NIF的共享库文件句柄handle,NIF描述符entry,NIF对应的模块的描述符mod */

if (entry->load != NULL) {

erts_pre_nif(&env, BIF_P, lib);

veto = entry->load(&env, &lib->priv_data, BIF_ARG_2);

erts_post_nif(&env);

}

/* 此处仅仅介绍第一次加载时的动作,即调用NIF描述符的load函数进行加载,该函数也是由NIF文件定义的 */

mod->nif = lib;

for (i=0; i < entry->num_of_funcs; i++)

/* 遍历NIF描述符中的每一个函数 */

{

BeamInstr* code_ptr;

erts_atom_get(entry->funcs[i].name, sys_strlen(entry->funcs[i].name), &f_atom);

/* 取得该函数的在erlang module中的同名函数的位置,记录到f_atom中 */

code_ptr = *get_func_pp(mod->code, f_atom, entry->funcs[i].arity);

/* 由f_atom得到对于的函数描述符,mod->code是一个数组,记录了erlang module的所有信息,包括每个函数的名字、参数个数、入口点、表达式等 */

if (code_ptr[1] == 0) {

code_ptr[5+0] = (BeamInstr) BeamOp(op_call_nif);

/* 将原先的函数指令替换为op_call_nif,表名下一步将进行调用nif的过程,该指令的执行过程将在稍后介绍 */

}

else { /* Function traced, patch the original instruction word */

BpData** bps = (BpData**) code_ptr[1];

BpData*  bp  = (BpData*) bps[erts_bp_sched2ix()];

bp->orig_instr = (BeamInstr) BeamOp(op_call_nif);

}

code_ptr[5+1] = (BeamInstr) entry->funcs[i].fptr;

/* 紧接在op_call_nif指令后要放入实际的c函数入口点,才能在调用具体nif函数时正确找到对应的c函数 */

code_ptr[5+2] = (BeamInstr) lib;

}

/* 进行一个patch工作,将原先erlang module中定义的函数替换为NIF描述符包含的同名函数 */

}

至此,加载的主要工作就完成了,其主要任务是通过dlopen加载NIF的共享库文件,然后调用其中定义的函数nif_init,该函数将返回一个NIF描述符,该描述符中若包含一个load函数,则调用该函数进行NIF的一些加载工作,NIF描述符中必须包含NIF向外导出的函数定义数组,每个函数定义是一个ErlNifFunc结构,定义了函数名、参数个数、c函数入口点,根据该数组的定义,需要原先erlang module中的同名函数替换为函数数组中的定义,替换过程为首先找到erlang module中同名函数的函数描述符,然后将其第一条指令替换为op_call_nif,后跟NIF中该函数对于的c函数入口点,以保证在虚拟机执行process_main时,能够正确地将对erlang module的函数的调用导向到对NIF的c函数的调用。

接下来看这个宏的定义:

erl_nif.h

#define ERL_NIF_INIT_DECL(MODNAME) ErlNifEntry* nif_init(void)

#define ERL_NIF_INIT(NAME, FUNCS, LOAD, RELOAD, UPGRADE, UNLOAD) \

ERL_NIF_INIT_PROLOGUE                   \

ERL_NIF_INIT_GLOB                       \

ERL_NIF_INIT_DECL(NAME); \

ERL_NIF_INIT_DECL(NAME) \

{ \

static ErlNifEntry entry = \

{ \

ERL_NIF_MAJOR_VERSION, \

ERL_NIF_MINOR_VERSION, \

#NAME, \

sizeof(FUNCS) / sizeof(*FUNCS), \

FUNCS, \

LOAD, RELOAD, UPGRADE, UNLOAD, \

ERL_NIF_VM_VARIANT \

};                                  \

ERL_NIF_INIT_BODY;                  \

return &entry; \

}                                       \

ERL_NIF_INIT_EPILOGUE

原来,NIF文件中使用ERL_NIF_INIT将定义一个名为nif_init的函数,该函数将会在load_nif_2中加载NIF共享库文件后进行调用,这个函数会填充并返回一个ErlNifEntry结构,来看看它的定义:

typedef struct enif_entry_t

{

int major;

int minor;

const char* name;

int num_of_funcs;

ErlNifFunc* funcs;

int  (*load)   (ErlNifEnv*, void** priv_data, ERL_NIF_TERM load_info);

int  (*reload) (ErlNifEnv*, void** priv_data, ERL_NIF_TERM load_info);

int  (*upgrade)(ErlNifEnv*, void** priv_data, void** old_priv_data, ERL_NIF_TERM load_info);

void (*unload) (ErlNifEnv*, void* priv_data);

const char* vm_variant;

}ErlNifEntry;

nif_init将ErlNifEntry.name填充为宏参数NAME,ErlNifEntry.funcs填充为FUNCS,ErlNifEntry.load填充为LOAD,对于niftest.c,NAME为"niftest",这也是niftest模块的模块名,FUNCS为nif_funcs,LOAD为NULL。

ErlNifEntry.funcs是一个数组,其元素ErlNifFunc的定义如下:

typedef struct

{

const char* name;

unsigned arity;

ERL_NIF_TERM (*fptr)(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]);

}ErlNifFunc;

对于niftest.c,仅包含一个函数hello的定义,name函数名为"hello",arity参数个数为0,c函数入口点为static ERL_NIF_TERM hello(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[])。

对于每一个用户实现的NIF函数,其原型必须是:

ERL_NIF_TERM (*fptr)(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]);

对于该原型的解释,官方文档上已经很详尽了,此处就不再介绍。

在成功的将erlang module的函数调用替换为对c函数的调用后,再来看一下它在虚拟机中的实际执行过程:

beam_emu.c

void process_main(void)

{

...

OpCase(call_nif):

{

/*

* call_nif is always first instruction in function:

*

* I[-3]: Module

* I[-2]: Function

* I[-1]: Arity

* I[0]: &&call_nif

* I[1]: Function pointer to NIF function

* I[2]: Pointer to erl_module_nif

*/

/* 在这里我们也可以看到之前patch函数时code_ptr数组的全貌了 */

BifFunction vbf;

DTRACE_NIF_ENTRY(c_p, (Eterm)I[-3], (Eterm)I[-2], (Uint)I[-1]);

c_p->current = I-3; /* current and vbf set to please handle_error */

SWAPOUT;

c_p->fcalls = FCALLS - 1;

PROCESS_MAIN_CHK_LOCKS(c_p);

bif_nif_arity = I[-1];

ERTS_SMP_UNREQ_PROC_MAIN_LOCK(c_p);

ERTS_VERIFY_UNUSED_TEMP_ALLOC(c_p);

ASSERT(!ERTS_PROC_IS_EXITING(c_p));

{

typedef Eterm NifF(struct enif_environment_t*, int argc, Eterm argv[]);

NifF* fp = vbf = (NifF*) I[1];

struct enif_environment_t env;

erts_pre_nif(&env, c_p, (struct erl_module_nif*)I[2]);

/* 初始化env,将env附着在当前进程c_p上 */

reg[0] = r(0);

nif_bif_result = (*fp)(&env, bif_nif_arity, reg);

/* 调用具体的c函数,reg为参数数组 */

erts_post_nif(&env);

}

ASSERT(!ERTS_PROC_IS_EXITING(c_p) || is_non_value(nif_bif_result));

PROCESS_MAIN_CHK_LOCKS(c_p);

ERTS_VERIFY_UNUSED_TEMP_ALLOC(c_p);

DTRACE_NIF_RETURN(c_p, (Eterm)I[-3], (Eterm)I[-2], (Uint)I[-1]);

goto apply_bif_or_nif_epilogue;

...

}

NIF函数在执行时,就相当于调用c函数本身一样,稍微特别的地方在于,需要将输入输出参数的类型转换为erlang类型,每次调用时,NIF函数会建立一个上下文环境结构enif_environment_t,它记录了NIF执行时必要的进程上下文,其定义如下:

struct enif_environment_t /* ErlNifEnv */

{

struct erl_module_nif* mod_nif;

Process* proc;

Eterm* hp;

Eterm* hp_end;

ErlHeapFragment* heap_frag;

int fpe_was_unmasked;

struct enif_tmp_obj_t* tmp_obj_list;

};

其中,proc成员即为当前进程,env的主要作用体现在内存分配和进程身份标识上,通过NIF接口分配的内存,实际都是由env附着的进程的堆分配的。

至此,NIF的加载和执行位置已经介绍完毕了,编写NIF主要会涉及到如下类型的NIF库接口:

1.类型系统,由于nif和erlang代码分处在两个世界,因此需要对输入输出参数进行转换,这类接口主要包括get系列和make系列函数,包括元组、列表、整型、字符串、binary等;

2.内存管理,类型系统的make系列函数已经包含了对特定erlang类型的数据结构的内存分配,但对于c类型的数据结构,还需要一些通用的接口,enif_alloc/enif_free等;

3.消息发送,NIF只能附着在一个进程上执行,因此没有消息接收的能力,只能进行消息发送,使用enif_send接口进行消息发送;

4.持久资源,这是NIF的一大特色,可以创建一个持久资源描述符,跨进程传递数据,资源可以是任意的数据结构;

5.条件变量、信号量、读写锁,这些功能是通过driver的同类操作实现的;

6.操作系统线程及线程私有资源,这个功能也是通过driver的同类操作实现的;

7.系统信息,这个功能也是通过driver的同类操作实现的。

稍侯将介绍nif的几类接口的实现。

erlang NIF部分接口实现(一)加载过程及编写框架相关推荐

  1. 前端资源(css,js,图片,接口等)加载过程

    查看前端各资源css,js,图片,接口等加载速度 前言 查看前端各资源加载速度 加载过程中各指标详解 前言 网页打开的速度快慢直接影响了用户体验.据统计,Google网站访问速度每慢400ms就导致用 ...

  2. java虚拟机学习(四)类的加载过程

    2019独角兽企业重金招聘Python工程师标准>>> 类从虚拟机内存加载到从内存卸载,经历的生命周期是:加载,验证,准备,解析,初始化,使用,卸载这几个阶段, 其中验证,解析,初始 ...

  3. Android开发之通过接口回调机制加载数据(源代码分享)

    Android开发之通过接口回调机制加载数据的简单实现,在实际开发中通过callback方法得到网络加载的数据的使用频率远比通过直接开启线程或异步任务加载数据的频率高的多,这篇文章的代码将简单实现该机 ...

  4. erlang NIF部分接口实现(四)消息发送

    erlang中不能没有消息和异步过程,NIF也必须有此项能力,这个能力是通过enif_send实现的,它可以在NIF中向一个进程发送消息,但由于消息本身需要跨进程传递,消息的生命周期可能很长,而在er ...

  5. Java虚拟机中 类的加载过程

    Java中 类的加载过程 例如下面的一段简单的代码 public class HelloWorld {public static void main(String[] args) {System.ou ...

  6. ClassLoader(二)- 加载过程

    本文源代码在Github. 本文仅为个人笔记,不应作为权威参考. 原文 在前一篇文章初步了解ClassLoader里提到了委托模型(又称双亲委派模型),解释了ClassLoader hierarchy ...

  7. 面试官:讲讲Spring框架Bean的加载过程

    spring作为目前我们开发的基础框架,每天的开发工作基本和他形影不离,作为管理bean的最经典.优秀的框架,它的复杂程度往往令人望而却步. 不过作为朝夕相处的框架,我们必须得明白一个问题就是spri ...

  8. Spring component-scan类扫描加载过程

    2019独角兽企业重金招聘Python工程师标准>>> https://github.com/javahongxi 有朋友最近问到了spring加载类的过程,尤其是基于annotat ...

  9. 重温.NET下Assembly的加载过程 ASP.NET Core Web API下事件驱动型架构的实现(三):基于RabbitMQ的事件总线...

    重温.NET下Assembly的加载过程 最近在工作中牵涉到了.NET下的一个古老的问题:Assembly的加载过程.虽然网上有很多文章介绍这部分内容,很多文章也是很久以前就已经出现了,但阅读之后发现 ...

最新文章

  1. HDU5017(模拟退火算法)
  2. 网页瀑布流效果实现的几种方式
  3. 虚拟机与系统文件互传(VMware Tools)的使用
  4. 高性能网站架构设计之缓存篇(6)- Redis 集群(中)
  5. WDCP管理面板安装启动EXIF、bcmath完整步骤
  6. java亮剑_黄金矿工3-太空版
  7. 前端学习(3061):vue+element今日头条管理-接口分页参数说明
  8. mysql录数据总是错误_MySQL数据库出错
  9. uboot源码——汇编阶段的start.S文件
  10. DTS-SHOP微信小程序多店铺商城平台java源码
  11. 如何使用mock应对测试所需随机数据
  12. C语言排序方法-----二分插入排序
  13. 用SIR模型处理新冠疫情
  14. java计算机毕业设计疫情期间医院挂号管理系统源码+数据库+lw文档+系统+部署
  15. 为开发者们准备的10款超棒的jQuery视频插件
  16. 传染病模型SIS及相应的matlab代码
  17. 微星如何于BIOS中开启/关闭AMD虚拟化技术
  18. MyEclipse 使用教程
  19. yy直播接口php,api.php · yyboss/phpcms - Gitee.com
  20. 正样本/反(负)样本/易区分样本/难区分样本

热门文章

  1. BZOJ 1925 地精部落 DP
  2. 迅雷、快车、QQ旋风下载链接解析
  3. 计算机毕设选题推荐基于SSM咨询交流网
  4. pve添加新硬盘---U盘安装Proxmox VE(二)
  5. 8090的你该知道的......很长,但看完后,你会成长
  6. java 对证书文件以及秘钥.key的解析
  7. vue v-model赋值立即生效问题 set get
  8. Mac Pro下安装Homebrew教程来啦~
  9. redis sentinel(哨兵)
  10. Rodrigues formula