和 C 库交互#

MoonBit 的原生后端能够用 C ABI 直接调用各种原生库,而且调用时几乎没有额外开销。本页面将介绍如何在 MoonBit 中和使用 C ABI 的原生库交互。

绑定 C 函数#

想要在 MoonBit 里调用一个外部的 C 函数,首先需要用 extern "C" 语法绑定这个外部函数:

extern "C" fn c_lib_function(..) -> .. = "function_name"

现在,可以把 c_lib_function 当作一个普通 MoonBit 函数随意使用。在背后,c_lib_function 会被链接到名为 function_name 的符号。绑定 C 函数时不需要显式指定任何头文件,只需要保证 c_lib_function 的签名和 C 库中的 function_name 的二进制接口兼容即可。

Linking with C library#

如果一个包需要动态链接外部 C 库,可以在它的 moon.pkg.json 里添加下列内容:

{
  ...
  "link": {
    "native": {
      "cc-link-flags": "-l<c library>"
    }
  },
  ...
}

cc-link-flags 会直接被传递给 C 编译器。

和 C 交互时的二进制接口#

在绑定外部的 C 函数时,必须保证 MoonBit 中的声明和实际的函数的二进制接口兼容。下面的表格列出了各种 MoonBit 类型对应的二进制接口:

MoonBit 中的类型

对应的二进制接口

Int

int32_t

UInt

uint32_t

Int64

int64_t

UInt64

uint64_t

常量枚举

int32_t

抽象类型(type T

指针(必须指向合法的 MoonBit 对象)

外部类型(extern type T

void*

FixedArray[Byte]/Bytes

uint8_t*

FixedArray[T]

T*

上表中未提及的类型不具有稳定的二进制接口,请尽量避免依赖它们的实际二进制表示。

MoonBit 的自动内存管理系统需要在堆上的每个 MoonBit 对象的头部存储一段元数据。因此,把没有元数据的、指向外部对象的裸指针绑定到普通 MoonBit 类型是不合法的操作。如果想要在 MoonBit 中表示一个指向外部对象的裸指针,可以使用 extern type T 声明的类型。MoonBit 的内存管理系统会无视用这种语法声明的类型,因此它们不需要带有对象头。

Unit 的二进制表示目前是不稳定的。如果想要绑定一个不返回值(void)的 C 函数,就不要写任何返回类型标注。

向 C 传递回调函数#

有时候,我们需要向 C 库传递一个 MoonBit 函数作为回调。为此,MoonBit 提供了一个特殊的内建类型 FuncRef[T],它表示类型为 T 的无捕获的函数。类型为 FuncRef[T] 的值必须是无捕获的函数。在 C FFI 中,FuncRef[T] 会对应签名为 T 的函数指针。此外,FuncRef[(..) -> Unit] 会对应返回 void 的函数指针(Unit 自身的二进制接口是不稳定的)。下面是一个绑定 UNIX 的 signal 函数的例子:

enum Signal {
  SIGHUP = 1
  SIGINT = 2
  SIGQUIT = 3
} derive(Show)

typealias SignalHandler = FuncRef[(Signal) -> Unit]

extern "C" fn signal(
  signum : Signal,
  handler : SignalHandler
) -> SignalHandler = "signal"

fn init {
  signal(SIGQUIT, fn (sig) {
    println("received signal \{sig}")
  })
}

向 C 传递闭包#

有些 C 函数允许在回调函数之外额外提供一些附加数据。例如,假设我们有下面的 C 函数:

void register_callback(void (*callback)(void*), void *data);

通过一个小技巧,我们可以向这个 C 函数传递 MoonBit 中的闭包:

extern "C" fn register_callback_ffi(
  callback : FuncRef[(() -> Unit) -> Unit],
  data : () -> Unit
) = "register_callback"

fn register_callback(callback : () -> Unit) -> Unit {
  register_callback_ffi(
    fn (closure_callback) { closure_callback() },
    callback
  )
}

这里,我们把 MoonBit 闭包当作不透明的额外数据传递给了 C 函数,并使用一个无捕获的封装函数来调用这个闭包。

编写胶水 C 代码#

有些 C 函数难以用纯 MoonBit 语法来绑定。此时,可以写一些小的 C 胶水函数来连接 C 与 MoonBit。如果想要在一个包中引入一些 C 胶水,需要向 moon.pkg.json 文件加入下面的内容:

{
  ...
  "native-stub": [ <包含胶水函数的 C 文件列表> ],
  ...
}

现在,就可以在 native-sutb 中列出的文件里写 C 胶水函数,并在 MoonBit 中绑定这些胶水函数了。你很可能会想要 #include "moonbit.h",这个头文件包含了 MoonBit 的 C FFI 接口中的类型定义和一些实用的辅助函数。这个头文件自身通常位于 ~/.moon/include,如果想要了解 moonbit.h 中有哪些可用的定义,可以直接查看它的内容。

外部对象的生命周期管理#

在 MoonBit 中处理来自外部的对象和资源时,需要即使释放这些外部对象占用的内存和资源以避免泄漏。moonbit.h 中提供了一个实用的函数 moonbit_make_external_object,它可以借助 MoonBit 的自动内存管理系统来管理外部对象的生命周期:

void *moonbit_make_external_object(
  void (*finalize)(void *self),
  uint32_t payload_size
);

moonbit_make_external_object 会创建一个大小为 payload_size + sizeof(finalize) 的新的 MoonBit 对象,这个对象的内存布局如下:

| MoonBit 对象头 | ... 外部数据 | 释放资源的回调 |
                 ^
                 |
                 |_
                    `moonbit_make_external_object` 返回的指针

因此,moonbit_make_external_object 返回的指针可以直接当作指向外部数据的指针使用。当 MoonBit 的自动内存管理系统发现 moonbit_make_external_object 返回的对象生命周期已经结束时,它会以对象自身为参数,调用创建对象时提供的 finalize 函数来释放这个对象占有的外部资源。不过,finalize 一定不能 释放对象自身,因为这部分工作是由 MoonBit 运行时负责的。

在 MoonBit 侧,moonbit_make_external_object 返回的对象应当被绑定到 抽象 类型,即用 type T 语法声明的类型。这样一来,MoonBit 的内存管理系统就不会无视这个对象。

MoonBit 对象的生命周期管理#

设计仍在演进中,待补充