Extern Modules (Native C/C++ Libraries)
Stone packages can include native C or C++ code as an extern kernel. This lets you wrap existing C/C++ libraries (like OpenCascade, BLAS, or any system library) and call them from Stone through thin .stn wrapper files — no manual FFI bindings needed.
How It Works
The extern system uses a naming convention to connect Stone and C:
- Stone wrapper files import from a kernel module prefixed with
_(e.g.,_my_kernel) - Each imported function
_my_funcmaps to a C functionstone_my_func stone extern buildscans your.stnimports and C signatures to auto-derive the export list and compile everything
Setting Up an Extern Module
1. Declare the extern in package.son
Add an extern block to your package manifest. extern is a map of named kernels — the key is the kernel name (used for the output .wasm file and import module name in Stone):
name = "my-physics"
version = "1.0.0"
extern = {
physics_kernel = {
type = "c"
sources = ["kernel/physics_kernel.c"]
}
}
Per-entry fields:
type—"c"(C and C++ both supported),"wasm"(pre-built), or"js"sources— list of C/C++ source files relative to the package root (fortype = "c")path— path to a pre-built.wasmor.jsfile (fortype = "wasm"/"js")bindings— path to a.sonbindings file (fortype = "wasm")deps— external native library dependencies (see "Working with External C/C++ Libraries" below)
A slab can declare multiple kernels by adding more entries to the map.
2. Write the C kernel
Create your C source files. Include stone_record_abi.h to work with Stone records and arrays. Every function you want to expose to Stone must be prefixed with stone_:
// kernel/physics_kernel.c
#include "stone_record_abi.h"
double stone_physics_gravity(double mass1, double mass2, double distance) {
double G = 6.674e-11;
return G * mass1 * mass2 / (distance * distance);
}
StoneRecord* stone_physics_simulate(StoneRecord* config) {
double dt = stone_record_get(config, "dt");
double mass = stone_record_get(config, "mass");
double velocity = stone_record_get(config, "velocity");
// Run simulation...
StoneRecord* result = stone_record_new(3);
stone_record_set(result, "position", velocity * dt);
stone_record_set(result, "velocity", velocity);
stone_record_set(result, "time", dt);
return result;
}
Return types are auto-detected from C signatures:
double→numint→numStoneRecord*→ptr(record)void→ no return value
3. Write Stone wrapper files
Import the registered kernel as a namespace, then bind each exported symbol with
an explicit extern fn declaration. The kernel and internal function names use
the _ prefix:
// main.stn
import _physics_kernel as physics_kernel
extern fn _physics_gravity(m1: num, m2: num, dist: num) -> num = physics_kernel._physics_gravity
extern fn _physics_simulate(config: record) -> record = physics_kernel._physics_simulate
export fn gravity(m1, m2, dist) = _physics_gravity(m1, m2, dist)
export fn simulate(config) = _physics_simulate(config)
The naming convention: _physics_gravity in Stone maps to stone_physics_gravity in C.
4. Build the kernel
stone extern build
stone extern build --verbose # show the emcc command
This compiles your C sources to WASM via Emscripten. The output goes to kernel/builds/wasm/<name>.wasm.
Working with External C/C++ Libraries
To wrap an existing C/C++ library, add a deps block inside the kernel entry:
extern = {
my_kernel = {
type = "c"
sources = ["kernel/my_kernel.c", "kernel/lib_wrapper.cpp"]
deps = {
mylib = {
wasm = {
include = "include" // header path relative to root
lib = "lib" // static library path relative to root
libs = ["mylib", "myutil"] // library names (passed as -lmylib -lmyutil)
}
native = {
libs = ["mylib", "myutil", "stdc++"] // for LLVM native builds
}
}
}
}
}
Each dependency has separate wasm and native configurations:
| Field | Description |
|---|---|
wasm.include | Path to headers, relative to the dependency root |
wasm.lib | Path to static .a libraries, relative to the dependency root |
wasm.libs | Library names to link (without lib prefix or extension) |
native.libs | Library names for native/LLVM builds |
Dependency Resolution
External library dependencies are resolved via environment variables. For each dependency named <depname>, set <DEPNAME>_WASM_ROOT to the directory containing the library's WASM build:
# Example: wrapping OpenCascade
export OPENCASCADE_WASM_ROOT=/path/to/opencascade-wasm-build
# The build system expects this structure:
# /path/to/opencascade-wasm-build/
# ├── include/opencascade/ ← headers (from wasm.include)
# └── lin32/clang/lib/ ← static .a files (from wasm.lib)
If a required environment variable is missing, stone extern build will print exactly what it needs:
Missing dependency: opencascade
Set OPENCASCADE_WASM_ROOT to the path containing the opencascade WASM build.
Expected: <root>/include/opencascade/ (headers)
Expected: <root>/lin32/clang/lib/ (static libs)
Prerequisites
- Emscripten SDK — required for WASM compilation. Either set the
EMSDKenvironment variable or haveemccon your PATH.
The Stone Record ABI
The header stone_record_abi.h provides the C interface for Stone's data types. It is automatically available during stone extern build — you just need to #include it.
Records (key-value objects)
#include "stone_record_abi.h"
StoneRecord* stone_my_func(StoneRecord* input) {
// Read fields
double x = stone_record_get(input, "x");
double y = stone_record_get(input, "y");
// Read pointer fields (nested records, arrays, strings)
StoneRecord* nested = (StoneRecord*)stone_record_get_ptr(input, "options");
// Create a new record
StoneRecord* result = stone_record_new(3); // capacity hint
stone_record_set(result, "sum", x + y);
stone_record_set(result, "product", x * y);
// Set pointer fields (strings, nested records, arrays)
stone_record_set_ptr(result, "label", stone_alloc_string("result"));
stone_record_set_ptr(result, "details", nested);
return result;
}
Arrays
StoneRecord* stone_my_transform(StoneRecord* spec) {
// Get an array from a record field
StoneArray* data = (StoneArray*)stone_record_get_ptr(spec, "data");
long long n = stone_array_len(data);
// Create a new array
StoneArray* output = stone_array_new(n);
for (long long i = 0; i < n; i++) {
double val = stone_array_get(data, i);
stone_array_set(output, i, val * 2.0);
}
StoneRecord* result = stone_record_new(1);
stone_record_set_ptr(result, "data", output);
return result;
}
API Summary
| Function | Description |
|---|---|
stone_record_new(n) | Create a record with capacity for n fields |
stone_record_get(r, key) | Get a numeric field value |
stone_record_get_ptr(r, key) | Get a pointer field (record, array, or string) |
stone_record_set(r, key, value) | Set a numeric field |
stone_record_set_ptr(r, key, ptr) | Set a pointer field (type auto-detected from allocation tag) |
stone_array_new(length) | Create an array of the given length |
stone_array_get(a, index) | Get element at index |
stone_array_set(a, index, value) | Set element at index |
stone_array_len(a) | Get array length |
stone_alloc_string(s) | Copy a string into the arena allocator |
stone_to_str(value) | Convert a number to a string |
All allocations use an arena allocator — no manual free() calls needed.
Mixing C and C++
For C++ libraries, keep all C++ code in .cpp files and expose a pure C interface via a wrapper header. The kernel's main .c file calls through the C wrapper:
kernel/
├── my_kernel.c # Main kernel (pure C, includes stone_record_abi.h)
├── lib_wrapper.h # C-compatible header (extern "C" declarations)
└── lib_wrapper.cpp # C++ implementation (calls the C++ library)
// lib_wrapper.h
#ifdef __cplusplus
extern "C" {
#endif
void* lib_create_object(double x, double y, double z);
double lib_compute(void* obj);
#ifdef __cplusplus
}
#endif
// lib_wrapper.cpp
#include "lib_wrapper.h"
#include <SomeLibrary.hpp> // C++ library headers
extern "C" {
void* lib_create_object(double x, double y, double z) {
auto* obj = new SomeLibrary::Object(x, y, z);
return (void*)obj;
}
double lib_compute(void* obj) {
auto* typed = (SomeLibrary::Object*)obj;
return typed->compute();
}
} // extern "C"
List both source files in package.son:
extern = {
my_kernel = {
type = "c"
sources = ["kernel/my_kernel.c", "kernel/lib_wrapper.cpp"]
}
}
Complete Example: Package Structure
Here is the structure of the Clay CAD package, which wraps OpenCascade:
clay/
├── package.son # Manifest with extern block + deps
├── main.stn # Re-exports from submodules
├── primitives.stn # box(), cylinder(), sphere() wrappers
├── operations.stn # union(), subtract(), fillet() wrappers
├── query.stn # mesh extraction wrappers
├── types.stn # Type schemas (Feature, Body, etc.)
└── kernel/
├── clay_kernel.c # Main kernel (pure C, calls occt_wrapper)
├── occt_wrapper.h # C interface to C++ library
├── occt_wrapper.cpp # C++ implementation (calls OpenCascade)
└── builds/
└── wasm/
└── clay_kernel.wasm # Built output