Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Plugins in any language

A plugin is a core WebAssembly module. Rust gets the SDK; any other language that builds wasm32 without WASI implements the ABI below. The contract lives in crates/wire/src/plugin.rs; this page follows it.

Toolchains

LanguageBuildNotes
C, C++zig cc -target wasm32-freestanding -O2 -nostdlib -Wl,--no-entry -Wl,--export-dynamic -o p.wasm p.cOr clang --target=wasm32 with the same flags and wasm-ld installed
Zigzig build-exe p.zig -target wasm32-freestanding -fno-entry -rdynamic -O ReleaseSmallexport fn for each export
AssemblyScriptasc p.ts -o p.wasm -O --use abort=Hand out buffers from heap.alloc, which the GC never moves or frees
Gotinygo build -target=wasm-unknown -o p.wasm .//export name on each function. Standard Go needs WASI or JS imports and is refused

Module rules

  • Export the linear memory as memory.
  • Import nothing but the sdrmm module below: no WASI, no env. An unknown import fails the load.
  • Numbers in memory are little-endian. Strings are UTF-8 JSON.
  • The host opens one instance for control calls and one more per channel, so per-channel state can be global.
  • A call that runs past half a second stops its channel.

Exports

ExportSignatureDoes
sdrmm_abi() -> u32ABI version, currently 1
sdrmm_alloc(len: u32) -> ptrA buffer for the host to write len bytes into
sdrmm_manifest() -> i64Packed manifest JSON
sdrmm_call(op: i32, ptr, len) -> i64A control call, JSON in, packed JSON reply out
sdrmm_open(rate: f64, ptr, len) -> i32Opens a channel at rate Hz with settings JSON; a handle, or -1
sdrmm_apply(handle, ptr, len) -> i32New settings JSON; 0, or -1
sdrmm_input(handle, samples: u32) -> ptrWhere the host writes the next block
sdrmm_process(handle, samples: u32) -> ptrRuns the block; where the output header is
sdrmm_retuned(handle)Optional. The channel moved; drop what you accumulated
sdrmm_close(handle)The channel is gone
sdrmm_error() -> i64Packed message for the last -1

ptr, len and handle are u32, u32 and i32. A packed i64 is ptr << 32 | len. Every buffer you return stays yours and must stay valid until the host's next call into the instance. A buffer from sdrmm_alloc is yours again once the call it was written for returns.

Control calls

sdrmm_call answers {"ok": value} or {"err": "message"}.

opInputok value
1 layoutSettings{"input_rate_hz": 48000.0, "band": [-6000.0, 6000.0], "audio_channels": 1}
2 normalizeSettings, {} for the defaultsThe settings with every default filled in
3 present{"kind": "burst", "data": {...}}A Presentation: {"summary": "Burst", "fields": [...]}

Settings are the JSON object your form produces. Treat absent fields as defaults.

Manifest

{
  "abi": 1,
  "descriptor": {
    "type_id": "burst", "name": "Burst", "summary": "Strong bursts",
    "family": "utility", "bandwidth_hz": 12000.0, "input_rate_hz": 48000.0,
    "emits": ["burst"]
  },
  "form": [
    { "key": "threshold_db", "label": "Threshold",
      "control": { "type": "number", "min": -60.0, "max": 0.0, "step": 1.0, "unit": "dB" } }
  ],
  "kinds": [
    { "kind": "burst", "name": "Burst", "noun": "Bursts", "caps": ["log"],
      "hue": "amber", "lifetime": { "type": "instant" } }
  ]
}

descriptor, form entries and kinds entries are the ChannelDescriptor, SettingField and EventKindEntry schemas in openapi.json. Optional: home_hz, the frequency a new channel starts on, and processor, a ProcessorType.

One block

  1. sdrmm_input(handle, n) returns a pointer; the host writes n samples there, each an f32 I then an f32 Q.
  2. sdrmm_process(handle, n) returns a pointer to nine u32 words:
WordHolds
0, 1Audio pointer and length in f32 samples, interleaved by audio_channels
2Audio rate in Hz
3, 4Events: pointer and byte length of a JSON array, 0 for none
5, 6Status: pointer and byte length of a JSON object, 0 when unchanged
7Frames heard but failing their checks, counted over the channel's life
8Blocks dropped, counted over the channel's life
[{"kind": "burst", "data": {"db": -12.5}}]
{"health": {"lock": true, "snr_db": 21.5, "freq_error_hz": 30.0, "errors_per_s": 0.0}}

A status may also carry readouts, a list of fields like those a presentation has.

Host imports

The sdrmm module runs the heavy kernels natively on your memory. Samples are the same interleaved f32 pairs. A negative return is a refusal.

ImportSignatureDoes
log(level: u32, ptr, len)Writes a UTF-8 line to the server log
fir_new(taps_ptr, taps_len, decimation: u32) -> i32A complex FIR over real f32 taps
fir_process(handle, in_ptr, in_len, out_ptr, out_cap) -> i32Samples written
fft_new(len: u32, inverse: u32) -> i32An FFT of len points
fft_run(handle, ptr, len) -> i32Transforms len samples in place
resampler_new(ratio: f64) -> i32A fractional resampler, output rate over input rate
resampler_process(handle, in_ptr, in_len, out_ptr, out_cap) -> i32Samples written
kernel_drop(handle)Frees a kernel

Example

A complete plugin in C without libc. It builds as in the table and is tested against the real host by crates/plugins/tests/c_plugin.rs.

/*
 * Burst: logs each block whose power rises above -20 dBFS.
 * The sdr-- plugin ABI in plain C, no libc. Build:
 *   zig cc -target wasm32-freestanding -O2 -nostdlib -Wl,--no-entry -Wl,--export-dynamic \
 *     -o burst.wasm burst.c
 */
