内容简介:Erlang支持一种方式,就是用C来实现函数,并在Erlang中透明地使用它们。这些函数被叫做NIFs(原生实现的函数)。在两种场景下,NIF被证明是完美的解决方案:当你需要原始的计算速度时;当你需要从Erlang调用已有的C接口时。在本文中,我们一起来看看这两种场景。请注意,如果我们想要使用C程序(也就是我们想要与现有的C程序进行交互),那么NIF并不是我们唯一的选择。Erlang有其他方式处理外部函数接口来与其他语言交互。其中之一就是Port;如果你想深入了解,可以阅读 Sasa Juric 写的非常
Erlang支持一种方式,就是用C来实现函数,并在Erlang中透明地使用它们。这些函数被叫做NIFs(原生实现的函数)。在两种场景下,NIF被证明是完美的解决方案:当你需要原始的计算速度时;当你需要从Erlang调用已有的C接口时。在本文中,我们一起来看看这两种场景。
请注意,如果我们想要使用C程序(也就是我们想要与现有的C程序进行交互),那么NIF并不是我们唯一的选择。Erlang有其他方式处理外部函数接口来与其他语言交互。其中之一就是Port;如果你想深入了解,可以阅读 Sasa Juric 写的非常棒的 文章 。
我们将全面了解NIF。首先,我们将看看如何写简单的NIF来执行算术计算;接着,我们看看如何在Elixir里使用这些NIF。然后,我们了解如何从NIF里访问已有的C程序。最后,我们学习如何将C的编译合并到我们Elixir代码编译中。
我在本文中所讲的绝大部分内容都可以在Erlang官方文档中 erl_nif C 库 中阅读到更多细节。
本文中讨论的内容适用于Erlang和Elixir,只需进行最小限度的调整。 我会在Elixir中展示我所有的例子,但我会随时提及Erlang和Elixir两者。
严肃的NIF警告
NIF是危险的。我猜你肯定听说过Erlang(Elixir)如何可靠、容错,进程是如何隔离的,一个进程内部的崩溃只会影响到它自己,以及其他的很棒的特性。当你开始玩NIF的时候,你可以和所有Erlang好的东西说再见。一个NIF里的崩溃(比如可怕的段错误)将会使得整个Erlang虚拟机崩溃。没有监督者来恢复,没有容错,没有隔离。这意味着当你使用NIF的时候需要极度的小心谨慎,并且你应该总是确信你有一个好的理由来使用它。
另一个值得注意的是NIF不会被Erlang的调度器抢占:一个NIF做为一个单独的计算单元,它不会被中断。这意味着你的NIF应该尽可能地快; 正如Erlang的NIF文档所建议的那样,一个很好的经验法则是将NIF保持在毫秒级的执行时间内。查看Erlang文档,了解当你的NIF需要更多时间完成时应采取的措施。
基础知识
NIF的工作方式简单:你写一个C文件,然后在一些Erlang提供的设施下导出一些函数,然后编译这个文件。接着,你定义一个Erlang/Elixir文件,在里面调用 :erlang.load_nif/2 。这个函数将把C文件里的所有NIF定义为调用模块里的函数。
在实践中更容易明白这一点。
让我们从容易的开始:写一个没有副作用的NIF,它有一个入参和一个返回值。为完成这个例子,我们写一个 fast_compare 函数,它有两个整数入参,然后比较它们,如果相等就返回0,如果第一个比第二个小,就返回-1,否则就返回1。
定义一个NIF
我们开始写 fast_compare.c 文件。首先我们必须包含 erl_nif.h 这个头文件,它包含了使用NIF的时候,我们需要的所有东西(类型、函数、宏)。
#include"erl_nif.h"
C编译器并不知道erl_nif.h在哪里,因此,当稍后我们编译我们的程序的时候,必须指出它的所在。
现在,定义NIF的所有C文件有相似的结构:C函数列表,接着是被导出到Erlang/Elixir的C函数列表(以及它们的名字),最后,调用 ERL_NIF_INIT 宏,它执行把所有这个一切串联起来的神秘的事情。
在我们的例子里,C函数列表就只有 fast_compare 函数。这个函数的签名如下所示:
static ERL_NIF_TERM fast_compare(ErlNifEnv *env,int argc, const ERL_NIF_TERM argv[]) { // cool stuff here }
这里有两个NIF的特定类型:ERL_NIF_TERM 和 ErlNifEnv。
ERL_NIF_TERM 是一个“包装”类型,它表示在C里所有的Erlang数据类型(像binary,list,tuple,等等)。我们必须使用 erl_nif.h 提供的函数来将一个 ERL_NIF_TERM 转换为一个C的值(或者多个C的值),反之亦然。
ErlNifEnv仅仅是NIF被执行所在的Erlang环境,我们绝大多数只是把它在函数中进行传递而不必实际对它进行操作。
我们来看看 fast_compare 的参数(所有的NIF参数都如此):
- env 如上所述,仅是NIF被执行所在的Erlang环境,我们不比太关心它。
- argc 当从Erlang调用NIF的时候,传递给它的参数个数。后面我们将详述。
- argv 传递给NIF的参数的数组。
读取Erlang/Elixir类型的值为C类型的值
我们从Elixir调用 fast_compare ,如下:
fast_compare(99, 100) #=> -1
当执行 fast_compare ,argc为2,argv 是 99 和 100 组成的数组。然而,这些参数的类型是 ERL_NIF_TERM ,因此我们必须将它们“转换”为C的数据类型才能操作它们。erl_nif.h 提供了函数将Erlang的数据类型转换为C的数据类型。在本例子里,我们需要用enif_get_int这个函数。enif_get_int的签名如下:
int enif_get_int(ErlNifEnv *env, ERL_NIF_TERM term,int *ip);
我们必须传入变量 env,我们需要转换的Erlang数据(从argv中取出的),以及转换得到的值所存储的地址。
将C类型的值转换为Erlang类型的值
erl_nif.h 提供了几个 enif_make_* 类似的函数来将C类型的值转换为Erlang类型的值。它们都有相似的签名(只是根据被转换的数据类型不同而有差别),并且它们都返回 ERL_NIF_TERM类型的值。在本例子中,我们需要 enif_make_int 函数,它的签名如下:
ERL_NIF_TERMenif_make_int(ErlNifEnv *env,int i);
编写NIF
我们已经知道如何在Erlang类型数据和C类型数据之间进行转换,那么编写NIF就很直观了。
static ERL_NIF_TERM fast_compare(ErlNifEnv *env,int argc, const ERL_NIF_TERM argv[]) { int a, b; // Fill a and b with the values of the first two args enif_get_int(env, argv[0], &a); enif_get_int(env, argv[1], &b); // Usual C unreadable code because this way is more true int result = a == b ? 0 : (a > b ? 1 : -1); return enif_make_int(env, result); }
连接我们的C
我们现在必须将我们写的函数导出到Erlang。我们必须使用ERL_NIF_INIT这个宏。
ERL_NIF_INIT(erl_module, functions, load, upgrade, unload, reload)
- erl_module 是Erlang模块,我们导出的函数将被定义在里面。它不需要被双引号括起来,因为它将被ERL_NIF_INIT字符串化(例如,用my_module而不是用”my_module”);
- functions 是ErlNifFunc结构类型数据的数组,它定义哪些NIF被导出,以及对应的Erlang函数和它的参数个数;
- load,upgrade,unload,reload是函数指针,它们指向那些NIF被装载卸载等操作的回调函数;我们现在不太关心这些回调函数,把它们全部设置为NULL。
我们所需的所有元素都准备好了。完整的C文件如下所示:
#include"erl_nif.h" static ERL_NIF_TERM fast_compare(ErlNifEnv *env,int argc, const ERL_NIF_TERM argv[]) { int a, b; enif_get_int(env, argv[0], &a); enif_get_int(env, argv[1], &b); int result = a == b ? 0 : (a > b ? 1 : -1); return enif_make_int(env, result); } // Let's define the array of ErlNifFunc beforehand: static ErlNifFunc nif_funcs[] = { // {erl_function_name, erl_function_arity, c_function} {"fast_compare", 2, fast_compare} }; ERL_NIF_INIT(Elixir.FastCompare, nif_funcs, NULL, NULL, NULL, NULL)
要记得我们必须要在ERL_NIF_INIT宏里使用Elixir模块的全名的原子(是Elixir.FastCompare而不是FastCompare)。
编译我们的C代码
NIF文件应该编译为 .so 共享库。编译标志在不同的系统和编译器中有所不同,但它们应该看起来像这样:
$ cc -fPIC -I$(ERL_INCLUDE_PATH) \ -dynamiclib -undefined dynamic_lookup \ -o fast_compare.so fast_compare.c
使用这个命令,我们用一些为了生产动态代码的标志把 fast_compare.c 编译为 fast_compare.so (-o fast_compare.so)。注意我们如何把 $(ERL_INCLUDE_PATH) 包含在搜索路径里:这个路径包含了erl_nif.h 头文件。这个路径通常在Erlang的安装目录里,即 lib/erts-VERSION/include。
在Elixir中装载NIF
剩下的事情是装载在Elixir模块 FastCompare 里定义的NIF。如Erlang中关于NIF的文档所建议,钩子 @on_load 是做这件事的最适合的地方。
请注意,对于我们要定义的每个NIF,我们也需要在加载模块中定义相应的Erlang / Elixir函数。 这可以被利用来在NIF不可用的情况下定义例如回退代码。
# fast_compare.ex defmodule FastCompare do @on_load :load_nifs def load_nifs do :erlang.load_nif('./fast_compare', 0) end def fast_compare(_a, _b) do raise "NIF fast_compare/2 not implemented" end end
:erlang.load_nif/2 的第二个参数可以是任何东西,它会被传递给我们上面提到的load回调函数。 你可以看看 erlang.load_nif/2 的文档以获取更多信息。
搞定!我们可以在IEx里测试一下我们的模块:
iex> c "fast_compare.ex" iex> FastCompare.fast_compare(99, 100) -1
其他例子
写“纯”的NIF(没有副作用,只是转换)非常有用。我非常喜欢的一个例子是 devinus/markdown 这个Elixir库:这个库用NIF封装了一个C的markdown解析器。这个用例是完美的,因为将Markdown转换为HTML可能是一项昂贵的任务,而通过将该工作委托给C来做可以获得更好的性能。
有用的东西:资源
正如我上面提到的,NIF的一个非常有用的地方是包装已有的C库。然而,这些库常常提供它们自己的数据抽象和数据结构。例如,一个C的数据库驱动导出一个 db_conn_t 类型来表示一条数据库链接,其定义如下:
typedef struct { // fields } db_conn_t;
相应的函数初始化链接、发起查询、释放链接,如下所示:
db_conn_t *db_init_conn(); db_typedb_query(db_conn_t *conn, const char *query); void db_free_conn(db_conn_t *conn);
如果我们能够在Erlang/Elixir里处理db_conn_t数据类型并且在NIF调用之间传递它们的话,这将非常有用。NIF的API有一个叫做 resources 的概念。没有比用Erlang的官方文档更好的方式来快速解释什么是 resources 。
资源对象的使用是一种从NIF返回指向原生数据结构的指针的安全方法。 资源对象只是一块内存。 […].
资源是内存块,我们可以构建并返回指向该内存的安全指针作为Erlang的类型数据。
让我们来探讨一下如何在NIF内部包装上面简单的API。 我们将从这个骨架C文件开始:
#include"db.h" #include"erl_nif.h" typedef struct { // fields here } db_conn_t; db_conn_t *db_init_conn(); db_typedb_query(db_conn_t *conn, const char *query); void db_free_conn(db_conn_t *conn);
资源的创建
要创建一个资源,我们必须要使用 enif_alloc_resource 函数的帮助来分配一些内存。从这个函数的签名你可以看出它和 malloc 函数相似(原则上):
void *enif_alloc_resource(ErlNifResourceType *res_type,unsigned size);
enif_alloc_resource 的第一个参数是一个资源类型(这只是我们用来区分不同类型资源的东西),第二个参数是需要分配的内存大小,返回值是已分配内存的指针。
资源类型
资源类型是用 enif_open_resource_type 函数来创建的。我们可以在我们的C文件里声明资源类型作为全局变量。同时,利用传递给 ERL_NIF_INIT的load回调函数的便利性来创建资源类型并且把它们赋值给全局变量。如下所示:
ErlNifResourceType *DB_RES_TYPE; // 每次当一个资源被释放的时候这个函数被调用 // 资源被释放在enif_release_resource函数被调用 // 以及Erlang回收内存的时候发生 void db_res_destructor(ErlNifEnv *env,void *res) { db_free_conn((db_conn_t *) res); } int load(ErlNifEnv *env,void **priv_data, ERL_NIF_TERM load_info) { int flags = ERL_NIF_RT_CREATE | ERL_NIF_RT_TAKEOVER; DB_RES_TYPE = enif_open_resource_type(env, NULL, "db", db_res_destructor, flags, NULL); }
创建资源
我们现在可以包装db_init_conn并且创建我们的资源。
static ERL_NIF_TERM db_init_conn_nif(ErlNifEnv *env,int argc, const ERL_NIF_TERM argv[]) { // 让我们给一个 db_conn_t 指针分配内存 db_conn_t **conn_res = enif_alloc_memory(DB_RES_TYPE, sizeof(db_conn_t *)); // 让我们创建一条链接并且把它拷贝到指针所指的内存 db_conn_t *conn = db_init_conn(); memcpy((void *) conn_res, (void *) &conn, sizeof(db_conn_t *)); // 我们现在可以让Erlang类型数据持有这个资源... ERL_NIF_TERM term = enif_make_resource(env, conn_res); // ...然后释放这个资源以便在Erlang垃圾回收的时候它将被释放 enif_release_resource(conn_res); return term; }
获取资源
为了包装db_query,我们需要获取 db_init_conn_nif 返回的资源。为实现这个功能,我们需要使用 enif_get_resource 函数。
static ERL_NIF_TERM db_query_nif(ErlNifEnv *env,int argc, const ERL_NIF_TERM argv[]) { db_conn_t **conn_res; enif_get_resource(env, argv[0], DB_RES_TYPE, (void *) conn_res); db_conn_t *conn = *conn_res; // 我们现在可以运行我们的查询 db_query(conn, ...); return argv[0]; }
在Elixir里使用资源
让我们跳过在DB模块里导出我们创建的NIF,直接跳到IEx shell环节,而且假设C代码已经编译并被DB模块装载进虚拟机了。正如我前面所述,当资源返回给Erlang/Elixir的时候完全是一个不透明的数据。它们表现得就像是空的的二进制数据。
iex> conn_res = DB.db_conn_init() "" iex> DB.db_query(conn_res, ...) ...
因为资源是不透明的数据,除了将它们回传给其他NIF,你无法在Erlang/Elixir里对它进行任何有意义的处理。它们的行为和看起来像二进制数据,这甚至可以导致问题,因为他们可以被误认为二进制数据。基于这个缘故,我建议将资源包装到结构当中。这样我们可以限制我们的公共API仅能处理结构并且在内部处理资源。我们也可以通过实现结构的 Inspect 协议来获得好处,这种方式使得我们可以安全地检测资源,而隐藏了它们看起来像是二进制数据的事实。
defmodule DBConn do defstruct [:resource] defimpl Inspect do # ... end end
用Mix编译
Mix提供了一个特性,叫做 Mix 编译器 。每一个Mix项目在编译的时候可以指定一个编译器列表来运行。新的Mix编译器是自动编译C源代码的完美场所。对于本节的范围,假设我们正在构建一个叫做 :my_nifs 的Elixir应用程序,该应用程序将使用my_nifs.c C源文件中的NIF。
首先我们创建 Makefile 来编译C源码(反正我们可能会这么做)。
ERL_INCLUDE_PATH=$(...) all: priv/my_nifs.so priv/my_nifs.so: my_nifs.c cc -fPIC -I$(ERL_INCLUDE_PATH) -dynamiclib -undefined dynamic_lookup -o my_nifs.so my_nifs.c
这个 Makefile 文件假设 my_nifs.c 存储在我们的Mix项目的根目录。我们将把 .so 共享库存放在我们应用的 priv 目录中,以便在发布的时候它是可用的。现在,无论何时,只要我们修改了 my_nifs.c ,然后运行 make ,priv/my_nifs.so 都会被重新编译。
我们现在可以挂接一个只调用make的新的Mix编译器。在 mix.exs 中,我们来实现之:
defmodule Mix.Tasks.Compile.MyNifs do def run(_args) do {result, _errcode} = System.cmd("make", [], stdout_to_stderr: true) IO.binwrite(result) end end
我们调用 IO.binwrite/1 来将 make 的运行结果输出到终端。在一个真实的场景里,我们肯定要检查 make 的结果,同时也要确认 cc 和 make 已经安装到系统里,并且其路径可用;不过在这里,我们简单地忽略了这些步骤。
我们现在需要将 :my_nifs 编译器添加到 :my_nifs 应用的编译器列表。
# in mix.exs defmodule MyNifs.Mixfile do use Mix.Project def project do [app: :my_nifs, compilers: [:my_nifs] ++ Mix.compilers, ...] end end
现在,任何时候我们运行 $ mix compiler ,我们的C代码就被自动重新编译(如果需要)。当其他库把 :my_nifs 作为依赖的话,这个过程也会一样地执行,因为现在运行 make 是 :my_nifs 项目编译的一个过程。
结论
这是一个很长的帖子,但我希望我覆盖了NIF的大部分内容。如你所见,在Erlang/Elixir里使用NIF是相当方便的。正如本文开头提到的那样,由于NIF的脆弱性(记住NIF可能导致整个Erlang虚拟机崩溃)和速度要求,因此要谨慎使用NIF,而且它并不总是正确的工具。
感谢您的阅读!
原文链接: http://andrealeopardi.com/posts/using-c-from-elixir-with-nifs/
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:- Tor的高级使用方式
- 使用 AJP 方式配置反向代理
- LiveData基础使用方式+工作原理(上篇)
- 使用 C# 代码创建快捷方式文件
- 使用 Golang Timer 的正确方式
- 用 gopher 的方式使用 panic
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。