Skip to main content
A node is a Rust struct annotated with #[cerulion_node(...)] paired with an adjacent impl block annotated with #[cerulion_node_impl]. Together they turn your struct into a node that cerulion node build compiles and cerulion graph run loads β€” you never call the generated code yourself. Import everything from the prelude:
The prelude re-exports cerulion_node, cerulion_node_impl, NodeError, NodeResult, and the message/transport types.

The two macros

The macros are used as a pair:
  • #[cerulion_node(...)] on the struct declares the node type, its ports (via field attributes), and its trigger policy.
  • #[cerulion_node_impl] on an adjacent impl <Name> { fn tick(&mut self) -> Result<(), NodeError> { ... } } lets tick() read inputs and write outputs through ordinary field access (self.<port>.…) β€” reads and writes go directly to shared memory, zero-copy.
#[cerulion_node_impl] takes no arguments. The struct must appear before the impl block.

Complete example

What the macros do for you

The macros generate the glue that lets the Cerulion runtime build, load, and tick your node β€” lifecycle wiring, port plumbing, and the dynamic-library entry points. You never interact with the generated code: write the struct and the tick(), then drive everything through the CLI (cerulion node build, cerulion node stage, cerulion graph run).

Node-level attributes

#[cerulion_node(...)] attributes are all optional. Exactly one trigger-policy hint applies (or the trigger is inferred from a field marked #[input(trigger)]). tick_within_ms is orthogonal and stacks with any trigger policy. throttle_ms stacks with any trigger policy except period_ms (mutually exclusive with it).

Mutual exclusion

unbounded_sync is mutually exclusive with sync_window_ms, period_ms, and external. unbounded_sync and sync_window_ms each require β‰₯2 trigger inputs. throttle_ms is mutually exclusive with period_ms. tick_within_ms stacks with any trigger policy; throttle_ms stacks with any trigger policy except period_ms.

Rejected attributes

These attributes are rejected at parse time:

#[input(...)] attribute

Forms: #[input], #[input(trigger)], #[input(trigger, lifo, depth = 1)], #[input(fifo, depth = 100, backpressure = drop_oldest, max_age_ms = 200)], #[input(filter = "fn_name")], #[input(expect_within_ms = N)].

#[output(...)] attribute

Forms: #[output] (fixed-only schema), #[output(data, encoding)] (lists simple variable fields β†’ two write paths), #[output(data, complex(header))] (complex(...) lists nested-typed variable fields β†’ set_<f>_bytes(...)?), #[output(promise_within_ms = N)]. Fixed primitive fields are written directly (for example self.image.height = ...) β€” no attribute entry needed.

fill_from: zero-copy producer writes

For producers that write into a destination buffer β€” camera drivers, codecs, file readers, network sockets β€” fill_from passes the shared-memory destination buffer (&mut [T]) directly to the producer; the producer just fills it.
The producer type must implement FillFrom<T> (re-exported from cerulion_core::prelude). T defaults to u8; typed-array fields use the element type directly (for example &mut [f32] for a float32[] field). If the producer returns Err, nothing is published for that tick.