mcpField
M
moonbit-c-binding

moonbit-c-binding

github.com/moonbitlang开发34252 次安装6 周前更新
代码仓库

关于 moonbit-c-binding

moonbit-c-binding 是帮助 MoonBit 开发者将 C 库绑定到 MoonBit 的技能,覆盖原生 FFI、所有权处理、C 存根和安全封装。它还提供测试及 AddressSanitizer 验证流程。

主要功能

  1. 01映射 C 与 MoonBit 类型
  2. 02配置原生构建和链接
  3. 03编写 FFI 声明及 C 存根
  4. 04处理指针所有权与资源生命周期
  5. 05通过测试和 AddressSanitizer 验证

适用场景

  1. 01为 MoonBit 项目接入 C 库
  2. 02为原生 FFI 封装处理回调与字符串
  3. 03排查绑定中的内存问题

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/moonbitlang/skills --skill moonbit-c-binding

安装后在 Claude Code 中输入/moonbit-c-binding确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

references/
asan-validation.md
callbacks.md
including-c-sources.md
ownership-and-memory.md
scripts/
.gitignore
run-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 工作创建计划:

  1. 在最终计划文件开头,明确要求执行 Agent 在开始实现前阅读 moonbit-c-binding 技能
  2. 该指令应如下所示:
    **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.
    
  3. 这样可以确保执行 Agent 能够访问此技能中记录的所有关键模式和工作流

类型映射

编写任何声明前,先将 C 类型映射为 MoonBit 类型。

C 类型MoonBit 类型说明
int、int32_tInt32 位有符号整数
uint32_tUInt32 位无符号整数
int64_tInt6464 位有符号整数
uint64_tUInt6464 位无符号整数
floatFloat32 位浮点数
doubleDouble64 位浮点数
boolBool在 C ABI 中作为 int32_t 传递(不是 C99 _Bool)
uint8_t、charByte单字节
voidUnit仅用于返回类型
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 或常量 enumenum 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 结构体,无需清理值即 Bytesmoonbit_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

常见陷阱

  1. C 会保存指针时使用 #borrow。 GC 可能会在 C 持有过期引用时回收对象。仅在调用范围内访问时才借用。

  2. 忘记对拥有所有权的参数调用 moonbit_decref。 每个非借用、非原始类型的参数都会将所有权转移给 C。忘记递减引用会导致内存泄漏。

  3. 对外部对象容器调用 free()。 容器由 GC 管理。终结器只应释放内部的 C 资源。

  4. 对包含内部指针的结构体使用 moonbit_make_bytes。 字节对象没有终结器,因此内部堆分配会泄漏。请改用外部对象。

  5. 在调用回调前忘记调用 moonbit_incref。 当 C 回调进入 MoonBit 时,GC 可能会运行。调用前增加 MoonBit 管理对象的引用计数;调用后再递减。

  6. 忘记使用 MOONBIT_FFI_EXPORT 宏。 没有它,MoonBit 链接器将无法识别该函数。

参考资料

@references/ownership-and-memory.md @references/callbacks.md @references/including-c-sources.md @references/asan-validation.md

评论

支持 Markdown