Using Messages
Message is the resolved execution input for a component.
This is the key logic to keep in mind:
- instructions tell a component what operation to perform
- messages provide execution input that the composite runtime resolves or generates
- effects are returned after execution and later interpreted by the runtime or delivered to observers
That means:
- components consume
Message - composites resolve or build
Message - observers do not consume
Message - observers do not receive a separate delivery context; any extra data should be staged into effects or owned locally
This guide focuses on both sides of that contract:
- how a crate author defines a message type
- how a composite author resolves and supplies messages during execution
If you have not read the instruction guide yet, start with Defining Instructions With vihaco.
What A Message Is For
Section titled “What A Message Is For”Use a message when a component needs step-local execution input that should not live directly in the instruction encoding.
For example, a composite runtime may need to:
- look up runtime state
- pop values from a stack
- derive timing information
- validate access to a device before execution
The composite can do that work first, then pass the result into the component as a message.
That keeps responsibilities clean:
Instructionis the bytecode-visible requestMessageis the resolved input to execute that requestEffectis the value returned after execution
A Small Message Type
Section titled “A Small Message Type”Message types are usually plain Rust types annotated with #[derive(Message)].
use vihaco::Message;
#[derive(Debug, Clone, Message)]pub struct PlayMsg { pub when_ns: u64, pub channel_id: u32,}A component can then declare that message type in its #[dispatch(...)] impl:
use eyre::Result;use vihaco::{dispatch, Effects, Instruction, Message};
#[derive(Debug, Clone, Instruction)]pub enum WaveInst { SetAmplitude(f64), Play,}
#[derive(Debug, Clone, Message)]pub struct PlayMsg { pub when_ns: u64, pub channel_id: u32,}
#[derive(Debug, Clone)]pub struct ChannelSample { pub when_ns: u64, pub channel_id: u32, pub value: f64,}
#[derive(Debug, Default)]pub struct WaveGenerator { amplitude: f64,}
#[dispatch(instruction = WaveInst, message = PlayMsg, effect = ChannelSample)]impl WaveGenerator { fn execute(&mut self, inst: &WaveInst, msg: PlayMsg) -> Result<Effects<ChannelSample>> { match inst { WaveInst::SetAmplitude(v) => { self.amplitude = v; Ok(Effects::none()) } WaveInst::Play => Ok(Effects::one(ChannelSample { when_ns: msg.when_ns, channel_id: msg.channel_id, value: self.amplitude, })), } }}The important thing is that WaveGenerator does not decide when_ns or channel_id.
It just consumes the already-resolved PlayMsg.
Why The Composite Owns Message Resolution
Section titled “Why The Composite Owns Message Resolution”The composite runtime is the right place to build messages because it owns the broader execution context.
That often includes:
- cross-component state
- scheduler or runtime state
- stacks, clocks, frames, or device metadata
- validation and access control
The component should not have to reconstruct that context on its own.
So the execution flow usually looks like this:
- the composite receives or dispatches an instruction
- the composite inspects runtime state and the instruction
- the composite builds the message
- the composite executes the component with
(instruction, message)
That is the mental model to keep throughout the rest of this guide.
A Small Composite-Author Example
Section titled “A Small Composite-Author Example”#[composite] generates component instruction types, but message resolution is plain Rust that you write next to the composite: build the message from runtime context, then hand (instruction, message) to the component via the generated execute_generated method.
use eyre::Result;use vihaco::{dispatch, Component, Effects, GeneratedComponent, HasInstructionSet, Instruction, Message, Parse, composite};
#[derive(Debug, Clone, Instruction)]enum DeviceInst { Pulse,}
#[derive(Debug, Clone, PartialEq, Parse)]#[syntax_class(instruction, head = "device")]enum DeviceSyntax { #[pattern = "'pulse"] Pulse }
#[derive(Message)]struct DeviceMsg(&'static str);
#[derive(Default)]struct Device { seen: Vec<&'static str>,}
#[dispatch(instruction = DeviceInst, message = DeviceMsg)]impl Device { fn execute(&mut self, inst: &DeviceInst, msg: DeviceMsg) -> Result<Effects<()>> { match inst { DeviceInst::Pulse => { self.seen.push(msg.0); Ok(Effects::none()) } } }}
impl HasInstructionSet for Device { type Runtime = DeviceInst; type Syntax = DeviceSyntax;}
impl Component for Device {}
#[composite]#[derive(Default)]struct Pilot { #[device(0x02, alias = "pulse")] device: Device,}
impl Pilot { // The composite owns message resolution, then executes the component. fn step(&mut self, inst: DeviceInst) -> Result<Effects<()>> { let msg = self.resolve_device(&inst)?; self.device.execute_generated(inst, msg) }
fn resolve_device(&mut self, _inst: &DeviceInst) -> Result<DeviceMsg> { Ok(DeviceMsg("resolved")) }}This is the core composite-author contract:
- the component says which message type it needs
- the composite provides a resolver for that instruction family
- the resolver returns the message value the component will consume
In other words, the component defines the input shape, but the composite decides the actual input value for that step.
A Richer Example: Resolving A Signal Message
Section titled “A Richer Example: Resolving A Signal Message”A real runtime shows a richer version of the same idea. A signal-generator component expects a SignalMessage:
use vihaco::{dispatch, Effects, Message};
#[derive(Debug, Clone, Copy, PartialEq, Message)]pub enum SignalMessage { None, Poly4([f64; 4]), Duration(u64),}
#[dispatch(instruction = SignalInst, message = SignalMessage)]impl SignalGenerator { fn execute(&mut self, inst: &SignalInst, msg: SignalMessage) -> eyre::Result<Effects<()>> { // component consumes a resolved message here let _ = (inst, msg); Ok(Effects::none()) }}But the component does not know how to create Poly4([f64; 4]) or Duration(u64) on its own.
That comes from composite-owned runtime state. The composite resolves the message first
(the exact accessors — a stack, a clock — depend on your runtime; the shape is what matters):
use vihaco_cpu::{decode_f64, decode_u64};
fn resolve_signal(&mut self, inst: &SignalInst) -> eyre::Result<SignalMessage> { match inst { SignalInst::Poly(_addr) => { let p3 = decode_f64(self.cpu.stack_pop()?); let p2 = decode_f64(self.cpu.stack_pop()?); let p1 = decode_f64(self.cpu.stack_pop()?); let p0 = decode_f64(self.cpu.stack_pop()?); Ok(SignalMessage::Poly4([p0, p1, p2, p3])) } SignalInst::Play if self.signal.is_idle() => { let cycles = decode_u64(self.cpu.stack_pop()?); let duration_ns = cycles .checked_mul(self.clock.resolution_ns()) .ok_or_else(|| eyre::eyre!("play duration overflow"))?; Ok(SignalMessage::Duration(duration_ns)) } SignalInst::Play => Ok(SignalMessage::None), }}Then the runtime executes the component with that resolved value:
use vihaco::GeneratedComponent;
let msg = self.resolve_signal(&signal_inst)?;let effects = self.signal.execute_generated(signal_inst, msg)?;assert_eq!(effects, Effects::one(()));This example shows why composites own message resolution:
- the message depends on host stack state
- the message depends on clock resolution
- the message depends on whether the generator is idle
- the component can stay focused on execution once the message is ready
When a component returns a non-unit effect, hand-written runtimes normally either:
- extract exactly one control/data effect with
expect_exactly_one_effect(...), or - lift the returned values into a runtime-local sum-effect enum and continue that effect set explicitly
When To Use message = ()
Section titled “When To Use message = ()”Use message = () when the component can execute directly from:
- the instruction itself
- the component’s own local state
For example:
use eyre::Result;use vihaco::{dispatch, Effects, Instruction};
#[derive(Debug, Clone, Instruction)]pub enum LampInst { On, Off,}
#[derive(Debug, Default)]pub struct Lamp { on: bool,}
#[dispatch(instruction = LampInst, message = ())]impl Lamp { fn execute(&mut self, inst: &LampInst, _msg: ()) -> Result<Effects<()>> { self.on = matches!(inst, LampInst::On); Ok(Effects::none()) }}In this case there is nothing meaningful for the composite to resolve, so a unit message is the right fit.
Composite Message Types
Section titled “Composite Message Types”When an outer composite wraps inner components, it can also wrap their message types.
use vihaco::Message;
#[derive(Message)]struct DemoMsg;
#[derive(Message)]enum CompositeMsg { Inner(DemoMsg),}This pattern keeps the outer component or composite boundary explicit:
- outer instructions wrap inner instructions
- outer messages wrap inner messages
- routing stays visible in the outer type signatures
The same design rule applies here too: the outer composite layer decides which inner message variant to construct.
Instruction Vs Message Vs Effect
Section titled “Instruction Vs Message Vs Effect”A simple way to choose the right type is:
- use
Instructionfor bytecode-visible operations - use
Messagefor resolved execution input produced by the composite - use
Effectfor returned values consumed after execution
Good candidates for Message:
- timing data derived from a runtime clock
- values popped from a stack before execution
- validated handles or resolved addresses
- execution-local context that should not be part of source syntax
Usually not a good fit for Message:
- the main operation being requested
- long-lived component state
- broadcast or runtime follow-up values that belong in the effect stream
Practical Guidance
Section titled “Practical Guidance”- Start with
message = ()unless execution genuinely needs resolved input. - If a component needs context from the wider runtime, prefer resolving that context into a message.
- Keep message types plain and specific to execution needs.
- Let composites do lookups, stack access, timing derivation, and validation before calling component execution.
- Keep effects separate from messages so post-execution output stays explicit.
What Comes Next
Section titled “What Comes Next”Messages make the most sense alongside the surrounding component and composite model.
Continue with: