跳到主要内容

MoonBit C-FFI 开发指南

· 阅读需 17 分钟


引言

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 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
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 
(s : String) -> Bytes
string_to_c_bytes
(
String
s
:
String
String
) ->
Bytes
Bytes
{
let mut
Array[Byte]
arr
=
String
s
.
(self : String) -> Bytes

String holds a sequence of UTF-16 code units encoded in little endian format

to_bytes
().
(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
.
(self : Array[Byte]) -> Byte?

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
()
(x : Byte?, y : Byte?) -> Bool
!=
(Byte) -> Byte?
Some
(0) {
Array[Byte]
arr
.
(self : Array[Byte], value : Byte) -> 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)
}
(Array[Byte]) -> Bytes
Bytes::
(arr : Array[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"\x68\x69"
    ),
  )
from_array
(
Array[Byte]
arr
)
} // FFI 声明,注意参数类型是 Bytes #borrow(s) // 告诉编译器我们只是借用 s,不要增加其引用计数 extern "C" fn __parse_int(s:
Bytes
Bytes
) ->
Int
Int
= "parse_int"
// 封装成一个对用户友好的 MoonBit 函数 fn
(str : String) -> Int
parse_int
(
String
str
:
String
String
) ->
Int
Int
{
let
Bytes
s
=
(s : String) -> Bytes
string_to_c_bytes
(
String
str
)
(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
() -> String
version
() ->
String
String
{
() -> CStr
__version
().
(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 细节
fn 
type Complex
Complex
::
(self : Complex, other : Complex) -> Complex
mul
(
Complex
self
:
type Complex
Complex
,
Complex
other
:
type Complex
Complex
) ->
type Complex
Complex
{
// 创建一个临时的 Ref 用于接收结果 let
Ref[Complex]
res
:
struct Ref[A] {
  mut val: A
}
Ref
[
type Complex
Complex
] =
struct Ref[A] {
  mut val: A
}
Ref
::{
Complex
val
:
(r : Double, i : Double) -> Complex
new_complex
(0, 0) }
(a : Complex, b : Complex, res : Ref[Complex]) -> Unit
multiply
(
Complex
self
,
Complex
other
,
Ref[Complex]
res
)
Ref[Complex]
res
.
Complex
val
// 返回结果
} fn
(n : Int) -> Array[Complex]
init_n
(
Int
n
:
Int
Int
) ->
type Array[T]

An Array is a collection of values that supports random access and can grow in size.

Array
[
type Complex
Complex
] {
// 使用 FixedArray::make 创建数组 let
FixedArray[Complex]
arr
=
type FixedArray[A]
FixedArray
::
(len : Int, init : Complex) -> FixedArray[Complex]

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] with length elements, where each element is initialized to initial_value.

Throws a panic if length is 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.

make
(
Int
n
,
(r : Double, i : Double) -> Complex
new_complex
(0, 0))
(n : Int, complex_array : FixedArray[Complex]) -> Unit
init_n_complexes
(
Int
n
,
FixedArray[Complex]
arr
)
// 将 FixedArray 转换为对用户更友好的 Array
type Array[T]

An Array is a collection of values that supports random access and can grow in size.

Array
::
(FixedArray[Complex]) -> Array[Complex]

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]")
from_fixed_array
(
FixedArray[Complex]
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
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​就可以了,只要函数没有捕获外部变量,就可以声明成功。

fn 
(c : Complex) -> Unit
print_complex
(
Complex
c
:
type Complex
Complex
) ->
Unit
Unit
{ ... }
fn main { let
FuncRef[(Complex) -> Unit]
print_complex
FuncRef[(Complex) -> Unit]
: FuncRef[(
type Complex
Complex
FuncRef[(Complex) -> Unit]
) ->
Unit
Unit
FuncRef[(Complex) -> Unit]
]
= (
Complex
c
) =>
(c : Complex) -> Unit
print_complex
(
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 中声明两种类型

#extern
type 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: 
type C_Complex
C_Complex
) ->
type Complex
Complex
= "new_mbt_complex"
// 原始的 C new_complex 函数返回一个裸指针 extern "C" fn __new_unmanaged_complex(r:
Double
Double
, i:
Double
Double
) ->
type C_Complex
C_Complex
= "new_complex"
// 最终提供给用户的、安全的、GC 友好的 new 函数 fn
type Complex
Complex
::
(r : Double, i : Double) -> Complex
new
(
Double
r
:
Double
Double
,
Double
i
:
Double
Double
) ->
type Complex
Complex
{
let
C_Complex
c_ptr
=
(r : Double, i : Double) -> C_Complex
__new_unmanaged_complex
(
Double
r
,
Double
i
)
(c_complex : C_Complex) -> Complex
__new_managed_complex
(
C_Complex
c_ptr
)
}

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

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

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

结语 (Conclusion)

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