The C header
What `--emit=export` hands you, and what it costs to link.
Anyone writing the C or C++ that drives a compiled patch.
--emit=export writes an object and a header. The header is everything needed
to drive the object: its entry points, its state size, its parameters and its
buffer layout.
A four-node effect compiles to something on this scale:
fx.o 1.5 KB, four node functions; undefined symbols: sinf, expf
fx.h 13 KB, everything needed to drive themThat is the whole dependency footprint. The undefined symbols are libm and
nothing else — no runtime, no allocator, no compiler.
What the header gives you#
- A render entry per node, and a fused one for a top-level graph.
- The state size, so the host allocates the block; Pole never allocates.
- The seed values, so a fresh instance starts where a fresh instance should.
- An
initentry when the patch has one, called before the first block. - The parameter table: name, slot, range, default.
Walk through a single-processor export#
Start with this small processor and save it as gain.pole in tools/pole/.
The smoothing gives it a little persistent state, which makes the host's
initialization step visible in the example.
processor Gain
{
input stream float in;
param float gain = 0.5f [0.0f, 2.0f] smooth 20.0f;
output stream float out;
void main()
{
loop { out <- in * gain; advance(); }
}
}From tools/pole, export it:
./build/polec gain.pole --emit=export --out=gainThe generated header declares this frame function. The name Gain comes from
the processor, so another processor name produces another symbol.
void pole_node_Gain(const float* inputs, float* outputs,
double sampleRate, float* state,
const pole_buffer* externals);| Argument | What the caller supplies |
|---|---|
inputs | One frame of input slots, including parameters; allocate POLE_GAIN_INPUTS floats. |
outputs | Writable storage for one frame; allocate POLE_GAIN_OUTPUTS floats. |
sampleRate | The actual processing rate in Hz, passed as a double by the C API. |
state | Persistent storage initialized with pole_Gain_init_state; allocate at least POLE_GAIN_STATE floats. |
externals | The buffer bindings declared by the patch. This example declares none, so an unused placeholder suffices. |
Returns: nothing. The function writes the output slots and updates state for the next frame. It does not allocate storage. Keep separate state for each instance, and do not reset it on every sample.
A complete C caller#
Save this as host.c beside gain.h:
#include <stdio.h>
#include "gain.h"
int main(void)
{
float state[POLE_GAIN_STATE ? POLE_GAIN_STATE : 1];
float inputs[POLE_GAIN_INPUTS] = {0};
float outputs[POLE_GAIN_OUTPUTS] = {0};
pole_buffer externals[1] = {0};
pole_Gain_init_state(state);
for (int i = 0; i < POLE_GAIN_PARAM_COUNT; ++i)
inputs[pole_Gain_params[i].slot] = pole_Gain_params[i].init;
inputs[0] = 0.8f;
pole_node_Gain(inputs, outputs, 48000.0, state, externals);
printf("%0.3f\n", outputs[0]);
return 0;
}Build and run it on the machine used for the export:
cc host.c gain.o -lm -o gain-host
./gain-hostIt prints 0.400: an input of 0.8 multiplied by the default gain of 0.5.
The parameter table supplies the slot and initial value, so the caller does
not need to guess where a control lives in the input array.
For a processor with an explicit init block, call its generated
pole_init_Name(state, NULL, sampleRate, externals) once after state seeding
and binding buffers, before rendering. init_state applies the initial values;
the separate init entry runs the source's initialization code. This gain
processor has no such block.
Graphs render a block#
A generated graph runner owns the per-node state and wiring. Its API has this
shape for a graph named FxChain:
void pole_FxChain_render(pole_FxChain* graph, const float* inputs,
float* outputs, size_t frames, double sampleRate);Initialize the graph with pole_FxChain_init(&graph) before the first render.
The runner invokes any node init blocks before processing its first block.
frames counts audio frames, not individual channel values. The buffers hold
consecutive frames, each containing the graph's declared input or output slots.
Include parameter values in the input layout; use the generated endpoint and
parameter tables to find those slots.
The exact header generated for your patch is the authority for names, sizes and buffer layout. The key distinction is between a node call, which processes one frame, and a graph render, which processes a block.
It sounds the same, and that is checked#
The exported object is the same optimised module the JIT would have run, so an exported plugin is not a port of what you were hearing — it is the same code.
A bit-identity gate renders both and diffs the floats rather than trusting
that. The browser runtime is held to the same standard:
tools/pole/scripts/check_wasm.py renders every example twice, once through
the JIT and once through the browser runtime under node, and 24 of 29 come
out bit-identical. The five that do not are named rather than rounded away:
poly_events needs note dispatch the gate does not drive yet; feedback_lfo
has no fused entry, because a graph with feedback is rendered node by node and
only the C header does that; and fft, spectral and wt_osc differ by one
or two ulp through libm's transcendentals, macOS against musl.
That last group is the reason
--approx-math exists: inline
polynomials are the same arithmetic on every target, which libm never was.