Bend 外部函数接口(FFI)完全指南:动态链接库的加载、编写与跨后端调用
2026/9/13 4:22:40 网站建设 项目流程

Bend 外部函数接口(FFI)完全指南:动态链接库的加载、编写与跨后端调用

【免费下载链接】BendA massively parallel, high-level programming language项目地址: https://gitcode.com/GitHub_Trending/be/Bend

本文以 Bend 的官方文档 docs/ffi.md 为骨架,系统讲解如何在运行时通过动态链接库(DyLib)为 Bend 程序扩展 IO 能力:从IO/DyLib/open/IO/DyLib/call/IO/DyLib/close的用法,到使用 C/Cuda 语言与 HVM API 编写可被 Bend 调用的外部函数,再到-rdynamic等编译链接细节。读完本文,你将能够为 Bend 编写、编译并调用自己的 C/CUDA 动态库,把任意外部系统能力接入这门大规模并行编程语言。


1. 概览:为什么需要 FFI 与动态链接库

Bend 是一门大规模并行的、高层级编程语言(见 README.md)。它自带一套围绕文件、网络与标准输入输出设计的 IO 原语(定义于 src/fun/builtins.bend),但任何语言都无法穷尽所有系统能力。FFI(Foreign Function Interface)的价值在于:在程序运行期间加载动态链接库(.so/.dylib),从而把任意 C/CUDA 实现的函数接入 Bend

Bend 的 FFI 设计有以下特点:

  • 运行时加载:库文件在 Bend 程序运行期才被IO/DyLib/open加载,无需在编译期链接,这让 Bend 程序可以按需、动态地扩展自身能力;
  • with IO块深度融合:加载、调用、关闭都是 IO 操作,遵循 Bend 既有的IO单子约定;
  • 按后端分为两套 API:面向 C 运行时的Port fn(Net*, Book*, Port),以及面向 CUDA 运行时的Port fn(GNet*, Port)
  • 结果通过Result类型返回:与 Bend 内建的 IO 原语保持一致,便于错误处理与模式匹配。

从源码层面看,Bend 的三大 DyLib 原语定义在 src/fun/builtins.bend:

  • IO/DyLib/open(path: String, lazy: u24) -> IO(Result(u24, String)):加载动态库,底层通过IO/call("DL_OPEN", (path, lazy))实现;
  • IO/DyLib/call(dl: u24, fn: String, args: Any) -> IO(Result(Any, String)):调用库中函数,底层为IO/call("DL_CALL", (dl, (fn, args)))
  • IO/DyLib/close(dl: u24) -> IO(Result(None, String)):关闭库,底层为IO/call("DL_CLOSE", dl)

这三个原语都经由IO/unwrap_inner统一包装,返回值是Result,Bend 侧通常配合Result/unwrap使用。也就是说,Bend 的 DyLib 能力实际上是建立在更底层的IO/call运行时原语之上的,理解这一点有助于你排查与调试 FFI 问题。

2. 在 Bend 中加载并调用动态库

2.1 完整示例:目录操作库

官方文档 docs/ffi.md 给出了一个处理目录的完整示例。假设我们已经有了一个名为libbend_dirs.so的动态库(包含lsmkdir两个函数),Bend 侧的使用方式如下:

def main(): with IO: # 打开动态库文件 # 第二个参数为 '0' 表示立即加载所有函数; # 为 '1' 则表示在使用到函数时才懒加载。 # 'dl' 是动态库的唯一 id。 dl <- IO/DyLib/open("./libbend_dirs.so", 0) # 现在可以调用动态库中的函数了。 # 调用者需要知道动态库中提供了哪些函数; # 如果你在为一个依赖动态库的 Bend 库编写封装, # 应当把这些 IO 调用包装起来,让使用者无需关心动态库内部细节。 # 第一个参数是动态库 id。 # 第二个参数是要调用的函数名(String)。 # 第三个参数是传给函数的参数。 # 你需要知道该函数每个参数的类型以及返回值类型。 # 在本例中,'ls' 接收一个路径(String), # 返回 'ls' 命令执行结果的字符串。 unwrapped_dl = Result/unwrap(dl) files_bytes <- IO/DyLib/call(unwrapped_dl, "ls", "./") files_str = String/decode_utf8(Result/unwrap(files_bytes)) files = String/split(files_str, '\n') # 我们想在用户 "my_user" 的目录不存在时创建它。 my_dir = List/filter(files, String/equals("my_dir")) match my_dir: case List/Cons: # 目录已存在,什么都不做。 * <- IO/print("Directory already exists.\n") status = wrap(-1) case List/Nil: # 目录不存在,创建它。 * <- IO/DyLib/call(unwrapped_dl, "mkdir", "./my_dir") * <- IO/print("Directory created.\n") status = wrap(+0) status <- status # 程序到这里就结束了,所以即使不关闭动态库也没有关系, # 但一旦确认不再需要它,主动关闭是好习惯。 * <- IO/DyLib/close(unwrapped_dl) return wrap(status)

