外部函数接口 (FFI)
外部函数接口 (FFI)
我们已经介绍的是纯粹的计算。在现实中,需要与真实世界互动。然而,对于每个后端(C、JS、Wasm、WasmGC),“世界”是不同的,并且基于运行时(Wasmtime、Deno、浏览器等)。
后端
MoonBit 目前有五个后端:
Wasm
Wasm GC
JavaScript
C
LLVM(实验性)
Wasm
Wasm GC
JavaScript
C
C 后端会生成一个 C 文件。MoonBit 工具链还会编译项目并根据 配置 生成可执行文件。
LLVM
声明外部类型
您可以利用 extern 属性,像这样声明一个外部类型:
#external
type ExternalRef
Wasm & Wasm GC
JavaScript
C
这将被解释为 void*。
声明外部函数
要和外部世界互动,您可以声明外部函数。
备注
MoonBit 不支持多态外部函数。
Wasm & Wasm GC
JavaScript
C
您可以通过函数名称导入一个外部函数:
extern "C" fn put_char(ch : UInt) = "function_name"
如果一个包需要动态链接外部 C 库,可以在它的 moon.pkg.json 里添加 cc-link-flags。它会被直接传递给 C 编译器。
{
// ...
"link": {
"native": {
"cc-link-flags": "-l
}
},
// ...
}
要定义 C 胶水函数来连接 C 与 MoonBit,可以在一个包中引入一些 C 胶水文件,并向 moon.pkg.json 文件加入下面的内容:
{
// ...
"native-stub": [
// <包含胶水函数的 C 文件列表>
],
// ...
}
你可能会想要 #include "moonbit.h",这个头文件包含了 MoonBit 的 C FFI 接口中的类型定义和一些实用的辅助函数。这个头文件自身通常位于 ~/.moon/include,如果想要了解 moonbit.h 中有哪些可用的定义,可以直接查看它的内容。
类型
当声明函数时,您需要确保函数签名与实际的外部函数相对应。当一个函数不返回任何值(例如 void)时,忽略函数声明中的返回类型注释。下面的表格展示了一些 MoonBit 类型的底层表示:
Wasm
Wasm GC
JavaScript
C
MoonBit type
ABI
Bool
int32_t
Int
int32_t
UInt
uint32_t
Int64
int64_t
UInt64
uint64_t
Float
float
Double
double
constant enum
int32_t
abstract type (type T)
pointer (must be valid MoonBit object)
external type (#external type T)
void*
FixedArray[Byte]/Bytes
uint8_t*
FixedArray[T]
T*
FuncRef[T]
Function pointer
备注
如果 FuncRef[T] 中的 T 的返回类型是 Unit,那么它指向一个返回 void 的函数。
上表中未提及的类型不具有稳定的二进制接口,请尽量避免依赖它们的实际二进制表示。
回调
有时,我们想要将 MoonBit 函数作为回调传递给外部接口。在 MoonBit 中,可以定义闭包。根据 MDN 的定义:
闭包是由捆绑起来(封闭的)的函数和函数周围状态(词法环境)的引用组合而成。换言之,闭包让函数能访问它的外部作用域。在 JavaScript 中,闭包会随着函数的创建而同时创建。
在一些场合,我们希望将传递一个回调函数,其不捕获任何自由变量。为此,MoonBit 提供了一个特殊的类型 FuncRef[T],它表示类型为 T 的无捕获函数。类型为 FuncRef[T] 的值必须是类型为 T 的无捕获函数,否则会产生 类型错误 。
在其他情况下,MoonBit 函数参数会被表示为一个函数和一个包含周围状态的对象。
Wasm & Wasm GC
JavaScript
C
有些 C 函数允许在回调函数之外额外提供一些附加数据。例如,假设我们有下面的 C 函数:
void register_callback(void (callback)(void), void *data);
通过一个小技巧,我们可以向这个 C 函数传递 MoonBit 中的闭包:
extern "C" fn register_callback_ffi(
call_closure : FuncRef[(() -> Unit) -> Unit],
closure : () -> Unit
) = "register_callback"
fn register_callback(callback : () -> Unit) -> Unit {
register_callback_ffi(
fn (f) { f() },
callback
)
}
自定义常量枚举的整数表示
在所有后端,常量枚举(所有构造器都没有参数的枚举)都会被编译成整数。在此基础上,MoonBit 允许用户自定义常量枚举的构造器的整数表达式。只需在构造器的声明后加上 = <整数字面量> 即可:
enum SpecialNumbers {
Zero = 0
One
Two
Three
Ten = 10
FourtyTwo = 42
}
如果一个构造器没有用户指定的整数值,默认的值是上一个构造器的值加一。(第一个构造器的默认值是 0)。自定义整数表示的功能在绑定一些 C 库里的 flag 是很有用。
导出函数
对于既不是方法也不是多态的公开函数,可以通过配置 链接选项 中的 exports 字段来导出它们。
{
"link": {
"
"exports": [ "add", "fib:test" ]
}
}
}
上述例子中导出函数 add 和 fib,其中 fib 会被导出为 test。
Wasm & Wasm GC
JavaScript
C
备注
这仅对配置它的包有效,即它不会影响下游包。
目前不支持重命名导出的函数。
生命周期管理
MoonBit 是一门具有垃圾回收的编程语言。因此在处理外部对象或将 MoonBit 对象传递给宿主时,必须牢记生命周期管理。目前,MoonBit 对 Wasm 后端和 C 后端使用引用计数。对于 Wasm GC 后端和 JavaScript 后端,复用运行时的垃圾回收机制。
外部对象的生命周期管理
在 MoonBit 中处理来自外部的对象和资源时,需要及时释放这些外部对象占用的内存和资源以避免泄漏。
备注
仅限 C 后端
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 对象的生命周期管理
当通过函数将 MoonBit 对象传递给宿主时,必须注意 MoonBit 对象本身的生命周期管理。如前所述,MoonBit 的 Wasm 后端和 C 后端使用编译器优化的引用计数来管理对象的生命周期。为了避免内存错误或泄漏,FFI 函数必须正确维护 MoonBit 对象的引用计数。
备注
仅限 C 后端和 Wasm 后端。
引用计数的调用约定
MoonBit 的引用计数默认使用被调用者持有所有权的调用约定。也就是说,被调用的函数需要调用 moonbit_decref 函数来释放它的参数。如果参数被多次使用,被调用的函数需要调用 moonbit_incref 函数来增加引用计数。下面是不同场合下维护正确引用计数需要做的操作:
场合
操作
读取字段/元素
什么都不做
存储进数据结构
调用 incref
作为参数传递给 MoonBit 函数
调用 incref
作为参数传递给其他外部函数
什么都不做
作为返回值被返回
什么都不做
作用域结束(且没有返回)
调用 decref
下面的例子是一个正确维护引用计数的、标准的打开文件的 open 函数的绑定:
extern "C" fn open(filename : Bytes, flags : Int) -> Int = "open_ffi"
int open_ffi(moonbit_bytes_t filename, int flags) {
int fd = open(filename, flags);
moonbit_decref(filename);
return fd;
}
被管理的类型
下面的类型不是分配在堆上的,不需要管理生命周期:
内置数字类型,例如 Int 和 Double
常量枚举(所有构造器都不带参数的枚举)
下面的类型总是分配在堆上的,并且需要引用计数:
FixedArray[T], Bytes and String
抽象类型(type T)
外部类型(#external type T)也被分配在堆上,但它们表示外部指针,因此 MoonBit 不会对它们执行任何引用计数操作。
struct/有参数的 enum 的内存表示是不稳定的。
borrow 和 owned 标记
通过 FFI 传递参数时,参数的所有权可能会被保留,也可能不会。#borrow 和 #owned 标记可以用来指定这两种情况。
警告
我们正在将默认语义从 #owned 迁移到 #borrow。
#borrow 和 #owned 的语法如下:
#borrow(params..)
extern "C" fn c_ffi(..) -> .. = ..
其中,params 是 c_ffi 的参数列表的一个子集。
被 #borrow 标记的参数会使用基于借用的调用约定,也就是说,被调用的函数不需要对这些参数调用 decref。如果 FFI 函数只会在运行期间读取它的参数(不会返回它的参数或把它们存进数据结构),那么使用 #borrow 标记可以直接绑定 FFI 函数。上面的 open 的例子可以使用 #borrow 标记来简化:
#borrow(filename)
extern "C" fn open(filename : Bytes, flags : Int) -> Int = "open"
这里不再需要 C 胶水函数:我们直接绑定到了原版 open。#borrow 标记保证了这个简化的版本依然能正确维护引用计数。
即使由于某些其他原因依然需要写 C 胶水函数,#borrow 标记也可以用于简化 C 胶水内部的生命周期管理。下面是不同场合下,正确维护 借用参数 的引用计数需要做的操作:
场合
操作
读取字段/元素
什么都不做
存储进数据结构
调用 incref
作为参数传递给 MoonBit 函数
调用 incref
传递给其他 C 函数 / #borrow MoonBit 函数
什么都不做
作为返回值被返回
调用 incref
作用域结束(且没有返回)
什么都不做
#owned 语义与之相反,表示参数会被 FFI 函数存储,并且需要在稍后手动调用 decref。一个使用场景是注册回调函数,其中闭包会被 持有所有权。
原生后端链接选项
cc 选项用于指定用于编译 moonc 生成的 C 源文件的编译器。它可以是编译器的完整路径,也可以是通过 PATH 环境变量可访问的简单名称。
{
"link": {
"native": {
"cc": "/usr/bin/gcc13"
}
}
}
cc-flags 选项用于覆盖传递给编译器的默认标志。例如,您可以使用以下标志来定义一个名为 MOONBIT 的宏。
{
"link": {
"native": {
"cc-flags": "-DMOONBIT"
}
}
}
cc-link-flags 选项用于覆盖传递给链接器的默认标志。由于链接器是通过编译器驱动程序调用的(例如,通过 cc 而不是 ld,通过 cl 而不是 link),因此在传递特定选项时,应该使用 -Wl, 或 /link 前缀。
以下示例从生成的二进制文件中剥离符号信息。
{
"link": {
"native": {
"cc-link-flags": "-s"
}
}
}
stub-cc 选项与 cc 类似,但控制用于编译存根的编译器。虽然它可以与 cc 不同,但不建议这样做,应仅用于调试目的。因此,我们强烈建议同时指定 cc 和 stub-cc 并使它们保持一致,以避免潜在冲突。
stub-cc-flags 选项类似于 cc-flags。它仅对存根编译有效。
stub-cc-link-flags 与 cc-link-flags 类似,但有微妙的区别。通常,存根被编译为目标文件,并与从 moonc 生成的 C 源文件的目标文件链接。这种链接仅由前面提到的 cc-flags 和 cc-link-flags 控制。然而,在特定模式下,存根目标文件将有一个单独的链接过程,在该过程中,stub-cc-link-flags 将生效。
原生后端的默认 C 编译器和编译器标志
以下是对 compiler_flags.rs 的简要总结。
C 编译器
在 PATH 中从上到下搜索以下项目。
cl
gcc
clang
cc
内部的 tcc
对于类似 GCC 的编译器,默认的编译和链接命令如下。[] 用于指示某些模式下可能不存在的标志。
cc -o $target -I$MOON_HOME/include -L$MOON_HOME/lib [-g] [-shared -fPIC]
-fwrapv -fno-strict-aliasing (-O2|-Og) [$MOON_HOME/lib/libmoonbitrun.o]
$sources -lm $cc_flags $cc_link_flags
对于 MSVC,默认的编译和链接命令如下。
cl (/Fo|/Fe)$target -I$MOON_HOME/include [/LD] /utf-8 /wd4819 /nologo (/O2|/Od)
/link /LIBPATH:$MOON_HOME/lib
完成第二阶段
评论