1. Erl_Interface User's Guide

1 Erl_Interface用户指南

1.1简介

Erl_Interface库包含的功能可以帮助您集成用C和Erlang编写的程序。这些功能Erl_Interface支持以下内容:

  • 表示为Erlang数据类型的数据操作

  • C和Erlang格式之间的数据转换

  • 传输或存储用Erlang数据类型的编解码

  • C节点与Erlang进程之间的通信

  • 备份和恢复C节点状态 Mnesia

默认情况下,这些Erl_Interface库只能保证与库本身的相同版本中的其他Erlang / OTP组件兼容。有关如何与早期版本的Erlang / OTP组件进行通信的信息,请参阅函数ei:ei_set_compat_relerl_eterm:erl_set_compat_rel

范围

在以下部分中,将介绍这些主题:

  • 编译您的代码以供使用 Erl_Interface

  • 初始化Erl_Interface

  • 编码,解码和发送Erlang条款

  • 建立术语和模式

  • 模式匹配

  • 连接到分布式Erlang节点

  • 使用Erlang端口Mapper Daemon%28 EPMD%29

  • 发送和接收Erlang消息

  • 远程过程调用

  • 使用全局名称

  • 使用注册表

先决条件

假定读者熟悉Erlang编程语言。

1.2编译和链接代码

使用任何Erl_Interface函数,在代码中包括以下行:

#include "erl_interface.h" #include "ei.h"

确定OTP安装的顶部目录在哪里。要找到这一点,请启动Erlang并在Eshell提示符下输入以下命令:

Eshell V4.7.4 (abort with ^G) 1> code:root_dir(). /usr/local/otp

要编译代码,请确保C编译器知道在哪里查找erl_interface.h,方法是-I在命令行上指定适当的参数,或将其添加到CFLAGS您的定义中Makefile。这条道路的正确值是$OTPROOT/lib/erl_interface-$EIVSN/include,其中:

  • $OTPROOT是上code:root_dir/0例中报告的路径。

  • $ EIVSN是Erl_Interface应用程序的版本,例如,erl_interface-3.2.3。

汇编守则:

$ cc -c -I/usr/local/otp/lib/erl_interface-3.2.3/include myprog.c

连接时:

  • 指定的路径liberl_interface.alibei.a-L$OTPROOT/lib/erl_interface-3.2.3/lib

  • 指定库的名称-lerl_interface -lei...

在命令行上执行此操作,或将标志添加到LDFLAGS定义Makefile...

链接代码:

$ ld -L/usr/local/otp/lib/erl_interface-3.2.3/ lib myprog.o -lerl_interface -lei -o myprog

在某些系统上,可能有必要与一些更多的库链接(例如,libnsl.alibsocket.aSolaris上,或wsock32.lib在Windows上)使用的通讯设施Erl_Interface

如果在基于POSIX线程或Solaris线程的线程化应用程序中使用Erl_Interface函数,则Erl_Interface需要访问线程包中的某些同步工具。 您必须指定额外的编译器标志来指示您使用哪个软件包。 定义_REENTRANT和STHREADS或PTHREADS。 如果指定了_REENTRANT,则默认使用POSIX线程。

1.3初始化Erl_Interface库

在调用任何其他Erl_Interface函数之前,erl_init()只需调用一次即可初始化库。erl_init()有两个参数。但是,参数不再被使用,Erl_Interface因此被指定为erl_init(NULL,0)

1.4编码,解码和发送Erlang术语

在分布式Erlang节点之间发送的数据以Erlang外部格式编码。因此,如果要使用分发协议在C程序和Erlang之间进行通信,则必须将Erlang术语编码并解码为字节流。

Erl_Interface库支持此活动。它有几个C函数来创建和操作Erlang数据结构。该库还包含一个编码和解码功能。以下示例显示如何创建和编码Erlang元组{tobbe,3928}