2.2 关键用法拆解

  • IO/DyLib/open(path, lazy)的第二个参数lazy是一个编码为u24的布尔值——0表示打开库时立即解析所有函数(upfront),1表示按需懒加载(lazy)。前者启动稍慢但调用稳定,后者启动快但首次调用某个函数时有额外开销。
  • 返回值是Result(u24, String):成功时拿到动态库的唯一整数 id(后续所有调用的第一个参数);失败时返回错误信息字符串。示例中通过Result/unwrap取出 id。
  • IO/DyLib/call(dl, fn, args)的参数约定dl是库 id,fn是函数名字符串,args是任意类型的参数。参数与返回值的具体类型由被调函数决定,调用者必须事先了解 C 侧的签名约定(如字符串会被转换为字节列表)。
  • 字节与字符串的往返:示例中ls返回的是字节列表(Bytes),因此需要String/decode_utf8(Result/unwrap(files_bytes))解码为字符串,再用String/split(files_str, '\n')按换行拆分出文件名列表。
  • 资源管理:示例注释明确说明——程序结束时即使不关闭也无碍,但养成主动IO/DyLib/close的习惯能及时释放底层句柄。

值得留意的是示例中的status = wrap(-1)/wrap(+0):Bend 中以负数约定“失败”状态(如返回码-1)、以非负数约定“成功”状态(如返回码0),这是一种常见于系统编程的约定,你在设计自己的 FFI 返回码时也可以遵循这一模式。

3. 编写 Bend 的动态库(C 运行时)

3.1 必备前提:Bend IO 库的底层要求

Bend 的动态库必须使用C 或 CUDA(取决于你面向的后端)并基于HVM API实现。HVM(Higher-order Virtual Machine)是 Bend 的底层运行时,因此 FFI 函数的参数、返回值都以 HVM 的Port(端口引用)为媒介。

3.2 函数签名与语义

从 Bend 中通过IO/DyLib/call调用的函数必须具有以下签名:

Port function_name(Net* net, Book* book, Port arg);

各参数含义:

  • net:指向当前网络(图)状态的指针,即程序当前运行时的整体结构;
  • book:指向函数定义集(book of function definitions)的指针;
  • arg:指向该函数参数的Port(在本例中即传入的路径字符串)。

返回值必须是指向函数返回值的Port

HVM 提供了若干工具函数用于 HVM ↔ C 之间的数据转换,让你无需深究 HVM 运行时的内部细节即可完成开发:

  • readback_str(net, book, arg):把 HVM 侧的字符串参数读回为 C 的Str结构;
  • inject_bytes(net, &output):把 C 侧的字节缓冲区注入为 HVM 侧的字节列表Bytes
  • new_port(ERA, 0):构造一个空端口,常用来表示“无返回值/失败”。

3.3 完整 C 实现:lsmkdir

以下代码实现第 2 节示例中使用的库,保存为libbend_dirs.c

