Code Generation
Cerulion Core can automatically generate Rust structs (and future Python/C++ classes) from YAML schema files. This eliminates manual type definitions and ensures consistency across your codebase.
What is Code Generation?
Code generation reads YAML schema files and produces type-safe message structs in your target language. Instead of manually writing struct definitions, you define your message types once in YAML, and Cerulion Core generates the code automatically.
Benefits :
✅ Single source of truth : Define types once, use everywhere
✅ Type safety : Generated code is type-checked at compile time
✅ Consistency : Same schema produces identical layouts across languages
✅ Less boilerplate : No manual struct definitions needed
Quick Start
1. Create a Schema
Create a YAML file in your schemas/ directory:
# schemas/sensor_data.yaml
schemas :
SensorData :
description : Temperature and pressure sensor readings
fields :
float temperature :
description : Temperature in Celsius
float pressure :
description : Pressure in hPa
uint64 timestamp :
description : Timestamp in nanoseconds
2. Build Your Project
Run cargo build - code generation happens automatically:
You should see output like: Generated: sensor_data_generated.rs
3. Use Generated Code
Include the generated code in your project:
use_generated.rs
use_generated.py
use_generated.cpp
use cerulion_core :: prelude ::* ;
// Include generated code
include! ( concat! ( env! ( "OUT_DIR" ), "/sensor_data_generated.rs" ));
fn main () -> Result <(), Box < dyn std :: error :: Error >> {
// Use generated struct
let publisher = Publisher :: < SensorData > :: create ( "sensors" ) ? ;
let data = SensorData {
temperature : 23.5 ,
pressure : 1013.25 ,
timestamp : 1234567890 ,
};
publisher . send ( data ) ? ;
Ok (())
}
Cerulion Core supports two schema formats:
Modern, concise format with inline field syntax:
schemas :
SensorData :
description : Sensor readings
fields :
float temperature :
float pressure :
uint64 timestamp :
Features :
Inline syntax: float temperature: instead of verbose YAML
Multi-schema files: Define multiple types in one file
Shared index groups: Reuse array index definitions
Imports: Share common types across files
See the Schema Format page for complete details.
Original format still supported:
name : SensorData
fields :
- name : temperature
type : float
- name : pressure
type : float
- name : timestamp
type : uint64
The parser automatically detects format version. V2 format is recommended for new schemas, but legacy format continues to work for backward compatibility.
Generated Code Structure
Simple Message
Schema :
schemas :
SensorData :
fields :
float temperature :
float pressure :
uint64 timestamp :
Generated Code :
generated_rust.rs
generated_python.py
generated_cpp.hpp
#[derive( Clone , Copy , Debug )]
#[repr( C )]
pub struct SensorData {
pub temperature : f32 ,
pub pressure : f32 ,
pub timestamp : u64 ,
}
Arrays with Partial Naming
Schema :
schemas :
JointState :
fields :
float[20] positions :
indexes :
shoulder_left : 0
elbow_left : 1
wrist_left : 2
uint64 timestamp :
Generated Code :
generated_arrays.rs
generated_arrays.py
generated_arrays.hpp
#[derive( Clone , Copy , Debug )]
#[repr(transparent)]
pub struct PositionsArray {
data : [ f32 ; 20 ],
}
impl PositionsArray {
// Named accessors
pub fn shoulder_left ( & self ) -> f32 { self . data[ 0 ] }
pub fn set_shoulder_left ( & mut self , val : f32 ) { self . data[ 0 ] = val ; }
// Generic accessors
pub fn get ( & self , index : usize ) -> Option < f32 > { ... }
pub fn set ( & mut self , index : usize , val : f32 ) -> Result <(), ()> { ... }
}
#[derive( Clone , Copy , Debug )]
#[repr( C )]
pub struct JointState {
pub positions : PositionsArray ,
pub timestamp : u64 ,
}
Partial naming lets you name only the array indices you know, while still accessing all elements. This is useful for robot joints where you know some joint names but not all.
Build Process
Automatic Generation
Code generation happens automatically during cargo build:
Generated File Locations
Generated files are placed in the build output directory:
target/debug/build/cerulion-*/out/
├── sensor_data_generated.rs
├── joint_state_generated.rs
└── user_data_generated.rs
Including Generated Code
Use include!() macro to include generated code:
include! ( concat! ( env! ( "OUT_DIR" ), "/sensor_data_generated.rs" ));
OUT_DIR is set by Cargo and points to the build output directory. The concat!() macro combines the path with the generated filename.
Usage Examples
Example 1: Simple Message
Schema : schemas/sensor_data.yaml
schemas :
SensorData :
fields :
float temperature :
float pressure :
uint64 timestamp :
Usage :
usage1.rs
usage1.py
usage1.cpp
include! ( concat! ( env! ( "OUT_DIR" ), "/sensor_data_generated.rs" ));
let publisher = Publisher :: < SensorData > :: create ( "sensors" ) ? ;
publisher . send ( SensorData {
temperature : 23.5 ,
pressure : 1013.25 ,
timestamp : 1234567890 ,
}) ? ;
Example 2: Arrays with Named Indices
Schema : schemas/joint_state.yaml
schemas :
JointState :
fields :
float[20] positions :
indexes :
shoulder_left : 0
elbow_left : 1
uint64 timestamp :
Usage :
usage2.rs
usage2.py
usage2.cpp
include! ( concat! ( env! ( "OUT_DIR" ), "/joint_state_generated.rs" ));
let mut state = JointState {
positions : PositionsArray { data : [ 0.0 ; 20 ] },
timestamp : 0 ,
};
// Named access
state . positions . set_shoulder_left ( 1.5 );
state . positions . set_elbow_left ( 2.3 );
// Index access
state . positions . set ( 15 , 0.5 ) . ok ();
let publisher = Publisher :: < JointState > :: create ( "robot/joints" ) ? ;
publisher . send ( state ) ? ;
Example 3: Multi-Schema File
Schema : schemas/robot_messages.yaml
schemas :
Position3D :
fields :
float x :
float y :
float z :
Quaternion :
fields :
float w :
float x :
float y :
float z :
Pose :
fields :
Position3D position :
Quaternion orientation :
Usage :
usage3.rs
usage3.py
usage3.cpp
include! ( concat! ( env! ( "OUT_DIR" ), "/robot_messages_generated.rs" ));
let pose = Pose {
position : Position3D { x : 1.0 , y : 2.0 , z : 3.0 },
orientation : Quaternion { w : 1.0 , x : 0.0 , y : 0.0 , z : 0.0 },
};
Type Mappings
Schema types map to Rust types as follows:
Schema Type Rust Type Size Copy? boolbool1 ✅ int8i81 ✅ int16i162 ✅ int32i324 ✅ int64i648 ✅ uint8u81 ✅ uint16u162 ✅ uint32u324 ✅ uint64u648 ✅ floatf324 ✅ doublef648 ✅ string_fixed[N][u8; N]N ✅ array[T] size:N[T; N]N×sizeof(T) ✅
All generated types implement Copy, making them compatible with Cerulion Core’s automatic serialization. This ensures zero-copy local transport and efficient network serialization.
Multi-Language Support
Current Status
Language Status Generator Rust ✅ Working build.rs (automatic)Python ⏳ Designed Future script C++ ⏳ Designed Future script
Future Generators
Python and C++ generators are designed but not yet implemented. They will:
Read the same YAML schema files
Generate language-specific code (Python dataclasses, C++ POD structs)
Maintain binary compatibility with Rust
See the Multi-Language Support page for details.
Best Practices
# ✅ Good: V2 format (concise)
schemas :
SensorData :
fields :
float temperature :
uint64 timestamp :
# ⚠️ Works but verbose: Legacy format
name : SensorData
fields :
- name : temperature
type : float
# ✅ Good: Related types together
schemas :
Position3D :
fields : ...
Quaternion :
fields : ...
Pose :
fields : ...
# ⚠️ Less organized: Separate files
# position3d.yaml
# quaternion.yaml
# pose.yaml
3. Use Shared Index Groups
# ✅ Good: Define once, reuse
index_groups :
joint_names :
shoulder_left : 0
elbow_left : 1
schemas :
JointState :
fields :
float[20] positions :
indexes : joint_names
float[20] velocities :
indexes : joint_names # Reuse!
4. Document Your Schemas
# ✅ Good: Documented
schemas :
SensorData :
description : Temperature and pressure sensor readings
fields :
float temperature :
description : Temperature in Celsius
float pressure :
description : Pressure in hPa
Troubleshooting
”Generated file not found”
Problem : Generated file path is incorrect.
Solution : Check the build output directory:
// ✅ Correct: Uses OUT_DIR
include! ( concat! ( env! ( "OUT_DIR" ), "/sensor_data_generated.rs" ));
// ❌ Wrong: Hard-coded path
include! ( "target/debug/sensor_data_generated.rs" );
“Schema parse error”
Problem : YAML syntax error in schema file.
Solution : Validate YAML syntax:
# Check YAML syntax
yamllint schemas/your_schema.yaml
“Type not found”
Problem : Generated code not included.
Solution : Ensure include!() is called before use:
// ✅ Correct: Include before use
include! ( concat! ( env! ( "OUT_DIR" ), "/sensor_data_generated.rs" ));
fn main () {
let data = SensorData { ... }; // Now available
}
Next Steps