17 KiB
title, description, weight, type
| title | description | weight | type |
|---|---|---|---|
| Writing C Modules | Extending ƿit with native code | 50 | 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:
// 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_usegitea.pockle.world/john/lib/render.c->js_gitea_pockle_world_john_lib_render_usemypackage/internal/helpers.c->js_mypackage_internal_helpers_usemypackage/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:
#include "cell.h"
This provides:
- QuickJS types and functions
- Conversion helpers
- Module definition macros
Conversion Functions
JavaScript <-> 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
// 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:
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:
JSC_SCALL(mymodule_strlen,
ret = number2js(js, strlen(str));
)
MIST_FUNC_DEF
Register a function in the function list:
MIST_FUNC_DEF(prefix, function_name, arg_count)
Module Export Macros
CELL_USE_FUNCS
Export an object with functions:
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:
CELL_USE_INIT(
JSValue obj = JS_NewObject(js);
// Custom setup...
return obj;
)
Complete Example
// 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:
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:
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.
Compilation Flags (cell.toml)
Use the [compilation] section in cell.toml to pass compiler and linker flags:
[compilation]
CFLAGS = "-Isrc -Ivendor/include"
LDFLAGS = "-lz -lm"
Include paths
Relative -I paths are resolved from the package root:
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:
LDFLAGS = "-Lsdk/lib -lmylib"
Target-specific flags
Add sections named [compilation.<target>] for platform-specific flags:
[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
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
[compilation]
CFLAGS = "-Isdk/public"
[compilation.macos_arm64]
LDFLAGS = "-Lsdk/lib/osx -lmylib"
[compilation.linux]
LDFLAGS = "-Lsdk/lib/linux64 -lmylib"
// 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)
- Count the
JS_New*,JS_SetProperty*, andjs_new_blob*calls in the function - If there are 2 or more, the function MUST use
JS_FRAME/JS_ROOT/JS_RETURN - Every
JSValueheld 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:
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:
// 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:
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:
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:
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:
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.
// 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:
// 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:
// 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_FRAMEcan use plainreturn JS_ThrowTypeError(...)etc. - Error returns after
JS_FRAMEmust useJS_RETURN_EX()orJS_RETURN_NULL()— never plainreturn, 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:
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:
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:
- The TOML syntax is valid (strings must be quoted)
- The section header is exactly
[compilation](not[compile]etc.) - 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_IsArraytakes one argument (the value), not two — remove the context argument- Use
JS_GetPropertyNumber/JS_SetPropertyNumberinstead ofJS_GetPropertyUint32/JS_SetPropertyUint32 - Use
JS_NewStringinstead ofJS_NewAtomString - There is no
undefined— useJS_IsNullandJS_NULLonly