---
title: "Writing C Modules"
description: "Extending ƿit with native code"
weight: 50
type: "docs"
---

ƿit makes it easy to extend functionality with C code. C files in a package are compiled into a dynamic library and can be imported like any other module.

## Basic Structure

A C module exports a single function that returns a JavaScript value:

```c
// mymodule.c
#include "cell.h"

#define CELL_USE_NAME js_mypackage_mymodule_use

static JSValue js_add(JSContext *js, JSValue self, int argc, JSValue *argv) {
  double a = js2number(js, argv[0]);
  double b = js2number(js, argv[1]);
  return number2js(js, a + b);
}

static JSValue js_multiply(JSContext *js, JSValue self, int argc, JSValue *argv) {
  double a = js2number(js, argv[0]);
  double b = js2number(js, argv[1]);
  return number2js(js, a * b);
}

static const JSCFunctionListEntry js_funcs[] = {
  MIST_FUNC_DEF(mymodule, add, 2),
  MIST_FUNC_DEF(mymodule, multiply, 2),
};

CELL_USE_FUNCS(js_funcs)
```

## Symbol Naming

The exported function must follow this naming convention:

```
js_<package>_<filename>_use
```

Where:
- `<package>` is the package name with `/` and `.` replaced by `_`
- `<filename>` is the C file name without extension

Examples:
- `mypackage/math.c` -> `js_mypackage_math_use`
- `gitea.pockle.world/john/lib/render.c` -> `js_gitea_pockle_world_john_lib_render_use`
- `mypackage/internal/helpers.c` -> `js_mypackage_internal_helpers_use`
- `mypackage/game.ce` (AOT actor) -> `js_mypackage_game_program`

Actor files (`.ce`) use the `_program` suffix instead of `_use`.

Internal modules (in `internal/` subdirectories) follow the same convention — the `internal` directory name becomes part of the symbol. For example, `internal/os.c` in the core package has the symbol `js_core_internal_os_use`.

**Note:** Having both a `.cm` and `.c` file with the same stem at the same scope is a build error.

## Required Headers

Include `cell.h` for all ƿit integration:

```c
#include "cell.h"
```

This provides:
- QuickJS types and functions
- Conversion helpers
- Module definition macros

## Conversion Functions

### JavaScript <-> C

```c
// Numbers
double js2number(JSContext *js, JSValue v);
JSValue number2js(JSContext *js, double g);

// Booleans
int js2bool(JSContext *js, JSValue v);
JSValue bool2js(JSContext *js, int b);

// Strings (must free with JS_FreeCString)
const char *JS_ToCString(JSContext *js, JSValue v);
void JS_FreeCString(JSContext *js, const char *str);
JSValue JS_NewString(JSContext *js, const char *str);
```

### Blobs

```c
// Get blob data (returns pointer, sets size in bytes)
void *js_get_blob_data(JSContext *js, size_t *size, JSValue v);

// Get blob data in bits
void *js_get_blob_data_bits(JSContext *js, size_t *bits, JSValue v);

// Create new stone blob from data
JSValue js_new_blob_stoned_copy(JSContext *js, void *data, size_t bytes);

// Check if value is a blob
int js_is_blob(JSContext *js, JSValue v);
```

## Function Definition Macros

### JSC_CCALL

Define a function with automatic return value:

```c
JSC_CCALL(mymodule_greet,
  const char *name = JS_ToCString(js, argv[0]);
  char buf[256];
  snprintf(buf, sizeof(buf), "Hello, %s!", name);
  ret = JS_NewString(js, buf);
  JS_FreeCString(js, name);
)
```

### JSC_SCALL

Shorthand for functions taking a string first argument:

```c
JSC_SCALL(mymodule_strlen,
  ret = number2js(js, strlen(str));
)
```

### MIST_FUNC_DEF

Register a function in the function list:

```c
MIST_FUNC_DEF(prefix, function_name, arg_count)
```

## Module Export Macros

