Multi-Language Support
Cerulion Core is designed for multi-language support from the ground up. Generate Rust, Python, and C++ code from a single YAML schema with guaranteed binary compatibility.
Current Status
Language Code Generation Publisher/Subscriber Status Rust ✅ Working ✅ Working Fully implemented Python ⏳ Designed ⏳ Future Design complete C++ ⏳ Designed ⏳ Future Design complete
Rust support is fully working. Python and C++ generators are designed but not yet implemented. The foundation is solid - same schema format, same memory layout, maximum compatibility.
Architecture
Binary Compatibility
All languages produce identical memory layouts when using the same YAML schema:
Cross-Language Compatibility :
✅ Rust sends → Python/C++ receives
✅ Python sends → Rust/C++ receives
✅ C++ sends → Rust/Python receives
Memory layout verification :
Rust: #[repr(C)] attribute
Python: struct.pack format strings
C++: __attribute__((packed))
Binary compatibility means you can send messages from Rust and receive them in Python or C++ (when implemented) without any conversion. The memory layout is identical across all languages.
Type Mappings
Schema types map to language-specific types as follows:
Schema Type Rust Python C++ Size Binary boolboolboolbool1 0x00/0x01uint8u8intuint8_t1 0x00-0xFFuint16u16intuint16_t2 Little-endian uint32u32intuint32_t4 Little-endian uint64u64intuint64_t8 Little-endian int8i8intint8_t1 Two’s complement int16i16intint16_t2 Little-endian int32i32intint32_t4 Little-endian int64i64intint64_t8 Little-endian floatf32floatfloat4 IEEE 754 doublef64floatdouble8 IEEE 754 string_fixed[N][u8; N]byteschar[N]N UTF-8 bytes array[T] size:N[T; N]List[T]std::array<T,N>N×sizeof(T) Packed
All types use little-endian byte order and standard IEEE 754 for floating-point numbers. This ensures compatibility across different architectures.
Rust (Current Implementation)
Code Generation
Rust code generation is fully working via build.rs:
# schemas/sensor_data.yaml
schemas :
SensorData :
fields :
float temperature :
float pressure :
uint64 timestamp :
Generated Rust :
#[derive( Clone , Copy , Debug )]
#[repr( C )]
pub struct SensorData {
pub temperature : f32 ,
pub pressure : f32 ,
pub timestamp : u64 ,
}
Usage
rust_usage.rs
python_usage.py
cpp_usage.cpp
use cerulion_core :: prelude ::* ;
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 ,
}) ? ;
Rust code generation happens automatically during cargo build. No manual steps required.
Python (Future Implementation)
Planned Code Generation
Python generator will produce dataclasses with serialization:
"""Auto-generated from sensor_data.yaml"""
from dataclasses import dataclass
import struct
from typing import ClassVar
@dataclass
class SensorData :
"""Temperature and pressure sensor readings"""
temperature: float # Temperature in Celsius
pressure: float # Pressure in hPa
timestamp: int # Unix timestamp in nanoseconds
# C struct layout compatibility
_FORMAT : ClassVar[ str ] = '<ffQ' # little-endian: float, float, uint64
_SIZE : ClassVar[ int ] = 16 # 4 + 4 + 8 bytes
def to_bytes ( self ) -> bytes :
"""Serialize to bytes (compatible with Rust cerulion_core)"""
return struct.pack(
self . _FORMAT ,
self .temperature,
self .pressure,
self .timestamp
)
@ classmethod
def from_bytes ( cls , data : bytes ) -> 'SensorData' :
"""Deserialize from bytes (compatible with Rust cerulion_core)"""
if len (data) != cls . _SIZE :
raise ValueError ( f "Expected { cls . _SIZE } bytes, got { len (data) } " )
temperature, pressure, timestamp = struct.unpack( cls . _FORMAT , data)
return cls (
temperature = temperature,
pressure = pressure,
timestamp = timestamp
)
Planned Usage
python_subscriber.py
rust_publisher.rs
cpp_subscriber.cpp
import zenoh
from python_generated.sensor_data import SensorData
# Subscribe to Rust-published messages
session = zenoh.open()
sub = session.declare_subscriber( "sensors" )
for sample in sub.receiver:
data = SensorData.from_bytes(sample.payload)
print ( f "Temp: { data.temperature } °C, Pressure: { data.pressure } hPa" )
Python generator will be a standalone script that reads the same YAML schema files and generates Python dataclasses. Binary compatibility with Rust is guaranteed through struct.pack format strings.
C++ (Future Implementation)
Planned Code Generation
C++ generator will produce POD structs with standard layout:
#ifndef CERULION_SENSOR_DATA_HPP
#define CERULION_SENSOR_DATA_HPP
#include <cstdint>
#include <array>
#include <stdexcept>
#include <cstring>
namespace cerulion {
/// Sensor data structure for temperature and pressure readings
struct SensorData {
float temperature; ///< Temperature reading in Celsius
float pressure; ///< Pressure reading in hPa
uint64_t timestamp; ///< Unix timestamp in nanoseconds
// Constructor
SensorData ()
: temperature ( 0.0 f )
, pressure ( 0.0 f )
, timestamp ( 0 )
{}
SensorData ( float temp , float press , uint64_t ts )
: temperature (temp)
, pressure (press)
, timestamp (ts)
{}
// Size in bytes (matches Rust layout)
static constexpr size_t size () {
return sizeof (SensorData);
}
// Serialize to bytes (compatible with Rust cerulion_core)
std :: array < uint8_t , sizeof (SensorData)> to_bytes () const {
std ::array < uint8_t , sizeof (SensorData) > bytes;
std :: memcpy ( bytes . data (), this , sizeof (SensorData));
return bytes;
}
// Deserialize from bytes (compatible with Rust cerulion_core)
static SensorData from_bytes ( const uint8_t* data , size_t len ) {
if (len != sizeof (SensorData)) {
throw std :: invalid_argument ( "Invalid byte length" );
}
SensorData result;
std :: memcpy ( & result, data, sizeof (SensorData));
return result;
}
} __attribute__(( packed )) ; // Ensure no padding
// Verify size matches Rust (4 + 4 + 8 = 16 bytes)
static_assert ( sizeof (SensorData) == 16 , "Size mismatch with Rust layout" );
} // namespace cerulion
#endif // CERULION_SENSOR_DATA_HPP
Planned Usage
cpp_subscriber.cpp
rust_publisher.rs
python_subscriber.py
#include "cpp_generated/sensor_data.hpp"
#include <zenohcxx/zenoh.hxx>
using namespace cerulion ;
// Subscribe to Rust-published messages
auto session = zenoh :: open ();
auto subscriber = session . declare_subscriber ( "sensors" );
for ( const auto & sample : subscriber . receiver ()) {
auto data = SensorData :: from_bytes (
sample . payload (). data (),
sample . payload (). size ()
);
std ::cout << "Temp: " << data . temperature << "°C, "
<< "Pressure: " << data . pressure << " hPa \n " ;
}
C++ generator will be a standalone script that reads the same YAML schema files and generates C++ headers. Binary compatibility with Rust is guaranteed through __attribute__((packed)) and memcpy.
Future Generators
Implementation Plan
Python and C++ generators will be implemented as standalone scripts:
# Future: Generate Python/C++ bindings
python scripts/generate_bindings.py --lang python
python scripts/generate_bindings.py --lang cpp
# Or use Makefile
make python
make cpp
Benefits of this approach :
✅ Separation of concerns (Rust generation in build.rs, others in scripts)
✅ Flexibility (generate only what you need)
✅ No extra build dependencies in Rust
✅ Cross-platform (Python scripts work everywhere)
Generator Features
Both generators will support:
✅ All primitive types (bool, int8-64, uint8-64, float, double)
✅ Fixed-size arrays
✅ Fixed-size strings (string_fixed[N])
✅ Partial array naming (named accessors for known indices)
✅ Nested custom types
✅ Imports and shared index groups
Cross-Language Examples
Rust → Python (Future)
rust_publisher.rs
python_subscriber.py
let publisher = Publisher :: < SensorData > :: create ( "sensors" ) ? ;
publisher . send ( SensorData {
temperature : 23.5 ,
pressure : 1013.25 ,
timestamp : 1234567890 ,
}) ? ;
Python → Rust (Future)
python_publisher.py
rust_subscriber.rs
session = zenoh.open()
pub = session.declare_publisher( "sensors" )
data = SensorData( temperature = 23.5 , pressure = 1013.25 , timestamp = 1234567890 )
pub.put(data.to_bytes())
Testing Binary Compatibility
When Python/C++ generators are ready, you can test cross-language compatibility:
# Terminal 1: Rust publisher
cargo run --example v2_publisher
# Terminal 2: Python subscriber
python examples/python_subscriber.py
# Terminal 3: C++ subscriber
./examples/cpp_subscriber
# All should receive identical data! ✅
Binary compatibility testing will verify that messages sent from one language are correctly received and deserialized in another language. This ensures the memory layout is truly identical.
Summary
Now :
✅ Create YAML schema → Rust code auto-generated
✅ Use with cerulion_core Publisher/Subscriber
✅ Zero-copy local + async network
Later :
⏳ Add Python generator script
⏳ Add C++ generator script
⏳ Cross-language compatibility
⏳ API wrappers for Python/C++ Zenoh integration
Foundation is solid! The same YAML schema format, same memory layout, maximum compatibility.
Next Steps