Skip to main content

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

LanguageCode GenerationPublisher/SubscriberStatus
Rust✅ Working✅ WorkingFully implemented
Python⏳ Designed⏳ FutureDesign complete
C++⏳ Designed⏳ FutureDesign 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 TypeRustPythonC++SizeBinary
boolboolboolbool10x00/0x01
uint8u8intuint8_t10x00-0xFF
uint16u16intuint16_t2Little-endian
uint32u32intuint32_t4Little-endian
uint64u64intuint64_t8Little-endian
int8i8intint8_t1Two’s complement
int16i16intint16_t2Little-endian
int32i32intint32_t4Little-endian
int64i64intint64_t8Little-endian
floatf32floatfloat4IEEE 754
doublef64floatdouble8IEEE 754
string_fixed[N][u8; N]byteschar[N]NUTF-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

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

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.0f)
        , pressure(0.0f)
        , 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

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

let publisher = Publisher::<SensorData>::create("sensors")?;
publisher.send(SensorData {
    temperature: 23.5,
    pressure: 1013.25,
    timestamp: 1234567890,
})?;

Python → Rust (Future)

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