Agent Skills: MoonBit C Binding Guide

Guide for writing MoonBit bindings to C libraries using native FFI. Use when adding extern "c" declarations, writing C stubs with moonbit.h, configuring native-stub and link.native in moon.pkg, choosing #borrow/#owned ownership annotations, designing callback trampolines, wrapping C pointers with external objects and finalizers, converting strings across FFI, or validating bindings with AddressSanitizer.

UncategorizedID: moonbitlang/moonbit-agent-guide/moonbit-c-binding

Install this agent skill to your local

pnpm dlx add-skill https://github.com/moonbitlang/moonbit-agent-guide/tree/HEAD/moonbit-c-binding

Skill Files

Browse the full folder contents for moonbit-c-binding.

Download Skill

Loading file tree…

moonbit-c-binding/SKILL.md

Skill Metadata

Name
moonbit-c-binding
Description
Guide for writing MoonBit bindings to C libraries using native FFI. Use when adding extern "c" declarations, writing C stubs with moonbit.h, configuring native-stub and link.native in moon.pkg, choosing #borrow/#owned ownership annotations, designing callback trampolines, wrapping C pointers with external objects and finalizers, converting strings across FFI, or validating bindings with AddressSanitizer.

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