当前位置: 首页 > news >正文

MoonBit Pearls Vol.06: MoonBit C-FFI 开发指南

MoonBit C-FFI 开发指南


引言

MoonBit 是一门现代化函数式编程语言,它有着严谨的类型系统,高可读性的语法,以及专为AI设计的工具链等。然而,重复造轮子并不可取。无数经过时间检验、性能卓越的库是用C语言(或兼容C ABI的语言,如C++、Rust)编写的。从底层硬件操作到复杂的科学计算,再到图形渲染,C的生态系统蕴含着无尽的宝藏。

那么,我们能否让现代的MoonBit与这些经典的C库协同工作,让MoonBit也能使用C世界的强大工具呢?答案是肯定的。通过C语言外部函数接口(C Foreign Function Interface, C-FFI),MoonBit拥有调用C函数的能力,将新旧两个世界连接起来。

这篇文章将带你一步步探索MoonBit C-FFI的机理。我们将通过一个具体的例子——为一个C语言编写的数学库 mymath 创建MoonBit绑定——来学习如何处理不同类型的数据、指针、结构体乃至函数指针。最后,我们会讲述一个高级主题—GC。

预先准备

要连接到任何一个C库,我们需要知道这个C库的头文件的函数,如何找到头文件,如何找到库文件。对于我们这篇文章的任务来说。C语言数学库的头文件就是 mymath.h​。它定义了我们希望在MoonBit中调用的各种函数和类型。我们这里假设我们的mymath​是安装到系统上的,编译时使用-I/usr/inluclude​来找到头文件,使用-L/usr/lib -lmymath​来链接库,下面是我们的mymath.h​的部分内容。


// mymath.h// --- 基础函数 ---void print_version();int version_major();int is_normal(double input);// --- 浮点数计算 ---float sinf(float input);float cosf(float input);float tanf(float input);double sin(double input);double cos(double input);double tan(double input);// --- 字符串与指针 ---int parse_int(char* str);char* version();int tan_with_errcode(double input, double* output);// --- 数组操作 ---int sin_array(int input_len, double* inputs, double* outputs);int cos_array(int input_len, double* inputs, double* outputs);int tan_array(int input_len, double* inputs, double* outputs);// --- 结构体与复杂类型 ---typedef struct {double real;double img;} Complex;Complex* new_complex(double r, double i);void multiply(Complex* a, Complex* b, Complex** result);void init_n_complexes(int n, Complex** complex_array);// --- 函数指针 ---void for_each_complex(int n, Complex** arr, void (*call_back)(Complex*));

基础准备 (The Groundwork)

在编写任何 FFI 代码之前,我们需要先搭建好 MoonBit 与 C 代码之间的桥梁。

编译到 Native

首先,MoonBit 代码需要被编译成原生机器码。这可以通过以下命令完成:


moon build --target native

这个命令会将你的 MoonBit 项目编译成 C 代码,并使用系统上的 C 编译器(如 GCC 或 Clang)将其编译为最终的可执行文件。编译后的 C 文件位于 target/native/release/build/​ 目录下,按包名存放在相应的子目录中。例如,main/main.mbt​ 会被编译到 target/native/release/build/main/main.c​。

配置链接

仅仅编译是不够的,我们还需要告诉 MoonBit 编译器如何找到并链接到我们的 mymath​ 库。这需要在项目的 moon.pkg.json​ 文件中进行配置。


{"supported-targets" : ["native"],"link" : {"native" : {"cc": "clang","cc-flags":  "-I/usr/include","cc-link-flags":"-L/usr/lib -lmymath"}}}
  • cc​: 指定用于编译C代码的编译器,例如 clang​ 或 gcc​。

  • cc-flags​: 编译C文件时需要的标志,通常用来指定头文件搜索路径(-I​)。

  • cc-link-flags​: 链接时需要的标志,通常用来指定库文件搜索路径(-L​)和具体要链接的库(-l​)。