// 包含 HVM API 的头文件。 #include "hvm.h" // 打开和读取目录所需的头文件。 #include <stdio.h> #include <stdlib.h> #include <string.h> #include <errno.h> // IO 函数必须使用这个精确签名。 // 第一个参数是指向程序当前状态的图指针。 // 第二个参数是指向函数定义集的指针。 // 第三个参数指向函数的参数。 // 返回值必须是指向函数返回值的端口。 Port ls(Net* net, Book* book, Port arg) { // 参数需要先从 HVM 转换到 C。 // 对 'ls' 而言,参数就是一个字符串。 Str path = readback_str(net, book, arg); // 现在可以执行真正的 IO 操作了。 // 这里通过把 'ls' 作为子进程调用来列出目录内容。 char* cmd = malloc(path.len + strlen("ls ") + 1); sprintf(cmd, "ls %s", path.buf); free(path.buf); FILE* pipe = popen(cmd, "r"); if (pipe == NULL) { // 最佳实践是返回 Result 类型,而不是空值(ERA)。 // 如果命令失败而调用它的 Bend 程序又试图使用结果, // 结果会被破坏并输出垃圾数据。 fprintf(stderr, "failed to run command '%s': %s\n", cmd, strerror(errno)); return new_port(ERA, 0); } char buffer[512]; Bytes output = { .buf = NULL, .len = 0 }; while (fgets(buffer, sizeof(buffer), pipe) != NULL) { size_t len = strlen(buffer); char* new_result = realloc(output.buf, output.len + len + 1); if (new_result == NULL) { fprintf(stderr, "failed to allocate space for output of '%s': %s\n", cmd, strerror(errno)); free(cmd); free(output.buf); pclose(pipe); return new_port(ERA, 0); } output.buf = new_result; strcpy(output.buf + output.len, buffer); output.len += len; } // IO 操作完成后,把结果转换回 HVM 格式。 // 这里输出的是 'ls' 命令的输出,即字节列表。 // 后续需要在 Bend 中进一步处理,把它转换成文件名列表。 Port output_port = inject_bytes(net, &output); // 记得释放所有分配的内存。 free(cmd); free(output.buf); pclose(pipe); return output_port; } Port mkdir(Net* net, Book* book, Port arg) { // 这里与 'ls' 函数做的事相同,只是调用不产生输出的 'mkdir'。 Str path = readback_str(net, book, arg); char* cmd = malloc(path.len + strlen("mkdir ") + 1); sprintf(cmd, "mkdir %s", path.buf); int res = system(cmd); free(path.buf); free(cmd); return new_port(ERA, 0); }

3.4 关键实现细节解读

  • 签名一致性是硬约束:注释中反复强调 “IO functions must have this exact signature”,任何偏差都会导致 Bend 运行时无法正确调用;
  • 类型转换是双向的:进入时用readback_str把 HVM 数据读成 C 结构;返回时用inject_bytes把 C 缓冲区封回 HVM 的Bytes;文档同时提示你无需深入 HVM 内部细节即可完成这些转换;
  • 错误处理建议:文档明确建议返回Result类型而非ERA空值——若失败时返回 ERA 而 Bend 端仍尝试解引用结果,会得到损坏的数据甚至垃圾输出;
  • 内存管理是开发者的责任malloccmdpath.bufoutput.buf都需要在返回前free,同时popen的管道要pclose,与 C 语言的常规纪律一致;
  • mkdir直接返回 ERA:因为它没有有意义的返回值,Bend 端用* <- ...丢弃即可。

3.5 编译为共享库

假设文件保存为libbend_dirs.c,需要使用gcc并以共享库 + 未解析符号(unresolved symbols)的方式编译,同时包含 HVM 的头文件路径:

# 需要编译为带有未解析符号的共享库。 # macOS: gcc -shared -o libbend_dirs.so -I /path/to/HVM/src/ libbend_dirs.c -undefined dynamic_lookup -fPIC # Linux: gcc -shared -o libbend_dirs.so -I /path/to/HVM/src/ libbend_dirs.c -Wl,--unresolved-symbols=ignore-all -fPIC

要点说明:

  • -fPIC:生成位置无关代码,是共享库的标配;
  • -undefined dynamic_lookup(macOS)/-Wl,--unresolved-symbols=ignore-all(Linux):允许库中存在来自主程序(Bend 生成的 C 可执行文件)的符号,这正是后面-rdynamic能配合工作的前提;
  • -I /path/to/HVM/src/:替换为你本机 HVM 源码的实际路径,确保能找到hvm.h

编译完成后,把库文件路径传给IO/DyLib/open即可在 Bend 中使用。

