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, MOONBITFFIEXPORT)
- 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:
- 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
- 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. ``
- 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 int32t 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 uint8t , uint8t |
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):
```mbt nocheck // ffi.mbt (gated to native in targets)
///| type Parser // opaque type backed by external object
///| extern "c" fn tsparsernew() -> Parser = "moonbittsparser_new"
///| #borrow(parser) extern "c" fn tsparserlanguage(parser : Parser) -> Language = "moonbittsparser_language"
// stub.c #include "tree_sitter/api.h" #include <moonbit.h>
typedef struct { TSParser *parser; } MoonBitTSParser;
static void moonbittsparserdestroy(void ptr) { tsparser_delete(((MoonBitTSParser )ptr)->parser); // Do NOT free ptr -- GC manages the container }
MOONBITFFIEXPORT MoonBitTSParser moonbittsparsernew(void) { MoonBitTSParser p = (MoonBitTSParser )moonbitmakeexternalobject( moonbittsparserdestroy, sizeof(TSParser ) ); p->parser = tsparser_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 moonbitmakeexternal_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 moonbitmakebytes + 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
}
```mbt nocheck // MoonBit side: decode UTF-8 Bytes to String // Requires import "moonbitlang/core/encoding/utf8" in moon.pkg ///| pub fn getname(handle : Handle) -> String { @utf8.decodelossy(getnameffi(handle)) }
**Value-as-Bytes pattern** (small struct, no cleanup):
MOONBITFFIEXPORT void *moonbitsettingsnew(void) { return moonbitmakebytes(sizeof(settings_t), 0); }
```mbt nocheck
///|
struct Settings(Bytes) // backed by GC-managed Bytes, no finalizer
moonbit.h core API:
| API |
Purpose |
moonbitmakeexternal_object(finalizer, size) |
GC-tracked object with cleanup finalizer |
moonbitmakebytes(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) |
Moonbitarraylength(arr) |
Length of GC-managed array or Bytes |
MOONBITFFIEXPORT |
Required macro on all exported functions |
For the full API, read $MOONHOME/lib/moonbit.h (default MOONHOME 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 resultfromstatus(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