From moonbit-skills
Guides writing MoonBit FFI bindings to C libraries: extern declarations, C stubs with moonbit.h, ownership annotations, callback trampolines, external objects with finalizers, string conversion, and AddressSanitizer validation.
How this skill is triggered — by the user, by Claude, or both
Slash command
/moonbit-skills:moonbit-c-bindingThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
Step-by-step workflow for binding any C library to MoonBit using native FFI.
Step-by-step workflow for binding any C library to MoonBit using native FFI.
Use this skill when:
extern "c" fn declarations for a C librarymoonbit.h, MOONBIT_FFI_EXPORT)moon.pkg for native builds (native-stub, link.native)#borrow vs ownership transfer for FFI parametersFuncRef)If you are in plan mode and creating a plan for moonbit-c-binding work:
**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.
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 |
Follow these 4 phases in order.
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: Avoidsupported-targets: ["native"]. It prevents downstream packages from building on other targets. Usetargetsto gate individual files instead.
Warning —
cc/cc-flagsportability: Settingccdisables TCC for debug builds. Settingcc-flagswith-I/-Lbreaks 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.
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:
#borrow or #owned.Int, UInt, Bool, Double, etc.) are passed by value — no annotation needed.#borrow.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).
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.
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.
| 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 |
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.
Forgetting moonbit_decref on owned parameters. Every non-borrowed, non-primitive parameter transfers ownership to C. Missing decrefs leak memory.
Calling free() on external object containers. The GC manages the container. Finalizers must only release the inner C resource.
Using moonbit_make_bytes for structs with inner pointers. Bytes have no finalizer, so inner heap allocations leak. Use external objects instead.
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.
Forgetting the MOONBIT_FFI_EXPORT macro. Without it, the function is invisible to the MoonBit linker.
@references/ownership-and-memory.md @references/callbacks.md @references/including-c-sources.md @references/asan-validation.md
npx claudepluginhub moonbitlang/skills --plugin moonbit-skillsEnd-to-end workflow for creating maintainable MoonBit FFI bindings for C/C++ libraries, from upstream survey to vendoring, safe API design, tests, and ASan validation.
Guides Zig and C interoperability: importing C headers with @cImport, exporting Zig functions to C, mapping types, using translate-c, and linking C libraries.
This skill should be used when the user asks about "bun:ffi", "foreign function interface", "calling C from Bun", "native libraries", "dlopen", "shared libraries", "calling native code", or integrating C/C++ libraries with Bun.