4. 编写面向 CUDA 后端的动态库

编写面向 CUDA 运行时的库与 C 运行时非常相似,主要区别在于函数签名

Port function_name(GNet* gnet, Port argm)

其中:

  • gnet:指向当前网络状态的指针;
  • argm:函数的参数。

返回值同样必须是指向函数返回值的Port

使用nvcc编译器并包含 HVM 头文件来编译。假设文件保存为libbend_dirs.cu

nvcc -shared -o libbend_dirs.so -I /path/to/hvm/ libbend_dirs.cu

与 C 版本相比,CUDA 版本签名更精简(少了一个book参数),这反映了两个后端在运行时架构上的差异:CUDA 后端在 GPU 上运行,其网络状态由GNet描述。面向不同后端时,你的库实现可能需要相应调整。

5. 编译使用动态库的 Bend 程序

要让动态库能解析来自主程序的符号(如 HVM 运行时的readback_strinject_bytes等),编译 Bend 生成的 C/CUDA 程序时必须加上-rdynamic标志,把主程序的所有符号导出到动态符号表。

假设有一个使用libbend_dirs.so的 Bend 程序my_app.bend,编译命令如下:

# 面向 C 编译 bend gen-c my_app.bend > my_app.c gcc -rdynamic -lm my_app.c -o my_app # 面向 CUDA 编译 bend gen-cu my_app.bend > my_app.cu nvcc --compiler-options=-rdynamic my_app.cu -o my_app

从命令行入口源码 src/main.rs 可以看到,bend gen-cbend gen-cu是 Bend CLI 的正式子命令,分别“把程序编译为独立的 C / CUDA 文件并输出到 stdout”。由此形成完整的调用链:

  1. bend gen-c把 Bend 程序编译为 C 代码(内部会调用hvm生成器,见 src/main.rs);
  2. gcc使用-rdynamic -lm链接生成可执行文件——-rdynamic导出符号供动态库回引,-lm链接数学库;
  3. 运行时IO/DyLib/open加载libbend_dirs.soDL_CALL通过符号名查找并调用其中的ls/mkdir

同时 README.md 也印证了这一工作流:Bend 支持使用gen-cgen-cu把程序编译为独立的 C/CUDA 文件以获得最佳性能,并提示代码生成器仍处于早期阶段,成熟度不及 GCC、GHC 等编译器——因此在把生产代码完全依赖 FFI 之前,建议先在较小范围内验证。

6. 常见问题与最佳实践

结合文档与源码实现,整理出以下实操建议:

  • 封装优于裸调:文档明确建议——如果你在编写一个依赖动态库的 Bend 库,应当把IO/DyLib/call包装成语义化的 Bend 函数,让库使用者不需要了解动态库内部细节;
  • 先查函数签名再调用IO/DyLib/call的参数与返回值类型完全由被调函数决定,误用类型(如把字符串当整数)会产生难以排查的错误;
  • 错误处理优先使用Result:C 侧失败时返回Result而非 ERA,避免 Bend 侧解引用损坏数据;Bend 侧统一用Result/unwrap解包;
  • 及时关闭动态库:虽然进程结束时会自动清理,但主动IO/DyLib/close更规范;
  • 平台差异不可忽视:C 库编译时 macOS 用-undefined dynamic_lookup,Linux 用-Wl,--unresolved-symbols=ignore-all;链接主程序时-rdynamic两个平台通用;
  • 区分后端 API:面向 C 运行时签名是Port fn(Net*, Book*, Port),面向 CUDA 是Port fn(GNet*, Port),不要混用。

7. 延伸阅读

  • docs/ffi.md:官方 FFI 文档原文,包含全部示例代码;
  • src/fun/builtins.bend:IO/DyLib/open/call/close的原语定义及其参数语义;
  • src/fun/builtins.bend:IO/unwrap_inner的实现,理解Result包装层如何工作;
  • src/main.rs:gen-c/gen-cu命令行子命令的定义;
  • README.md:Bend 编译为独立 C/CUDA 文件的说明与代码生成器成熟度提示。

【免费下载链接】BendA massively parallel, high-level programming language项目地址: https://gitcode.com/GitHub_Trending/be/Bend

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询