#include <stddef.h>
#include <stdint.h>

#define EXPORT __attribute__((visibility("default")))
#define PAGE 65536
#define THRESHOLD 0.01f

enum { OP_LAYOUT = 1, OP_NORMALIZE = 2, OP_PRESENT = 3 };

static const char MANIFEST[] =
    "{\"abi\":1,"
    "\"descriptor\":{\"type_id\":\"burst\",\"name\":\"Burst\",\"summary\":\"Strong bursts\","
    "\"family\":\"utility\",\"bandwidth_hz\":12000.0,\"input_rate_hz\":48000.0,"
    "\"emits\":[\"burst\"]},"
    "\"kinds\":[{\"kind\":\"burst\",\"name\":\"Burst\",\"noun\":\"Bursts\",\"caps\":[\"log\"],"
    "\"hue\":\"amber\",\"lifetime\":{\"type\":\"instant\"}}]}";
static const char LAYOUT[] =
    "{\"ok\":{\"input_rate_hz\":48000.0,\"band\":[-6000.0,6000.0]}}";
static const char NORMALIZED[] = "{\"ok\":{}}";
static const char PRESENTED[] = "{\"ok\":{\"summary\":\"Burst\"}}";
static const char UNKNOWN[] = "{\"err\":\"unknown op\"}";
static const char EVENT[] = "[{\"kind\":\"burst\",\"data\":{}}]";
static const char LOCKED[] = "{\"health\":{\"lock\":true}}";
static const char UNLOCKED[] = "{\"health\":{\"lock\":false}}";
static const char NO_ERROR[] = "";

extern unsigned char __heap_base;
static uintptr_t heap_top = (uintptr_t)&__heap_base;

typedef struct {
    void *ptr;
    size_t cap;
} Region;

static Region inbox;
static Region input;
static uint32_t header[9];
static int loud;

static void *grab(size_t bytes) {
    uintptr_t at = (heap_top + 7) & ~(uintptr_t)7;
    uintptr_t end = at + bytes;
    size_t have = __builtin_wasm_memory_size(0) * PAGE;
    if (end > have && __builtin_wasm_memory_grow(0, (end - have + PAGE - 1) / PAGE) < 0)
        __builtin_trap();
    heap_top = end;
    return (void *)at;
}

static void *fit(Region *region, size_t bytes) {
    if (bytes > region->cap) {
        region->ptr = grab(bytes);
        region->cap = bytes;
    }
    return region->ptr;
}

static int64_t pack(const void *ptr, size_t len) {
    return ((int64_t)(uintptr_t)ptr << 32) | (int64_t)len;
}

EXPORT uint32_t sdrmm_abi(void) { return 1; }

EXPORT uint32_t sdrmm_alloc(uint32_t len) { return (uint32_t)(uintptr_t)fit(&inbox, len); }

EXPORT int64_t sdrmm_manifest(void) { return pack(MANIFEST, sizeof MANIFEST - 1); }

EXPORT int64_t sdrmm_call(int32_t op, uint32_t ptr, uint32_t len) {
    switch (op) {
    case OP_LAYOUT: return pack(LAYOUT, sizeof LAYOUT - 1);
    case OP_NORMALIZE: return pack(NORMALIZED, sizeof NORMALIZED - 1);
    case OP_PRESENT: return pack(PRESENTED, sizeof PRESENTED - 1);
    default: return pack(UNKNOWN, sizeof UNKNOWN - 1);
    }
}

EXPORT int32_t sdrmm_open(double rate, uint32_t ptr, uint32_t len) {
    loud = 0;
    return 0;
}

EXPORT int32_t sdrmm_apply(int32_t handle, uint32_t ptr, uint32_t len) { return 0; }

EXPORT uint32_t sdrmm_input(int32_t handle, uint32_t samples) {
    return (uint32_t)(uintptr_t)fit(&input, (size_t)samples * 2 * sizeof(float));
}

EXPORT uint32_t sdrmm_process(int32_t handle, uint32_t samples) {
    const float *iq = input.ptr;
    float power = 0.0f;
    for (uint32_t n = 0; n < 2 * samples; n++)
        power += iq[n] * iq[n];
    int now = samples > 0 && power / (float)samples > THRESHOLD;
    for (int word = 0; word < 9; word++)
        header[word] = 0;
    if (now && !loud) {
        header[3] = (uint32_t)(uintptr_t)EVENT;
        header[4] = sizeof EVENT - 1;
    }
    if (now != loud) {
        const char *status = now ? LOCKED : UNLOCKED;
        header[5] = (uint32_t)(uintptr_t)status;
        header[6] = now ? sizeof LOCKED - 1 : sizeof UNLOCKED - 1;
    }
    loud = now;
    return (uint32_t)(uintptr_t)header;
}

EXPORT void sdrmm_close(int32_t handle) {}

EXPORT int64_t sdrmm_error(void) { return pack(NO_ERROR, 0); }

Transmit and arrays

A transmitter sets can_transmit and adds the exports in crates/wire/src/plugin/transmit.rs:

ExportSignature
sdrmm_tx_open(rate: f64, ptr, len) -> handle
sdrmm_tx_apply(handle, ptr, len) -> i32
sdrmm_tx_submit(handle, kind: u32, ptr, len) -> i32, kind 0 audio as f32, 1 frame bytes
sdrmm_tx_generate(handle, max_samples: u32) -> i64, packed pointer and sample count of I/Q
sdrmm_tx_close(handle)

An array processor declares processor in the manifest and adds the exports in crates/wire/src/plugin/processor.rs. sdrmm_array_input takes every lane's block one after the other; sdrmm_array_process returns a ten-word header laid out in ArrayHeader there.