MoonBit C-FFI 开发指南

引言
MoonBit 是一门现代化函数式编程语言,它有着严谨的类型系统,高可读性的语法,以及专为AI设计的工具链等。然而,重复造轮子并不可取。无数经过时间检验、性能卓越的库是用C语言(或兼容C ABI的语言,如C++、Rust)编写的。从底层硬件操作到复杂的科学计算,再到图形渲染,C的生态系统是一座蕴藏着无尽宝藏的富矿。
那么,我们能否让现代的MoonBit与这些经典的C库协同工作,让新世界的开拓者也能使用旧时代的强大工具呢?答案是肯定的。通过C语言外部函数接口(C Foreign Function Interface, C-FFI),MoonBit拥有调用C函数的能力,将新旧两个世界连接起来。
这篇文章将作为你的向导,带你一步步探索MoonBit C-FFI的奥秘。我们将通过一个具体的例子——为一个C语言编写的数学库 mymath 创建MoonBit绑定——来学习如何处理不同类型的数据、指针、结构体乃至函数指针。
预先准备
要连接到任何一个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
Int = "version_major"
注意:MoonBit 拥有强大的死代码消除(DCE)能力。如果你只是声明了上面的 FFI 函数但从未在代码中(例如
main 函数)实际调用它,编译器会认为它是无用代码,并不会在最终生成的 C 代码中包含它的声明。所以,请确保你至少在一个地方调用了它!
跨越类型系统的鸿沟 (Navigating the Type System Chasm)
真正的挑战在于处理两种语言之间的数据类型差异,对于一些复杂的类型情况,需要读者有一定的C语言知识。
3.1 基本类型:(Basic Types)
对于基础的数值类型,MoonBit 和 C 之间有直接且清晰的对应关系。
| MoonBit Type | C Type | Notes |
|---|---|---|
Int | int32_t | |
Int64 | int64_t | |
UInt | uint32_t | |
UInt64 | uint64_t | |
Float | float | |
Double | double | |
Bool | int32_t | C语言标准没有原生 bool,通常用 int32_t (0/1) 表示 |
Unit | void (返回值) | 用于表示 C 函数没有返回值的情况 |
Byte | uint8_t |
根据这个表格,我们可以轻松地为 mymath.h 中的大部分简单函数编写 FFI 声明:
extern "C" fn print_version() -> Unit
Unit = "print_version"
extern "C" fn version_major() -> Int
Int = "version_major"
// 返回值语义上是布尔值,使用 MoonBit 的 Bool 类型更清晰
extern "C" fn is_normal(input: Double
Double) -> Bool
Bool = "is_normal"
extern "C" fn sinf(input: Float
Float) -> Float
Float = "sinf"
extern "C" fn cosf(input: Float
Float) -> Float
Float = "cosf"
extern "C" fn tanf(input: Float
Float) -> Float
Float = "tanf"
extern "C" fn sin(input: Double
Double) -> Double
Double = "sin"
extern "C" fn cos(input: Double
Double) -> Double
Double = "cos"
extern "C" fn tan(input: Double
Double) -> 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 array
fn fn string_to_c_bytes(s : String) -> Bytes
string_to_c_bytes(String
s: String
String) -> Bytes
Bytes {
let mut Array[Byte]
arr = String
s.fn String::to_bytes(self : String) -> Bytes
String holds a sequence of UTF-16 code units encoded in little endian format
to_bytes().fn Bytes::to_array(self : Bytes) -> Array[Byte]
Converts a bytes sequence into an array of bytes.
Parameters:
bytes : A sequence of bytes to be converted into an array.
Returns an array containing the same bytes as the input sequence.
Example:
let bytes = b"hello"
let arr = bytes.to_array()
inspect(arr, content="[b'\\x68', b'\\x65', b'\\x6C', b'\\x6C', b'\\x6F']")
to_array()
// 确保以 \0 结尾
if Array[Byte]
arr.fn[A] Array::last(self : Array[A]) -> A?
Returns the last element of the array, or None if the array is empty.
Parameters:
array : The array to get the last element from.
Returns an optional value containing the last element of the array. The
result is None if the array is empty, or Some(x) where x is the last
element of the array.
Example:
let arr = [1, 2, 3]
inspect(arr.last(), content="Some(3)")
let empty : Array[Int] = []
inspect(empty.last(), content="None")
last() fn[T : Eq] @moonbitlang/core/builtin.op_notequal(x : T, y : T) -> Bool
!= (Byte) -> Byte?
Some(0) {
Array[Byte]
arr.fn[T] Array::push(self : Array[T], value : T) -> Unit
Adds an element to the end of the array.
If the array is at capacity, it will be reallocated.
Example
let v = []
v.push(3)
push(0)
}
(ArrayView[Byte]) -> Bytes
Bytes::fn Bytes::from_array(arr : ArrayView[Byte]) -> Bytes
Creates a new bytes sequence from a byte array.
Parameters:
array : An array of bytes to be converted.
Returns a new bytes sequence containing the same bytes as the input array.
Example:
let arr = [b'h', b'i']
let bytes = Bytes::from_array(arr)
inspect(
bytes,
content=(
#|b"hi"
),
)
from_array(Array[Byte]
arr)
}
// FFI 声明,注意参数类型是 Bytes
#borrow(s) // 告诉编译器我们只是借用 s,不要增加其引用计数
extern "C" fn __parse_int(s: Bytes
Bytes) -> Int
Int = "parse_int"
// 封装成一个对用户友好的 MoonBit 函数
fn fn parse_int(str : String) -> Int
parse_int(String
str: String
String) -> Int
Int {
let Bytes
s = fn string_to_c_bytes(s : String) -> Bytes
string_to_c_bytes(String
str)
fn __parse_int(s : Bytes) -> Int
__parse_int(Bytes
s)
}
#borrow 标记 borrow 标记是一个优化提示。它告诉编译器,C函数只是"借用"这个参数,不会持有它的所有权。这可以避免不必要的引用计数操作,防止潜在的内存泄漏。
返回值:从 C 到 MoonBit
反过来,当 C 函数返回一个 char* 时(如 version),情况更加复杂。我们绝对不能直接将其声明为返回 Bytes 或 String:
// 错误的做法!
extern "C" fn version() -> Bytes
Bytes = "version"
这是因为 C 函数返回的只是一个裸指针,它缺少 MoonBit GC 所需的头部信息。直接这样转换会导致运行时崩溃。
正确的做法是,将返回的 char* 视为一个不透明的句柄,然后在 C "胶水" 代码中编写一个转换函数,手动将其转换为一个合法的 MoonBit 字符串。
MoonBit 侧:
// 1. 声明一个外部类型来代表 C 字符串指针
#extern
type CStr
// 2. 声明一个 FFI 函数,它调用 C 包装器
extern "C" fn type CStr
CStr::to_string(self: type CStr
Self) -> String
String = "cstr_to_moonbit_str"
// 3. 声明原始的 C 函数,它返回我们的不透明类型
extern "C" fn __version() -> type CStr
CStr = "version"
// 4. 封装成一个安全的 MoonBit 函数
fn fn version() -> String
version() -> String
String {
fn __version() -> CStr
__version().fn CStr::to_string(self : CStr) -> String
to_string()
}
C 侧 (在 cwrap.c 中添加):
#include <string.h> // for strlen
// 这个函数负责将 char* 正确地转换为带 GC 头的 moonbit_string_t
moonbit_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
Double, output: struct Ref[A] {
mut val: A
}
Ref[Double
Double]) -> Int
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
Int, inputs: type FixedArray[A]
FixedArray[Double
Double], outputs: type FixedArray[A]
FixedArray[Double
Double]) -> Int
Int = "sin_array"
extern "C" fn cos_array(len: Int
Int, inputs: type FixedArray[A]
FixedArray[Double
Double], outputs: type FixedArray[A]
FixedArray[Double
Double]) -> Int
Int = "cos_array"
extern "C" fn tan_array(len: Int
Int, inputs: type FixedArray[A]
FixedArray[Double
Double], outputs: type FixedArray[A]
FixedArray[Double
Double]) -> Int
Int = "tan_array"
3.4 外部类型:拥抱不透明的 C 结构体 (External Types: Embracing Opaque C Structs)
对于 C 中的 struct,比如 Complex,最佳实践通常是将其视为一个"不透明类型"(Opaque Type)。我们只在 MoonBit 中创建一个对它的引用(或句柄),而不关心其内部的具体字段。
这通过 #extern type 语法实现:
#extern
type 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
Double, i: Double
Double) -> type Complex
Complex = "new_complex"
// C: void multiply(Complex* a, Complex* b, Complex** result);
// Complex* 对应 Complex,而 Complex** 对应 Ref[Complex]
extern "C" fn multiply(a: type Complex
Complex, b: type Complex
Complex, res: struct Ref[A] {
mut val: A
}
Ref[type Complex
Complex]) -> Unit
Unit = "multiply"
// C: void init_n_complexes(int n, Complex** complex_array);
// Complex** 在这里作为数组使用,对应 FixedArray[Complex]
extern "C" fn init_n_complexes(n: Int
Int, complex_array: type FixedArray[A]
FixedArray[type Complex
Complex]) -> Unit
Unit = "init_n_complexes"
最佳实践:封装原生 FFI 直接暴露 FFI 函数会让使用者感到困惑(比如
Ref 和FixedArray)。强烈建议在 FFI 声明之上再构建一层对 MoonBit 用户更友好的 API。// 在 Complex 类型上定义方法,隐藏 FFI 细节 fnComplex::type Complexmul(fn Complex::mul(self : Complex, other : Complex) -> Complexself:ComplexComplex,type Complexother:ComplexComplex) ->type ComplexComplex { // 创建一个临时的 Ref 用于接收结果 lettype Complexres:Ref[Complex]Ref[struct Ref[A] { mut val: A }Complex] =type ComplexRef::{struct Ref[A] { mut val: A }val:Complexnew_complex(0, 0) }fn new_complex(r : Double, i : Double) -> Complexmultiply(fn multiply(a : Complex, b : Complex, res : Ref[Complex]) -> Unitself,Complexother,Complexres)Ref[Complex]res.Ref[Complex]val // 返回结果 } fnComplexinit_n(fn init_n(n : Int) -> Array[Complex]n:IntInt) ->IntArray[type Array[T]An
Arrayis a collection of values that supports random access and can grow in size.Complex] { // 使用 FixedArray::make 创建数组 lettype Complexarr =FixedArray[Complex]FixedArray::type FixedArray[A]make(fn[T] FixedArray::make(len : Int, init : T) -> FixedArray[T]Creates a new fixed-size array with the specified length, initializing all elements with the given value.
Parameters:
length: The length of the array to create. Must be non-negative.initial_value: The value used to initialize all elements in the array.Returns a new fixed-size array of type
FixedArray[T]withlengthelements, where each element is initialized toinitial_value.Throws a panic if
lengthis negative.Example:
let arr = FixedArray::make(3, 42) inspect(arr[0], content="42") inspect(arr.length(), content="3")WARNING: A common pitfall is creating with the same initial value, for example:
let two_dimension_array = FixedArray::make(10, FixedArray::make(10, 0)) two_dimension_array[0][5] = 10 assert_eq(two_dimension_array[5][5], 10)This is because all the cells reference to the same object (the FixedArray[Int] in this case). One should use makei() instead which creates an object for each index.
n,Intnew_complex(0, 0))fn new_complex(r : Double, i : Double) -> Complexinit_n_complexes(fn init_n_complexes(n : Int, complex_array : FixedArray[Complex]) -> Unitn,Intarr) // 将 FixedArray 转换为对用户更友好的 ArrayFixedArray[Complex]Array::type Array[T]An
Arrayis a collection of values that supports random access and can grow in size.from_fixed_array(fn[T] Array::from_fixed_array(arr : FixedArray[T]) -> Array[T]Creates a new dynamic array from a fixed-size array.
Parameters:
arr: The fixed-size array to convert. The elements of this array will be copied to the new array.Returns a new dynamic array containing all elements from the input fixed-size array.
Example:
let fixed = FixedArray::make(3, 42) let dynamic = Array::from_fixed_array(fixed) inspect(dynamic, content="[42, 42, 42]")arr) }FixedArray[Complex]
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
Int,
arr: type FixedArray[A]
FixedArray[type Complex
Complex],
call_back: FuncRef[(type Complex
Complex) -> Unit
Unit] // 使用 FuncRef 包装函数类型
) -> Unit
Unit = "for_each_complex"
任何被 FuncRef 包裹的函数类型,在传递给 C 时,都会被转换成一个标准的 C 函数指针。
如何声明一个
FuncRef?只要使用let就可以了,只要函数没有捕获外部变量,就可以声明成功。fnprint_complex(fn print_complex(c : Complex) -> Unitc:ComplexComplex) ->type ComplexUnit { ... } fn main { letUnitprint_complexFuncRef[(Complex) -> Unit]: FuncRef[(FuncRef[(Complex) -> Unit]Complextype Complex) ->FuncRef[(Complex) -> Unit]UnitUnit] = (FuncRef[(Complex) -> Unit]c) =>Complexprint_complex(fn print_complex(c : Complex) -> Unitc) // ... }Complex