### CELL_USE_FUNCS

Export an object with functions:

```c
static const JSCFunctionListEntry js_funcs[] = {
  MIST_FUNC_DEF(mymod, func1, 1),
  MIST_FUNC_DEF(mymod, func2, 2),
};

CELL_USE_FUNCS(js_funcs)
```

### CELL_USE_INIT

For custom initialization:

```c
CELL_USE_INIT(
  JSValue obj = JS_NewObject(js);
  // Custom setup...
  return obj;
)
```

## Complete Example

```c
// vector.c - Simple 2D vector operations
#include "cell.h"
#include <math.h>

#define CELL_USE_NAME js_mypackage_vector_use

JSC_CCALL(vector_length,
  double x = js2number(js, argv[0]);
  double y = js2number(js, argv[1]);
  ret = number2js(js, sqrt(x*x + y*y));
)

JSC_CCALL(vector_normalize,
  double x = js2number(js, argv[0]);
  double y = js2number(js, argv[1]);
  double len = sqrt(x*x + y*y);
  if (len > 0) {
    JS_FRAME(js);
    JS_ROOT(result, JS_NewObject(js));
    JS_SetPropertyStr(js, result.val, "x", number2js(js, x/len));
    JS_SetPropertyStr(js, result.val, "y", number2js(js, y/len));
    JS_RestoreFrame(_js_ctx, _js_gc_frame, _js_local_frame);
    ret = result.val;
  }
)

JSC_CCALL(vector_dot,
  double x1 = js2number(js, argv[0]);
  double y1 = js2number(js, argv[1]);
  double x2 = js2number(js, argv[2]);
  double y2 = js2number(js, argv[3]);
  ret = number2js(js, x1*x2 + y1*y2);
)

static const JSCFunctionListEntry js_funcs[] = {
  MIST_FUNC_DEF(vector, length, 2),
  MIST_FUNC_DEF(vector, normalize, 2),
  MIST_FUNC_DEF(vector, dot, 4),
};

CELL_USE_FUNCS(js_funcs)
```

Usage in ƿit:

```javascript
var vector = use('vector')

var len = vector.length(3, 4)  // 5
var n = vector.normalize(3, 4) // {x: 0.6, y: 0.8}
var d = vector.dot(1, 0, 0, 1) // 0
```

## Build Process

C files are automatically compiled when you run:

```bash
cell --dev build
```

