Generating Rust Code#
Rust is a modern programming language focusing on memory safety and thread safety. It is fast and memory-efficient, with no runtime or garbage collector. More information is available at rust-lang.org.
To generate Rust code, call the code generator with:
java -cp "path to installation/bin/*" codegen.Main -l rust -p ssc -o src/oven oven.xml-o src/oven writes src/oven.rs. The machine name is the file name, oven, so the generated type is Oven. In the example, oven.xml and codegen.cfg stay at the crate root. src/ contains main.rs, the generated oven.rs, magnetron.rs and cooking_timer.rs.
The generator reads codegen.cfg from the directory it is run in. Two keys name the types the handler borrows. Both are required. If either is missing, generation stops and the .rs file is left as it was.
RustDataType = crate::magnetron::Magnetron
RustTimerServiceType = crate::cooking_timer::CookingTimerRustDataType is the type guards and actions use, under the name data. In the oven it is the magnetron, switched with data.on() and data.off(). With -Trace, the data type also provides a log method, for example one that prints the transition:
pub fn log(&self, from: &str, to: &str, event: &str) {
println!("From: {from}, To: {to}, Event {event}");
}RustTimerServiceType is the timer service actions start and stop, under the name timer_service. In the oven it is the cooking timer: timer_service.start(), timer_service.pause(), and the guard timer_service.preset() > 0. A machine that does not start a timer sets RustTimerServiceType to () and passes &mut (). The same works for RustDataType when the guards and actions need no data.
Further fields of the machine are attributes in the same file. They become public fields of the struct:
INSTANCE_ATTRIBUTE_0 = timer_id
INSTANCE_ATTRIBUTE_TYPE_0 = usize
INSTANCE_ATTRIBUTE_DEFAULT_VALUE_0 = 99
INSTANCE_ATTRIBUTE_1 = time
INSTANCE_ATTRIBUTE_TYPE_1 = u32
INSTANCE_ATTRIBUTE_DEFAULT_VALUE_1 = 0The output name is the generated file name, the state machine type, and the prefix of the event, state and outcome enums. A path is allowed: only the file name is used for the type. Use a lower-case name so the file can be a Rust module.
The generated code uses match and if / else, and core only: no std, no allocation, no trait objects and no unsafe. It compiles unchanged in a #![no_std] crate, for example in firmware for a Cortex-M microcontroller, and passes rustc and Clippy without warnings. The file starts with inner #![allow(...)] attributes, so include it as a module of its own (mod oven;), not with include!.
After writing the file, the generator runs rustfmt when RustFormatCommand is set (the default is rustfmt --edition 2021) and rustfmt is on PATH or in ~/.cargo/bin. An empty RustFormatCommand skips formatting.

