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

Claude Code and agent skill by moonbitlang/moonbit-agent-guide

Trust tier 3
  • community
Source check pending

Checking that the SKILL.md file is still in the repository.

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.

Install

npx vskill@latest install moonbitlang/moonbit-agent-guide/moonbit-c-binding

SKILL.md

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

  1. 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 TypeMoonBit TypeNotes
int, int32_tInt32-bit signed
uint32_tUInt32-bit unsigned
int64_tInt6464-bit signed
uint64_tUInt6464-bit unsigned
floatFloat32-bit float
doubleDouble64-bit float
boolBoolPassed as int32_t in the C ABI (not C99 _Bool)
uint8_t, charByteSingle byte
voidUnitReturn type only
void * (opaque, GC-managed)type Handle (opaque)External object with finalizer
void * (opaque, C-managed)type Handle with #external annotationNo 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)BytesNull-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 enumenum Foo { A = 0; B = 1; C = 10 } maps to int32_t
callback function pointerFuncRef[...] or closureSee @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:

json
{
"preferred-target": "native"
}

Package configuration (`moon.pkg`):

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

Key fields:

FieldPurpose
"native-stub"C source files to compile. Must be in the same directory as moon.pkg.
targetsGate .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):

mbt nocheck
// 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"
c
// 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:

mbt nocheck
///|
#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:

AnnotationWhen 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
// 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
}
mbt nocheck
// 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):

c
MOONBIT_FFI_EXPORT
void *moonbit_settings_new(void) {
return moonbit_make_bytes(sizeof(settings_t), 0);
}
mbt nocheck
///|
struct Settings(Bytes) // backed by GC-managed Bytes, no finalizer

`moonbit.h` core API:

APIPurpose
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_EXPORTRequired 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:

mbt nocheck
///|
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:

mbt nocheck
///|
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:

mbt nocheck
///|
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
bash
moon test --target native -v

Run with AddressSanitizer to catch memory bugs:

bash
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

SituationPatternKey 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 GCExternal object + finalizermoonbit_make_external_object
C pointer, C manages lifetime#external annotation on typeNo GC tracking; call C destroy explicitly
Small C struct, no cleanupValue-as-Bytesmoonbit_make_bytes + struct Foo(Bytes)
C returns null on failureNullable wrapperCheck null, return Option or raise error
Callback with data parameterFuncRef + Callback trickSee @references/callbacks.md
Callback without data parameterFuncRef onlySee @references/callbacks.md
C string (UTF-8) outputBytes across FFImoonbit_make_bytes + memcpy in C; @utf8.decode_lossy in MoonBit
Output parameter (int *result)Ref[T] with #borrowC 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.
  1. Forgetting `moonbit_decref` on owned parameters. Every non-borrowed, non-primitive parameter transfers ownership to C. Missing decrefs leak memory.
  1. Calling `free()` on external object containers. The GC manages the container. Finalizers must only release the inner C resource.
  1. Using `moonbit_make_bytes` for structs with inner pointers. Bytes have no finalizer, so inner heap allocations leak. Use external objects instead.
  1. 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.
  1. 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

Security

  • Security scanPassed, 100 of 100Pattern checks on the skill's files.
  • Model reviewPassed

Quality evaluation

  • Task evaluationNot runNo evaluation yet. Run one in Skill Studio.

Source

Popularity

Repository stars
54
Repository forks
7
7-day trend score
20

Stars and forks are for the whole GitHub repository, not this skill alone. Refreshed 21 Mar 2026, 7 months ago.

Works with

Any agent vskill installs to: 56 targets in all. These 39 have a page on this site.

  • Claude Code
  • Cursor
  • GitHub Copilot
  • Windsurf
  • Codex
  • Gemini CLI
  • Cline
  • Amp
  • Roo Code
  • Goose
  • Continue
  • Kilo Code
  • Junie
  • Trae
  • OpenHands
  • Qwen Code
  • AI Agent
  • Tabnine Chat
  • Sourcegraph Cody
  • Amazon Q
  • Firebase Genkit
  • Sweep
  • Devin
  • Phind
  • Mentat
  • Claude Desktop
  • Aider
  • GPT Pilot
  • Plandex
  • bolt.new
  • v0
  • Replit Agent
  • Anse
  • TypingMind
  • BetterChatGPT
  • LibreChat
  • Warp AI
  • Pieces
  • Fig AI
Embed badge
vskill VERIFIED badge
[![vskill VERIFIED](https://verified-skill.com/api/v1/skills/moonbitlang/moonbit-agent-guide/moonbit-c-binding/badge)](https://verified-skill.com/skills/moonbitlang/moonbit-agent-guide/moonbit-c-binding)

Something wrong with this skill or its page? Tell us.