1. Erl_Interface User's Guide
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_rel
和erl_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.a
与libei.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.a
和libsocket.a
Solaris上,或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);
在这种情况下,您可以独立地释放这两个术语。你释放条款的顺序ep
和ep2
并不重要,因为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的内容。
*pattern,*term; pattern = erl_format("{madonna,Age,Age}"); term = erl_format("{madonna,21,21}"); if (erl_match(pattern, term)) { fprintf(stderr, "Yes, they matched: Age = "); ep = erl_var_content(pattern, "Age"); erl_print_term(stderr, ep); fprintf(stderr,"\n"); erl_free_term(ep); } erl_free_term(pattern); erl_free_term(term);
有关更多信息,请参阅erl_format:erl_match函数。1.7连接到分布式Erlang节点要连接到分布式Erlang节点,您必须首先使用erl_connect:erl_connect_init初始化连接例程,erl_connect_init存储信息,例如主机名,节点名称和 IP地址供以后使用:
int identification_number = 99; int creation=1; char *cookie="a secret cookie string"; /* An example */ erl_connect_init(identification_number, cookie, creation); For more information, see the erl_connect
module.After initialization, you set up the connection to the Erlang node. To specify the Erlang node you want to connect to, use erl_connect()
. The following example sets up the connection and is to result in a valid socket file descriptor:int sockfd; char *nodename="xyz@chivas.du.etx.ericsson.se"; /* An example */ if ((sockfd = erl_connect(nodename)) < 0) erl_err_quit("ERROR: erl_connect failed"); erl_err_quit()
prints the specified string and terminates the program.
有关更多信息,请参阅erl_error模块。1.8使用EPMDerts:epmd是Erlang端口映射器守护进程。分布式Erlang节点向本地主机上的epmd注册,以向其他节点指示它们存在并且可以接受连接。 epmd维护一个节点和端口号信息的寄存器,当一个节点希望连接到另一个节点时,它首先联系epmd以找到连接的正确端口号。当你使用erl_connect连接到一个Erlang节点时,连接是首先是epmd,如果节点已知,则连接到Erlang节点。如果他们希望系统中的其他节点能够找到并连接到它们,C节点也可以使用epmd注册自己。注册之前使用epmd时,您必须先创建一个侦听套接字并将其绑定到端口。然后:int pub;
pub = erl_publish(port); pub是一个现在连接到epmd的文件描述符。 epmd监视连接的另一端。如果它检测到连接已关闭,则该节点变为未注册。因此,如果明确地关闭描述符或者节点发生故障,它将从epmd注销。注意在某些系统(如VxWorks)中,此机制未检测到故障节点,因为操作系统不会自动关闭描述符当节点失败时它们保持打开状态。如果节点以这种方式失败,那么epmd会阻止您使用旧名称注册新节点,因为它认为旧名称仍在使用中。在这种情况下,您必须显式注销名称:erl_unpublish(node);这会导致epmd关闭远端的连接。请注意,如果名称实际上仍然由节点使用,则此操作的结果是不可预知的。另外,这样做不会导致连接的本地结束,因此可以占用资源.1.9发送和接收Erlang消息使用以下两个函数之一来发送消息:
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_connect
和erl_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
模块已知的所有名称的缓冲区Kernel
。count
被初始化以指示数组中的名称数量。名称中的字符串数组由一个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。
本文档系腾讯云开发者社区成员共同维护,如有问题请联系 cloudcommunity@tencent.com