最近在项目中频繁用到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生态系统中,Native Implemented Functions (NIFs) 是一种机制,允许开发者用其他语言(如C、C++或Rust)编写性能关键部分的代码,然后在Erlang虚拟机(VM)中调用。Rustler是一个库,专门用于简化使用Rust...
Erlang NIF(Native Implemented Functions)是Erlang虚拟机提供的一种机制,允许开发者用C语言或者其他低级语言编写性能关键部分的代码,并在Erlang系统中无缝调用。这种方式可以充分利用C语言的高效性,同时保持...
在Erlang中,`NIF (Native Implemented Functions)` 是一种机制,允许开发者使用其他语言(如C或Rust)编写高效性能的代码,并在Erlang虚拟机中调用。本篇主要介绍如何利用Rust语言来开发Erlang NIF,并探讨`erl_nif...
在Erlang中,为了实现与C或其他低级语言的高效交互,Erlang提供了一个名为`erl_nif`的接口。本文将深入探讨`erl_nif`,了解它是如何扩展Erlang的功能,并讨论如何使用它来提升性能。 `erl_nif`(Erlang NIF,Native...
示例Rustler是一个用于以安全的Rust代码编写Erlang NIF的库。 这意味着应该没有任何办法。 入门指南 示例Rustler是一个用于以安全的Rust代码编写Erlang NIF的库。 这意味着应该没有办法使BEAM(Erlang VM)崩溃。 该...
Windows下使用NIF扩展Erlang完整例子,包含nif工程项目,erlang引用例子。 配套文章:http://blog.csdn.net/mycwq/article/details/17527485
Erlang与C语言接口主要通过两种方式实现:一种是将C语言编写的代码直接嵌入到Erlang程序中;另一种是通过进程间通信(IPC)的方式让Erlang与C语言程序进行交互。Erlang倾向于采用第二种方式,即进程间通信,来与C语言...
Rust 中的 Erlang NIF 这是一个如何在 Rust 中实现 NIF 的示例。 它对我有用,也可能对你有用,但如果它吃掉了你的作业,请不要生气。 虽然这将是可行的写现实世界的代码下面这个例子,因为整个erl_nif.h接口可用...
ErlPuzzle - Erlang 的 libpuzzle NIF Puzzle 库旨在快速找到视觉上相似的图像(gif、png、jpg),即使它们已被调整大小、重新压缩、重新着色或稍微修改。 该库是免费的、轻量级的但非常快速、可配置、易于使用,...
盗贼文档|入门|例子Rustler 是一个用安全的 Rust 代码编写 Erlang NIF 的库。这意味着应该没有办法让 BEAM (Erlang VM) 崩溃。该库提供了用于生成与 BEAM 交互的样板的工具,处理 Erlang 术语的编码和解码,并在它们...
**Erlang NIF(Native Implemented Functions) 是Erlang编程语言中的一种机制,它允许开发者集成用其他语言(如C、C++等)编写的代码,以提高性能或利用Erlang VM(虚拟机)不支持的功能。NIFs在Erlang中的主要目的是...
Erlang框架是一种基于Erlang编程语言的全功能Web框架,主要设计用于构建高度并发、可扩展且容错性强的Web应用。Erlang语言以其独特的并发模型和分布式计算能力而闻名,使得Erlang框架在处理大量并发连接和实时系统...
在Erlang NIF中实现的SHA-224,SHA-256,SHA-384,SHA-512。描述erlsha2库应用程序使用Erlang NIF实施SHA-2安全哈希标准(SHA-224,SHA-256,SHA-384,SHA-512)。 (它也提供纯的Erlang实现,尽管它们比C NIF实现慢...
NIF是Erlang虚拟机(VM)提供的一种机制,使得Erlang代码能够调用C或C++编写的库,从而利用这些语言的高性能特性。Rustler的出现,将这种能力扩展到了Rust,为开发者提供了更多优势。 Rust是一种系统级编程语言,以...
Erlang NIF是一种机制,允许Erlang虚拟机(VM)调用用其他语言(如C或C++)编写的原生代码,从而提高性能并利用已有的库。 **Google Protobuf** 是一种广泛使用的数据序列化协议,它定义了一种紧凑、高效的二进制...
Erlang是一种面向并发的、动态类型的编程语言,尤其适合构建高可用性和容错性的分布式系统...总之,Erlang的timer模块是其强大并发能力的一个体现,通过熟练掌握这一部分,开发者可以构建出更加高效和可靠的实时系统。
这是第一卷。 在2008 CN Erlounge III的“Erlang应用程序接口”讲演的视频。PPT等其它资料在这里: http://blog.csdn.net/aimingoo/archive/2009/01/14/3777765.aspx 有关信息参见: ...
该库以nif库的形式实现,可以最快地访问sqlite数据库。 这可能是有风险的,因为nif库或sqlite数据库中的错误可能会使整个Erlang VM崩溃。 如果您不想冒险,总是可以从单独的erlang节点访问sqlite nif。 特别注意...
在Erlang社区,为了利用protobuf的优势,出现了`erl_protobuffs`,这是一个针对Erlang实现的protobuf库。 **erl_protobuffs的优势** 1. **性能提升**:`erl_protobuffs`经过优化,相比其他Erlang的protobuf实现,...