14 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/game.ce(AOT actor) ->js_mypackage_game_program
Actor files (.ce) use the _program suffix instead of _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 .cell/lib/<pkg>/<stem>.dylib.
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;
)
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