Erlang/OTP 驱动(Driver)实现指南:从同步、异步到 driver_async 的完整实战
编程语言语言运行时标准库编译器并发编程【免费下载链接】otpErlang/OTP项目地址https://gitcode.com/gh_mirrors/ot/otp点击查看免费下载导读本文是 Erlang/OTP 官方驱动指南 erts/doc/guides/driver.md 的深度展开版以仓库自带的三个可编译示例pg_sync、pg_async、next_perm为主线完整讲解如何用 C/C 为 Erlang 虚拟机编写动态加载驱动从driver_entry结构与DRIVER_INIT宏、control回调与port_control/3的数据往返、ready_input/ready_output事件驱动到driver_async异步任务线程池与driver_output_term直接回传 Erlang 项的完整链路。读完本文你将掌握驱动生命周期、同步与异步两种实现范式以及如何把耗时计算安全地移出虚拟机主线程。需注意官方原文提示该章节写于较早时期示例面向旧版驱动接口因此本文同时结合当前仓库的 erl_driver 与 driver_entry 参考文档对接口细节进行了校对与扩充。什么是 Erlang 驱动Driver驱动是用 C 编写的库它被链接进 Erlang 虚拟机emulator并从 Erlang 侧调用。选择使用驱动的典型场景包括当 C 比 Erlang 更合适时计算密集、位操作、底层数据结构处理需要加速某些操作时需要访问 Erlang 无法直接访问的 OS 资源系统调用、原生 socket、设备文件等。驱动有两种加载方式动态加载编译为共享库Windows 上称为 DLL运行时通过erl_ddll:load_driver/2加载这也是本指南唯一讲解的方式静态加载在虚拟机编译链接阶段直接编入模拟器超出本指南范围。警告驱动被加载后它运行在虚拟机的上下文里与虚拟机共享同一内存、运行在同一线程。这意味着驱动内的所有操作都必须是非阻塞的任何一次驱动崩溃都会拖垮整个模拟器。简而言之务必小心。关于这条警告的严格程度可以参考 driver_entry 参考文档 中对驱动回调的警告驱动回调是 VM 原生代码的直接扩展执行环境并非安全环境VM 无法提供 Erlang 代码那样的抢占式调度与内存保护——回调崩溃会导致整个 VM 崩溃错误的回调实现可能造成 VM 内部状态不一致长时间占用则会拖慢 VM 响应。同时参考文档明确说明驱动回调由模拟器同步调用如果耗时过长会导致模拟器超时必要时应当使用队列或异步调用。驱动生命周期与 driver_entry 结构一个 Erlang 驱动只导出一个函数驱动入口函数。它由宏DRIVER_INIT定义返回一个指向 CstructErlDrvEntry的指针该结构包含模拟器会调用的各个入口点entry points。未使用/未定义的入口以NULL填充。驱动结构体中包含驱动名与约 15 个函数指针driver_entry 参考文档 对每个字段都有精确定义关键字段如下字段签名调用时机initint (*init)(void)驱动加载后立即调用静态驱动为系统启动时只执行一次返回0表示成功-1表示初始化失败startErlDrvData (*start)(ErlDrvPort port, char *command)open_port/2实例化驱动时调用返回驱动句柄失败时返回ERL_DRV_ERROR_GENERAL/ERL_DRV_ERROR_ERRNO/ERL_DRV_ERROR_BADARGstopvoid (*stop)(ErlDrvData drv_data)端口关闭时调用port_close/1或端口属主进程退出是释放start中分配内存的地方outputvoid (*output)(ErlDrvData, char *buf, ErlDrvSizeT len)Erlang 进程向端口发送数据时调用port_command/2或Port ! {self(), {command, Data}}controlErlDrvSSizeT (*control)(ErlDrvData, unsigned int command, char *buf, ErlDrvSizeT len, char **rbuf, ErlDrvSizeT rlen)port_control/3调用时触发相当于驱动的 ioctlready_input/ready_outputvoid (*)(ErlDrvData, ErlDrvEvent event)驱动事件Unix 下为 socket/pipe 句柄被select检测到可读/可写时调用timeoutvoid (*timeout)(ErlDrvData)通过driver_set_timer设置的定时器归零后调用outputvvoid (*outputv)(ErlDrvData, ErlIOVec *ev)端口被写入时调用直接接收ErlIOVec免拷贝比output更快要求二进制模式ready_asyncvoid (*ready_async)(ErlDrvData, ErlDrvThreadData)driver_async发起的异步任务完成后在主模拟器线程中回调callErlDrvSSizeT (*call)(ErlDrvData, unsigned int command, ...)port_call/3调用时触发输入输出均为外部项格式external term formatflushvoid (*flush)(ErlDrvData)端口即将关闭且驱动队列中仍有数据需要冲刷时调用另有driver_name必须与open_port/2中的原子及驱动库文件名一致、finish驱动卸载前调用仅动态驱动、handle/handle2保留给模拟器内部使用这也是不要把driver_entry声明为const的原因、extended_marker/major_version/minor_version/driver_flags扩展接口版本管理与能力标志例如ERL_DRV_FLAG_USE_PORT_LOCKING、ERL_DRV_FLAG_SOFT_BUSY等字段。若extended_marker为0旧驱动其后所有扩展字段必须为0或NULL。用 C 编写驱动时驱动入口需要C链接例如extern C DRIVER_INIT(drivername);。start返回的驱动句柄会被传给后续所有回调常见做法是像示例一样在start中分配一个应用自定义结构体并把port句柄也存进去以便后续通过驱动 API 函数如driver_output使用。同步示例驱动pg_sync文档以一个通过 libpq C 客户端库访问 PostgreSQL 数据库的简单驱动为例。PostgreSQL 之所以被选中是因为它免费且开源。该驱动是同步的使用 libpq 的同步调用——这仅仅是为了简单但并不好因为等待数据库期间会阻塞整个模拟器后文将用异步示例改进。所有 Erlang 与驱动之间的通信都通过port_control/3完成驱动通过rbuf返回数据。驱动入口与状态结构/* Driver interface declarations */ static ErlDrvData start(ErlDrvPort port, char *command); static void stop(ErlDrvData drv_data); static int control(ErlDrvData drv_data, unsigned int command, char *buf, int len, char **rbuf, int rlen); static ErlDrvEntry pq_driver_entry { NULL, /* init */ start, stop, NULL, /* output */ NULL, /* ready_input */ NULL, /* ready_output */ pg_sync, /* the name of the driver */ NULL, /* finish */ NULL, /* handle */ control, NULL, /* timeout */ NULL, /* outputv */ NULL, /* ready_async */ NULL, /* flush */ NULL, /* call */ NULL /* event */ };驱动需要保存自己的状态这里只需保存数据库连接typedef struct our_data_s { PGconn* conn; } our_data_t;控制码定义/* Keep the following definitions in alignment with the * defines in erl_pq_sync.erl */ #define DRV_CONNECT C #define DRV_DISCONNECT D #define DRV_SELECT S注意注释中的要求C 侧控制码必须与 Erlang 侧的-define保持对齐。对照 pg_sync.erl 第 24-26 行Erlang 侧定义的是?DRV_CONNECT $C、?DRV_DISCONNECT $D、?DRV_SELECT $S两侧一致本文使用的 pg_sync.c 也如此。驱动初始化DRIVER_INIT/* INITIALIZATION AFTER LOADING */ /* * This is the init function called after this driver has been loaded. * It must *not* be declared static. Must return the address to * the driver entry. */ DRIVER_INIT(pq_drv) { return pq_driver_entry; }DRIVER_INIT宏定义的唯一导出函数是driver_init它返回指向driver_entry结构的指针其余所有函数都是static的不会从库中导出。正如 driver_entry 参考文档 所述该宏的存在是因为不同操作系统对符号导出的命名约定不同。示例中还刻意不使用init入口它只执行一次而驱动需要支持多个实例每个open_port/2创建一个实例。start分配用户数据/* DRIVER INTERFACE */ static ErlDrvData start(ErlDrvPort port, char *command) { our_data_t* data; data (our_data_t*)driver_alloc(sizeof(our_data_t)); >static int do_disconnect(our_data_t* data, ei_x_buff* x); static void stop(ErlDrvData drv_data) { our_data_t* data (our_data_t*)drv_data; do_disconnect(data, NULL); driver_free(data); }stop在端口关闭时被调用注意即使 Erlang 侧忘了断开数据库连接这里也会兜底执行do_disconnect随后用driver_free释放start中分配的内存。control命令分发输入数据方面驱动只用二进制格式回传connect和select的输入参数是字符串。返回数据由 Erlang 项terms构成借助erl_interface库ei以二进制项格式binary term format编码Erlang 侧用binary_to_term/1还原为项。两个工具函数让代码更简短get_s复制字符串并补上\0结尾libpq 需要ei_x_to_new_binary把ei_x_buff缓冲区中的内容拷贝到新建的驱动二进制中并通过*rbuf返回——该二进制由模拟器释放而不是我们。static char* get_s(const char* buf, int len); static int do_connect(const char *s, our_data_t* data, ei_x_buff* x); static int do_select(const char* s, our_data_t* data, ei_x_buff* x); /* As we are operating in binary mode, the return value from control * is irrelevant, as long as it is not negative. */ static int control(ErlDrvData drv_data, unsigned int command, char *buf, int len, char **rbuf, int rlen) { int r; ei_x_buff x; our_data_t* data (our_data_t*)drv_data; char* s get_s(buf, len); ei_x_new_with_version(x); switch (command) { case DRV_CONNECT: r do_connect(s, data, x); break; case DRV_DISCONNECT: r do_disconnect(data, x); break; case DRV_SELECT: r do_select(s, data, x); break; default: r -1; break; } *rbuf (char*)ei_x_to_new_binary(x); ei_x_free(x); driver_free(s); return r; }代码注释点明了control在二进制模式下的返回约定返回值只要不为负即可实际数据走*rbuf。control是调用驱动并取得响应最快的路径——参考文档指出它不做上下文切换、不经过消息传递适合Erlang 太慢时直接调 C 函数提速的场景。ei_x_new_with_version在缓冲区头部写入版本号字节这样 Erlang 侧binary_to_term可直接解码。do_connect / do_disconnect / do_selectstatic int do_connect(const char *s, our_data_t* data, ei_x_buff* x) { PGconn* conn PQconnectdb(s); if (PQstatus(conn) ! CONNECTION_OK) { encode_error(x, conn); PQfinish(conn); conn NULL; } else { encode_ok(x); } >static int do_disconnect(our_data_t* data, ei_x_buff* x) { if (data-conn NULL) return 0; PQfinish(data-conn); >static int do_select(const char* s, our_data_t* data, ei_x_buff* x) { PGresult* res PQexec(data-conn, s); encode_result(x, res,>void encode_result(ei_x_buff* x, PGresult* res, PGconn* conn) { int row, n_rows, col, n_cols; switch (PQresultStatus(res)) { case PGRES_TUPLES_OK: n_rows PQntuples(res); n_cols PQnfields(res); ei_x_encode_tuple_header(x, 2); encode_ok(x); ei_x_encode_list_header(x, n_rows1); ei_x_encode_list_header(x, n_cols); for (col 0; col n_cols; col) { ei_x_encode_string(x, PQfname(res, col)); } ei_x_encode_empty_list(x); for (row 0; row n_rows; row) { ei_x_encode_list_header(x, n_cols); for (col 0; col n_cols; col) { ei_x_encode_string(x, PQgetvalue(res, row, col)); } ei_x_encode_empty_list(x); } ei_x_encode_empty_list(x); break; case PGRES_COMMAND_OK: ei_x_encode_tuple_header(x, 2); encode_ok(x); ei_x_encode_string(x, PQcmdTuples(res)); break; default: encode_error(x, conn); break; } }这里检查 PostgreSQL 的返回状态若是数据PGRES_TUPLES_OK编码为**列数据组成的列表的列表**——列表头部是列名之后每行一个列表。因为 PostgreSQL 返回的全是 C 字符串所以统一用ei_x_encode_string以字符串形式送到 Erlang 侧对应 ei 库文档 中的ei_x_encode_string等编码函数。若是命令PGRES_COMMAND_OK编码{ok, CmdTuples}其他情况编码错误。编译与链接示例驱动驱动需要编译链接为共享库Windows 上是 DLL。用 gcc 时使用-shared与-fpic链接标志由于使用了ei库还需要链接它。ei有多种版本debug / non-debug、多线程 / 单线程示例的 Makefile 使用obj目录下的版本即non-debug、单线程版本。仓库 erts/example/Makefile 给出了完整可用的构建规则关键片段如下EI_ROOT $(ERL_TOP)/lib/erl_interface EI_INCLUDE -I$(EI_ROOT)/include -I$(ERL_TOP)/erts/emulator/beam EI_LIB -L$(EI_ROOT)/obj/$(TARGET) -lei PQ_LIB -lpq OUR_C_FLAGS -g -Wall -fpic $(EI_INCLUDE) CFLAGS $(OUR_C_FLAGS) CXXFLAGS $(OUR_C_FLAGS) TARGETS pg_sync.beam pg_async.beam pg_sync.so pg_async.so \ next_perm.so next_perm.beam time_compat.beam all: $(TARGETS) pg_sync.so pg_async.so: pg_encode.o %.so: %.o $(CC) $(CFLAGS) pg_encode.o -shared $ $(EI_LIB) $(PQ_LIB) -o $ %.so: %.cc $(CXX) $(CXXFLAGS) $ -shared -o $要点头文件搜索路径包含lib/erl_interface/include与erts/emulator/beam后者用于定位erl_driver.h链接-lei非 debug 单线程版与-lpqlibpq-fpic生成位置无关代码-shared生成共享库C 源文件next_perm.cc走CXX编译分支同样-shared输出.so.beam文件由 Erlang 源文件编译而来Erlang 侧模块与驱动配套使用。从 Erlang 以端口方式调用驱动驱动被调用前必须先加载再打开加载用erl_ddll模块完成加载动态驱动的erl_ddll驱动本身也是一个驱动打开加载成功后用open_port/2打开端口。端口名必须与共享库文件名以及驱动入口结构中的名字一致即pg_sync三处一致。打开端口后即可调用驱动。pg_sync示例中驱动不主动产生数据只依赖port_control/3的返回值。以下是同步驱动的 Erlang 侧完整代码 pg_sync.erl-module(pg_sync). -define(DRV_CONNECT, $C). -define(DRV_DISCONNECT, $D). -define(DRV_SELECT, $S). -export([connect/1, disconnect/1, select/2]). connect(ConnectStr) - case erl_ddll:load_driver(., pg_sync) of ok - ok; {error, already_loaded} - ok; E - exit({error, E}) end, Port open_port({spawn, ?MODULE}, []), case binary_to_term(port_control(Port, ?DRV_CONNECT, ConnectStr)) of ok - {ok, Port}; Error - Error end. disconnect(Port) - R binary_to_term(port_control(Port, ?DRV_DISCONNECT, )), port_close(Port), R. select(Port, Query) - binary_to_term(port_control(Port, ?DRV_SELECT, Query)).API 很简单connect/1加载驱动 → 打开端口 → 登录数据库成功则返回 Erlang 端口连接串是 PostgreSQL 的连接字符串select/2向驱动发送查询并返回结果disconnect/1关闭数据库连接与端口注意并不卸载驱动。加载用erl_ddll:load_driver/2成功或already_loaded均可继续随后open_port({spawn, ?MODULE}, [])会触发 C 侧的start函数。所有调用都走port_control/3返回值立即同步返回并用binary_to_term/1转成项示例信任驱动返回的项是良构的更稳妥的做法是用catch包裹binary_to_term/1调用。异步示例驱动pg_async数据库查询有时会耗时很长——在pg_sync中驱动干活时模拟器被整体阻塞其他 Erlang 进程完全没有机会运行这在很多场景下不可接受。于是文档用 libpq 的异步调用重写了驱动即pg_async。驱动入口与扩展的状态/* Driver interface declarations */ static ErlDrvData start(ErlDrvPort port, char *command); static void stop(ErlDrvData drv_data); static int control(ErlDrvData drv_data, unsigned int command, char *buf, int len, char **rbuf, int rlen); static void ready_io(ErlDrvData drv_data, ErlDrvEvent event); static ErlDrvEntry pq_driver_entry { NULL, /* init */ start, stop, NULL, /* output */ ready_io, /* ready_input */ ready_io, /* ready_output */ pg_async, /* the name of the driver */ NULL, /* finish */ NULL, /* handle */ control, NULL, /* timeout */ NULL, /* outputv */ NULL, /* ready_async */ NULL, /* flush */ NULL, /* call */ NULL /* event */ }; typedef struct our_data_t { PGconn* conn; ErlDrvPort port; int socket; int connecting; } our_data_t;与pg_sync.c相比的变化用同一个ready_io函数同时充当ready_input与ready_output入口仅当 socket 上有可读/可写事件时才由模拟器调用模拟器内部用select监视 socketsocket 被信号触发、表示有数据可读时调用ready_input驱动数据扩展为四项conn连接、port端口句柄driver_output回发数据时需要、socket与 PostgreSQL 通信的 socket、connecting标志当前是等待连接还是等待查询结果——因为ready_io在两种情况下都会被调用。异步连接do_connectstatic int do_connect(const char *s, our_data_t* data) { PGconn* conn PQconnectStart(s); if (PQstatus(conn) CONNECTION_BAD) { ei_x_buff x; ei_x_new_with_version(x); encode_error(x, conn); PQfinish(conn); conn NULL; driver_output(data-port, x.buff, x.index); ei_x_free(x); } PQconnectPoll(conn); int socket PQsocket(conn); >static int do_select(const char* s, our_data_t* data) { >static void ready_io(ErlDrvData drv_data, ErlDrvEvent event) { PGresult* res NULL; our_data_t* data (our_data_t*)drv_data; PGconn* conn >-module(pg_async). -define(DRV_CONNECT, $C). -define(DRV_DISCONNECT, $D). -define(DRV_SELECT, $S). -export([connect/1, disconnect/1, select/2]). connect(ConnectStr) - case erl_ddll:load_driver(., pg_async) of ok - ok; {error, already_loaded} - ok; _ - exit({error, could_not_load_driver}) end, Port open_port({spawn, ?MODULE}, [binary]), port_control(Port, ?DRV_CONNECT, ConnectStr), case return_port_data(Port) of ok - {ok, Port}; Error - Error end. disconnect(Port) - port_control(Port, ?DRV_DISCONNECT, ), R return_port_data(Port), port_close(Port), R. select(Port, Query) - port_control(Port, ?DRV_SELECT, Query), return_port_data(Port). return_port_data(Port) - receive {Port, {data, Data}} - binary_to_term(Data) end.与同步版的差异在于结果不再从port_control/3同步返回而是由driver_output以数据消息投递到消息队列。return_port_data/1负责receive端口数据并binary_to_term/1还原。关键细节驱动以二进制模式打开——open_port/2传了[binary]选项因此驱动回发到模拟器的数据是二进制若不传binary这些数据会变成整数列表。使用 driver_async 的异步驱动next_perm最后一个示例演示driver_async并顺带展示驱动项接口driver term interface。驱动用 C 编写以便使用 STL 算法用next_permutation求整数列表的下一个排列。对超过 100,000 个元素的大列表这需要一些时间因此作为异步任务执行。异步驱动的工作原理异步 API 分三步且顺序严格准备主线程在output回调也可用control示例选output是为了展示不同入口中分配一个结构装下异步任务需要的一切。async_data会在异步任务完成前一直有效执行驱动线程异步函数在独立于主模拟器线程的驱动线程中运行。注意驱动 API 函数不可重入因此在异步线程里绝不能调用它们回传主线程任务完成后ready_async回调在主模拟器线程中被调用在这里把结果返回给 Erlang。不能在异步函数内部返回结果因为那里无法调用驱动 API。驱动入口含 ready_asyncstatic ErlDrvEntry next_perm_driver_entry { NULL, /* init */ start, NULL, /* stop */ output, NULL, /* ready_input */ NULL, /* ready_output */ next_perm, /* the name of the driver */ NULL, /* finish */ NULL, /* handle */ NULL, /* control */ NULL, /* timeout */ NULL, /* outputv */ ready_async, NULL, /* flush */ NULL, /* call */ NULL /* event */ };注意这里start直接返回了端口句柄本身port不为NULL时返回(ErlDrvData)port否则返回ERL_DRV_ERROR_GENERAL没有分配用户结构。output准备异步任务struct our_async_data { bool prev; vectorint data; our_async_data(ErlDrvPort p, int command, const char* buf, int len); }; our_async_data::our_async_data(ErlDrvPort p, int command, const char* buf, int len) : prev(command 2), data((int*)buf, (int*)buf len / sizeof(int)) { } static void do_perm(void* async_data); static void output(ErlDrvData drv_data, char *buf, int len) { if (*buf 1 || *buf 2) return; ErlDrvPort port reinterpret_castErlDrvPort(drv_data); void* async_data new our_async_data(port, *buf, buf1, len); driver_async(port, NULL, do_perm, async_data, do_free); }要点首字节是命令1 next2 prev后面的字节按int解释为整数数组——Erlang 侧用I:32/integer-native构造即32 位本机字节序整数二进制保证两侧内存布局一致必须复制原始数据output返回后缓冲区即失效而do_perm稍后才在另一个线程运行driver_async(port, NULL, do_perm, async_data, do_free)最后一个参数是async_free回调仅当任务被编程式取消时才用到示例中传了NULLnext_perm.cc 实际为NULL。关于driver_async的细节可参考 erl_driver 参考文档async_invoke在独立于模拟器线程的线程中运行异步线程池大小可用erl命令行参数Aasync_thread_pool_size配置栈大小用a配置若线程池不可用则退化为同步调用。key为NULL时线程池以轮询round-robin方式分配线程指定*key则相同 key 值始终得到同一线程可用driver_async_port_key让同一驱动实例串行执行。任务完成后调用驱动入口的ready_async若ready_async为NULL则调用async_free。do_perm在驱动线程中干活static void do_perm(void* async_data) { our_async_data* d reinterpret_castour_async_data*(async_data); if (d-prev) prev_permutation(d-data.begin(), d-data.end()); else next_permutation(d-data.begin(), d-data.end()); }这里只操作output中分配的结构不触碰任何驱动 API——完全符合驱动函数不可重入的约束。ready_async用驱动项接口回传static void ready_async(ErlDrvData drv_data, ErlDrvThreadData async_data) { ErlDrvPort port reinterpret_castErlDrvPort(drv_data); our_async_data* d reinterpret_castour_async_data*(async_data); int n d-data.size(), result_n n*2 3; ErlDrvTermData *result new ErlDrvTermData[result_n], *rp result; for (vectorint::iterator i d-data.begin(); i ! d-data.end(); i) { *rp ERL_DRV_INT; *rp *i; } *rp ERL_DRV_NIL; *rp ERL_DRV_LIST; *rp n1; driver_output_term(port, result, result_n); delete[] result; delete d; }ready_async在主模拟器线程中把结果送回模拟器这里改用驱动项格式driver term format而非ei二进制项格式。这是唯一能直接把 Erlang 项发给驱动、且 Erlang 侧无需binary_to_term/1的方式。对简单例子而言它足够好省去了ei的二进制项编码。ErlDrvTermData数组由类型标记 值对构成ERL_DRV_INT后跟整数、ERL_DRV_NIL表示空表、ERL_DRV_LIST后跟列表长度这些标记定义于 erl_driver.hdriver_output_term一次性把这些项发给端口注意参考文档提示driver_output_term已被标记为 deprecated推荐使用erl_drv_output_term。发送完成后释放临时数组与异步数据。Erlang 侧next_perm.erl-module(next_perm). -export([next_perm/1, prev_perm/1, load/0, all_perm/1]). load() - case whereis(next_perm) of undefined - case erl_ddll:load_driver(., next_perm) of ok - ok; {error, already_loaded} - ok; E - exit(E) end, Port open_port({spawn, next_perm}, []), register(next_perm, Port); _ - ok end. list_to_integer_binaries(L) - [I:32/integer-native || I - L]. next_perm(L) - next_perm(L, 1). prev_perm(L) - next_perm(L, 2). next_perm(L, Nxt) - load(), B list_to_integer_binaries(L), port_control(next_perm, Nxt, B), receive Result - Result end. all_perm(L) - New prev_perm(L), all_perm(New, L, [New]). all_perm(L, L, Acc) - Acc; all_perm(L, Orig, Acc) - New prev_perm(L), all_perm(New, Orig, [New | Acc]).与前面两个示例的差异load/0先把端口注册为进程名next_perm之后直接port_control(next_perm, Nxt, B)向驱动发命令这里发命令用的是port_control/3C 侧入口是output——port_control会把数据交给output/outputv回调路径示例借此展示output的用法输入列表经list_to_integer_binaries/1转成 32 位本机字节序整数二进制流因为驱动用driver_output_term直接发送 Erlang 项Erlang 侧不再需要binary_to_term/1直接receive即可all_perm/1展示如何基于prev_perm/1迭代枚举全部排列。对照仓库中的 next_perm.erl 可以发现实际示例在next_perm(L, Nxt)中取端口时用了whereis(next_perm)并通过port_command发送而 next_perm.cc 的ready_async构造结果时还包含了ERL_DRV_PORT与driver_mk_port(port)把端口自身放进返回列表中Erlang 侧receive匹配[Port | Result]并剥掉端口头部——这些正是文档示例之外、仓库现行代码在驱动项格式细节上的差异体现阅读源码时可留意。汇总三种数据回传路径三个示例恰好覆盖了驱动向 Erlang 回传数据的三种路径可作为选型参考路径使用场景驱动 APIErlang 侧同步返回快速、低延迟调用control无上下文切换、无消息传递control通过*rbuf返回port_control/3的返回值 binary_to_term/1异步消息推送基于 socket/pipe 的事件驱动连接、查询等外部 I/O 完成时driver_output端口消息{Port, {data, Data}}binary_to_term/1异步任务完成回传driver_async线程池中的耗时计算driver_output_term或推荐的erl_drv_output_term直接receive到 Erlang 项进一步阅读erl_driver 参考文档驱动 API 函数全集driver_alloc、driver_output、driver_select、driver_async、set_port_control_flags、driver_set_timer等及多线程/锁相关说明driver_entry 参考文档ErlDrvEntry结构每个字段的完整语义、DRIVER_INIT宏、扩展接口版本管理ERL_DRV_EXTENDED_MARKER与 major/minor version、驱动标志ERL_DRV_FLAG_*ei 库文档erl_interface的ei编码函数族ei_x_encode_string、ei_x_new_with_version、ei_x_to_new_binary等与连接库说明driver.md 原始指南本文的官方出处含历史注释示例基于旧版驱动接口阅读与移植时请以 erl_driver 与 driver_entry 为准示例源码pg_syncC / Erlang、pg_asyncC / Erlang、pg_async2C / Erlang、next_permC / Erlang以及编码辅助模块 pg_encode.c、pg_encode.h构建规则见 Makefile。实现驱动前请务必牢记 driver_entry 参考文档 的告诫驱动回调崩溃会拖垮整个 VM回调长时间占用会降低 VM 响应并可能引发内存与调度方面的怪异行为。需要长时间工作的场景优先考虑driver_async线程池或事件驱动driver_selectready_input/ready_output让模拟器始终保持响应。赞分享编程语言语言运行时标准库编译器并发编程【免费下载链接】otpErlang/OTP项目地址https://gitcode.com/gh_mirrors/ot/otp点击查看免费下载相关推荐WebdriverIO 从同步到异步迁移完整指南SyncAsyncMigration 实战手册WebdriverIO 从同步到异步迁移完整指南SyncAsyncMigration 实战手册 本文基于 WebdriverIO 官方迁移文档 SyncAs测试质量保障Prism命令系统终极指南从同步到异步命令的完整实现Prism命令系统终极指南从同步到异步命令的完整实现 Prism框架的命令系统是现代.NET应用程序开发中的核心组件提供了一套强大而灵活的命令执行机制。通过前端跨平台UI组件Django Channels消费者编写指南从同步到异步的完整实践Django Channels消费者编写指南从同步到异步的完整实践 想要为你的Django项目添加实时通信功能吗Django Channels消费者是构建W后端WebSocket异步编程上一篇Mac Mouse Fix 完整指南从安装、按键重映射到平滑滚动调优让十元鼠标好用过触控板下一篇TachiyomiJ2K插件生态系统第三方扩展的完整指南创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考