Each C file is compiled into a per-file dynamic library at a content-addressed path in `~/.cell/build/<hash>`. A manifest is written for each package so the runtime can find dylibs without rerunning the build pipeline — see [Dylib Manifests](/docs/shop/#dylib-manifests).

## Compilation Flags (cell.toml)

Use the `[compilation]` section in `cell.toml` to pass compiler and linker flags:

```toml
[compilation]
CFLAGS = "-Isrc -Ivendor/include"
LDFLAGS = "-lz -lm"
```

### Include paths

Relative `-I` paths are resolved from the package root:

```toml
CFLAGS = "-Isdk/public"
```

If your package is at `/path/to/mypkg`, this becomes `-I/path/to/mypkg/sdk/public`.

Absolute paths are passed through unchanged.

The build system also auto-discovers `include/` directories — if your package has an `include/` directory, it is automatically added to the include path. No need to add `-I$PACKAGE/include` in cell.toml.

### Library paths

Relative `-L` paths work the same way:

```toml
LDFLAGS = "-Lsdk/lib -lmylib"
```

### Target-specific flags

Add sections named `[compilation.<target>]` for platform-specific flags:

```toml
[compilation]
CFLAGS = "-Isdk/public"

[compilation.macos_arm64]
LDFLAGS = "-Lsdk/lib/osx -lmylib"

[compilation.linux]
LDFLAGS = "-Lsdk/lib/linux64 -lmylib"

[compilation.windows]
LDFLAGS = "-Lsdk/lib/win64 -lmylib64"
```

Available targets: `macos_arm64`, `macos_x86_64`, `linux`, `linux_arm64`, `windows`.

### Sigils

Use sigils in flags to refer to standard directories:

- `$LOCAL` — absolute path to `.cell/local` (for prebuilt libraries)
- `$PACKAGE` — absolute path to the package root

```toml
CFLAGS = "-I$PACKAGE/vendor/include"
LDFLAGS = "-L$LOCAL -lmyprebuilt"
```

### Example: vendored SDK

A package wrapping an external SDK with platform-specific shared libraries:

```
mypkg/
├── cell.toml
├── wrapper.cpp
└── sdk/
    ├── public/
    │   └── mylib/
    │       └── api.h
    └── lib/
        ├── osx/
        │   └── libmylib.dylib
        └── linux64/
            └── libmylib.so
```

```toml
[compilation]
CFLAGS = "-Isdk/public"

[compilation.macos_arm64]
LDFLAGS = "-Lsdk/lib/osx -lmylib"

[compilation.linux]
LDFLAGS = "-Lsdk/lib/linux64 -lmylib"
```

```cpp
// wrapper.cpp
#include "cell.h"
#include <mylib/api.h>
// ...
```

## Platform-Specific Code

Use filename suffixes for platform variants:

```
audio.c           # default
audio_playdate.c  # Playdate
audio_emscripten.c # Web/Emscripten
```

ƿit selects the appropriate file based on the target platform.

## Multi-File C Modules

If your module wraps a C library, place the library's source files in a `src/` directory. Files in `src/` are compiled as support objects and linked into your module's dylib — they are not treated as standalone modules.

```
mypackage/
  rtree.c       # module (exports js_mypackage_rtree_use)
  src/
    rtree.c      # support file (linked into rtree.dylib)
    rtree.h      # header
```

The module file (`rtree.c`) includes the library header and uses `cell.h` as usual. The support files are plain C — they don't need any cell macros.

## GC Safety

ƿit uses a **Cheney copying garbage collector**. Any JS allocation — `JS_NewObject`, `JS_NewString`, `JS_NewInt32`, `JS_SetPropertyStr`, `js_new_blob_stoned_copy`, etc. — can trigger GC, which **moves** heap objects to new addresses. Bare C locals holding `JSValue` become **dangling pointers** after any allocating call. This is not a theoretical concern — it causes real crashes that are difficult to reproduce because they depend on heap pressure.

### Checklist (apply to EVERY C function you write or modify)

1. Count the `JS_New*`, `JS_SetProperty*`, and `js_new_blob*` calls in the function
2. If there are **2 or more**, the function **MUST** use `JS_FRAME` / `JS_ROOT` / `JS_RETURN`
3. Every `JSValue` held in a C local across an allocating call must be rooted

### When you need rooting

If a function creates **one** heap object and returns it immediately, no rooting is needed:

```c
JSC_CCALL(mymod_name,
  ret = JS_NewString(js, "hello");
)
```

If a function creates an object and then sets properties on it, you **must** root it — each `JS_SetPropertyStr` call is an allocating call that can trigger GC:

```c
// UNSAFE — will crash under GC pressure:
JSValue obj = JS_NewObject(js);
JS_SetPropertyStr(js, obj, "x", JS_NewInt32(js, 1));  // can GC → obj is stale
JS_SetPropertyStr(js, obj, "y", JS_NewInt32(js, 2));  // obj may be garbage
return obj;

// SAFE:
JS_FRAME(js);
JS_ROOT(obj, JS_NewObject(js));
JS_SetPropertyStr(js, obj.val, "x", JS_NewInt32(js, 1));
JS_SetPropertyStr(js, obj.val, "y", JS_NewInt32(js, 2));
JS_RETURN(obj.val);
```

### Patterns

**Object with properties** — the most common pattern in this codebase:

```c
JS_FRAME(js);
JS_ROOT(result, JS_NewObject(js));
JS_SetPropertyStr(js, result.val, "width", JS_NewInt32(js, w));
JS_SetPropertyStr(js, result.val, "height", JS_NewInt32(js, h));
JS_SetPropertyStr(js, result.val, "pixels", js_new_blob_stoned_copy(js, data, len));
JS_RETURN(result.val);
```

**Array with loop** — root the element variable *before* the loop, then reassign `.val` each iteration:

```c
JS_FRAME(js);
JS_ROOT(arr, JS_NewArray(js));
JS_ROOT(item, JS_NULL);
for (int i = 0; i < count; i++) {
  item.val = JS_NewObject(js);
  JS_SetPropertyStr(js, item.val, "index", JS_NewInt32(js, i));
  JS_SetPropertyStr(js, item.val, "data", js_new_blob_stoned_copy(js, ptr, sz));
  JS_SetPropertyNumber(js, arr.val, i, item.val);
}
JS_RETURN(arr.val);
```

**WARNING — NEVER put `JS_ROOT` inside a loop.** `JS_ROOT` declares a `JSGCRef` local and calls `JS_PushGCRef(&name)`, which pushes its address onto a linked list. Inside a loop the compiler reuses the same stack address, so on iteration 2+ the list becomes self-referential (`ref->prev == ref`). When GC triggers it walks the chain and **hangs forever**. This bug is intermittent — it only manifests when GC happens to run during the loop — making it very hard to reproduce.

**Nested objects** — root every object that persists across an allocating call:

```c
JS_FRAME(js);
JS_ROOT(outer, JS_NewObject(js));
JS_ROOT(inner, JS_NewArray(js));
// ... populate inner ...
JS_SetPropertyStr(js, outer.val, "items", inner.val);
JS_RETURN(outer.val);
```

**Inside `JSC_CCALL`** — use `JS_RestoreFrame` and assign to `ret`:

```c
JSC_CCALL(mymod_make,
  JS_FRAME(js);
  JS_ROOT(obj, JS_NewObject(js));
  JS_SetPropertyStr(js, obj.val, "x", number2js(js, 42));
  JS_RestoreFrame(_js_ctx, _js_gc_frame, _js_local_frame);
  ret = obj.val;
)
```

### C Argument Evaluation Order (critical)

In C, the **order of evaluation of function arguments is unspecified**. This interacts with the copying GC to create intermittent crashes that are extremely difficult to diagnose.

```c
// UNSAFE — crashes intermittently:
JS_FRAME(js);
JS_ROOT(obj, JS_NewObject(js));
JS_SetPropertyStr(js, obj.val, "format", JS_NewString(js, "rgba32"));
//                     ^^^^^^^ may be evaluated BEFORE JS_NewString runs
// If JS_NewString triggers GC, the already-read obj.val is a dangling pointer.
```

The compiler is free to evaluate `obj.val` into a register, *then* call `JS_NewString`. If `JS_NewString` triggers GC, the object moves to a new address. The rooted `obj` is updated by GC, but the **register copy** is not — it still holds the old address. `JS_SetPropertyStr` then writes to freed memory.

**Fix:** always separate the allocating call into a local variable:

```c
// SAFE:
JS_FRAME(js);
JS_ROOT(obj, JS_NewObject(js));
JSValue fmt = JS_NewString(js, "rgba32");
JS_SetPropertyStr(js, obj.val, "format", fmt);
// obj.val is read AFTER JS_NewString completes — guaranteed correct.
```

This applies to **any** allocating function used as an argument when another argument references a rooted `.val`:

```c
// ALL of these are UNSAFE:
JS_SetPropertyStr(js, obj.val, "pixels", js_new_blob_stoned_copy(js, data, len));
JS_SetPropertyStr(js, obj.val, "x", JS_NewFloat64(js, 3.14));
JS_SetPropertyStr(js, obj.val, "name", JS_NewString(js, name));

// SAFE versions — separate the allocation:
JSValue pixels = js_new_blob_stoned_copy(js, data, len);
JS_SetPropertyStr(js, obj.val, "pixels", pixels);
JSValue x = JS_NewFloat64(js, 3.14);
JS_SetPropertyStr(js, obj.val, "x", x);
JSValue s = JS_NewString(js, name);
JS_SetPropertyStr(js, obj.val, "name", s);
```

**Functions that allocate** (must be separated): `JS_NewString`, `JS_NewFloat64`, `JS_NewInt64`, `JS_NewObject`, `JS_NewArray`, `JS_NewCFunction`, `js_new_blob_stoned_copy`

**Functions that do NOT allocate** (safe inline): `JS_NewInt32`, `JS_NewUint32`, `JS_NewBool`, `JS_NULL`, `JS_TRUE`, `JS_FALSE`

### Macros

| Macro | Purpose |
|-------|---------|
| `JS_FRAME(js)` | Save the GC frame. Required before any `JS_ROOT`. |
| `JS_ROOT(name, init)` | Declare a `JSGCRef` and root its value. Access via `name.val`. |
| `JS_LOCAL(name, init)` | Declare a rooted `JSValue` (GC updates it through its address). |
| `JS_RETURN(val)` | Restore the frame and return a value. |
| `JS_RETURN_NULL()` | Restore the frame and return `JS_NULL`. |
| `JS_RETURN_EX()` | Restore the frame and return `JS_EXCEPTION`. |
| `JS_RestoreFrame(...)` | Manual frame restore (for `JSC_CCALL` bodies that use `ret =`). |

### Error return rules

- Error returns **before** `JS_FRAME` can use plain `return JS_ThrowTypeError(...)` etc.
- Error returns **after** `JS_FRAME` must use `JS_RETURN_EX()` or `JS_RETURN_NULL()` — never plain `return`, which would leak the GC frame.

### Migrating from gc_mark

The old mark-and-sweep GC had a `gc_mark` callback in `JSClassDef` for C structs that held JSValue fields. This no longer exists. The copying GC needs to know the **address** of every pointer to update it when objects move.

If your C struct holds a JSValue that must survive across GC points, root it for the duration it's alive:

```c
typedef struct {
  JSValue callback;
  JSLocalRef callback_lr;
} MyWidget;

// When storing:
widget->callback = value;
widget->callback_lr.ptr = &widget->callback;
JS_PushLocalRef(js, &widget->callback_lr);

// When done (before freeing the struct):
// The local ref is cleaned up when the frame is restored,
// or manage it manually.
```

In practice, most C wrappers hold only opaque C pointers (like `SDL_Window*`) and never store JSValues in the struct — these need no migration.

## Static Declarations

Keep internal functions and variables `static`:

```c
static int helper_function(int x) {
  return x * 2;
}

static int module_state = 0;
```

This prevents symbol conflicts between packages.

## Troubleshooting

### Missing header / SDK not installed

If a package wraps a third-party SDK that isn't installed on your system, the build will show:

```
module.c: fatal error: 'sdk/header.h' file not found (SDK not installed?)
```

Install the required SDK or skip that package. These warnings are harmless — other packages continue building normally.

### CFLAGS not applied

If your `cell.toml` has a `[compilation]` section but flags aren't being picked up, check:

1. The TOML syntax is valid (strings must be quoted)
2. The section header is exactly `[compilation]` (not `[compile]` etc.)
3. Target-specific sections use valid target names: `macos_arm64`, `macos_x86_64`, `linux`, `linux_arm64`, `windows`

### API changes from older versions

If C modules fail with errors about function signatures:

- `JS_IsArray` takes one argument (the value), not two — remove the context argument
- Use `JS_GetPropertyNumber` / `JS_SetPropertyNumber` instead of `JS_GetPropertyUint32` / `JS_SetPropertyUint32`
- Use `JS_NewString` instead of `JS_NewAtomString`
- There is no `undefined` — use `JS_IsNull` and `JS_NULL` only
