moonbit-c-binding

📁 moonbitlang/moonbit-agent-guide 📅 14 days ago
3
总安装量
3
周安装量
#58359
全站排名
安装命令
npx skills add https://github.com/moonbitlang/moonbit-agent-guide --skill moonbit-c-binding

Agent 安装分布

opencode 3
antigravity 3
claude-code 3
github-copilot 3
codex 3
kimi-cli 3

Skill 文档

MoonBit C Binding Guide

Step-by-step workflow for binding any C library to MoonBit using native FFI.

When To Use

Use this skill when:

  • Adding extern "c" fn declarations for a C library
  • Writing C stub files (moonbit.h, MOONBIT_FFI_EXPORT)
  • Configuring moon.pkg or moon.pkg.json for native builds (native-stub, link.native)
  • Choosing #borrow vs ownership transfer for FFI parameters
  • Wrapping C handles with external objects and finalizers
  • Implementing callback trampolines (closures or FuncRef)
  • Converting strings between MoonBit (UTF-16) and C (UTF-8)
  • Running AddressSanitizer to catch memory bugs in bindings

Plan Mode Instructions

If you are in plan mode and creating a plan for moonbit-c-binding work:

  1. In your final plan file, include an explicit instruction at the beginning that tells the executing agent to read the moonbit-c-binding skill before starting implementation
  2. The instruction should look like this:
    **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. This ensures the executing agent has access to all the critical patterns and workflows documented in this skill

Type Mapping

Map C types to MoonBit types before writing any declarations.

C Type MoonBit Type Notes
int, int32_t Int 32-bit signed
uint32_t UInt 32-bit unsigned
int64_t Int64 64-bit signed
uint64_t UInt64 64-bit unsigned
float Float 32-bit float
double Double 64-bit float
bool Bool Passed as int32_t in the C ABI (not C99 _Bool)
uint8_t, char Byte Single byte
void Unit Return type only
void * (opaque, GC-managed) type Handle (opaque) External object with finalizer
void * (opaque, C-managed) type Handle with #external annotation No GC tracking; C manages lifetime
const uint8_t *, uint8_t * Bytes or FixedArray[Byte] Use #borrow if C doesn’t store it
const char * (UTF-8 string) Bytes Null-terminated by runtime; pass directly to C
struct * (small, no cleanup) struct Foo(Bytes) Value-as-Bytes pattern
struct * (needs cleanup) type Foo (opaque) External object with finalizer
int (enum/flags) UInt, Int, or constant enum enum Foo { A = 0; B = 1; C = 10 } maps to int32_t
callback function pointer FuncRef[...] or closure See @references/callbacks.md
output int * Ref[Int] Borrow the Ref

Workflow

Follow these 4 phases in order.

Phase 1: Project Setup

Set up moon.mod.json and moon.pkg for native compilation.

Module configuration (moon.mod.json): Add "preferred-target": "native" so that moon build, moon test, and moon run default to the native backend:

{
  "preferred-target": "native"
}

Package configuration (moon.pkg):

options(
  "native-stub": ["stub.c"],
  targets: {
    "ffi.mbt": ["native"]
  },
)

Key fields:

Field Purpose
"native-stub" C source files to compile. Must be in the same directory as moon.pkg.
targets Gate .mbt files to backends: "ffi.mbt": ["native"]
link(native("cc-flags": ...)) Compile flags (-I, -D). Only for system libraries.
link(native("cc-link-flags": ...)) Linker flags (-L, -l). Only for system libraries.
link(native("stub-cc-flags": ...)) Compile flags for stub files only
link(native(exports: ...)) Export MoonBit functions to C (reverse direction)

Warning — supported-targets: Avoid supported-targets: ["native"]. It prevents downstream packages from building on other targets. Use targets to gate individual files instead.

Warning — cc/cc-flags portability: Setting cc disables TCC for debug builds. Setting cc-flags with -I/-L breaks Windows portability. Only set these for system libraries.

Including library sources: All files in "native-stub" must be in the same directory as moon.pkg. For inclusion strategies (flattening, header-only, system library linking), see @references/including-c-sources.md.

Phase 2: FFI Layer

Write extern declarations and C stubs together. Keep externs private; expose safe wrappers in Phase 3. Both extern "c" and extern "C" are valid — choose one casing and be consistent (e.g., match extern "js" if also targeting JS).

External object pattern (C handle with cleanup, GC-managed):

