外部函数接口 (FFI)#
我们已经介绍的是纯粹的计算。在现实中,需要与真实世界互动。然而,对于每个后端(C、JS、Wasm、WasmGC),“世界”是不同的,并且基于运行时(Wasmtime、Deno、浏览器等)。
后端#
MoonBit 目前有五个后端:
Wasm
Wasm GC
JavaScript
C
LLVM(实验性)
Wasm 是指包含一些 MVP 之后的提案的 WebAssembly,包括:
bulk-memory-operations
multi-value
reference-types
为了更好的兼容性,init
函数会被编译成 start function,而 main
函数会被导出为 _start
。
备注
对于 Wasm 后端,所有与外部世界交互的函数都依赖于宿主环境。例如,Wasm 和 Wasm GC 后端的 println
函数依赖于导入一个函数 spectest.print_char
,它在每次调用时打印一个 UTF-16 码元。标准库中的 env
包和一些 moonbitlang/x
包依赖于 MoonBit 运行时定义的特定宿主函数。如果您想让生成的 Wasm 可移植,请避免使用它们。
Wasm GC 指的是使用垃圾回收提案的 WebAssembly,这意味着数据结构将使用引用类型(例如 struct
和 array
)表示,线性内存不会被默认使用。它还支持其他 MVP 之后的提案,包括:
multi-value
JS string builtins
为了更好的兼容性,init
函数会被编译成 start function,而 main
函数会被导出为 _start
。
备注
对于 Wasm 后端,所有与外部世界交互的函数都依赖于宿主环境。例如,Wasm 和 Wasm GC 后端的 println
函数依赖于导入一个函数 spectest.print_char
,它在每次调用时打印一个 UTF-16 码元。标准库中的 env
包和一些 moonbitlang/x
包依赖于 MoonBit 运行时定义的特定宿主函数。如果您想让生成的 Wasm 可移植,请避免使用它们。
JavaScript 后端会生成一个 JavaScript 文件,这个文件可以是一个 CommonJS 模块、ES 模块或 IIFE,具体取决于 配置。
C 后端会生成一个 C 文件。MoonBit 工具链还会编译项目并根据 配置 生成可执行文件。
LLVM 后端会生成一个对象文件。该后端是实验性的,不支持 FFI。
声明外部类型#
您可以利用 extern
关键字,像这样声明一个外部类型:
extern type ExternalRef
声明外部函数#
要和外部世界互动,您可以声明外部函数。
备注
MoonBit 不支持多态外部函数。
声明外部函数有两种方式:导入一个函数或编写一个内联函数。
您可以通过模块名和函数名导入一个函数:
fn cos(d : Double) -> Double = "math" "cos"
或者,您可以使用 Wasm 语法编写一个内联函数:
extern "wasm" fn identity(d : Double) -> Double =
#|(func (param f64) (result f64))
备注
编写内联函数时,请不要提供函数名称。
声明外部函数有两种方式:导入一个函数或编写一个内联函数。
您可以通过模块名和函数名导入一个函数,它会被解读为 {模块名}.{函数名}
。例如,
fn cos(d : Double) -> Double = "Math" "cos"
会引用函数 const cos = (d) => Math.cos(d)
或者,您可以编写一个内联函数,定义一个 JavaScript lambda:
extern "js" fn cos(d : Double) -> Double =
#|(d) => Math.cos(d)
您可以通过函数名称导入一个外部函数:
extern "C" fn put_char(ch : UInt) = "function_name"
如果一个包需要动态链接外部 C 库,可以在它的 moon.pkg.json
里添加 cc-link-flags
。它会被直接传递给 C 编译器。
{
// ...
"link": {
"native": {
"cc-link-flags": "-l<c library>"
}
},
// ...
}
要定义 C 胶水函数来连接 C 与 MoonBit,可以在一个包中引入一些 C 胶水文件,并向 moon.pkg.json
文件加入下面的内容:
{
// ...
"native-stub": [
// <包含胶水函数的 C 文件列表>
],
// ...
}
你可能会想要 #include "moonbit.h"
,这个头文件包含了 MoonBit 的 C FFI 接口中的类型定义和一些实用的辅助函数。这个头文件自身通常位于 ~/.moon/include
,如果想要了解 moonbit.h
中有哪些可用的定义,可以直接查看它的内容。
类型#
当声明函数时,您需要确保函数签名与实际的外部函数相对应。当一个函数不返回任何值(例如 void
)时,忽略函数声明中的返回类型注释。下面的表格展示了一些 MoonBit 类型的底层表示:
MoonBit type |
ABI |
---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
constant |
|
external type ( |
|
|
|
MoonBit type |
ABI |
---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
constant |
|
external type ( |
|
|
|
|
|
MoonBit type |
ABI |
---|---|
|
|
|
|
|
|
|
|
|
|
constant |
|
external type ( |
|
|
|
|
|
|
|
|
|
备注
对于数字类型,FixedArray[T]
在将来可能会被迁移到 TypedArray
。
MoonBit type |
ABI |
---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
constant |
|
abstract type ( |
pointer (must be valid MoonBit object) |
external type ( |
|
|
|
|
|
|
Function pointer |
备注
如果 FuncRef[T]
中的 T
的返回类型是 Unit
,那么它指向一个返回 void
的函数。
上表中未提及的类型不具有稳定的二进制接口,请尽量避免依赖它们的实际二进制表示。
回调#
有时,我们想要将 MoonBit 函数作为回调传递给外部接口。在 MoonBit 中,可以定义闭包。根据 MDN 的定义:
闭包是由捆绑起来(封闭的)的函数和函数周围状态(词法环境)的引用组合而成。换言之,闭包让函数能访问它的外部作用域。在 JavaScript 中,闭包会随着函数的创建而同时创建。
在一些场合,我们希望将传递一个回调函数,其不捕获任何自由变量。为此,MoonBit 提供了一个特殊的类型 FuncRef[T]
,它表示类型为 T
的无捕获函数。类型为 FuncRef[T]
的值必须是类型为 T
的无捕获函数,否则会产生 类型错误 。
在其他情况下,MoonBit 函数参数会被表示为一个函数和一个包含周围状态的对象。
对于 Wasm 后端,回调函数会被传递为 externref
,它表示宿主的一个函数。然而,将函数和捕获的数据转换为宿主的函数是非常重要的。
为此,Wasm 模块会在模块 moonbit:ffi
下导入一个函数 make_closure
。这个函数接受一个函数和一个对象,其中函数的第一个参数应该是这个对象;并且应该返回一个宿主的函数。也就是说,宿主负责做部分调用。一个可能的实现是:
{
"moonbit:ffi": {
"make_closure": (funcref, closure) => funcref.bind(null, closure)
}
}
JavaScript 支持闭包,因此这里没有什么特别需要做的。
有些 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": {
"<backend>": {
"exports": [ "add", "fib:test" ]
}
}
}
上述例子中导出函数 add
和 fib
,其中 fib
会被导出为 test
。
备注
这仅对配置它的包有效,即它不会影响下游包。
备注
这仅对配置它的包有效,即它不会影响下游包。
还有另一个 format
选项可以导出为 CommonJS 模块(cjs
)、ES 模块(esm
)或立即调用的函数表达式(iife
)。
备注
这仅对配置它的包有效,即它不会影响下游包。
目前不支持重命名导出的函数。
生命周期管理#
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
函数来增加引用计数。下面是不同场合下维护正确引用计数需要做的操作:
场合 |
操作 |
---|---|
读取字段/元素 |
什么都不做 |
存储进数据结构 |
调用 |
作为参数传递给 MoonBit 函数 |
调用 |
作为参数传递给其他外部函数 |
什么都不做 |
作为返回值被返回 |
什么都不做 |
作用域结束(且没有返回) |
调用 |
下面的例子是一个正确维护引用计数的、标准的打开文件的 open
函数的绑定:
extern "C" 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
andString
抽象类型(
type T
)
外部类型(extern type T
)也被分配在堆上,但它们表示外部指针,因此 MoonBit 不会对它们执行任何引用计数操作。
struct
/有参数的 enum
的内存表示是不稳定的。
borrow 标记#
为了正确维护引用计数,往往需要写一个胶水 C 函数来调用 moonbit_decref
。对这种情况,MoonBit 提供了 #borrow
标记来改变 C FFI 的调用约定,把引用计数的调用约定改为传递借用。#borrow
标记的语法是:
#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 胶水内部的生命周期管理。下面是不同场合下,正确维护 借用参数 的引用计数需要做的操作:
场合 |
操作 |
---|---|
读取字段/元素 |
什么都不做 |
存储进数据结构 |
调用 |
作为参数传递给 MoonBit 函数 |
调用 |
传递给其他 C 函数 / |
什么都不做 |
作为返回值被返回 |
调用 |
作用域结束(且没有返回) |
什么都不做 |