关于 moonbit-c-binding
moonbit-c-binding 是帮助 MoonBit 开发者将 C 库绑定到 MoonBit 的技能,覆盖原生 FFI、所有权处理、C 存根和安全封装。它还提供测试及 AddressSanitizer 验证流程。
主要功能
- 01映射 C 与 MoonBit 类型
- 02配置原生构建和链接
- 03编写 FFI 声明及 C 存根
- 04处理指针所有权与资源生命周期
- 05通过测试和 AddressSanitizer 验证
适用场景
- 01为 MoonBit 项目接入 C 库
- 02为原生 FFI 封装处理回调与字符串
- 03排查绑定中的内存问题
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ moonbitlang/ skills --skill moonbit-c-binding安装后在 Claude Code 中输入/moonbit-c-binding确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
references/asan-validation.mdcallbacks.mdincluding-c-sources.mdownership-and-memory.mdscripts/.gitignorerun-asan.py详细介绍
MoonBit C 绑定指南
使用原生 FFI 将任意 C 库绑定到 MoonBit 的分步工作流。
适用场景
在以下情况下使用此技能:
- 为 C 库添加
extern "c" fn声明 - 编写 C 桩文件(
moonbit.h、MOONBIT_FFI_EXPORT) - 为原生构建配置
moon.pkg(native-stub、link.native) - 为 FFI 参数选择
#borrow或所有权转移 - 使用外部对象和终结器封装 C 句柄
- 实现回调跳板(闭包或
FuncRef) - 在 MoonBit(UTF-16)和 C(UTF-8)之间转换字符串
- 运行 AddressSanitizer,检查绑定中的内存问题
计划模式说明
如果你处于计划模式,并且正在为 moonbit-c-binding 工作创建计划:
- 在最终计划文件开头,明确要求执行 Agent 在开始实现前阅读 moonbit-c-binding 技能
- 该指令应如下所示:
**Before starting implementation:** Use the Skill tool to load the moonbit-c-binding skill, which provides comprehensive guidance on FFI declarations, ownership annotations, C stubs, and AddressSanitizer validation. - 这样可以确保执行 Agent 能够访问此技能中记录的所有关键模式和工作流
类型映射
编写任何声明前,先将 C 类型映射为 MoonBit 类型。
| C 类型 | MoonBit 类型 | 说明 |
|---|---|---|
int、int32_t | Int | 32 位有符号整数 |
uint32_t | UInt | 32 位无符号整数 |
int64_t | Int64 | 64 位有符号整数 |
uint64_t | UInt64 | 64 位无符号整数 |
float | Float | 32 位浮点数 |
double | Double | 64 位浮点数 |
bool | Bool | 在 C ABI 中作为 int32_t 传递(不是 C99 _Bool) |
uint8_t、char | Byte | 单字节 |
void | Unit | 仅用于返回类型 |
void *(不透明,由 GC 管理) | type Handle(不透明) | 带终结器的外部对象 |
void *(不透明,由 C 管理) | 带 #external 注解的 type Handle | 不由 GC 跟踪;生命周期由 C 管理 |
const uint8_t *、uint8_t * | Bytes 或 FixedArray[Byte] | 如果 C 不会存储它,则使用 #borrow |
const char *(UTF-8 字符串) | Bytes | 由运行时以空字符结尾;直接传递给 C |
struct *(较小且无需清理) | struct Foo(Bytes) | 将值视为 Bytes 的模式 |
struct *(需要清理) | type Foo(不透明) | 带终结器的外部对象 |
int(枚举/标志) | UInt、Int 或常量 enum | enum Foo { A = 0; B = 1; C = 10 } 映射为 int32_t |
| 回调函数指针 | FuncRef[...] 或闭包 | 参见 @references/callbacks.md |
输出 int * | Ref[Int] | 借用该 Ref |
工作流
按顺序完成以下 4 个阶段。
阶段 1:项目设置
设置 moon.mod.json 和 moon.pkg,以便进行原生编译。
模块配置(moon.mod.json): 添加 "preferred-target": "native",使 moon build、moon test 和 moon run 默认使用原生后端:
{
"preferred-target": "native"
}
包配置(moon.pkg):
options(
"native-stub": ["stub.c"],
targets: {
"ffi.mbt": ["native"]
},
)
关键字段:
| 字段 | 用途 |
|---|---|
"native-stub" | 要编译的 C 源文件。必须与 moon.pkg 位于同一目录。 |
targets | 指定 .mbt 文件适用的后端:"ffi.mbt": ["native"] |
link(native("cc-flags": ...)) | 编译标志(-I、-D)。仅用于系统库。 |
link(native("cc-link-flags": ...)) | 链接器标志(-L、-l)。仅用于系统库。 |
link(native("stub-cc-flags": ...)) | 仅用于桩文件的编译标志 |
link(native(exports: ...)) | 将 MoonBit 函数导出给 C(反向调用) |
警告 —
supported-targets: 避免使用supported-targets: ["native"]。这会阻止下游包在其他目标上构建。请改用targets指定单个文件适用的后端。
警告 —
cc/cc-flags的可移植性: 设置cc会在调试构建中禁用 TCC。设置带有-I/-L的cc-flags会破坏 Windows 可移植性。仅对系统库设置这些选项。
包含库源文件: "native-stub" 中的所有文件都必须与 moon.pkg 位于同一目录。有关包含策略(扁平化、仅头文件、系统库链接),请参见 @references/including-c-sources.md。
阶段 2:FFI 层
同时编写 extern 声明和 C 存根。将 extern 声明设为私有;在阶段 3 中公开安全包装器。extern "c" 和 extern "C" 都有效——选择一种大小写并保持一致(例如,如果也要面向 JS,则与 extern "js" 保持一致)。
外部对象模式(带清理的 C 句柄,由 GC 管理):
// ffi.mbt (gated to native in targets)
///|
type Parser // opaque type backed by external object
///|
extern "c" fn ts_parser_new() -> Parser = "moonbit_ts_parser_new"
///|
#borrow(parser)
extern "c" fn ts_parser_language(parser : Parser) -> Language = "moonbit_ts_parser_language"
// stub.c
#include "tree_sitter/api.h"
#include <moonbit.h>
typedef struct { TSParser *parser; } MoonBitTSParser;
static void moonbit_ts_parser_destroy(void *ptr) {
ts_parser_delete(((MoonBitTSParser *)ptr)->parser);
// Do NOT free ptr -- GC manages the container
}
MOONBIT_FFI_EXPORT
MoonBitTSParser *moonbit_ts_parser_new(void) {
MoonBitTSParser *p = (MoonBitTSParser *)moonbit_make_external_object(
moonbit_ts_parser_destroy, sizeof(TSParser *)
);
p->parser = ts_parser_new();
return p;
}
#external 注解模式(C 指针,生命周期由 C 管理):
当 C 完全管理指针的生命周期(无需 GC 清理)时,使用 #external 注解类型。指针以原始 void* 形式传递,不进行引用计数:
///|
#external
type RawPtr // void*, not GC-tracked
///|
extern "c" fn raw_create() -> RawPtr = "lib_create"
///|
extern "c" fn raw_destroy(ptr : RawPtr) = "lib_destroy"
#external 是一种注解(类似 #borrow 和 #owned)——它应单独占一行,放在 type 声明之前,而不是与声明写在同一行。
不需要 C 存根包装器或 moonbit_make_external_object——MoonBit extern 会直接调用 C 函数。当 C API 有显式的创建/销毁函数,且你想手动控制生命周期时,请使用此模式。
所有权注解:
| 注解 | 使用场景 |
|---|---|
#borrow(param) | C 只在调用期间读取,不存储引用 |
#owned(param) | 所有权转移给 C;使用完毕后,C 必须调用 moonbit_decref |
规则:
- 为每个非基本类型参数添加
#borrow或#owned注解。 - 基本类型(
Int、UInt、Bool、Double等)按值传递——无需注解。 - 如果不确定 C 是否会存储引用,请勿使用
#borrow。 - 对于 C 会写回值的输出参数,使用带
#borrow的Ref[T]。
有关所有权语义的详细信息,请参阅 @references/ownership-and-memory.md。
跨 FFI 的字符串转换:
MoonBit Bytes 由运行时以 null 结尾,因此可以直接传递给接受 const char * 的 C 函数。反向转换(将 C 字符串转换为 MoonBit)时,使用 moonbit_make_bytes + memcpy:
// C side: return a C string as MoonBit Bytes
MOONBIT_FFI_EXPORT
moonbit_bytes_t moonbit_get_name(void *handle) {
const char *str = lib_get_name(handle);
int32_t len = strlen(str);
moonbit_bytes_t bytes = moonbit_make_bytes(len, 0);
memcpy(bytes, str, len);
return bytes; // if str was malloc'd, free(str) before returning
}
// MoonBit side: decode UTF-8 Bytes to String
// Requires import "moonbitlang/core/encoding/utf8" in moon.pkg
///|
pub fn get_name(handle : Handle) -> String {
@utf8.decode_lossy(get_name_ffi(handle))
}
值即 Bytes 模式(小型结构体,无需清理):
MOONBIT_FFI_EXPORT
void *moonbit_settings_new(void) {
return moonbit_make_bytes(sizeof(settings_t), 0);
}
///|
struct Settings(Bytes) // backed by GC-managed Bytes, no finalizer
moonbit.h 核心 API:
| API | 用途 |
|---|---|
moonbit_make_external_object(finalizer, size) | 带清理终结器、由 GC 跟踪的对象 |
moonbit_make_bytes(len, init) | 由 GC 管理的字节数组(MoonBit Bytes) |
moonbit_incref(ptr) | 防止 GC 回收由 C 持有的对象 |
moonbit_decref(ptr) | 释放 C 持有的引用(与 incref 配对使用) |
Moonbit_array_length(arr) | 由 GC 管理的数组或 Bytes 的长度 |
MOONBIT_FFI_EXPORT | 所有导出函数都必须使用的宏 |
完整 API 请查阅 $MOON_HOME/lib/moonbit.h(MOON_HOME 默认为 ~/.moon)。
阶段 3:MoonBit API
基于原始 extern 构建安全的公开包装器。
类型声明:
///|
type Parser // opaque, backed by external object (has finalizer)
///|
struct Settings(Bytes) // value type, backed by GC-managed Bytes
///|
struct Node(Bytes) // small value struct
安全的构造函数和方法:
///|
pub fn Parser::new() -> Parser {
ts_parser_new()
}
///|
pub fn Parser::set_language(self : Parser, language : Language) -> Bool {
ts_parser_set_language(self, language)
}
错误映射:
///|
pub fn result_from_status(status : Int) -> Unit raise {
if status < 0 {
raise MyLibError(status)
}
}
有关回调模式(FuncRef、闭包、跳板函数),请参阅 @references/callbacks.md。
阶段 4:测试
moon test --target native -v
使用 AddressSanitizer 运行以捕获内存错误:
python3 scripts/run-asan.py \
--repo-root <project-root> \
--pkg moon.pkg \
--pkg main/moon.pkg
详情请参阅 @references/asan-validation.md。
决策表
| 情况 | 模式 | 关键操作 |
|---|---|---|
| C 只在调用期间读取指针 | #borrow(param) | C 中不调用 decref |
| C 获取指针的所有权 | #owned(param) | C 必须调用 moonbit_decref |
| C 句柄需要在 GC 时清理 | 外部对象 + 终结器 | moonbit_make_external_object |
| C 指针,生命周期由 C 管理 | type 上的 #external 注解 | 不进行 GC 跟踪;显式调用 C 销毁函数 |
| 小型 C 结构体,无需清理 | 值即 Bytes | moonbit_make_bytes + struct Foo(Bytes) |
| C 失败时返回 null | 可空包装器 | 检查 null,返回 Option 或引发错误 |
| 带数据参数的回调 | FuncRef + Callback 技巧 | 参阅 @references/callbacks.md |
| 不带数据参数的回调 | 仅 FuncRef | 参阅 @references/callbacks.md |
| C 字符串(UTF-8)输出 | 跨 FFI 使用 Bytes | 在 C 中使用 moonbit_make_bytes + memcpy;在 MoonBit 中使用 @utf8.decode_lossy |
输出参数(int *result) | 带 #borrow 的 Ref[T] | C 写入 Ref,MoonBit 读取 .val |
常见陷阱
-
C 会保存指针时使用
#borrow。 GC 可能会在 C 持有过期引用时回收对象。仅在调用范围内访问时才借用。 -
忘记对拥有所有权的参数调用
moonbit_decref。 每个非借用、非原始类型的参数都会将所有权转移给 C。忘记递减引用会导致内存泄漏。 -
对外部对象容器调用
free()。 容器由 GC 管理。终结器只应释放内部的 C 资源。 -
对包含内部指针的结构体使用
moonbit_make_bytes。 字节对象没有终结器,因此内部堆分配会泄漏。请改用外部对象。 -
在调用回调前忘记调用
moonbit_incref。 当 C 回调进入 MoonBit 时,GC 可能会运行。调用前增加 MoonBit 管理对象的引用计数;调用后再递减。 -
忘记使用
MOONBIT_FFI_EXPORT宏。 没有它,MoonBit 链接器将无法识别该函数。
参考资料
@references/ownership-and-memory.md @references/callbacks.md @references/including-c-sources.md @references/asan-validation.md