Skip to content
Back to skills

003 Name Skill 5cb3d28f

ASecurity

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 or moon.pkg.json, 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.

  • 9 stars
  • 0 votes
  • 0 copies
  • 0 views
  • Added October 11, 2026
developmentpythongobashnodetestingapibackend

Works with

  • api
  • mcp

Security analysis

A100/100

Scanned October 11, 2026

npx -y skills add tools-only/X-Skills --skill 003-name-skill_5cb3d28f --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of 003 Name Skill 5cb3d28f?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for 003 Name Skill 5cb3d28f
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/tools-only-003-name-skill-5cb3d28f/badge)](https://www.skillsdirectory.com/skills/tools-only-003-name-skill-5cb3d28f)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

SKILL.md
---
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 or moon.pkg.json, 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` 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:

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

**Package configuration (`moon.pkg`):**

```moonbit
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 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:**

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

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

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

| 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

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.