351 lines
8.7 KiB
Markdown
351 lines
8.7 KiB
Markdown
---
|
|
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/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 <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:
|
|
|
|
```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
|
|
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:
|
|
|
|
```c
|
|
JSC_CCALL(mymod_name,
|
|
ret = JS_NewString(js, "hello");
|
|
)
|
|
```
|
|
|
|
If a function holds a heap object across further allocating calls, you must root it:
|
|
|
|
```c
|
|
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:
|
|
|
|
```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.
|