// 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 annotation pattern (C pointer, C-managed lifetime):

When C fully manages the pointer’s lifetime (no GC cleanup needed), annotate the type with #external. The pointer is passed as raw void* without reference counting:

///|
#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 is an annotation (like #borrow and #owned) — it goes on its own line before the type declaration, not on the same line.

No C stub wrapper or moonbit_make_external_object is needed — the MoonBit extern calls the C function directly. Use this when the C API has explicit create/destroy functions and you want manual lifetime control.

Ownership annotations:

Annotation When to use
#borrow(param) C only reads during the call, does not store a reference
#owned(param) Ownership transfers to C; C must moonbit_decref when done

Rules:

  • Annotate every non-primitive parameter as #borrow or #owned.
  • Primitives (Int, UInt, Bool, Double, etc.) are passed by value — no annotation needed.
  • If unsure whether C stores a reference, do NOT use #borrow.
  • Use Ref[T] with #borrow for output parameters where C writes a value back.

For detailed ownership semantics, see @references/ownership-and-memory.md.

String conversion across FFI:

MoonBit Bytes is null-terminated by the runtime, so it can be passed directly to C functions expecting const char *. For the reverse direction (C string to MoonBit), use 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))
}

Value-as-Bytes pattern (small struct, no cleanup):

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 core API:

API Purpose
moonbit_make_external_object(finalizer, size) GC-tracked object with cleanup finalizer
moonbit_make_bytes(len, init) GC-managed byte array (MoonBit Bytes)
moonbit_incref(ptr) Prevent GC collection of C-held object
moonbit_decref(ptr) Release C’s reference (pair with incref)
Moonbit_array_length(arr) Length of GC-managed array or Bytes
MOONBIT_FFI_EXPORT Required macro on all exported functions

For the full API, read $MOON_HOME/lib/moonbit.h (default MOON_HOME is ~/.moon).

Phase 3: MoonBit API

Build safe public wrappers over the raw externs.

Type declarations:

///|
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

Safe constructors and methods:

///|
pub fn Parser::new() -> Parser {
  ts_parser_new()
}

///|
pub fn Parser::set_language(self : Parser, language : Language) -> Bool {
  ts_parser_set_language(self, language)
}

Error mapping:

///|
pub fn result_from_status(status : Int) -> Unit raise {
  if status < 0 {
    raise MyLibError(status)
  }
}

For callback patterns (FuncRef, closures, trampolines), see @references/callbacks.md.

Phase 4: Testing

moon test --target native -v

Run with AddressSanitizer to catch memory bugs:

python3 scripts/run-asan.py \
  --repo-root <project-root> \
  --pkg moon.pkg \
  --pkg main/moon.pkg

See @references/asan-validation.md for details.

Decision Table

Situation Pattern Key Action
C reads pointer only during call #borrow(param) No decref in C
C takes ownership of pointer #owned(param) C must moonbit_decref
C handle needs cleanup on GC External object + finalizer moonbit_make_external_object
C pointer, C manages lifetime #external annotation on type No GC tracking; call C destroy explicitly
Small C struct, no cleanup Value-as-Bytes moonbit_make_bytes + struct Foo(Bytes)
C returns null on failure Nullable wrapper Check null, return Option or raise error
Callback with data parameter FuncRef + Callback trick See @references/callbacks.md
Callback without data parameter FuncRef only See @references/callbacks.md
C string (UTF-8) output Bytes across FFI moonbit_make_bytes + memcpy in C; @utf8.decode_lossy in MoonBit
Output parameter (int *result) Ref[T] with #borrow C writes into Ref, MoonBit reads .val

Common Pitfalls

  1. Using #borrow when C stores the pointer. The GC may collect the object while C holds a stale reference. Only borrow for call-scoped access.

  2. Forgetting moonbit_decref on owned parameters. Every non-borrowed, non-primitive parameter transfers ownership to C. Missing decrefs leak memory.

  3. Calling free() on external object containers. The GC manages the container. Finalizers must only release the inner C resource.

  4. Using moonbit_make_bytes for structs with inner pointers. Bytes have no finalizer, so inner heap allocations leak. Use external objects instead.

  5. Missing moonbit_incref before callback invocation. When C calls back into MoonBit, the GC may run. Incref MoonBit-managed objects before the call; decref afterward.

  6. Forgetting the MOONBIT_FFI_EXPORT macro. Without it, the function is invisible to the MoonBit linker.

References

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