Skip to main content

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:
cargo build
You should see output like: Generated: sensor_data_generated.rs

3. Use Generated Code

Include the generated code in your project:
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(())
}

Schema Format

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.

Legacy Format (Backward Compatible)

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:
#[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:
#[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:
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:
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:
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 TypeRust TypeSizeCopy?
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

LanguageStatusGenerator
Rust✅ Workingbuild.rs (automatic)
Python⏳ DesignedFuture script
C++⏳ DesignedFuture 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

1. Use V2 Format

# ✅ 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