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:

  1. Stone wrapper files import from a kernel module prefixed with _ (e.g., _my_kernel)
  2. Each imported function _my_func maps to a C function stone_my_func
  3. stone extern build scans your .stn imports 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 (for type = "c")
  • path — path to a pre-built .wasm or .js file (for type = "wasm" / "js")
  • bindings — path to a .son bindings file (for type = "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:

  • doublenum
  • intnum
  • StoneRecord*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:

FieldDescription
wasm.includePath to headers, relative to the dependency root
wasm.libPath to static .a libraries, relative to the dependency root
wasm.libsLibrary names to link (without lib prefix or extension)
native.libsLibrary 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 EMSDK environment variable or have emcc on 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

FunctionDescription
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