--- 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___use ``` Where: - `` is the package name with `/` and `.` replaced by `_` - `` 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/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: ```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 #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 `.cell/lib//.dylib`. ## 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.]` 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 // ... ``` ## 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 both the array and each element created in the loop: ```c JS_FRAME(js); JS_ROOT(arr, JS_NewArray(js)); for (int i = 0; i < count; i++) { JS_ROOT(item, 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); ``` **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; ) ``` ### 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