首页/新闻资讯/正文详情

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

发布时间:2026/9/25 2:26:11 来源:云帆数科 栏目:资讯中心
Erlang/OTP 端口(Ports)互操作实战:用 Port 从 Erlang 调用 C 程序完整教程
编程语言语言运行时标准库编译器并发编程【免费下载链接】otpErlang/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 x1; } 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注册名字。对外 APIfoo/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进程的主循环承担五步工作把消息编码成字节序列发送到端口等待回复解码回复把结果回传给调用者。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 列表即二进制或 0255 整数组成的可嵌套列表{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.这套方案的代价是显而易见的——它限定了参数与结果必须在 0255 之间实际项目中若数据范围更大要么改用多字节编码要么配合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, got0; do { if ((i read(0, bufgot, len-got)) 0){ return(i); } got i; } while (gotlen); return(len); } int write_exact(byte *buf, int len) { int i, wrote 0; do { if ((i write(1, bufwrote, len-wrote)) 0) return (i); wrote i; } while (wrotelen); 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 CtrlG 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)返回 4C 侧x1bar(5)返回 10C 侧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 退出信号该消息会直接终止连接进程生命周期关键规则端口由open_port({spawn, ExtPrg}, [{packet, N}])创建N取 1、2 或 4长度前缀自动附加在 Erlang 侧须由 C 侧显式处理创建端口的进程是 connected process端口所有者所有通信必须经过它它终止端口随之终止端口消息异步投递Erlang/OTP 16 之前为同步外部程序通过检测read返回 ≤ 0 来感知端口关闭并自行退出若需在 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均可直接编译运行验证本文全部内容。赞分享编程语言语言运行时标准库编译器并发编程【免费下载链接】otpErlang/OTP项目地址https://gitcode.com/gh_mirrors/ot/otp点击查看免费下载相关推荐Erlang/OTP 互操作实战用 linked-in Port Driver 在 Erlang 中调用 C 代码c_portdriver 教程详解Erlang/OTP 互操作实战用 linked in Port Driver 在 Erlang 中调用 C 代码c_portdriver 教程详解 本指编程语言语言运行时标准库编译器并发编程使用 vmctl 将 Grafana Mimir 历史指标迁移到 VictoriaMetricsremote-read 与对象存储双模式实战指南使用 vmctl 将 Grafana Mimir 历史指标迁移到 VictoriaMetricsremote read 与对象存储双模式实战指南 Grafan编程语言语言运行时标准库编译器并发编程3 步批量解除 PDF 复制打印限制PDF 补丁丁实操指南3 步批量解除 PDF 复制打印限制PDF 补丁丁实操指南 想把 PDF 里的一段话复制出来阅读器却弹出权限不足想打印按钮是灰的。这类锁由文档自身的编程语言语言运行时标准库编译器并发编程上一篇终极fscan网络安全扫描工具从入门到精通完整指南下一篇告别重复造轮子用Edgar for Unity打造无限可能的2D地牢世界创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

相关推荐

通用Agent大赛脑洞炸裂!用TaoToken统一Key打通OpenManus与Flowith的配置实战
通用Agent大赛脑洞炸裂!用TaoToken统一Key打通OpenManus与Flowith的配置实战

/* 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:26:11

抖音无水印批量下载手把手指南:3条命令存档任意创作者全部作品
抖音无水印批量下载手把手指南:3条命令存档任意创作者全部作品

抖音无水印批量下载手把手指南:3条命令存档任意创作者全部作品 【免费下载链接】douyin-downloader A practical Douyin downloader for both single-item and profile batch downloads, with progress display, retries, SQLite deduplication, and browser fallba… · 2026/9/25 2:26:11

transition-value 指南:在 React 360 中用 Tweening 函数实现平滑数值过渡
transition-value 指南:在 React 360 中用 Tweening 函数实现平滑数值过渡

前端3D渲染 【免费下载链接】react-360 Create amazing 360 and VR content using React 项目地址: https://gitcode.com/gh_mirrors/re/react-360 点击查看 免费下载 导读 transition-value 是 React 360 仓库中一个轻量级、无依赖的数值过渡库:它把&… · 2026/9/25 2:26:11

easy-vibe 前端进阶教程:Figma 与 MasterGo 实战入门,从零创建网页原型
easy-vibe 前端进阶教程:Figma 与 MasterGo 实战入门,从零创建网页原型

教程文档 【免费下载链接】easy-vibe 从 0 到 1 学会 vibe coding,项目制学习 项目地址: https://gitcode.com/datawhalechina/easy-vibe 点击查看 免费下载 本文基于 easy-vibe 教程 Stage 2(初级-中级开发)前端方向的《Figma 与… · 2026/9/25 3:05:37

F´ 中的规则与场景驱动测试:基于 STest 的组件单元测试框架详解
F´ 中的规则与场景驱动测试:基于 STest 的组件单元测试框架详解

嵌入式系统编程 【免费下载链接】fprime F - A flight software and embedded systems framework 项目地址: https://gitcode.com/gh_mirrors/fpri/fprime 点击查看 免费下载 导读 STest 是 F(F Prime)飞行软件与嵌入式系统框架中内置的一个… · 2026/9/25 3:05:37

AI芯片架构选型指南:从GPU到TPU的实战对比
AI芯片架构选型指南:从GPU到TPU的实战对比

/* 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 3:05:37

Win10 LTSC 2019老电脑优化指南:稳定、轻量、十年支持
Win10 LTSC 2019老电脑优化指南:稳定、轻量、十年支持

1. 为什么老电脑需要LTSC?不是“精简版”,而是“去冗余的官方原生系统”你手边那台奔腾G3258配4GB内存、机械硬盘还在吱呀作响的办公机,或者那台被塞进收银台底下、连USB3.0都没有的POS终端——它们真就该被淘汰吗?我去年帮本地一… · 2026/9/25 3:05:37

UI/UX Pro Max级技能进阶:设计决策链、视觉基本功与Figma工作流
UI/UX Pro Max级技能进阶:设计决策链、视觉基本功与Figma工作流

“ui-ux-pro-max-skill”这个标题,我第一眼看到的时候确实愣了一下。做了这么多年UI/UX相关的工作,见过叫“全链路设计师”的,也见过叫“全栈设计师”的,偶尔还冒出个“UX Writer”和“Product Designer”互相拉扯,但“… · 2026/9/25 3:05:37

崩溃后自动复活:Unreal Agent append-only 会话存储与 Resume 恢复机制深度解析
崩溃后自动复活:Unreal Agent append-only 会话存储与 Resume 恢复机制深度解析

崩溃后自动复活:Unreal Agent append-only 会话存储与 Resume 恢复机制深度解析 【免费下载链接】unreal-agent Async-first agent harness 项目地址: https://gitcode.com/gh_mirrors/un/unreal-agent Unreal Agent 是 Unreal Labs 出品的一个异步优先&… · 2026/9/25 3:05:31

数值优化(Numerical Optimization)学习系列-03-共轭梯度方法(Conjugate Gradient)
数值优化(Numerical Optimization)学习系列-03-共轭梯度方法(Conjugate Gradient)

/* 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 1:00:31

创维E900V22D刷机全攻略:S905L3SB芯片兼容性解析与救砖实战
创维E900V22D刷机全攻略:S905L3SB芯片兼容性解析与救砖实战

/* 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 1:00:31

MQTT协议原理与Broker服务器搭建实战:从Mosquitto到EMQX
MQTT协议原理与Broker服务器搭建实战:从Mosquitto到EMQX

/* 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 1:00:37

了解更多?预约专属演示

我们的顾问将为您一对一讲解产品与方案

企业微信二维码