Processor API and ABI reference
This document specifies the backend-neutral contract between a compiled Onda processor and its host. The contract is not WebAssembly-specific: LLVM native objects, LLVM WebAssembly objects, and complete modules emitted by the Binaryen backend implement the same logical processor interface. Target and artifact profiles define how that interface is represented on a particular platform.
Versioned artifact descriptor
Every processor artifact is paired with a JSON descriptor whose common envelope contains:
format: "onda-processor"andformat_versionfor the descriptor schema.abi_versionfor the logical entry-point and storage contract in this document.artifact_kind, currentlyrelocatable_objectorwebassembly_module.backendandmir_schema_versionfor provenance.target, including the resolved triple, pointer width, byte order, pointer model, and calling convention.integration, including symbols that must survive integration and an artifact-specific profile.compile,exports,runtime, andmetadatafor specialization, symbol names, allocation, and the program interface.
Hosts must reject unknown descriptor or ABI versions. A descriptor belongs to the exact bytes with
which it was emitted; physical layouts must not be borrowed from another backend or compilation.
The Rust onda_processor_abi types and the TypeScript @onda-lang/processor-abi declarations are
the same schema. A shared conformance fixture exercises the complete logical metadata record and
the directly loadable core-WebAssembly profile; profile-specific facts use one tagged integration
record rather than backend-specific descriptor shapes.
Logical processor interface
The signatures below use Ptr, an abstract pointer to host-owned storage. i32 arguments are
signed 32-bit values. Public LLVM entry points use the target’s C calling convention; complete core
WebAssembly modules use ordinary core-Wasm function calls.
onda_processor_init(
params: Ptr,
state: Ptr,
mode: InitMode,
buffers: Ptr,
buffer_frames: Ptr,
buffer_channels: Ptr,
buffer_sample_rates: Ptr,
output: Ptr,
) -> i32
onda_process(
state: Ptr,
params: Ptr,
inputs: Ptr,
outputs: Ptr,
start_frame: i32,
frames: i32,
flags: i32,
buffers: Ptr,
buffer_frames: Ptr,
buffer_channels: Ptr,
buffer_sample_rates: Ptr,
output: Ptr,
) -> i32
onda_event_N(
input: Ptr, // const EventInput*
params: Ptr,
state: Ptr,
buffers: Ptr,
buffer_frames: Ptr,
buffer_channels: Ptr,
buffer_sample_rates: Ptr,
output: Ptr,
) -> i32
Since processor ABI and descriptor version 6, every event requires an EventInput descriptor:
struct EventInput {
const uint8_t *payload;
uint32_t payload_bytes;
uint8_t *workspace;
uint32_t workspace_capacity_bytes;
};
It uses target C layout (16 bytes on wasm32, 32 bytes on native 64-bit targets). The descriptor itself is required even for an empty payload. Workspace starts at an eight-byte-aligned address; its capacity is provisioned by the host before realtime execution. Input, workspace, descriptor, state, parameters, and output storage are disjoint for the call. Native callers supply valid memory regions; wasm entry points also check regions against linear-memory bounds.
Before any handler or output access, generated code validates all lengths, checked byte extents,
exact input consumption, alignment, and workspace capacity. Rejection returns status 2 without
changing workspace, state, or output records. Accepted input is copied into aligned native tensors;
bools become zero or one and ranged integers are normalized before observation. Status 1 means
execution failed after entry and requires the usual instance recovery. Neither path allocates.
There is one onda_event_N for each declared event, in metadata order. The current ABI permits one
public processor namespace per artifact. A future ABI may add artifact-specific namespacing for
multi-processor libraries without changing MIR.
Native relocatable objects also advertise one trusted onda_event_views_N entry per event through
the optional exports.event_views array. It has the same trailing arguments and result as
onda_event_N, but its first argument is a pointer to the schema-derived array below:
struct onda_processor_event_tensor_view {
const void *data;
int32_t element_count;
};
Each view is one contiguous, naturally aligned, native-endian primitive tensor in canonical
depth-first SoA leaf order. element_count counts primitive scalars; for a slice leaf it is the
slice length times that leaf’s fixed inner element count. The entry borrows every tensor only for
the synchronous call, resets call-scoped output, and neither allocates nor uses an event workspace.
It deliberately does not receive a tensor count or validate shape, alignment, canonical bool and
ranged-integer values, storage overlap, or the other processor pointers. Passing anything other
than the exact views and processor storage required by the paired descriptor is undefined behavior.
Use onda_event_N at trust boundaries.
This entry is a native-object capability, not part of the WebAssembly profiles. Native hosts can
derive its flat tensor contract once from the event schema with
onda_processor_abi::payload::PayloadPlan; JavaScript hosts can inspect the equivalent
PayloadPlan.tensorMetadata. The metadata records expose each leaf’s path, primitive encoding,
fixed shape, owning top-level parameter, slice status, and fixed element count.
InitMode has two portable values: PRESERVE_PINNED = 0 and FULL = 1. Full initialization clears
the physical state before running every declaration initializer, including pinned state and task
continuations. Preserve-pinned initialization skips those guarded declarations and leaves their
existing values intact unless authored init code explicitly changes them. Raw ABI initialization is
not transactional: a host that needs rollback must provide that policy itself.
The initialization interface uses the named initialization mode, supplies current external-buffer
descriptors to initialization, and adds one optional ExecutionOutput to init, process, and event
entries. Its independently optional print and delegate batches share one call-local sequence so
hosts can preserve source order across both streams. Language validation forbids delegate
publication reachable from init, so initialization can populate only the print batch. The
instance-level C and WebAssembly host APIs use the same initialization-mode values. A failed
initialization leaves the physical state indeterminate.
Every entry point returns zero on success or a positive execution-failure code. Code 1 is
RUNTIME_SAFETY_FAILURE, produced when generated code encounters a checked condition from which it
cannot continue safely. After a nonzero init result, the supplied state image is indeterminate and
must not be processed until the host successfully initializes or restores it.
The process order intentionally places state, parameters, and audio tables before segment controls and optional buffer tables. This keeps the hottest pointers in argument registers on common native C ABIs without introducing a target-specific entry point.
Call-scoped execution output
For the language semantics, see delegates and printing. The internal delegate and print integration notes contain cross-host implementation details.
output is null when the host consumes neither occurrence stream. Otherwise it points to two
independently nullable pointers followed by the generated call-local counter:
delegate_batch: Ptr
print_batch: Ptr
next_sequence: u32
Each present pointer addresses an independent caller-owned batch with this physical shape; native targets use native pointers and complete wasm32 modules use little-endian 32-bit linear-memory offsets:
storage: Ptr
capacity_bytes: u32
used_bytes: u32
record_count: u32
overflow_count: u32
Every supplied batch descriptor and non-null storage region is mutually disjoint and does not overlap any other region accessed through the entry-point call. This includes parameter and state storage, audio pointer tables and samples, external-buffer descriptor tables and samples, packed event input and workspace, native event tensor views, and the execution-output descriptor itself. The invariant permits generated delegate packing to use non-overlap-assuming bulk copies.
The fixed header of every contiguous record is three u32 values followed immediately by payload
bytes. A delegate record stores declaration-order delegate index, payload byte count, and call-local
sequence. Delegate payloads use the recursive schema described below, in little-endian wire order.
Record headers retain the artifact target’s byte order.
A print record stores log-site index, payload byte count, and the same call-local sequence. Its
payload contains only the site’s
primitive scalar arguments without padding: four bytes for f32/i32, eight for f64/i64, and
one zero-or-one byte for bool. metadata.source_files and metadata.log_sites supply labels,
source spans, lexical ownership, argument types, and fixed payload sizes. Scalar and header byte
order is the artifact target’s byte order.
For one fixed-shape occurrence, exact storage is the twelve-byte record header plus the descriptor’s
payload_size_bytes. A dynamic delegate has no exact pre-execution size; its descriptor reports
payload_min_size_bytes, including each slice’s four-byte length prefix but no slice elements.
There is no exact whole-batch size because occurrence counts, delegate selection, and slice lengths
may depend on runtime control flow. Capacity is a host policy, and overflow_count reports when it
was insufficient.
Before every init or process entry, the host resets the counters of each supplied batch and resets
next_sequence to zero. An input-event entry performs that reset itself before preflight, so
rejected input returns empty batches. Publications into either present batch consume the
shared counter. A complete record is appended only when it fits. Otherwise it is discarded whole
and that batch’s overflow counter saturates at u32::MAX; a later smaller record may still fit. Null output,
batch, or storage is neutral and does not count overflow. Generated execution failure clears
delegate results but retains print records and overflow already produced, because they may diagnose
the failure. Storage
is caller-owned, never allocated or retained by the processor, and is not part of snapshots. Hosts
that expose a combined log merge the two decoded batches by sequence; sequences have no meaning
across separate entry calls.
Pointer and target profiles
For a native LLVM object, Ptr is a real pointer with the width, byte order, data layout, and C ABI
selected by LLVM from the resolved target triple and target options. The application owns storage,
linking, symbol visibility, and any platform runtime dependencies.
For an LLVM WebAssembly relocatable object, Ptr is the target’s linear-memory offset type. A
wasm32 object therefore lowers Ptr to a 32-bit offset. The application linker owns final memory,
symbol export, and module policy.
For a complete wasm32 module emitted by Binaryen, Ptr is an unsigned i32 byte offset into the
module’s exported linear memory. The module exports memory and an immutable i32 __heap_base;
the host allocates at or above that address and grows memory before creating views. Memory growth
invalidates JavaScript typed arrays and DataView instances.
The target triple is a code-generation choice, not a different processor ABI. It selects such facts as instruction set, pointer size, C calling convention details, object representation, relocation model, and physical alignment. The descriptor records the resolved facts that an embedding host needs but does not replace a target SDK, sysroot, linker, or platform ABI documentation.
Relocatable-object integration
The native compiler deliberately stops at a relocatable object for every LLVM AOT target. Onda does
not bundle or invoke a linker. The descriptor’s integration.required_symbols lists processor
symbols the final application must retain at its chosen integration boundary.
The native_relocatable_object profile requires no Onda-specific entry point. The user’s normal
platform linker combines the object with the host and any required runtime libraries.
The webassembly_relocatable_object profile additionally declares no_entry: true and
export_memory: true. A typical final link therefore has no conventional _start, retains the
processor symbols as exports, and exposes the selected linear memory. Exact linker flags belong to
the user’s toolchain rather than the Onda compiler.
The native compiler validates that LLVM WebAssembly output is a version-1 Wasm binary with a
relocatable linking section. It does not pretend that the object is directly instantiable.
Direct native use
include/onda_processor_abi.h is the canonical C declaration of the current ABI entry points. An
application links the emitted object, allocates storage from the exact paired descriptor, builds the
input/output and external-buffer pointer tables, optionally prepares an
onda_processor_execution_output_t containing independently allocated delegate and print batches,
resets it immediately before entry, and calls onda_processor_init, onda_process, and any
onda_event_N functions directly. Native descriptors may additionally advertise
onda_event_views_N; onda_processor_event_tensor_view_t and
onda_processor_event_views_fn declare that trusted entry. No Onda runtime or compiler library is
required.
The application must reject descriptor/ABI versions it does not implement and must verify that the descriptor’s target, pointer width, byte order, and calling convention match the linked process. It also owns its audio-thread floating-point environment, scheduling, validation, snapshots, and final linker policy. These responsibilities are ordinary consumers of the raw ABI, not generated object entrypoints.
Complete core-WebAssembly integration
The browser-safe Binaryen backend emits a complete, directly instantiable core-WebAssembly module
because browser WebAssembly APIs do not expose relocatable objects or a linker. Its integration
profile names the memory and __heap_base exports and declares the module imports. Current
Binaryen artifacts are self-contained and have no imports, including for transcendental math and
strict fused multiply-add helpers.
This complete-module profile is a packaging choice. It implements the same logical processor contract as an LLVM object and does not make Web Audio part of the ABI.
Storage and initialization
The host allocates non-overlapping parameter and physical-state regions using the sizes and minimum
alignments in runtime. It initializes parameter defaults from program metadata and calls
onda_processor_init(params, state, FULL, buffers, buffer_frames, buffer_channels,
buffer_sample_rates, output) before processing. The four buffer tables describe the bindings
current for this initialization call and follow the same rules as process and event calls. Pass
null for output when initialization output is not consumed. Physical state uses the backend’s
selected target layout and is otherwise opaque.
Hosts may replace buffer descriptors between entry-point calls. The next init, event, or process call observes the replacement; rebinding alone does not execute initialization or recompute state previously derived from a buffer. Supplying bindings to init is intended for one-time preprocessing when the host already owns the source buffers before processing begins.
State-backed control outputs and persistent snapshot entries expose their physical offsets in the artifact descriptor. Scratch state is deliberately absent from snapshots.
metadata.states includes every packed snapshot entry. Its authored flag preserves the explicit
MIR state provenance and is false for compiler-owned task frames, allowing snapshot implementations
to preserve suspended tasks while authored-state reflection omits their implementation storage.
Portable snapshots
The packed persistent-state snapshot is target-independent. The current snapshot format encodes persistent scalar elements in little-endian byte order, in metadata order, without physical padding or scratch state. It includes pinned authored roots and compiler-owned task frames. This is distinct from the target-native physical state image, which can use another byte order or alignment.
Restore begins with onda_processor_init(params, state, FULL, buffers, buffer_frames,
buffer_channels, buffer_sample_rates, NULL), using the current bindings, then overlays every
persistent entry from the packed snapshot. Restore initialization output is intentionally
suppressed. This resets instance scratch while preserving persistent state and task continuations.
A host converting between a big-endian physical target and the portable snapshot must encode
and decode each scalar according to metadata rather than copying physical bytes wholesale.
Processor ABI version 4 adds optional integer_range metadata to scalar i32 and i64 state.
After overlaying each such snapshot entry, the host must normalize it into the inclusive
min..=max interval using the declared clamp or Euclidean wrap mode. This restores the storage
invariant before generated code can rely on it to remove bounds checks. The packed snapshot byte
representation is unchanged, so its format version remains 1.
Audio ports
inputs and outputs point to tables of Ptr, one entry per flattened audio channel in metadata
order. Each channel points to a full compile-block scalar array. A null table is valid when the
corresponding flattened channel count is zero.
Audio scalar width belongs to the Onda interface. A platform adapter may convert its native audio format—for example, Web Audio f32 planar channels—to the declared scalar width. Such conversion is adapter behavior, not processor code generation.
Absent surfaces and null pointers
The paired descriptor is the authority for whether a pointer argument has storage behind it. A raw host passes null exactly when the corresponding surface is absent:
paramswhenruntime.param_size_bytesis zero;statewhenruntime.state_size_bytesis zero;inputsoroutputswhen the corresponding flattened metadata slot count is zero;- all four external-buffer table pointers when
metadata.buffersis empty; EventInput.payloadwhen its byte count is zero, and workspace when its required size is zero. TheEventInputdescriptor itself is always present.
A declared surface is not absent merely because the application does not use it. Every declared input/output slot requires valid compile-block storage. A non-empty buffer declaration list requires all four parallel tables. Each descriptor has positive frame/channel counts and a finite positive sample rate. A null sample pointer denotes an unbound buffer; generated code redirects reads to processor-private zero storage and writes to distinct discard storage. Hosts give unbound buffers one frame, retain the declared channel count for exact-channel buffers, and use one channel for dynamic-channel buffers. This keeps omitted resource bindings neutral without exposing the implementation’s separate read/write pointers to hosts.
Segmented processing
An artifact is specialized for compile.block_size. A host may divide one logical compile block
into calls using start_frame and frames; the range must remain inside the compile block.
flags & 1isBEGIN_BLOCKand gates the program’s beginning-of-block work.flags & 2isEND_BLOCKand gates its end-of-block work.
The flags do not maintain a hidden cursor. Zero-frame calls are valid. A host whose callback size
differs from the compile block owns a cursor and splits callbacks at logical block boundaries. Audio
pointer tables continue to address complete compile-block storage; generated code derives logical
audio indices from start_frame.
Parameters, events, delegates, buffers, and control outputs
Parameter storage follows the paired descriptor’s offsets and scalar shapes. Every event and
delegate also carries a recursive schema: named parameters with scalar, primitive tuple, nominal
struct, fixed array, or top-level slice types. Fields include constant defaults and integer domains.
Every struct schema contains at least one field. The schema is the authority for host encoding and
decoding; flattened executable parameters are validated against it.
Wire payloads are packed little-endian in parameter and depth-first field order. Every primitive
leaf is one contiguous tensor with outer array axes before inner field-array axes. A runtime slice
contributes one signed i32 logical length, then its field tensors; there is no padding or separate
length per struct field. Prepared workspace inserts scalar alignment padding and uses native byte
order. Its required size is computed by the shared payload planner, independently of wire size.
The Rust onda_processor_abi::payload::PayloadPlan and JavaScript PayloadPlan expose recursive
host codecs, checked sizing, and the same flattened tensor metadata. Construct plans and encode
logical host values off the rendering
thread. Rust instances initially reserve fixed requirements and at least 64 KiB for dynamic events;
Instance::reserve_event_workspace and onda_instance_reserve_event_workspace can increase this
capacity outside realtime execution, using the instance allocator. WebAudio transfers encoded
bytes to its worklet and uses the configured event capacity without memory growth during dispatch.
Internal event/delegate forwarding borrows read-only tensors without another payload-sized copy;
host publication writes a complete packed record or drops it whole on overflow.
Descriptor format version 2 gives every parameter range_min_repr, range_max_repr, and
param_control. param_control is null for a parameter without a numeric host-control domain;
otherwise it contains:
scale:linearorlog;curve: an optional finite SuperCollider-stylelincurvevalue, mutually exclusive withscale = log;unit: optional display text;step_repr: the optional plain-domain step encoded in the declared scalar representation;step_count: the number of equal intervals between the inclusive endpoints.
The raw processor object does not export parameter conversion functions. Native hosts decode each
numeric control into the onda_processor_param_domain structure from
include/onda_processor_abi.h, whose header-only functions implement clamping, snapping, and
plain/normalized conversion without linking the Onda runtime. The structure carries the declared
scalar type so stepped floating-point grids are validated at their actual storage precision. The
reference generator in
examples/native/raw_processor_object emits decoded tables, indexed wrappers, typed reads, and
typed writes around that shared header implementation.
For a scalar numeric parameter, normalized-to-plain conversion is:
- Map NaN to zero and clamp the normalized input to
[0, 1]. - Return the exact range endpoint for normalized zero or one.
- If
curveis present, transformnwith the SuperCollider-stylelincurvemapping, then applymin + n * (max - min). Otherwise apply that linear mapping directly forlinear, or the overflow-safe equivalentexp(log(min) + n * (log(max) - log(min)))forlog. - Map a plain NaN to
min, then clamp the plain value to the inclusive range. - For a stepped domain, snap to
min + round((plain - min) / step) * stepand clamp again. - Convert to the declared scalar width when writing parameter storage.
Plain-to-normalized first performs the same plain clamping and step snapping, preserves exact
endpoints, then applies the inverse curved, linear, or logarithmic mapping. Boolean plain and
normalized host-control values use the threshold value >= 0.5 and store one byte containing zero
or one. Fixed parameter arrays retain their logical name, type, length, and per-element defaults.
Their numeric range and control metadata apply independently to each element, addressed in storage
at byte_offset + index * element_size_bytes. Unranged numeric parameters have no normalized
host-control domain.
Because the shared host-control surface uses binary64 values, an i64 control domain and its
range width are restricted to the exactly representable integer interval
[-9007199254740991, 9007199254740991]. This restriction does not apply to unranged i64
parameters written through their typed/raw storage representation.
External buffers use four parallel tables in declaration order:
buffers: writable sample pointers, with null entries denoting unbound buffers.buffer_frames: i32 frame counts.buffer_channels: i32 channel counts.buffer_sample_rates: f32 sample rates.
All four descriptor tables remain immutable for an entry-point call and do not overlap parameter, state, audio, or external-buffer sample storage. The non-overlap rule is between descriptor-table storage and the regions reached through non-null host storage pointers.
Fixed resource arrays occupy contiguous physical slots. metadata.buffer_arrays records each
logical group name, its first physical slot, and its length, so hosts can bind a whole bank without
parsing generated slot names. Selection clamps once and computes first + selector in constant
time. Each physical slot has its own metadata.buffers[first + slot].may_write value. A false value
proves that code reachable from init, process, or an exported event does not write that slot;
selectors that cannot be resolved statically conservatively mark every slot they may select. Init
writes include writes reached transitively through top-level and proc initializer helpers.
Samples use interleaved frame-major storage. Metadata declares scalar width, read/write access, and mono, static, or dynamic channel constraints. Every sample-rate entry is finite and positive, and every non-null pointer, frame count, and channel count denotes nonempty prepared storage. Null buffer entries use processor-owned zero and discard storage. Control outputs are state-backed values at declared physical offsets and may be observed between processor calls.
Numerical and failure behavior
Strict compilation preserves declared scalar widths, NaNs, signed zero, rounding behavior, and
one-rounding FMA semantics. Fast math is an explicit compilation policy recorded by the artifact.
Native floating-point control registers belong to the calling thread. Onda’s realtime hosts enable
x86 FTZ/DAZ before entering init, process, or event code to prevent subnormal feedback-state stalls;
a raw-object host that wants the same audio policy must configure its calling threads likewise.
Bounds checks, integer division, and other generated safety checks return
RUNTIME_SAFETY_FAILURE instead of trapping. Invalid host pointers, storage extents, or other
violations of the raw ABI remain outside generated-code recovery and can still trap or cause
undefined behavior. RUNTIME_SAFETY_FAILURE means execution may have partially changed state or
output; a host must stop using that processor state until full initialization or snapshot restoration
establishes valid state again, and audio hosts discard partial output. INPUT_REJECTED completes no
handler work, preserves processor state, returns empty output batches, and leaves the processor
usable after the host corrects its payload or workspace capacity.
Web Audio reference adapter
packages/onda_webaudio is an optional reference adapter for complete WebAssembly modules. It owns
Web Audio node construction, render-quantum scheduling, f32 marshaling, parameter/event messages,
buffer bindings, and snapshot requests. None of those policies are required when embedding an Onda
processor in a native application, an offline renderer, a plugin API, a worker, or another Wasm
runtime. The adapter compiles the WebAssembly.Module outside the audio rendering thread, caches
typed views over processor memory, uses a bulk-copy fast path for full-block f32 audio, and locks
host linear-memory allocation after construction. Dynamic event payload storage is preallocated to a
configurable capacity; an oversized event fails rather than growing linear memory while audio is
running.
C header reference
The release SDK installs include/onda_processor_abi.h and this document together. The header is
self-contained and header-only except for the processor-specific onda_processor_init,
onda_process, generated onda_event_N, and optional native onda_event_views_N symbols supplied
by the compiled object. Include it from C or C++; no libonda linkage is required to call a
processor object.
The public declarations fall into four groups:
- ABI versions, execution results, initialization modes, and segmented-processing flags.
- Function-pointer signatures and the generated init, process, packed-event, and native event-view entry points.
- Caller-owned delegate, print, execution-output, occurrence, and cursor records.
- Inline batch iteration and parameter-domain validation/conversion helpers.
The inline batch iterators validate record boundaries before returning a payload view. Each next
call returns 1 for a record, 0 only at the exact end of a valid batch, and -1 for invalid or
malformed input. Sequential iteration with onda_processor_delegate_batch_next or
onda_processor_print_batch_next is linear in record count and constant-space. The random-access
convenience functions validate the complete batch and return 0 only when the requested index is
absent; repeated indexed iteration is therefore quadratic, so use a cursor when consuming a whole
batch.
All parameter conversion helpers are allocation-free and constant-time. Prepare and validate a domain once when constructing host controls rather than validating descriptor text on the audio thread.
Complete function index
This index is checked against include/onda_processor_abi.h so newly exposed functions cannot be
released without appearing in this reference.
onda_process onda_processor_batch_next_record onda_processor_batch_record_at onda_processor_delegate_batch_next onda_processor_delegate_batch_occurrence_at onda_processor_delegate_batch_reset onda_processor_execution_output_reset onda_processor_float_grid_value_matches onda_processor_init onda_processor_integer_domain_value_is_valid onda_processor_lincurve_normalized_to_unit onda_processor_lincurve_unit_to_normalized onda_processor_linear_plain_to_unit onda_processor_linear_unit_to_plain onda_processor_param_constrain_plain onda_processor_param_domain_is_valid onda_processor_param_normalized_to_plain onda_processor_param_plain_to_normalized onda_processor_print_batch_next onda_processor_print_batch_occurrence_at onda_processor_print_batch_reset