This guide explains how to transform Rust Hardware Abstraction Layer (HAL) traits into Idol interface definitions for use in Hubris-based systems. Based on practical experience converting the digest traits, this guide covers the key patterns, challenges, and solutions.
Rust HAL traits provide compile-time polymorphism with:
Idol interfaces provide runtime communication with:
| Rust Trait Concept | Idol Equivalent | Transformation Strategy |
|---|---|---|
&mut self methods | Session-based operations | Use session IDs |
| Associated types | Concrete types | Define enums/structs |
| Lifetimes | Ownership transfer | Memory leases |
| Generic parameters | Multiple operations | One operation per type |
| Zero-cost abstractions | IPC overhead | Optimize message structure |
Problem: Rust traits use &mut self for stateful operations. Solution: Use session IDs to track state across IPC boundaries.
// Original Trait pub trait DigestOp: ErrorType { fn update(&mut self, input: &[u8]) -> Result<(), Self::Error>; fn finalize(self) -> Result<Self::Output, Self::Error>; }
// Idol Interface Interface( name: "Digest", ops: { "init_sha256": ( reply: Result(ok: "u32", err: CLike("DigestError")), // Returns session ID ), "update": ( args: { "session_id": "u32", "len": "u32" }, leases: { "data": (type: "[u8]", read: true, max_len: Some(1024)) }, reply: Result(ok: "()", err: CLike("DigestError")), ), "finalize_sha256": ( args: { "session_id": "u32" }, leases: { "digest_out": (type: "[u32; 8]", write: true) }, reply: Result(ok: "()", err: CLike("DigestError")), ), }, )
Problem: Rust traits use generics to support multiple types. Solution: Create separate operations for each concrete type.
// Original Generic Trait pub trait DigestInit<T: DigestAlgorithm>: ErrorType { fn init(&mut self, params: T) -> Result<Self::OpContext<'_>, Self::Error>; }
// Idol Interface - Expanded Operations "init_sha256": (/* ... */), "init_sha384": (/* ... */), "init_sha512": (/* ... */), "init_sha3_256": (/* ... */), // etc.
Problem: Rust uses references and slices for zero-copy operations. Solution: Use Idol memory leases for efficient data transfer.
| Rust Pattern | Idol Lease Pattern | Use Case |
|---|---|---|
&[u8] | read: true | Input data |
&mut [u8] | write: true | Output buffers |
&T | read: true | Configuration structs |
&mut T | write: true | Result structs |
Problem: Traits use associated error types and generic error handling. Solution: Define comprehensive concrete error enums.
// Original - Generic Error pub trait ErrorType { type Error: Error; } pub trait Error: core::fmt::Debug { fn kind(&self) -> ErrorKind; }
// Idol - Concrete Error Enum #[derive(Copy, Clone, Debug, FromPrimitive, Eq, PartialEq, IdolError, counters::Count)] #[repr(u32)] pub enum DigestError { InvalidInputLength = 1, UnsupportedAlgorithm = 2, // ... comprehensive error cases #[idol(server_death)] ServerRestarted = 100, }
Identify State Management Patterns
&mut self → Need session managementself → Need session cleanupMap Data Flow
Catalog Error Cases
ErrorKind to specific error variantsCreate the IDL File
mkdir -p hubris/idl/ touch hubris/idl/my_trait.idol
Define Operations Structure
Interface( name: "MyTrait", ops: { // Initialization operations "init_*": (/* ... */), // State manipulation operations "operation_*": (/* ... */), // Cleanup operations "reset": (/* ... */), // Convenience operations "oneshot_*": (/* ... */), }, )
Design Session Management
u32 session IDsDirectory Structure
hubris/drv/my-trait-api/
├── Cargo.toml
├── build.rs
└── src/
└── lib.rs
Configure Cargo.toml
[package] name = "drv-my-trait-api" version = "0.1.0" edition = "2021" [dependencies] idol-runtime.workspace = true num-traits.workspace = true zerocopy.workspace = true zerocopy-derive.workspace = true counters = { path = "../../lib/counters" } derive-idol-err = { path = "../../lib/derive-idol-err" } userlib = { path = "../../sys/userlib" } [build-dependencies] idol.workspace = true [lib] test = false doctest = false bench = false [lints] workspace = true
Create build.rs
fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> { idol::client::build_client_stub("../../idl/my_trait.idol", "client_stub.rs")?; Ok(()) }
Create Zerocopy-Compatible Types
#[derive( Copy, Clone, Debug, PartialEq, Eq, zerocopy::IntoBytes, zerocopy::FromBytes, zerocopy::Immutable, zerocopy::KnownLayout, )] #[repr(C, packed)] // Use packed for complex structs pub struct MyConfig { pub field1: u32, pub field2: u8, // Avoid bool - use u8 instead pub enabled: u8, }
Define Error Types
#[derive( Copy, Clone, Debug, FromPrimitive, Eq, PartialEq, IdolError, counters::Count, )] #[repr(u32)] pub enum MyTraitError { // Map from original ErrorKind InvalidInput = 1, HardwareFailure = 2, // ... #[idol(server_death)] ServerRestarted = 100, }
Create Enum Types for IPC
#[derive( Copy, Clone, Debug, PartialEq, Eq, zerocopy::IntoBytes, zerocopy::Immutable, zerocopy::KnownLayout, FromPrimitive, )] #[repr(u32)] // Use u32 for enums pub enum MyAlgorithm { Algorithm1 = 0, Algorithm2 = 1, }
Input Data Patterns
"process_data": ( args: { "len": "u32" }, leases: { "input_data": (type: "[u8]", read: true, max_len: Some(4096)), }, ),
Output Data Patterns
"get_result": ( args: { "session_id": "u32" }, leases: { "output_buffer": (type: "[u8]", write: true, max_len: Some(1024)), }, ),
Configuration Patterns
"configure": ( args: { "session_id": "u32" }, leases: { "config": (type: "MyConfig", read: true), }, ),
Problem: Rust traits use associated types for flexibility.
pub trait DigestAlgorithm { const OUTPUT_BITS: usize; type Digest; }
Solution: Define concrete types and use constants.
pub const SHA256_WORDS: usize = 8; pub type Sha256Digest = DigestOutput<SHA256_WORDS>; #[repr(C)] pub struct DigestOutput<const N: usize> { pub value: [u32; N], }
Problem: Rust contexts have lifetime dependencies.
pub trait DigestInit<T>: ErrorType { type OpContext<'a>: DigestOp where Self: 'a; fn init<'a>(&'a mut self, params: T) -> Result<Self::OpContext<'a>, Self::Error>; }
Solution: Replace with session-based state management.
// Server maintains context mapping struct DigestServer { contexts: HashMap<u32, DigestContext>, next_session_id: u32, }
Problem: Single generic method supports multiple types.
fn process<T: Algorithm>(&mut self, data: &[u8], algo: T) -> Result<T::Output, Error>;
Solution: Create type-specific operations.
"process_sha256": (/* ... */), "process_sha384": (/* ... */), "process_aes": (/* ... */),
Problem: Rust can return complex generic types.
fn finalize(self) -> Result<Self::Output, Self::Error>;
Solution: Use output leases for complex types.
"finalize": ( args: { "session_id": "u32" }, leases: { "result": (type: "MyResult", write: true) }, reply: Result(ok: "()", err: CLike("MyError")), ),
All types used in Idol interfaces must be zerocopy-compatible:
// ✅ Good - Zerocopy compatible #[derive(zerocopy::IntoBytes, zerocopy::FromBytes, zerocopy::Immutable)] #[repr(C)] pub struct GoodConfig { pub value: u32, pub enabled: u8, // Not bool! pub _padding: [u8; 3], // Explicit padding } // ❌ Bad - Not zerocopy compatible pub struct BadConfig { pub value: u32, pub enabled: bool, // bool doesn't implement FromBytes pub data: Vec<u8>, // Dynamic allocation }
// ✅ Good - Use u32 for enums #[derive(FromPrimitive)] #[repr(u32)] pub enum MyEnum { Variant1 = 0, Variant2 = 1, } // ❌ Bad - u8 enums with FromBytes need 256 variants #[repr(u8)] pub enum SmallEnum { A = 0, B = 1, // Only 2 variants - FromBytes won't work }
// ✅ Good - Use packed for complex layouts #[repr(C, packed)] pub struct PackedStruct { pub field1: u8, pub field2: u32, // No padding issues } // ✅ Good - Manual padding control #[repr(C)] pub struct PaddedStruct { pub field1: u8, pub _pad: [u8; 3], // Explicit padding pub field2: u32, }
Map all possible error conditions from the original trait:
// Original trait error kinds pub enum ErrorKind { InvalidInputLength, UnsupportedAlgorithm, HardwareFailure, // ... } // Idol error enum - comprehensive mapping #[derive(Copy, Clone, Debug, FromPrimitive, IdolError, counters::Count)] #[repr(u32)] pub enum MyTraitError { // Map each ErrorKind to a specific variant InvalidInputLength = 1, UnsupportedAlgorithm = 2, HardwareFailure = 3, // Add IPC-specific errors InvalidSession = 10, TooManySessions = 11, // Required for Hubris #[idol(server_death)] ServerRestarted = 100, }
// Add context-specific error variants pub enum MyTraitError { // Operation-specific errors InitializationFailed = 20, UpdateFailed = 21, FinalizationFailed = 22, // Resource-specific errors OutOfMemory = 30, BufferTooSmall = 31, InvalidConfiguration = 32, }
Batch Operations: Combine related parameters into single calls
// ✅ Good - Single call with all parameters "configure_and_start": ( args: { "algorithm": "MyAlgorithm", "buffer_size": "u32", "timeout_ms": "u32", }, ), // ❌ Bad - Multiple round trips "set_algorithm": (args: {"algo": "MyAlgorithm"}), "set_buffer_size": (args: {"size": "u32"}), "set_timeout": (args: {"timeout": "u32"}), "start": (),
Efficient Data Transfer: Use appropriate lease sizes
leases: { // Size limits based on expected usage "small_data": (type: "[u8]", read: true, max_len: Some(256)), "large_data": (type: "[u8]", read: true, max_len: Some(4096)), }
ARM Target Build:
cargo build -p drv-my-trait-api --target thumbv7em-none-eabihf
Generated Code Inspection:
ls target/thumbv7em-none-eabihf/debug/build/drv-my-trait-api*/out/ head -50 target/thumbv7em-none-eabihf/debug/build/drv-my-trait-api*/out/client_stub.rs
Here's a complete example showing the transformation of a simple trait:
pub trait Crypto: ErrorType { type Algorithm: CryptoAlgorithm; type Context<'a>: CryptoOp where Self: 'a; fn init<'a>(&'a mut self, algo: Self::Algorithm) -> Result<Self::Context<'a>, Self::Error>; } pub trait CryptoOp: ErrorType { type Output; fn process(&mut self, data: &[u8]) -> Result<(), Self::Error>; fn finalize(self) -> Result<Self::Output, Self::Error>; }
Interface( name: "Crypto", ops: { "init_aes": ( reply: Result(ok: "u32", err: CLike("CryptoError")), ), "init_chacha": ( reply: Result(ok: "u32", err: CLike("CryptoError")), ), "process": ( args: { "session_id": "u32", "len": "u32" }, leases: { "data": (type: "[u8]", read: true, max_len: Some(1024)) }, reply: Result(ok: "()", err: CLike("CryptoError")), ), "finalize_aes": ( args: { "session_id": "u32" }, leases: { "output": (type: "[u8; 16]", write: true) }, reply: Result(ok: "()", err: CLike("CryptoError")), ), "finalize_chacha": ( args: { "session_id": "u32" }, leases: { "output": (type: "[u8; 32]", write: true) }, reply: Result(ok: "()", err: CLike("CryptoError")), ), }, )
// drv/crypto-api/src/lib.rs #![no_std] use derive_idol_err::IdolError; use userlib::{sys_send, FromPrimitive}; #[derive(Copy, Clone, Debug, PartialEq, Eq, zerocopy::IntoBytes, zerocopy::Immutable, FromPrimitive)] #[repr(u32)] pub enum CryptoAlgorithm { Aes = 0, ChaCha = 1, } #[derive(Copy, Clone, Debug, FromPrimitive, Eq, PartialEq, IdolError, counters::Count)] #[repr(u32)] pub enum CryptoError { InvalidInput = 1, InvalidSession = 2, HardwareFailure = 3, #[idol(server_death)] ServerRestarted = 100, } include!(concat!(env!("OUT_DIR"), "/client_stub.rs"));
Converting Rust HAL traits to Idol interfaces requires careful consideration of:
The key is to preserve the semantic meaning and safety guarantees of the original trait while adapting to the constraints and patterns of the Hubris IPC system.
By following these patterns and guidelines, you can successfully transform complex Rust HAL traits into efficient, type-safe Idol interfaces that maintain the robustness and performance characteristics expected in embedded systems.