同时,我们还需要一个 “胶水” C 文件,我们这里命名为 cwrap.c​,用来包含 C 库的头文件和 MoonBit 的运行时头文件。


// cwrap.c#include <mymath.h>#include <moonbit.h>

这个胶水文件也需要通过 moon.pkg.json​ 告知 MoonBit 编译器:


{// ... 其他配置"native-stub": ["cwrap.c"]}

完成这些配置后,我们的项目就已经准备好与 mymath​ 库进行链接了。

第一次跨语言调用 (The First FFI Call)

万事俱备,让我们来进行第一次真正的跨语言调用。在 MoonBit 中声明一个外部 C 函数,语法如下:


extern "C" fn moonbit_function_name(arg: Type) -> ReturnType = "c_function_name"
  • extern "C"​:告诉 MoonBit 编译器,这是一个外部 C 函数。

  • moonbit_function_name​:在 MoonBit 代码中使用的函数名。

  • "c_function_name"​:实际链接到的 C 函数的名称。

让我们用 mymath.h​ 中最简单的 version_major​ 函数来小试牛刀:


extern "C" fn version_major() -> Int = "version_major"

注意:MoonBit 拥有强大的死代码消除(DCE)能力。如果你只是声明了上面的 FFI 函数但从未在代码中(例如 main 函数)实际调用它,编译器会认为它是无用代码,并不会在最终生成的 C 代码中包含它的声明。所以,请确保你至少在一个地方调用了它!

跨越类型系统的鸿沟 (Navigating the Type System Chasm)

真正的挑战在于处理两种语言之间的数据类型差异,对于一些复杂的类型情况,需要读者有一定的C语言知识。

3.1 基本类型:(Basic Types)

对于基础的数值类型,MoonBit 和 C 之间有直接且清晰的对应关系。

MoonBit TypeC TypeNotes
Intint
Int64int64_t
UIntunsigned int
UInt64uint64_t
Floatfloat
Doubledouble
BoolintC语言标准没有原生 bool,通常用 int (0/1) 表示
Unitvoid (返回值)用于表示 C 函数没有返回值的情况
Byteuint8_t

根据这个表格,我们可以轻松地为 mymath.h 中的大部分简单函数编写 FFI 声明:


extern "C" fn print_version() -> Unit = "print_version"extern "C" fn version_major() -> Int = "version_major"// 返回值语义上是布尔值,使用 MoonBit 的 Bool 类型更清晰extern "C" fn is_normal(input: Double) -> Bool = "is_normal"extern "C" fn sinf(input: Float) -> Float = "sinf"extern "C" fn cosf(input: Float) -> Float = "cosf"extern "C" fn tanf(input: Float) -> Float = "tanf"extern "C" fn sin(input: Double) -> Double = "sin"extern "C" fn cos(input: Double) -> Double = "cos"extern "C" fn tan(input: Double) -> Double = "tan"

3.2 字符串 (Strings)

事情在遇到字符串时开始变得有趣。你可能会想当然地把 C 的 char*​ 映射到 MoonBit 的 String​,但这是一个常见的陷阱。

MoonBit​ 的 String​ 和 C 的 char*​ 在内存布局上完全不同。char*​ 是一个指向以 \0​ 结尾的字节序列的指针,而 MoonBit​ 的 String​ 是一个由 GC 管理的、包含长度信息和 UTF-16 编码数据的复杂对象。

参数传递:从 MoonBit 到 C

当我们需要将一个 MoonBit 字符串传递给一个接受 char*​ 的 C 函数时(如 parse_int​),我们需要手动进行转换。一个推荐的做法是将其转换为 Bytes​ 类型。


// 一个辅助函数,将 MoonBit String 转换为 C 期望的 null-terminated byte arrayfn string_to_c_bytes(s: String) -> Bytes {let mut arr = s.to_bytes().to_array()// 确保以 \0 结尾if arr.last() != Some(0) {arr.push(0)}Bytes::from_array(arr)}// FFI 声明,注意参数类型是 Bytes#borrow(s) // 告诉编译器我们只是借用 s,不要增加其引用计数extern "C" fn __parse_int(s: Bytes) -> Int = "parse_int"// 封装成一个对用户友好的 MoonBit 函数fn parse_int(str: String) -> Int {let s = string_to_c_bytes(str)__parse_int(s)}

#borrow标记
borrow​ 标记是一个优化提示。它告诉编译器,C函数只是"借用"这个参数,不会持有它的所有权。这可以避免不必要的引用计数操作,防止潜在的内存泄漏。

返回值:从 C 到 MoonBit

反过来,当 C 函数返回一个 char*​ 时(如 version​),情况更加复杂。我们绝对不能直接将其声明为返回 Bytes​ 或 String​:


// 错误的做法!extern "C" fn version() -> Bytes = "version"

这是因为 C 函数返回的只是一个裸指针,它缺少 MoonBit GC 所需的头部信息。直接这样转换会导致运行时崩溃。

正确的做法是,将返回的 char*​ 视为一个不透明的句柄,然后在 C “胶水” 代码中编写一个转换函数,手动将其转换为一个合法的 MoonBit 字符串。

MoonBit 侧:


// 1. 声明一个外部类型来代表 C 字符串指针#externtype CStr// 2. 声明一个 FFI 函数,它调用 C 包装器extern "C" fn CStr::to_string(self: Self) -> String = "cstr_to_moonbit_str"// 3. 声明原始的 C 函数,它返回我们的不透明类型extern "C" fn __version() -> CStr = "version"// 4. 封装成一个安全的 MoonBit 函数fn version() -> String {__version().to_string()}

C 侧 (在 cwrap.c中添加):


#include <string.h> // for strlen// 这个函数负责将 char* 正确地转换为带 GC 头的 moonbit_string_tmoonbit_string_t cstr_to_moonbit_str(char *ptr) {if (ptr == NULL) {return moonbit_make_string(0, 0);}int32_t len = strlen(ptr);// moonbit_make_string 会分配一个带 GC 头的 MoonBit 字符串对象moonbit_string_t ms = moonbit_make_string(len, 0);for (int i = 0; i < len; i++) {ms[i] = (uint16_t)ptr[i]; // 假设是 ASCII 兼容的}// 注意:是否需要 free(ptr) 取决于 C 库的 API 约定。// 如果 version() 返回的内存需要调用者释放,这里就需要 free。return ms;}

这个模式虽然初看有些繁琐,但它保证了内存安全,是处理 C 字符串返回值的标准做法。

3.3 指针的艺术:传递引用与数组 (The Art of Pointers: Passing by Reference and Arrays)

C 语言大量使用指针来实现"输出参数"和传递数组。MoonBit 为此提供了专门的类型。

单个值的"输出"参数

当 C 函数使用指针来返回一个额外的值时,如 tan_with_errcode(double input, double* output)​,MoonBit 使用 Ref[T]​ 类型来对应。


extern "C" fn tan_with_errcode(input: Double, output: Ref[Double]) -> Int = "tan_with_errcode"

Ref[T]​ 在 MoonBit 中是一个包含单个 T​ 类型字段的结构体。当它传递给 C 时,MoonBit 会传递这个结构体的地址。从 C 的角度看,一个指向 struct { T val; }​ 的指针和一个指向 T​ 的指针在内存地址上是等价的,因此可以直接工作。

数组:传递数据集合

当 C 函数需要处理一个数组时(例如 double* inputs​),MoonBit 使用 FixedArray[T]​ 类型来映射。FixedArray[T]​ 在内存中就是一块连续的 T​ 类型元素,其指针可以直接传递给 C。


extern "C" fn sin_array(len: Int, inputs: FixedArray[Double], outputs: FixedArray[Double]) -> Int = "sin_array"extern "C" fn cos_array(len: Int, inputs: FixedArray[Double], outputs: FixedArray[Double]) -> Int = "cos_array"extern "C" fn tan_array(len: Int, inputs: FixedArray[Double], outputs: FixedArray[Double]) -> Int = "tan_array"

3.4 外部类型:拥抱不透明的 C 结构体 (External Types: Embracing Opaque C Structs)

对于 C 中的 struct​,比如 Complex​,最佳实践通常是将其视为一个"不透明类型"(Opaque Type)。我们只在 MoonBit 中创建一个对它的引用(或句柄),而不关心其内部的具体字段。

这通过 #extern type​ 语法实现:


#externtype Complex

这个声明告诉 MoonBit:“存在一个名为 Complex​ 的外部类型。你不需要知道它的内部结构,只要把它当成一个指针大小的句柄来传递就行了。” 在生成的 C 代码中,Complex​ 类型会被处理成 void*​。这通常是安全的,因为所有对 Complex​ 的操作都是在 C 库内部完成的,MoonBit 侧只负责传递指针。

基于这个原则,我们可以为 mymath.h​ 中与 Complex​ 相关的函数编写 FFI:


// C: Complex* new_complex(double r, double i);// 返回一个指向 Complex 的指针,在 MoonBit 中就是返回一个 Complex 句柄extern "C" fn new_complex(r: Double, i: Double) -> Complex = "new_complex"// C: void multiply(Complex* a, Complex* b, Complex** result);// Complex* 对应 Complex,而 Complex** 对应 Ref[Complex]extern "C" fn multiply(a: Complex, b: Complex, res: Ref[Complex]) -> Unit = "multiply"// C: void init_n_complexes(int n, Complex** complex_array);// Complex** 在这里作为数组使用,对应 FixedArray[Complex]extern "C" fn init_n_complexes(n: Int, complex_array: FixedArray[Complex]) -> Unit = "init_n_complexes"

最佳实践:封装原生 FFI
直接暴露 FFI 函数会让使用者感到困惑(比如 Ref​ 和 FixedArray​)。强烈建议在 FFI 声明之上再构建一层对 MoonBit 用户更友好的 API。

// 在 Complex 类型上定义方法,隐藏 FFI 细节
fn Complex::mul(self: Complex, other: Complex) -> Complex {// 创建一个临时的 Ref 用于接收结果let res: Ref[Complex] = Ref::{ val: new_complex(0, 0) }multiply(self, other, res)res.val // 返回结果
}fn init_n(n: Int) -> Array[Complex] {// 使用 FixedArray::make 创建数组let arr = FixedArray::make(n, new_complex(0, 0))init_n_complexes(n, arr)// 将 FixedArray 转换为对用户更友好的 ArrayArray::from_fixed_array(arr)
}

3.5 函数指针:当 C 需要回调 MoonBit (Function Pointers: When C Needs to Call Back)

mymath.h​ 中最复杂的函数是 for_each_complex​,它接受一个函数指针作为参数。


void for_each_complex(int n, Complex** arr, void (*call_back)(Complex*));

一个常见的误解是试图将 MoonBit 的闭包类型 (Complex) -> Unit​ 直接映射到 C 的函数指针。这是不行的,因为 MoonBit 的闭包在底层是一个包含两部分的结构体:一个指向实际函数代码的指针,以及一个指向其捕获的环境数据的指针。

为了传递一个纯粹的、无环境捕获的函数指针,MoonBit 提供了 FuncRef​ 类型:


extern "C" fn for_each_complex(n: Int,arr: FixedArray[Complex],call_back: FuncRef[(Complex) -> Unit] // 使用 FuncRef 包装函数类型) -> Unit = "for_each_complex"

任何被 FuncRef​ 包裹的函数类型,在传递给 C 时,都会被转换成一个标准的 C 函数指针。

如何声明一个FuncRef​?只要使用let​就可以了,只要函数没有捕获外部变量,就可以声明成功。

fn print_complex(c: Complex) -> Unit { ... }fn main {let print_complex : FuncRef[(Complex) -> Unit] = (c) => print_complex(c)// ...
}

第四站:高级课题——GC管理(Advanced Topic: GC Management)

我们已经了解了大部分类型的转换问题,但还有一个非常重大的问题:内存管理。C 依赖手动的 malloc​/free​,而 MoonBit 拥有自动的垃圾回收(GC)。当 C 库创建了一个对象(如 new_complex​),谁来负责释放它?

可以不要GC吗?

一些库作者可能会选择不做GC,而是把所有的析构操作都留给用户。这种做法在一些库上有其合理性,因为有些库,例如一些高性能计算库,图形库等,为了提高性能或者稳定性,本身就会放弃掉一些GC特性,但带来的问题就是对程序员的水平要求较高。大多数库还是需要提供GC来增强用户体验的。

理想情况下,我们希望 MoonBit 的 GC 能够自动管理这些 C 对象的生命周期。MoonBit 提供了两种机制来实现这一点。

4.1 简单情况

如果 C 结构体非常简单,并且你确信它的内存布局在所有平台上都是稳定不变的,你可以直接在 MoonBit 中重新定义它。


// mymath.h: typedef struct { double real; double img; } Complex;// MoonBit:struct Complex {r: Double,i: Double}

这样做,Complex​ 就成了一个真正的 MoonBit 对象。MoonBit 编译器会自动为它管理内存,添加 GC 头。当你把它传递给 C 函数时,MoonBit 会传递一个指向其数据部分的指针,这通常是可行的。

但这种方法有很大的局限性

  • 它要求你精确知道 C 结构体的内存布局、对齐方式等,这可能很脆弱。

  • 如果 C 函数返回一个 Complex*​,你不能直接使用它。你必须像处理字符串返回值一样,编写一个 C 包装函数,将 C 结构体的数据复制到一个新创建的、带 GC 头的 MoonBit Complex​ 对象中。

因此,这种方法只适用于最简单的情况。对于大多数场景,我们推荐更健壮的析构方案。

4.2 复杂情况,使用析构函数(Finalizer) (The Complex Situation: Using Finalizers)

这是一种更通用和安全的方法。核心思想是:创建一个 MoonBit 对象来"包装"C 指针,并告诉 MoonBit 的 GC,当这个包装对象被回收时,应该调用一个特定的 C 函数(析构函数)来释放底层的 C 指针。

这个过程分为几步:

1. 在 MoonBit 中声明两种类型


#externtype C_Complex // 代表原始的、不透明的 C 指针type Complex C_Complex // 一个 MoonBit 类型,它内部包装了一个 C_Complex

type Complex C_Complex​ 是一个特殊的声明,它创建了一个名为 Complex​ 的 MoonBit 对象类型,其内部有一个字段,类型为 C_Complex​。我们可以通过 .inner()​ 方法访问到这个内部字段。

2. 在 C 中提供析构函数和包装函数

我们需要一个 C 函数来释放 Complex​ 对象,以及一个函数来创建我们带 GC 功能的 MoonBit 包装对象。

C 侧 (在 cwrap.c中添加):


// mymath 库应该提供一个释放 Complex 的函数,假设是 free_complex// void free_complex(Complex* c);// 我们需要一个 void* 版本的析构函数给 MoonBit GC 使用void free_complex_finalizer(void* obj) {// MoonBit 外部对象的布局是 { void (*finalizer)(void*); T data; }// 我们需要从 obj 中提取出真正的 Complex 指针// 假设 MoonBit 的 Complex 包装器只有一个字段Complex* c_obj = *((Complex**)obj);free_complex(c_obj); // 调用真正的析构函数, 如果mymath库提供的话// free(c_obj); // 如果是标准的 malloc 分配的}// 定义 MoonBit 的 Complex 包装器在 C 中的样子typedef struct {Complex* val;} MoonBit_Complex;// 创建 MoonBit 包装对象的函数MoonBit_Complex* new_mbt_complex(Complex* c_complex) {// `moonbit_make_external_obj` 是关键// 它创建一个由 GC 管理的外部对象,并注册其析构函数。MoonBit_Complex* mbt_complex = moonbit_make_external_obj(&free_complex_finalizer,sizeof(MoonBit_Complex));mbt_complex->val = c_complex;return mbt_complex;}

3. 在 MoonBit 中使用包装函数

现在,我们不直接调用 new_complex​,而是调用我们的包装函数 new_mbt_complex​。


// FFI 声明指向我们的 C 包装函数extern "C" fn __new_managed_complex(c_complex: C_Complex) -> Complex = "new_mbt_complex"// 原始的 C new_complex 函数返回一个裸指针extern "C" fn __new_unmanaged_complex(r: Double, i: Double) -> C_Complex = "new_complex"// 最终提供给用户的、安全的、GC 友好的 new 函数fn Complex::new(r: Double, i: Double) -> Complex {let c_ptr = __new_unmanaged_complex(r, i)__new_managed_complex(c_ptr)}

现在,当 Complex::new​ 创建的对象在 MoonBit 中不再被使用时,GC 会自动调用 free_complex_finalizer​,从而安全地释放了 C 库分配的内存。

当需要将我们管理的 Complex​ 对象传递给其他 C 函数时,只需使用 .inner()​ 方法:


// 假设有一个C函数 `double length(Complex*);`extern "C" fn length(c_complex: C_Complex) -> Double = "length"fn Complex::length(self: Self) -> Double {// self.inner() 返回内部的 C_Complex (即 C 指针)length(self.inner())}

结语 (Conclusion)

这篇文章带你从基本类型,到复杂的结构体类型,再到函数指针类型,梳理了在MoonBit中做C-FFI的流程。末尾讨论了MoonBit管理c对象的GC问题。希望对广大读者的库开发有帮助。

http://www.dtcms.com/a/352059.html

相关文章:

  • 【新启航】现场逆向抄数实战:手持 3D 扫描仪 + 移动建模 APP 的轻量化工具组合与快速响应能力
  • 三款音乐生成工具,你更喜欢哪一个?
  • 如何在pixel上验证webview的功能
  • 服务初始化
  • 基于单张图像的深度估计方法研究:利用 Hugging Face 与 FiftyOne 实现单目深度估计模型的运行与评估
  • 从零开始学MCP(7) | 实战:用 MCP 构建论文分析智能体
  • 零基础从头教学Linux(Day 20)
  • javascript 基础知识- 字面量/内置对象
  • LVGL学习
  • 【设计模式】 面向对象基础
  • K8S-Service资源对象
  • 虚拟机中kubeadim部署的k8s集群,虚拟机关机了,重新开机后集群状态能否正常恢复的两种可能(详解)
  • 114、【OS】【Nuttx】【周边】效果呈现方案解析:-print0 补充(下)
  • WeakAuras Lua Script ICC (BarneyICC) Simplified Chinese [Mini]
  • WeakAuras Lua Script (My Version)
  • 【数据分享】各地级市当年实际使用外商外资金额(2003-2021)-有缺失值
  • 【AI Agent三】工具使用设计模式
  • 系统设计(数据库/微服务)
  • 基于Python+AlphaBot 实现红外遥控且自动避障的嵌入式智能小车系统
  • Cursor 中文输出设置:繁体改为简体的方法
  • uniapp 页面favicon.ico文件不存在提示404问题解决
  • uniapp 自动升级-uni-upgrade-center
  • 家庭事务管理系统|基于java和vue的家庭事务管理系统设计与实现(源码+数据库+文档)
  • 【Python实战练习】用 Python与Pygame 打造完整的贪吃蛇小游戏
  • Elasticsearch中的设置refresh_interval
  • Linux SSH 密钥认证登录原理与配置指南
  • linux下的网络编程:TCP(传输控制协议)编程
  • 数据结构(C语言篇):(一)算法复杂度
  • 复盘一个诡异的Bug之FileNotFoundException
  • 数据结构的线性表 之 链表