ETERM *arr[2], *tuple; char buf[BUFSIZ]; int i; arr[0] = erl_mk_atom("tobbe" arr[1] = erl_mk_integer(3928 tuple = erl_mk_tuple(arr, 2 i = erl_encode(tuple, buf

或者,您可以使用erl_send()erl_receive_msg,它透明地处理消息的编码和解码。

有关完整的说明,请参见以下模块:

  • erl_eterm用于创建Erlang术语

  • erl_marshal用于编解码例程

1.5建立术语和模式

上一个示例可以通过使用erl_format模块创建一个Erlang术语:

ETERM *ep; ep = erl_format("{~a,~i}", "tobbe", 3928

有关不同格式指令的完整说明,请参阅erl_format模块。

以下示例更复杂:

ETERM *ep; ep = erl_format("[{name,~a},{age,~i},{data,~w}]", "madonna", 21, erl_format("[{adr,~s,~i}]", "E-street", 42) erl_free_compound(ep

与前面的示例一样,您有责任释放分配给Erlang术语的内存。在这个例子中,erl_free_compound()确保所指的全部术语ep被释放了。这是必要的,因为从第二次调用到erl_format丢失了。

下面的示例显示了一个略有不同的解决方案:

ETERM *ep,*ep2; ep2 = erl_format("[{adr,~s,~i}]","E-street",42 ep = erl_format("[{name,~a},{age,~i},{data,~w}]", "madonna", 21, ep2 erl_free_term(ep erl_free_term(ep2

在这种情况下,您可以独立地释放这两个术语。你释放条款的顺序epep2并不重要,因为Erl_Interface库使用引用计数来确定何时删除对象是安全的。

如果您不确定是否正确释放了这些术语,可以使用以下函数查看固定术语分配器的状态:

long allocated, freed; erl_eterm_statistics(&allocated,&freed printf("currently allocated blocks: %ld\n",allocated printf("length of freelist: %ld\n",freed /* really free the freelist */ erl_eterm_release(

有关更多信息,请参见erl_malloc模块。

1.6模式匹配

Erlang模式是一个可以包含未绑定变量或"do not care"符号。这样的模式可以与一个项相匹配,如果匹配成功,模式中的任何未绑定变量都将被绑定为一个副作用。然后可以检索绑定变量的内容:

ETERM *pattern; pattern = erl_format("{madonna,Age,_}"

erl_format:erl_match功能执行模式匹配。它需要一个模式和一个术语,并试图匹配它们。作为副作用,模式中的任何未绑定变量都将被绑定。在下面的例子中,一个模式是用一个变量创建的Age,它包含在元组中的两个位置。模式匹配执行如下:

  • erl_match在Age第一次到达变量时将Age的内容绑定到21。

  • 第二次出现Age会导致对术语之间的平等进行测试,这Age已经是必然的了21。如同Age必然的21,平等测试成功并且匹配一直持续到模式结束。

如果到达模式结束,匹配成功,您可以检索变量.ETERM * pattern * term的内容。

  • erl_connect:erl_send

  • erl_connect:erl_reg_send

与Erlang一样,消息可以发送到PID或注册名称。向注册名称发送消息比较容易,因为它避免了查找合适PID的问题。

使用以下两个函数之一接收消息:

  • erl_connect:erl_receive

  • erl_connect:erl_receive_msg

erl_receive()将消息接收到缓冲区,而erl_receive_msg()将信息解码为Erlang术语。

发送消息的示例

在下面的例子中,{Pid, hello_world}被发送到注册过程my_server。该消息由erl_send()编码:

extern const char *erl_thisnodename(void extern short erl_thiscreation(void #define SELF(fd) erl_mk_pid(erl_thisnodename(),fd,0,erl_thiscreation()) ETERM *arr[2], *emsg; int sockfd, creation=1; arr[0] = SELF(sockfd arr[1] = erl_mk_atom("Hello world" emsg = erl_mk_tuple(arr, 2 erl_reg_send(sockfd, "my_server", emsg erl_free_term(emsg

发送的元组的第一个元素是你自己的pid。这使my_server答复。有关基元的更多信息,请参阅erl_connect模块。

接收消息的示例

在这个例子中,{Pid, Something}收到。接收到的pid然后用于返回{goodbye,Pid}

ETERM *arr[2], *answer; int sockfd,rc; char buf[BUFSIZE]; ErlMessage emsg; if ((rc = erl_receive_msg(sockfd , buf, BUFSIZE, &emsg)) == ERL_MSG) { arr[0] = erl_mk_atom("goodbye" arr[1] = erl_element(1, emsg.msg answer = erl_mk_tuple(arr, 2 erl_send(sockfd, arr[1], answer erl_free_term(answer erl_free_term(emsg.msg erl_free_term(emsg.to }

为了提供健壮性,分布式Erlang节点偶尔轮询其所有连接的邻居,试图检测失败的节点或通信链路。接收这样一条消息的节点将立即使用ERL_TICK留言。这是由以下人员自动完成的erl_receive()然而,当这种情况发生时,erl_receive回报ERL_TICK,而不将消息存储到ErlMessage结构。

收到消息时,根据收到的消息类型,呼叫者有责任释放收到的消息emsg.msg和emsg.to或emsg.from。

有关更多信息,请参见erl_connecterl_eterm模块。

1.10远程过程调用

充当客户端到另一个Erlang节点的Erlang节点通常发送请求并等待答复。这种请求包含在远程节点的函数调用中,并称为远程过程调用。

下面的示例显示了Erl_Interface库支持远程过程调用:

char modname[]=THE_MODNAME; ETERM *reply,*ep; ep = erl_format("[~a,[]]", modname if (!(reply = erl_rpc(fd, "c", "c", ep))) erl_err_msg("<ERROR> when compiling file: %s.erl !\n", modname erl_free_term(ep ep = erl_format("{ok,_}" if (!erl_match(ep, reply)) erl_err_msg("<ERROR> compiler errors !\n" erl_free_term(ep erl_free_term(reply

c:c/1调用以在远程节点上编译指定的模块。erl_match()通过测试预期的ok...

有关erl_rpc()及其伙伴erl_rpc_to()和erl_rpc_from()的更多信息,请参阅erl_connect模块。

1.11使用全局名称

C节点可以访问通过global内核中的模块。可以查找名称,允许C节点向命名的Erlang服务发送消息。C节点还可以注册全局名称,允许它们向Erlang进程或其他C节点提供命名服务。

Erl_Interface不提供全局服务的本机实现。相反,它使用“附近”Erlang节点提供的全局服务。要使用本节中描述的服务,必须首先打开到Erlang节点的连接。

看看有什么名称:

char **names; int count; int i; names = erl_global_names(fd,&count if (names) for (i=0; i<count; i++) printf("%s\n",names[i] free(names

erl_global:erl_global_names分配并返回一个包含global模块已知的所有名称的缓冲区Kernelcount被初始化以指示数组中的名称数量。名称中的字符串数组由一个NULL指针终止,因此不需要使用count来确定何时到达姓。

呼叫者有责任释放阵列。erl_global_names使用一次调用来分配数组和所有字符串malloc()free(names)所有这一切都是必需的。

查找其中一个名称:

ETERM *pid; char node[256]; pid = erl_global_whereis(fd,"schedule",node

如果模块"schedule"已知,则会返回一个可用于将消息发送到计划服务的Erlang pid。此外,初始化为包含服务注册的节点的名称,以便您可以通过简单地将该变量传递给它来建立连接。globalKernelnodeerl_connect

注册名称之前,您应该已经注册了您的端口号epmd。这并非严格必要,但如果您忽视这样做,那么希望与您的服务进行通信的其他节点将无法找到或连接到您的流程。

创建一个PID,Erlang进程可以用来与您的服务进行通信:

ETERM *pid; pid = erl_mk_pid(thisnode,14,0,0 erl_global_register(fd,servicename,pid

注册名称后,使用erl_connect:erl_accept等待传入连接。

记得pid稍后用erl_malloc:erl_free_term

注销名称:

erl_global_unregister(fd,servicename

1.12使用注册表

本节介绍了注册表的使用,这是一种简单的机制,用于在C节点中存储键值对,并且可以Mnesia在Erlang节点上对其进行备份或恢复。有关各个API函数的更多详细信息,请参阅registry模块。

键是字符串,即 - NULL终止字符数组,值是任意对象。虽然整数和浮点数被注册表特别处理,但您可以将任何类型的字符串或二进制对象存储为指针。

要开始,请打开注册表:

ei_reg *reg; reg = ei_reg_open(45

45示例中的数字表示您希望在注册表中存储的近似对象数量。注册表在内部使用带有冲突链接的散列表,因此注册表可以包含的对象数量没有绝对的上限,但是如果性能或内存使用率很重要,那么您应该相应地选择一个数字。注册表可以稍后调整大小。

您可以根据需要打开尽可能多的注册表(如果内存允许)。

对象通过设置和获取函数进行存储和检索。以下示例显示如何存储整数,浮点数,字符串和任意二进制对象:

struct bonk *b = malloc(sizeof(*b) char *name = malloc(7 ei_reg_setival(reg,"age",29 ei_reg_setfval(reg,"height",1.85 strcpy(name,"Martin" ei_reg_setsval(reg,"name",name b->l = 42; b->m = 12; ei_reg_setpval(reg,"jox",b,sizeof(*b)

如果您尝试在注册表中存储对象,并且存在具有相同键的现有对象,则新值将替换旧对象。无论新对象和旧对象是否具有相同类型,都可以完成此操作,例如,可以使用整数替换字符串。如果现有值是字符串或二进制数,则在分配新值之前将其释放。

从注册表中检索存储的值如下:

long i; double f; char *s; struct bonk *b; int size; i = ei_reg_getival(reg,"age" f = ei_reg_getfval(reg,"height" s = ei_reg_getsval(reg,"name" b = ei_reg_getpval(reg,"jox",&size

在所有上面的例子中,对象必须存在,并且对于指定的操作它必须是正确的类型。如果你不知道对象的类型,你可以问:

struct ei_reg_stat buf; ei_reg_stat(reg,"name",&buf

Buf被初始化为包含对象属性。

可以从注册表中删除对象:

ei_reg_delete(reg,"name"

完成注册表后,关闭它以删除所有对象并将内存释放回系统:

ei_reg_close(reg

备份注册表给Mnesia

注册表的内容可以备份到Mnesia“附近的”Erlang节点上。您必须提供到Erlang节点的开放连接(请参阅参考资料erl_connect)。另外,Mnesia在启动备份之前,必须在Erlang节点上运行3.0或更高版本:

ei_reg_dump(fd, reg, "mtab", dumpflags

此示例将注册表的内容备份到指定的Mnesia表中"mtab"。一旦注册表被备份到Mnesia这样的状态,更多备份只会影响自最近一次备份以来已被修改的对象,即已创建,更改或删除的对象。备份操作作为单个原子事务完成,以便执行整个备份或不执行整个备份。

同样,可以从Mnesia表中恢复注册表:

ei_reg_restore(fd, reg, "mtab"

这会将整个内容读"mtab"入指定的注册表中。还原之后,注册表中的所有对象都被标记为未修改,因此稍后的备份仅影响自恢复以来修改过的对象。

请注意,如果您还原到非空注册表,表中的对象将使用相同的密钥覆盖注册表中的对象。此外,还原后,注册表的所有内容都将标记为未修改,包括未被还原操作覆盖的任何已修改对象。这可能不是你的意图。

存储字符串和二进制文件

当字符串或二进制对象存储在注册表中时,遵循一些简单的准则是很重要的。

最重要的是,该对象必须是通过一次调用malloc()(或类似)来创建的,以便稍后可以通过一次调用来删除该对象free()。当注册表关闭时,或者向先前包含字符串或二进制文件的对象分配新值时,注册表将释放对象。

请注意,如果存储与上下文相关的二进制对象(例如,包含指针或打开的文件描述符),如果它们备份到Mnesia表中并在稍后在不同的上下文中恢复,则它们将失去意义。

当您从注册表中检索存储的字符串或二进制值时,注册表会维护一个指向该对象的指针,并传递该指针的副本。您永远不应该释放以这种方式检索的对象,因为当注册表稍后尝试释放它时,会发生运行时错误,可能导致C节点崩溃。

您可以自由修改以这种方式检索的对象的内容。 但是,当您这样做时,注册表不知道您的更改,可能会导致在您下次对注册表内容进行Mnesia备份时错过更改。 如果在使用注册表:ei_reg_markdirty进行任何此类更改之后将对象标记为脏,则可以避免这种情况,或者将适当的标志传递给注册表:ei_reg_dump。