moonbit-c-binding について
moonbit-c-bindingは、ネイティブFFIを使ってCライブラリをMoonBitにバインドする開発者向けスキルです。型対応、所有権、Cスタブ、安全なラッパーに加え、テストとAddressSanitizerによる検証を案内します。
主な機能
- 01C型とMoonBit型の対応付け
- 02ネイティブビルドとリンクの設定
- 03FFI宣言とCスタブの作成
- 04ポインター所有権とリソース寿命の管理
- 05テストとAddressSanitizerによる検証
活用シーン
- 01MoonBitプロジェクトでの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 に関する作業計画を作成する場合:
- 最終的なプランファイルの冒頭に、実装を開始する前に 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. - これにより、実行エージェントがこのスキルに記載された重要なパターンとワークフローをすべて利用できます
型の対応付け
宣言を書く前に、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 | 1 バイト |
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 が無効になります。cc-flagsに-I/-Lを設定すると、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 に明示的な create/destroy 関数があり、ライフタイムを手動で管理したい場合に使用してください。
所有権アノテーション:
| アノテーション | 使用する場面 |
|---|---|
#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) | C が保持するオブジェクトが GC によって回収されるのを防ぐ |
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 の destroy 関数を明示的に呼び出す |
| 小さな C 構造体で、クリーンアップ不要 | 値を Bytes として扱う | moonbit_make_bytes + struct Foo(Bytes) |
| C が失敗時に null を返す | Nullable ラッパー | 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を使用すること。 C が古い参照を保持している間に、GC がオブジェクトを回収する可能性があります。呼び出しの間だけ有効なアクセスに限って借用してください。 -
所有権のあるパラメーターに対して
moonbit_decrefを忘れること。 借用されていないプリミティブ型以外のパラメーターは、すべて C に所有権が移ります。参照カウントの減算を忘れると、メモリリークが発生します。 -
外部オブジェクトのコンテナーに
free()を呼び出すこと。 コンテナーは GC が管理します。ファイナライザーで解放するのは、内部の C リソースだけにしてください。 -
内部にポインターを持つ構造体に
moonbit_make_bytesを使用すること。 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