States and events are enums. Each has as_str for the name as written in the model, and Display is built on it, so {} prints that name. The states derive Debug, Clone, Copy, PartialEq, Eq and Hash; for the events see Events that carry data.
pub enum OvenStates {
Super,
Completed,
Cooking,
CookingPause,
Idle,
}
impl OvenStates {
pub const fn as_str(self) -> &'static str { /* one arm per state */ }
}pub enum OvenEvents {
evTimeout,
evDec,
evDoorOpen,
evDoorClosed,
evInc,
evPwr { pwr: u16 },
OvenNoMsg,
}handle_event returns OvenStep:
pub enum OvenStep {
/// No transition was taken.
Ignored,
/// A transition on a conditional event (written with #) was taken.
Condition,
/// A transition was taken.
Taken,
}When a state has regions, each region handles the event and the results combine in that order, so Taken wins over Condition and Condition over Ignored. The parent handles the event when every region returns Ignored.
The machine struct keeps its state variables private. One reader per variable returns the active state: state() for the top level, state_main_region() for the region named MainRegion, and in general state_<name>(), in snake case, for each composite state and region. A reader of a nested state is meaningful while that state is active. Attributes from codegen.cfg become public fields.
type OvenData = crate::magnetron::Magnetron;
type OvenTimerService = crate::cooking_timer::CookingTimer;
pub struct Oven {
initialized: bool,
state: OvenStates,
state_main_region: OvenStates,
}
impl Oven {
pub fn state(&self) -> OvenStates { /* ... */ }
pub fn state_main_region(&self) -> OvenStates { /* ... */ }
pub fn innermost_active_states(&self) -> impl Iterator<Item = OvenStates> { /* ... */ }
pub fn handle_event(
&mut self,
ev: OvenEvents,
data: &mut OvenData,
timer_service: &mut OvenTimerService,
) -> OvenStep {
/* the first call runs the entry code of the initial states */
}
pub fn initialize(&mut self, data: &mut OvenData, timer_service: &mut OvenTimerService) {
/* the entry code of the initial states, once */
}
}Build the machine with Oven::default() and send events to it. The first handle_event runs the entry code of the initial states. Call initialize to run that code earlier, for example to start the timers of the initial states at power-up, before the first event arrives. Neither a later initialize nor handle_event runs it again.
let mut magnetron = Magnetron::default();
let mut timer = CookingTimer::default();
let mut oven = Oven::default();
oven.initialize(&mut magnetron, &mut timer);
let step = oven.handle_event(OvenEvents::evPwr { pwr: 1200 }, &mut magnetron, &mut timer);The innermost active states#
innermost_active_states() returns the states the machine is in at the innermost level, one per active region, like getInnermostActiveStates() of the C++ backend. It needs no allocation, so it works without std: the generated code collects the states in an array as large as the most states that can be active at once, and returns an iterator over them.
for state in oven.innermost_active_states() {
println!("in {}", state); // e.g. "in Cooking"
}This is all a log line needs, or a report of the target’s state to the code generator’s viewer (-V), which highlights the reported states in the diagram:
let names: Vec<&str> = oven.innermost_active_states().map(|state| state.as_str()).collect();
send(&format!("from=oven;state={}", names.join(",")));On a target without std, write the names one by one into the report instead of collecting them, for example over a serial port:
write!(uart, "from=oven;state=").ok();
for (i, state) in oven.innermost_active_states().enumerate() {
write!(uart, "{}{}", if i > 0 { "," } else { "" }, state).ok();
}
writeln!(uart).ok();Events that carry data#
An event can carry named fields. The fields belong to the event name, so every transition on that name uses the same fields. The type is Rust type text, such as u32 or crate::Id. An event with no fields stays a variant without data.
In the built-in editor the Event Overview shows the fields after the event name. Edit payload… opens the table of names and types. The transition dialog shows that payload under the event text, and Edit… opens the same table. A conditional event, marked with the checkbox or a leading #, carries no fields.
The diagram file stores the table. A type may contain :: and angle brackets. Each field has a name and a type. The name is an identifier, unique for that event. Avoid Rust keywords and the names the generated code uses itself: data, ev, timer_service, ev_consumed and self. Generation does not check them; rustc reports a clash.
The generator writes a variant with those fields. The oven uses this for the power event. The caller passes the value, and the transition action reads the field:
OvenEvents::evPwr { pwr: 1200 }evPwr / data.set_power(pwr);Guards and actions of that event, and a choice the transition enters, use the field names in the same way: value > 10, or println!("value={} from={}", value, from).
Entry, exit and do code also run on other paths, such as initialization and history. A value they need is stored in data by the transition action.
The event value is Copy and is passed to every region of a state, so each field type is Copy. as_str and Display print the event name. The derived Debug prints the fields. An event enum with payload fields derives Debug, Clone, Copy and PartialEq, which allows a field of type f32. An enum whose events carry no fields also derives Eq and Hash.
The complete example is microwave_rust.
Build it with cargo build, then run with cargo run. It needs no crate besides std, so it runs unchanged on Windows, macOS and Linux.
Expected command line output:
Oven::off
Use + for inc; - for dec; o for open and c for close; q to quit.
Use 'p' to set medium power (400 W); 'P' to set high power (1200 W).
Type the keys and press Enter.A second example, blinky, runs the generated code in no_std firmware: an LED blinker with a failsafe state, with composite states, regions and timers, on a NUCLEO-L432KC board (STM32L432KC) and, without a board, in QEMU. It is described step by step in How to use generated state machines in no_std Rust firmware.
How to continue#
- Download codegen with included examples
- Modify the state machine model and regenerate Rust code to explore behavior
- Read the manual for all code generator features