news 2026/9/25 2:26:06

Erlang/OTP 端口(Ports)互操作实战:用 Port 从 Erlang 调用 C 程序完整教程

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Erlang/OTP 端口(Ports)互操作实战:用 Port 从 Erlang 调用 C 程序完整教程
  • 编程语言
  • 语言运行时
  • 标准库
  • 编译器
  • 并发编程

【免费下载链接】otp

Erlang/OTP

项目地址:https://gitcode.com/gh_mirrors/ot/otp
点击查看免费下载

Port(端口)是 Erlang/OTP 内置的三大互操作机制之一,它以字节流为接口,让 Erlang 进程能够与运行在独立 OS 进程中的外部程序(通常是 C 程序)通信。本文以官方互操作教程 Ports 章节 为主体,围绕“在 Erlang 中调用 C 函数”这一经典场景,完整讲解端口的创建、消息协议、C 端通信代码的写法与端到端运行流程。读完本文,你将掌握open_port/2的用法、{packet,2}长度前缀协议、C 程序在 stdin/stdout 上的读写约定,以及端口失败检测与优雅关闭的完整套路。

端口机制概览:字节导向的外部世界接口

从 Erlang 的视角看,端口提供了与外部世界通信的基本机制。端口对外部程序呈现**字节导向(byte-oriented)**的接口:创建端口后,Erlang 与外部程序之间交换的是字节列表或二进制,而不是 Erlang 项(terms)。因此,程序员往往需要自行设计一套编码/解码(encoding/decoding)方案,把 Erlang 项转成字节序列,再在另一端还原。这一点在官方 Interoperability Tutorial 概述 中有着明确说明。

端口的底层实现依赖平台:在 UNIX 上使用管道(pipes),外部程序默认从标准输入(文件描述符 0)读取、向标准输出(文件描述符 1)写入。只要外部程序能够处理这种进程间通信机制,它可以用任何语言编写——本文以 C 为例。

在 Erlang/OTP 中,与端口并列的内建互操作机制还有分布式 Erlang(用于 Erlang 节点间通信)和NIF(把 C 代码直接链接进运行时系统)。端口适合“Erlang 程序与外部程序运行在同一台机器”的各种互操作场景,编程相对直白;而 NIF 由于直接链接进模拟器进程,调用开销最小但崩溃风险最高——一个出错的 NIF 会让整个运行时系统泄漏内存、挂起或崩溃,因此官方建议在开销可接受时优先使用外部端口。这一取舍在 overview.md 中有详细论述。

原文档用一张流程图概括了端口通信的参与方:

图中的关键点有两个:一是端口位于 ERTS 内部,作为 Erlang 进程与外部 OS 进程之间的桥梁;二是 Erlang 侧必须有一个**connected process(连接进程)**负责与端口交互——所有进出端口的数据都必须经过它。

问题场景:为什么需要端口

本教程要解决的经典互操作问题是:把一段求解复杂问题的 C 代码集成进 Erlang 程序。假设我们有如下两个 C 函数,希望从 Erlang 中调用(见 example.md 与配套的 complex.c):

/* complex.c */ int foo(int x) { return x+1; } int bar(int y) { return y*2; }

这两个函数被刻意保持得尽可能简单,以便读者把注意力集中在互操作机制本身。从 Erlang 的角度,理想情况是不必关心foo、bar是 C 函数,直接调用即可:

% Erlang code ... Res = complex:foo(X), ...

这里的诀窍在于:C 通信的细节被封装在complex.erl的实现内部。后续章节将展示如何用端口(以及教程中的 NIF 等其他机制)实现这个模块。本教程将用端口方案实现一个complex1模块来完成任务。

Erlang 端:创建端口、封装调用

创建端口:open_port/2与连接进程

Erlang 与 C 之间的一切通信,都必须从创建端口开始。创建端口使用 BIFopen_port/2,第一个参数传{spawn, ExtPrg}——字符串ExtPrg是外部程序的名字,可以包含命令行参数;第二个参数是选项列表,本例中只有一个{packet, 2}。

{packet, 2}的含义是:在 Erlang 与 C 之间传输的数据前,附加一个2 字节的长度指示符,以简化双端通信。Erlang 侧会自动加上这个长度指示符,而 C 侧必须显式地构造和解析它。

创建端口的 Erlang 进程被称为该端口的connected process(连接进程)。所有到端口的通信和来自端口的数据都必须经过该进程;如果连接进程终止,端口也随之终止(只要外部程序写得正确,它也会随之终止)。同时,进程被设置为trap_exit,以便检测外部程序的失败:

