查看源代码 Erl_Interface 用户指南

简介

Erl_Interface 库包含帮助您集成用 C 和 Erlang 编写的程序的函数。 Erl_Interface 中的函数支持以下功能:

  • 操作表示为 Erlang 数据类型的数据
  • 在 C 和 Erlang 格式之间转换数据
  • 编码和解码 Erlang 数据类型,以便传输或存储
  • C 节点和 Erlang 进程之间的通信
  • 将 C 节点状态备份和恢复到 Mnesia

注意

默认情况下,Erl_Interface 库仅保证与来自同一版本的其他 Erlang/OTP 组件兼容。有关如何与早期版本的 Erlang/OTP 组件进行通信的信息,请参阅函数 ei_set_compat_rel

范围

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

  • 编译代码以与 Erl_Interface 一起使用
  • 初始化 Erl_Interface
  • 编码、解码和发送 Erlang 项
  • 构建项和模式
  • 模式匹配
  • 连接到分布式 Erlang 节点
  • 使用 Erlang 端口映射守护进程 (EPMD)
  • 发送和接收 Erlang 消息
  • 远程过程调用
  • 使用全局名称

先决条件

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

编译和链接代码

要使用任何 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 报告的路径。
  • $EIVSNErl_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.alibsocket.a,或 Windows 上的 wsock32.lib)才能使用 Erl_Interface 的通信功能。

如果您在基于 POSIX 线程或 Solaris 线程的多线程应用程序中使用 Erl_Interface 函数,则 Erl_Interface 需要访问线程包中的某些同步设施。 您必须指定额外的编译器标志,以指示您使用的软件包。 定义 _REENTRANTSTHREADSPTHREADS。 如果指定了 _REENTRANT,则默认使用 POSIX 线程。

初始化库

在调用库中的任何其他函数之前,通过调用 ei_init() 准确初始化一次。

编码、解码和发送 Erlang 项

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

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 模块。

构建项

可以通过使用 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 指向的数据。

连接到分布式 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");

使用 EPMD

erts:epmd 是 Erlang 端口映射守护进程。 分布式 Erlang 节点在本地主机上向 epmd 注册,以向其他节点指示它们存在并且可以接受连接。epmd 维护节点和端口号信息的注册表,当节点希望连接到另一个节点时,它首先联系 epmd 以查找要连接的正确端口号。

当您使用 ei_connect 连接到 Erlang 节点时,首先与 epmd 建立连接,如果已知该节点,则与 Erlang 节点建立连接。

C 节点也可以向 epmd 注册自己,如果它们希望系统中的其他节点能够找到并连接到它们。

在向 epmd 注册之前,您必须首先创建一个侦听套接字并将其绑定到端口。然后

int pub = ei_publish(&ec, port);

pub 是现在连接到 epmd 的文件描述符。epmd 监视连接的另一端。如果检测到连接已关闭,则该节点将取消注册。因此,如果您显式关闭描述符或节点失败,它将从 epmd 中取消注册。

请注意,在某些系统上,此机制不会检测到失败的节点,因为操作系统不会自动关闭节点失败时保持打开状态的描述符。如果节点以这种方式失败,epmd 会阻止您使用旧名称注册新节点,因为它认为旧名称仍在使用。在这种情况下,您必须显式关闭端口:

发送和接收 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_connectei 模块。

远程过程调用

作为另一个 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 模块。

使用全局名称

C 节点可以访问通过 Kernel 中的 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 分配并返回一个缓冲区,其中包含 Kernelglobal 模块已知的所有名称。count 被初始化以指示数组中名称的数量。 名称中的字符串数组以 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"Kernelglobal 模块中已知,则会将 Erlang pid 写入 the_pid。可以使用此 pid 向 schedule 服务发送消息。此外,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);