16 releases
Uses new Rust 2024
| new 0.4.7 | Apr 10, 2026 |
|---|---|
| 0.4.6 | Apr 5, 2026 |
| 0.4.4 | Mar 26, 2026 |
| 0.3.7 | Feb 23, 2026 |
| 0.3.3 | Jan 26, 2026 |
#1155 in Encoding
Used in 2 crates
73KB
691 lines
OSAL-RS-Serde
An extensible, lightweight serialization/deserialization framework for Rust, inspired by Serde but optimized for embedded systems and no-std environments.
Features
- ✅ No-std compatible: Works perfectly in bare-metal embedded environments
- ✅ Memory-efficient: Optimized for resource-constrained systems with predictable memory usage
- ✅ Extensible: Easy to create custom serializers for any format (binary, JSON, MessagePack, etc.)
- ✅ Derive Macro: Full support for
#[derive(Serialize, Deserialize)] - ✅ Type-safe: Leverages Rust's type system for compile-time guarantees
- ✅ Zero-copy: Direct buffer reads/writes without intermediate allocations
- ✅ Reusable: Can be used in any project, not just with osal-rs
Supported Types
Primitives
- Integers:
u8,i8,u16,i16,u32,i32,u64,i64,u128,i128 - Floats:
f32,f64 - Boolean:
bool
Compound Types
- Arrays:
[T; N]for any serializable type T - Tuples:
(T1, T2),(T1, T2, T3)up to 3 elements - Option:
Option<T>for optional fields
Collections (with alloc feature)
- Vec:
Vec<T>for dynamic arrays - String:
Stringand&str
Custom Types
- Any struct with
#[derive(Serialize, Deserialize)] - Nested struct composition fully supported
Binary Format & Memory Sizes
The default ByteSerializer uses little-endian binary format with no padding:
bool: 1 byte (0 or 1)
u8/i8: 1 byte
u16/i16: 2 bytes
u32/i32: 4 bytes
u64/i64: 8 bytes
u128/i128: 16 bytes
f32: 4 bytes (IEEE 754)
f64: 8 bytes (IEEE 754)
Option<T>: 1 byte tag + sizeof(T) if Some, 1 byte if None
Array[T;N]: sizeof(T) * N (no length prefix)
Tuple: sum(sizeof each field)
Vec<T>: 4 bytes (u32 length) + sizeof(T) * length
String: 4 bytes (u32 length) + UTF-8 bytes
NOTE Due to the lack of ownership of the type &str it is only handled in serialization and not in deserialization
Installation
Add to your Cargo.toml:
[dependencies]
osal-rs-serde = { version = "0.4", features = ["derive"] }
Available features:
default: Includesallocfeature for Vec and String supportalloc: Enables dynamic allocation support (Vec, String)std: Enables standard library support (error traits, etc.)derive: Enables#[derive(Serialize, Deserialize)]macros (recommended)
For no-std environments without allocation:
[dependencies]
osal-rs-serde = { version = "0.4", default-features = false, features = ["derive"] }
Project Structure
The osal-rs-serde workspace includes:
- osal-rs-serde: Core library with traits and implementations
- osal-rs-serde/derive: Procedural macros for automatic derivation (optional, enabled via
derivefeature)
Everything is contained in a single package for ease of use.
Usage
With Derive Macros (Recommended)
Basic Struct Example
use osal_rs_serde::{Serialize, Deserialize, to_bytes, from_bytes};
#[derive(Serialize, Deserialize)]
struct SensorData {
temperature: i16,
humidity: u8,
pressure: u32,
}
fn main() {
// Create a structure
let data = SensorData {
temperature: 25,
humidity: 60,
pressure: 1013,
};
// Serialize
let mut buffer = [0u8; 32];
let len = to_bytes(&data, &mut buffer).unwrap();
println!("Serialized {} bytes", len);
// Deserialize
let read_data: SensorData = from_bytes(&buffer[..len]).unwrap();
println!("Temperature: {}", read_data.temperature);
}
Struct with Optional Fields
use osal_rs_serde::{Serialize, Deserialize, to_bytes, from_bytes};
#[derive(Serialize, Deserialize)]
struct Config {
device_id: u32,
name: Option<u8>, // Optional device name code
enabled: bool,
timeout: Option<u16>, // Optional timeout in ms
}
fn main() {
let config = Config {
device_id: 100,
name: Some(42),
enabled: true,
timeout: None,
};
let mut buffer = [0u8; 64];
let len = to_bytes(&config, &mut buffer).unwrap();
let decoded: Config = from_bytes(&buffer[..len]).unwrap();
}
Struct with Arrays and Tuples
use osal_rs_serde::{Serialize, Deserialize, to_bytes, from_bytes};
#[derive(Serialize, Deserialize)]
struct TelemetryPacket {
timestamp: u64,
coordinates: (i32, i32, i32), // x, y, z
samples: [u16; 8], // 8 sensor readings
status: u8,
}
fn main() {
let packet = TelemetryPacket {
timestamp: 1642857600,
coordinates: (100, 200, 50),
samples: [10, 20, 30, 40, 50, 60, 70, 80],
status: 0xFF,
};
let mut buffer = [0u8; 128];
let len = to_bytes(&packet, &mut buffer).unwrap();
println!("Telemetry packet: {} bytes", len);
}
Nested Structs
use osal_rs_serde::{Serialize, Deserialize, to_bytes, from_bytes};
#[derive(Serialize, Deserialize)]
struct Location {
latitude: i32,
longitude: i32,
}
#[derive(Serialize, Deserialize)]
struct Device {
id: u32,
battery: u8,
location: Location,
active: bool,
}
fn main() {
let device = Device {
id: 42,
battery: 85,
location: Location {
latitude: 45500000,
longitude: 9200000,
},
active: true,
};
let mut buffer = [0u8; 64];
let len = to_bytes(&device, &mut buffer).unwrap();
let decoded: Device = from_bytes(&buffer[..len]).unwrap();
println!("Device at {}, {}",
decoded.location.latitude,
decoded.location.longitude);
}
Complex Embedded System Example
use osal_rs_serde::{Serialize, Deserialize, to_bytes, from_bytes};
#[derive(Serialize, Deserialize)]
struct MotorControl {
motor_id: u8,
speed: i16, // -1000 to 1000
direction: bool, // true = forward, false = reverse
current: u16, // mA
}
#[derive(Serialize, Deserialize)]
struct RobotState {
timestamp: u64,
motors: [MotorControl; 4], // 4 motors
battery_voltage: u16, // mV
temperature: i8, // °C
error_flags: u32,
}
fn main() {
let state = RobotState {
timestamp: 1000000,
motors: [
MotorControl { motor_id: 0, speed: 500, direction: true, current: 1200 },
MotorControl { motor_id: 1, speed: 500, direction: true, current: 1150 },
MotorControl { motor_id: 2, speed: -300, direction: false, current: 800 },
MotorControl { motor_id: 3, speed: -300, direction: false, current: 850 },
],
battery_voltage: 12400, // 12.4V
temperature: 35,
error_flags: 0,
};
let mut buffer = [0u8; 256];
let len = to_bytes(&state, &mut buffer).unwrap();
println!("Robot state serialized: {} bytes", len);
// Deserialize and check
let decoded: RobotState = from_bytes(&buffer[..len]).unwrap();
println!("Battery: {}mV, Temp: {}°C",
decoded.battery_voltage,
decoded.temperature);
}
Manual Implementation
use osal_rs_serde::{Serialize, Deserialize, Serializer, Deserializer};
struct Point {
x: i32,
y: i32,
}
impl Serialize for Point {
fn serialize<S: Serializer>(&self, serializer: &mut S) -> Result<(), S::Error> {
serializer.serialize_i32(self.x)?;
serializer.serialize_i32(self.y)?;
Ok(())
}
}
impl Deserialize for Point {
fn deserialize<D: Deserializer>(deserializer: &mut D) -> Result<Self, D::Error> {
Ok(Point {
x: deserializer.deserialize_i32()?,
y: deserializer.deserialize_i32()?,
})
}
}
Usage with OSAL-RS Queue
Perfect for inter-task communication in RTOS environments:
use osal_rs::os::{Queue, QueueFn};
use osal_rs_serde::{Serialize, Deserialize, to_bytes, from_bytes};
#[derive(Serialize, Deserialize)]
struct Command {
id: u32,
action: u8,
params: [u16; 4],
}
fn sender_task(queue: &Queue) {
let cmd = Command {
id: 42,
action: 0x10,
params: [100, 200, 300, 400],
};
let mut buffer = [0u8; 32];
let len = to_bytes(&cmd, &mut buffer).unwrap();
queue.post(&buffer[..len], 100).unwrap();
}
fn receiver_task(queue: &Queue) {
let mut buffer = [0u8; 32];
queue.fetch(&mut buffer, 100).unwrap();
let cmd: Command = from_bytes(&buffer).unwrap();
println!("Received command: id={}, action=0x{:02X}", cmd.id, cmd.action);
}
Supported Types
The framework automatically supports serialization/deserialization for:
- Primitives:
bool,u8,i8,u16,i16,u32,i32,u64,i64,u128,i128,f32,f64 - Compound: Arrays
[T; N], tuples(T1, T2)and(T1, T2, T3),Option<T> - Collections (with
alloc):Vec<T>,String,&str - Custom: Any struct implementing
Serialize/Deserialize(or using derive) - Nested: Full support for nested struct composition
Custom Serializers
You can create custom serializers to support different formats (JSON, MessagePack, CBOR, etc.):
use osal_rs_serde::{Serializer, Deserializer, Error};
struct JsonSerializer<'a> {
buffer: &'a mut [u8],
position: usize,
}
impl<'a> Serializer for JsonSerializer<'a> {
type Error = Error;
fn serialize_u32(&mut self, name: &str, v: u32) -> Result<(), Self::Error> {
// Write JSON format: "name": value
// Your implementation here...
Ok(())
}
fn serialize_bool(&mut self, name: &str, v: bool) -> Result<(), Self::Error> {
// Write JSON format: "name": true/false
// Your implementation here...
Ok(())
}
// Implement other serialize_* methods...
}
// Similarly implement Deserializer trait for deserialization
See examples/custom_serializer.rs for a complete text-based serializer implementation.
Performance Considerations
Buffer Size Calculation
For fixed-size types, calculate buffer size at compile time:
#[derive(Serialize, Deserialize)]
struct Packet {
id: u32, // 4 bytes
value: i16, // 2 bytes
flags: u8, // 1 byte
}
// Total: 7 bytes
const BUFFER_SIZE: usize = 7;
let mut buffer = [0u8; BUFFER_SIZE];
Zero-Copy Operation
The serializer writes directly to your buffer with no intermediate allocations:
// Stack-allocated buffer - no heap allocation
let mut buffer = [0u8; 64];
let len = to_bytes(&data, &mut buffer)?;
// Use only the filled portion
send_to_uart(&buffer[..len]);
Compile-Time Guarantees
The type system ensures correctness:
- Cannot deserialize wrong type from buffer
- Compile-time checks for trait implementations
- No runtime type checks needed
Examples
The examples/ directory contains complete working examples demonstrating various features:
Running Examples
# Basic struct serialization
cargo run --example basic --features derive
# Using derive macros (recommended approach)
cargo run --example with_derive --features derive
# Arrays and tuples
cargo run --example arrays_tuples --features derive
# Nested struct composition
cargo run --example nested_structs --features derive
# Optional fields with Option<T>
cargo run --example optional_fields --features derive
# Complex embedded system (robot control)
cargo run --example robot_control --features derive
# Custom serializer implementation
cargo run --example custom_serializer
# Integration with OSAL-RS
cargo run --example integration --features derive
Example Descriptions
basic.rs: Simple manual implementation without derive macroswith_derive.rs: Same example using#[derive]macrosarrays_tuples.rs: Working with arrays and tuples in structsnested_structs.rs: Nested struct composition patternsoptional_fields.rs: UsingOption<T>for optional datarobot_control.rs: Complex real-world embedded system example with motor controlcustom_serializer.rs: Creating a custom text-based serializerintegration.rs: Integration with OSAL-RS queues for inter-task communication
Best Practices
1. Use Derive Macros
Always prefer derive macros for standard serialization:
#[derive(Serialize, Deserialize)]
struct MyStruct {
// fields...
}
2. Calculate Buffer Sizes
Pre-calculate buffer sizes for better performance:
const fn calculate_size() -> usize {
size_of::<u32>() + size_of::<i16>() + size_of::<bool>()
}
let mut buffer = [0u8; calculate_size()];
3. Error Handling
Always handle serialization errors appropriately:
match to_bytes(&data, &mut buffer) {
Ok(len) => send_data(&buffer[..len]),
Err(Error::BufferTooSmall) => {
// Handle buffer overflow
}
Err(e) => {
// Handle other errors
}
}
4. Versioning
Consider adding version fields for forward compatibility:
#[derive(Serialize, Deserialize)]
struct Message {
version: u8,
// other fields...
}
Comparison with Serde
| Feature | osal-rs-serde | serde |
|---|---|---|
| No-std support | ✅ Native | ✅ Via feature |
| Derive macros | ✅ Built-in | ✅ Separate crate |
| Binary size | Very small | Medium/Large |
| Supported formats | Custom (extendable) | Many built-in |
| Target use case | Embedded/RTOS | General purpose |
| Zero-copy | ✅ Always | Depends on format |
| Compile time | Fast | Slower |
| Learning curve | Gentle | Moderate |
Choose osal-rs-serde when:
- Working in embedded/no-std environments
- Need predictable memory usage
- Want minimal binary size
- Require simple, fast compilation
- Building RTOS applications
Choose serde when:
- Need many pre-built format implementations
- Working primarily with std
- Require advanced features (flatten, rename, etc.)
- Ecosystem integration is important
License
LGPL-2.1-or-later - See LICENSE for details.
Author
Antonio Salsi - passy.linux@zresa.it
Contributing
Contributions are welcome! Please feel free to submit a Pull Request.