-module(complex1). -export([start/1, init/1]). start(ExtPrg) -> spawn(?MODULE, init, [ExtPrg]). init(ExtPrg) -> register(complex, self()), process_flag(trap_exit, true), Port = open_port({spawn, ExtPrg}, [{packet, 2}]), loop(Port).

这里start/1会派生一个独立的init/1进程:它把自己的 PID 注册成全局名complex(这样其他进程可以通过名字发送消息),设置trap_exit,然后创建端口并进入主循环。注意:连接进程是init/1所在进程,而不是调用start/1的进程。

从 Ports and Port Drivers 参考手册 可以进一步确认端口 BIF 的细节:

  • PortName通常是{spawn, Command}元组,Command为外部程序名字符串;外部程序运行在 Erlang 工作区之外,除非找到了同名端口驱动(linked-in driver),此时启动的是驱动。
  • PortSettings是端口设置(选项)列表,通常至少包含{packet, N},指定传输数据前附加N 字节长度指示符,N 的合法取值为 1、2 或 4。若希望用二进制(binary)而非字节列表通信,则需加上binary选项。
  • 端口标识(port identifier)的使用方式与 PID 类似:可以像 PID 一样收发消息,也可以用link/1链接,或用register/2注册名字。

对外 API:foo/1与bar/1

现在实现complex1:foo/1和complex1:bar/1。两者都把请求发给complex进程,然后等待回复:

foo(X) -> call_port({foo, X}). bar(Y) -> call_port({bar, Y}). call_port(Msg) -> complex ! {call, self(), Msg}, receive {complex, Result} -> Result end.

调用者进程把{call, self(), Msg}消息发给注册名complex,然后阻塞等待{complex, Result}回复并返回结果。这样,从调用者角度看foo、bar与普通 Erlang 函数无异。

主循环:编码、发送、等待、解码、回传

complex进程的主循环承担五步工作:

  1. 把消息编码成字节序列;
  2. 发送到端口;
  3. 等待回复;
  4. 解码回复;
  5. 把结果回传给调用者。
loop(Port) -> receive {call, Caller, Msg} -> Port ! {self(), {command, encode(Msg)}}, receive {Port, {data, Data}} -> Caller ! {complex, decode(Data)} end, loop(Port) end.

这里使用的两条端口消息协议是(更完整的协议列表见 ports.md):

  • {Pid, {command, Data}}— 把Data发送给端口(Data必须是 I/O 列表,即二进制,或 0~255 整数组成的(可嵌套)列表);
  • {Port, {data, Data}}— 从外部程序收到的数据,发给端口所有者(connected process)。

此外,参考手册还列举了{Pid, close}(关闭端口,端口在缓冲区刷完后回复{Port, closed})和{Pid, {connect, NewPid}}(转移端口所有权,回复{Port, connected})等消息,并提醒:发往端口的消息是异步投递的(这一行为自 Erlang/OTP 16 起生效,更早版本是同步投递)。

简单的编码/解码方案

假设 C 函数的参数和返回值都小于 256,教程采用了一套极简编码方案:foo用字节 1 表示,bar用字节 2 表示,参数/结果各用一个字节表示:

encode({foo, X}) -> [1, X]; encode({bar, Y}) -> [2, Y]. decode([Int]) -> Int.

这套方案的代价是显而易见的——它限定了参数与结果必须在 0~255 之间;实际项目中若数据范围更大,要么改用多字节编码,要么配合binary选项 +term_to_binary/1走 Erlang 外部项格式(见 overview.md 中关于 Erl_Interface 与term_to_binary/1、binary_to_term/1的讨论)。

完整 Erlang 程序:停止与失败检测

完整的complex1模块(仓库中的 complex1.erl)还包含停止端口和检测端口失败的逻辑:

-module(complex1). -export([start/1, stop/0, init/1]). -export([foo/1, bar/1]). start(ExtPrg) -> spawn(?MODULE, init, [ExtPrg]). stop() -> complex ! stop. foo(X) -> call_port({foo, X}). bar(Y) -> call_port({bar, Y}). call_port(Msg) -> complex ! {call, self(), Msg}, receive {complex, Result} -> Result end. init(ExtPrg) -> register(complex, self()), process_flag(trap_exit, true), Port = open_port({spawn, ExtPrg}, [{packet, 2}]), loop(Port). loop(Port) -> receive {call, Caller, Msg} -> Port ! {self(), {command, encode(Msg)}}, receive {Port, {data, Data}} -> Caller ! {complex, decode(Data)} end, loop(Port); stop -> Port ! {self(), close}, receive {Port, closed} -> exit(normal) end; {'EXIT', Port, Reason} -> exit(port_terminated) end. encode({foo, X}) -> [1, X]; encode({bar, Y}) -> [2, Y]. decode([Int]) -> Int.

