1 Erl_Interface 用户指南
1.1 简介
The Erl_Interface 库包含帮助您集成用 C 和 Erlang 编写的程序的函数。 Erl_Interface 中的函数支持以下内容
- 对表示为 Erlang 数据类型的数据的操作
- 在 C 和 Erlang 格式之间转换数据
- 用于传输或存储的 Erlang 数据类型的编码和解码
- C 节点和 Erlang 进程之间的通信
- 将 C 节点状态备份和还原到 Mnesia 以及从 Mnesia 还原
默认情况下,Erl_Interface 库仅保证与与库本身相同版本的其他 Erlang/OTP 组件兼容。有关如何与早期版本的 Erlang/OTP 组件通信的信息,请参阅函数 ei_set_compat_rel.
范围
在以下部分中,将描述以下主题
- 编译您的代码以供 Erl_Interface 使用
- 初始化 Erl_Interface
- 编码、解码和发送 Erlang 项
- 构建项和模式
- 模式匹配
- 连接到分布式 Erlang 节点
- 使用 Erlang 端口映射守护进程 (EPMD)
- 发送和接收 Erlang 消息
- 远程过程调用
- 使用全局名称
先决条件
假设读者熟悉 Erlang 编程语言。
1.2 编译和链接您的代码
要使用任何 Erl_Interface 函数,请在您的代码中包含以下行
#include "ei.h"
确定 OTP 安装的顶层目录位置。要找到它,请启动 Erlang 并在 Eshell 提示符下输入以下命令
Eshell V4.7.4 (abort with ^G) 1> code:root_dir(). /usr/local/otp
要编译您的代码,请确保您的 C 编译器知道在哪里可以找到 ei.h,方法是在命令行上指定适当的 -I 参数,或者将其添加到 Makefile 中的 CFLAGS 定义中。此路径的正确值为 $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
链接时
- 使用 -L$OTPROOT/lib/erl_interface-3.2.3/lib 指定 libei.a 的路径。
- 使用 -lei 指定库的名称。
在命令行上执行此操作,或将标志添加到 Makefile 中的 LDFLAGS 定义中。
链接代码
$ ld -L/usr/local/otp/lib/erl_interface-3.2.3/ lib myprog.o -lei -o myprog
在某些系统上,可能需要链接更多库(例如,Solaris 上的 libnsl.a 和 libsocket.a,或 Windows 上的 wsock32.lib)才能使用 Erl_Interface 的通信功能。
如果您在基于 POSIX 线程或 Solaris 线程的线程化应用程序中使用 Erl_Interface 函数,则 Erl_Interface 需要访问线程包中的一些同步功能。您必须指定额外的编译器标志来指示您使用哪个包。定义 _REENTRANT 和 STHREADS 或 PTHREADS。默认情况下,如果指定了 _REENTRANT,则使用 POSIX 线程。
1.3 初始化库
在调用库中的任何其他函数之前,请通过调用 ei_init()(仅一次)对其进行初始化。
1.4 编码、解码和发送 Erlang 项
在分布式 Erlang 节点之间发送的数据以 Erlang 外部格式编码。因此,如果您想使用分布式协议在 C 程序和 Erlang 之间进行通信,则必须将 Erlang 项编码和解码为字节流。
The Erl_Interface 库支持此活动。它具有几个 C 函数来创建和操作 Erlang 数据结构。以下示例显示如何创建和编码 Erlang 元组 {tobbe,3928}
ei_x_buff buf; ei_x_new(&buf); int i = 0; ei_x_encode_tuple_header(&buf, 2); ei_x_encode_atom(&buf, "tobbe"); ei_x_encode_long(&buf, 3928);
有关完整描述,请参阅 ei 模块。
1.5 构建项
可以使用 ei_x_format_wo_ver 函数简化前面的示例,以创建一个 Erlang 项
ei_x_buff buf; ei_x_new(&buf); ei_x_format_wo_ver(&buf, "{~a,~i}", "tobbe", 3928);
有关不同格式指令的完整描述,请参阅 ei_x_format_wo_ver 函数。
以下示例更加复杂
ei_x_buff buf; int i = 0; ei_x_new(&buf); ei_x_format_wo_ver(&buf, "[{name,~a},{age,~i},{data,[{adr,~s,~i}]}]", "madonna", 21, "E-street", 42); ei_print_term(stdout, buf.buff, &i); ei_x_free(&buf);
与前面的示例一样,您有责任释放为 Erlang 项分配的内存。在此示例中,ei_x_free() 确保释放 buf 指向的数据。
1.6 连接到分布式 Erlang 节点
要连接到分布式 Erlang 节点,您必须首先使用 ei_connect_init_* 函数之一初始化连接例程,该例程存储信息(例如主机名和节点名称)以供日后使用
int identification_number = 99; int creation=1; char *cookie="a secret cookie string"; /* An example */ const char* node_name = "einode@durin"; const char *cookie = NULL; short creation = time(NULL) + 1; ei_cnode ec; ei_connect_init(&ec, node_name, cookie, creation);
有关更多信息,请参阅 ei_connect 模块。
初始化后,您将设置与 Erlang 节点的连接。要指定要连接到的 Erlang 节点,请使用 ei_connect_*() 函数族。以下示例设置了连接,并将导致有效的套接字文件描述符
int sockfd; const char* node_name = "einode@durin"; /* An example */ if ((sockfd = ei_connect(&ec, nodename)) < 0) fprintf(stderr, "ERROR: ei_connect failed");
1.7 使用 EPMD
erts:epmd 是 Erlang 端口映射守护进程。分布式 Erlang 节点在本地主机上注册到 epmd,以向其他节点表明它们存在并且可以接受连接。 epmd 保持着节点和端口号信息的注册表,当一个节点希望连接到另一个节点时,它首先联系 epmd 以找到要连接到的正确端口号。
当您使用 ei_connect 连接到 Erlang 节点时,首先会建立到 epmd 的连接,如果节点已知,则会建立到 Erlang 节点的连接。
如果 C 节点希望系统中的其他节点能够找到并连接到它们,它们也可以在 epmd 上注册自己。
在注册到 epmd 之前,您必须首先创建一个监听套接字并将其绑定到端口。然后
int pub; pub = ei_publish(&ec, port);
pub 是现在连接到 epmd 的文件描述符。 epmd 监控连接的另一端。如果它检测到连接已关闭,则该节点将取消注册。因此,如果您显式关闭描述符或您的节点发生故障,它将从 epmd 取消注册。
请注意,在某些系统上,此机制不会检测到故障节点,因为操作系统不会自动关闭节点故障时保持打开状态的描述符。如果节点以这种方式发生故障,epmd 会阻止您使用旧名称注册新节点,因为它认为旧名称仍在使用。在这种情况下,您必须显式关闭端口
1.8 发送和接收 Erlang 消息
使用以下两个函数之一发送消息
与 Erlang 一样,可以将消息发送到 pid 或注册名称。向注册名称发送消息更容易,因为它避免了查找合适 pid 的问题。
使用以下两个函数之一接收消息
发送消息的示例
在以下示例中,{Pid, hello_world} 被发送到注册进程 my_server
ei_x_buff buf; ei_x_new_with_version(&buf); ei_x_encode_tuple_header(&buf, 2); ei_x_encode_pid(&buf, ei_self(ec)); ei_x_encode_atom(&buf, "Hello world"); ei_reg_send(&ec, fd, "my_server", buf.buff, buf.index);
发送的元组的第一个元素是您自己的 pid。这使 my_server 能够回复。有关原语的更多信息,请参阅 ei_connect 模块。
接收消息的示例
在此示例中,{Pid, Something} 被接收。
erlang_msg msg; int index = 0; int version; int arity = 0; erlang_pid pid; ei_x_buff buf; ei_x_new(&buf); for (;;) { int got = ei_xreceive_msg(fd, &msg, &x); if (got == ERL_TICK) continue; if (got == ERL_ERROR) { fprintf(stderr, "ei_xreceive_msg, got==%d", got); exit(1); } break; } ei_decode_version(buf.buff, &index, &version); ei_decode_tuple_header(buf.buff, &index, &arity); if (arity != 2) { fprintf(stderr, "got wrong message"); exit(1); } ei_decode_pid(buf.buff, &index, &pid);
为了提供健壮性,分布式 Erlang 节点会偶尔轮询其所有连接的邻居,以尝试检测故障节点或通信链路。收到此类消息的节点应立即使用 ERL_TICK 消息进行响应。这是由 ei_xreceive_msg() 自动完成的。但是,发生这种情况时,ei_xreceive_msg 会将 ERL_TICK 返回给调用方,而不会将消息存储到 erlang_msg 结构中。
接收到消息后,调用方有责任释放接收到的消息。
有关更多信息,请参阅 ei_connect 和 ei 模块。
1.9 远程过程调用
充当另一个 Erlang 节点的客户端的 Erlang 节点通常会发送请求并等待回复。此类请求包含在远程节点的函数调用中,称为远程过程调用。
以下示例检查特定的 Erlang 进程是否存活
int index = 0, is_alive; ei_x_buff args, result; ei_x_new(&result); ei_x_new(&args); ei_x_encode_list_header(&args, 1); ei_x_encode_pid(&args, &check_pid); ei_x_encode_empty_list(&args); if (ei_rpc(&ec, fd, "erlang", "is_process_alive", args.buff, args.index, &result) < 0) handle_error(); if (ei_decode_version(result.buff, &index) < 0 || ei_decode_bool(result.buff, &index, &is_alive) < 0) handle_error();
有关 ei_rpc() 及其伴侣 ei_rpc_to() 和 ei_rpc_from() 的更多信息,请参阅 ei_connect 模块。
1.10 使用全局名称
C 节点可以访问通过内核中的 global 模块注册的名称。可以查找名称,使 C 节点能够将消息发送到命名的 Erlang 服务。C 节点还可以注册全局名称,使它们能够为 Erlang 进程或其他 C 节点提供命名服务。
Erl_Interface 不提供全局服务的本机实现。相反,它使用“附近”Erlang 节点提供的全局服务。要使用本节中描述的服务,必须首先打开与 Erlang 节点的连接。
要查看有哪些名称
char **names; int count; int i; names = ei_global_names(&ec,fd,&count); if (names) for (i=0; i<count; i++) printf("%s\n",names[i]); free(names);
ei_global_names 分配并返回一个包含所有已知于 Kernel 中 global 模块的名称的缓冲区。 count 被初始化为指示数组中的名称数量。names 中的字符串数组以一个 NULL 指针结尾,因此不需要使用 count 来确定最后一个名称何时到达。
释放数组的责任在于调用者。 ei_global_names 使用对 malloc() 的单次调用分配数组和所有字符串,因此 free(names) 就是所需要的全部操作。
要查找其中一个名称
ETERM *pid; char node[256]; erlang_pid the_pid; if (ei_global_whereis(&ec,fd,"schedule",&the_pid,node) < 0) fprintf(stderr, "ei_global_whereis error\n");
如果 "schedule" 为 Kernel 中 global 模块所知,一个 Erlang pid 将被写入 the_pid。此 pid 可用于向调度服务发送消息。此外, node 被初始化为包含服务注册所在的节点的名称,以便您只需将变量传递给 ei_connect 即可建立连接。
在注册名称之前,您应该已经使用 epmd 注册了您的端口号。这并非严格必要,但如果您忽略了此操作,那么其他希望与您的服务通信的节点将无法找到或连接到您的进程。
创建一个 Erlang 进程可以用来与您的服务通信的名称
ei_global_register(fd,servicename,ei_self(ec));
在注册名称后,使用 ei_accept 等待传入连接。
请记住之后使用 ei_x_free 释放 pid。
要注销名称
ei_global_unregister(&ec,fd,servicename);