Files
cell/docs/c-modules.md
2026-02-17 01:04:42 -06:00

8.7 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_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:

#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_LOCAL(result, JS_NewObject(js));
    JS_SetPropertyStr(js, result, "x", number2js(js, x/len));
    JS_SetPropertyStr(js, result, "y", number2js(js, y/len));
    JS_RestoreFrame(_js_ctx, _js_gc_frame, _js_local_frame);
    ret = result;
  }
)

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:

pit build
pit update

Each C file is compiled into a per-file dynamic library at ~/.pit/lib/<pkg>/<stem>.dylib.

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_SetPropertyStr, etc. — can trigger GC, which moves heap objects to new addresses. C locals holding JSValue become stale after any allocating call.

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 holds a heap object across further allocating calls, you must root it:

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_LOCAL(result, JS_NewObject(js));
    // result is rooted — GC can update it through these calls:
    JS_SetPropertyStr(js, result, "x", number2js(js, x/len));
    JS_SetPropertyStr(js, result, "y", number2js(js, y/len));
    JS_RestoreFrame(_js_ctx, _js_gc_frame, _js_local_frame);
    ret = result;
  }
)

Macros

Macro Purpose
JS_FRAME(js) Save the GC and local frames. Required before any JS_LOCAL.
JS_LOCAL(name, init) Declare and root a 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 =).

Rules of thumb

  1. One allocation, immediate return — no rooting needed.
  2. Object + property sets — root the object with JS_LOCAL.
  3. Array + loop — root the array; if loop body creates objects, root the loop variable too with a manual JSLocalRef.
  4. CELL_USE_INIT bodies — always use JS_FRAME / JS_LOCAL / JS_RETURN.

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.