主循环现在处理三种消息:

  • {call, Caller, Msg}— 正常调用流程,如上所述;
  • stop— 向端口发送{self(), close},等待{Port, closed}确认后以exit(normal)正常退出;
  • {'EXIT', Port, Reason}— 端口异常终止(如外部程序崩溃),此时进程以exit(port_terminated)退出。

后两种分支正是“连接进程 trap 端口退出信号”的实际应用:因为init/1进程设置了process_flag(trap_exit, true),端口终止时发送的{'EXIT', Port, Reason}会作为普通消息进入信箱,而不是直接把进程杀死,从而让进程有机会做清理并给出明确的失败语义。

C 端:在 stdin/stdout 上实现长度前缀协议

字节级读写:read_exact与write_exact

在 C 侧,需要编写从 Erlang 接收数据、向 Erlang 发送数据的函数,且要处理2 字节长度指示符。默认情况下,C 程序从标准输入(文件描述符 0)读取、向标准输出(文件描述符 1)写入(对应仓库中的 erl_comm.c):

/* erl_comm.c */ #include <stdio.h> #include <unistd.h> typedef unsigned char byte; int read_exact(byte *buf, int len) { int i, got=0; do { if ((i = read(0, buf+got, len-got)) <= 0){ return(i); } got += i; } while (got<len); return(len); } int write_exact(byte *buf, int len) { int i, wrote = 0; do { if ((i = write(1, buf+wrote, len-wrote)) <= 0) return (i); wrote += i; } while (wrote<len); return (len); } int read_cmd(byte *buf) { int len; if (read_exact(buf, 2) != 2) return(-1); len = (buf[0] << 8) | buf[1]; return read_exact(buf, len); } int write_cmd(byte *buf, int len) { byte li; li = (len >> 8) & 0xff; write_exact(&li, 1); li = len & 0xff; write_exact(&li, 1); return write_exact(buf, len); }

几个值得注意的实现细节:

  • read_exact/write_exact用循环确保读满/写完指定字节数。管道上的read/write可能因为信号中断或缓冲区限制而“短读/短写”,循环重试正是生产级代码的基本功;
  • read_cmd先读 2 字节长度前缀(大端序,(buf[0] << 8) | buf[1]),再按该长度读取完整数据;write_cmd则先写出 2 字节长度前缀,再写正文;
  • 教程特别提醒:stdin/stdout是带缓冲的输入/输出流,绝不能用于与 Erlang 的通信。必须直接使用文件描述符 0/1 上的read/write系统调用(这也是头文件引入<unistd.h>的原因),否则缓冲机制会破坏协议同步。

主程序:分发函数调用并回传结果

main函数监听来自 Erlang 的消息,按照约定的编码方案,用第一个字节决定调用哪个函数、第二个字节作为参数,然后把函数结果回传给 Erlang(对应仓库中的 port.c):

/* port.c */ typedef unsigned char byte; int main() { int fn, arg, res; byte buf[100]; while (read_cmd(buf) > 0) { fn = buf[0]; arg = buf[1]; if (fn == 1) { res = foo(arg); } else if (fn == 2) { res = bar(arg); } buf[0] = res; write_cmd(buf, 1); } }

注意:C 程序运行在一个while循环中,并检查read_cmd/1的返回值——这是为了检测端口何时关闭并自行终止。当 Erlang 侧关闭端口(或连接进程退出导致端口终止)时,管道写端关闭,read_cmd返回 ≤ 0,循环退出、进程自然结束。这正是 Erlang 文档中所说的“外部程序,只要写得正确,会随端口一起终止”的落地实现。

端到端运行:三步跑通示例

仓库的 tutorial 目录 下提供了全部示例源码:complex.c、erl_comm.c、port.c与complex1.erl,可以直接照此操作。

Step 1. 编译 C 代码

$ gcc -o extprg complex.c erl_comm.c port.c

这里把三个 C 文件链接成一个可执行文件extprg。编译命令在 UNIX 环境下运行,需要系统具备 gcc 工具链。

Step 2. 启动 Erlang 并编译 Erlang 代码

$ erl Erlang/OTP 26 [erts-14.2] [source] [64-bit] [smp:8:8] [ds:8:8:10] [async-threads:1] [jit:ns] Eshell V14.2 (press Ctrl+G to abort, type help(). for help) 1> c(complex1). {ok,complex1}

(上面的版本 banner 是教程撰写时 Erlang/OTP 26 环境的真实输出,实际以你本地安装的 Erlang/OTP 版本为准。)

Step 3. 运行示例

2> complex1:start("./extprg"). <0.34.0> 3> complex1:foo(3). 4 4> complex1:bar(5). 10 5> complex1:stop(). stop

执行结果验证了整条链路:foo(3)返回 4(C 侧x+1),bar(5)返回 10(C 侧y*2)。调用stop()后,complex1进程向端口发送close消息,等待{Port, closed}确认后正常退出;与此同时,extprg进程因为读不到新数据而退出while循环并终止——Erlang 进程、端口、外部 OS 进程三者的生命周期就此完整收束。

端口协议速查与生命周期要点

结合 ports.md 与本文示例,可以把端口通信的协议与生命周期归纳如下:

发送给端口的消息(Data须为 I/O 列表):

消息含义
{Pid, {command, Data}}把Data发送给端口
{Pid, close}关闭端口;端口在缓冲区刷完后回复{Port, closed}
{Pid, {connect, NewPid}}把端口所有权转移给NewPid,回复{Port, connected}

从端口接收的消息:

消息含义
{Port, {data, Data}}从外部程序收到数据
{Port, closed}对close的确认
{Port, connected}对connect的确认
{'EXIT', Port, Reason}端口已终止(若未 trap 退出信号,该消息会直接终止连接进程)

生命周期关键规则:

  1. 端口由open_port({spawn, ExtPrg}, [{packet, N}])创建,N取 1、2 或 4;长度前缀自动附加在 Erlang 侧,须由 C 侧显式处理;
  2. 创建端口的进程是 connected process(端口所有者),所有通信必须经过它;它终止,端口随之终止;
  3. 端口消息异步投递(Erlang/OTP 16 之前为同步);
  4. 外部程序通过检测read返回 ≤ 0 来感知端口关闭并自行退出;
  5. 若需在 C 侧使用 Erlang 项而非自定义字节编码,可结合binary选项与term_to_binary/1/binary_to_term/1,或改用 Erl_Interface 库(见 overview.md)。

延伸阅读

  • Interoperability Tutorial 目录:互操作机制的总体介绍与前置知识;
  • 问题示例:本文所解问题的原始定义;
  • NIFs 章节:同一问题用 NIF 的解法,便于对比端口与 NIF 的取舍(NIF 更快但崩溃风险更高,官方建议优先考虑外部端口);
  • C Nodes 章节 与 Erl_Interface 章节:把 C 程序包装成分布式节点的另一条路径;
  • Ports and Port Drivers 参考手册:端口 BIF、消息协议与端口驱动的权威参考;
  • 配套源码:complex1.erl、complex.c、erl_comm.c、port.c,均可直接编译运行验证本文全部内容。
  • 编程语言
  • 语言运行时
  • 标准库
  • 编译器
  • 并发编程

【免费下载链接】otp

Erlang/OTP

项目地址:https://gitcode.com/gh_mirrors/ot/otp
点击查看免费下载

相关推荐

上一篇:终极fscan网络安全扫描工具:从入门到精通完整指南
下一篇:告别重复造轮子:用Edgar for Unity打造无限可能的2D地牢世界

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/25 2:25:26

Python跳动的爱心代码:tkinter动画循环与心形曲线实现

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/25 2:24:59

applera1n工作原理揭秘:checkm8硬件漏洞如何支撑整个iCloud Bypass

applera1n工作原理揭秘&#xff1a;checkm8硬件漏洞如何支撑整个iCloud Bypass 【免费下载链接】applera1n icloud bypass for ios 15-16 项目地址: https://gitcode.com/gh_mirrors/ap/applera1n 还在为iPhone被iCloud账号锁住而烦恼&#xff1f;本文带你从底层原理讲透…

作者头像 李华
网站建设 2026/9/25 2:24:27

LinuxKit 内核性能分析指南:使用 perf 工具进行容器系统性能剖析

操作系统云原生容器运行时 【免费下载链接】linuxkit A toolkit for building secure, portable and lean operating systems for containers 项目地址&#xff1a; https://gitcode.com/gh_mirrors/li/linuxkit 点击查看 免费下载 导读 本文介绍如何在 LinuxKit 构建的最小化…

作者头像 李华
网站建设 2026/9/25 2:22:34

Windows原生命令certutil计算MD5的原理与实战

1. 为什么在Windows下必须亲手算MD5&#xff1f;不是有图形工具吗&#xff1f;你有没有遇到过这种情况&#xff1a;下载完一个ISO镜像&#xff0c;官网只给了MD5校验值&#xff0c;你双击打开某个“MD5计算器.exe”&#xff0c;拖进去文件&#xff0c;结果弹窗提示“无法读取文…

作者头像 李华