Operating System Abstraction Layer for Rust - A cross-platform compatibility layer for embedded and real-time systems development.
OSAL-RS provides a unified API for developing multi-platform embedded applications in Rust. It abstracts operating system-specific functionality, allowing you to write portable code that can run on different platforms with minimal changes.
- osal-rs: Main Operating System Abstraction Layer, with FreeRTOS and POSIX backends
- osal-rs-build: Build configuration tools and helpers (FreeRTOS type generation, POSIX porting shim compilation)
- osal-rs-porting: C FFI bridge layer for the FreeRTOS and POSIX backends
- osal-rs-tests: Comprehensive test suite for all components
- osal-rs-serde: ✨ Extensible serialization/deserialization framework with derive macros
- ✅ FreeRTOS: Fully implemented and tested
- ✅ POSIX: Fully implemented and tested (glibc/Linux) - native host backend for running, testing and simulating OSAL-RS applications without embedded hardware
- ✅ Serialization: Complete osal-rs-serde implementation with derive macros
- 🧪 Async/Await: Experimental, backend-agnostic, works on both FreeRTOS and POSIX
- 🚧 Other RTOSes: Under consideration
let timer = Timer::new("tick", 100, true, None, |_t, p| Ok(p.unwrap_or(Arc::new(()))))?;
let clone = timer.clone();
timer.start(0);
clone.stop(0); // the same timer: either handle drives it
drop(clone); // not the last handle: the timer stays alive
drop(timer); // last handle gone: the timer is destroyed hereTimer was already Clone, but on FreeRTOS a clone merely copied the raw handle: a delete() through one handle left every other one holding a dangling handle, and because Drop deleted per-handle, the first clone dropped destroyed the timer for everyone. Both backends now keep the timer in shared state, so every clone observes a deletion performed through any of them, and the timer is destroyed exactly once - when the last handle goes away. That last part is new on POSIX too, where the timer and its background thread previously leaked unless delete() was called explicitly.
Two further changes come with it, both bringing FreeRTOS in line with POSIX and with the trait documentation:
- the value a timer callback returns is now handed to the next firing; FreeRTOS discarded it and always re-passed the original parameter.
- the callback receives a non-owning handle. It used to receive one that owned the timer and destroyed it on return, so an auto-reload FreeRTOS timer deleted itself at its own first firing.
On FreeRTOS, destroying a timer by dropping its last handle completes on the timer daemon task rather than inline, so it takes effect one tick later; delete() is still immediate. Code that relied on Drop doing nothing (POSIX) or on a clone's Drop destroying the timer (FreeRTOS) needs revisiting.
// Before, on FreeRTOS only: join() deleted the task, killing it mid-work
spawned.join(core::ptr::null_mut())?;
// After, on both backends: join() blocks until the thread's closure returns
let mut ret: *mut core::ffi::c_void = core::ptr::null_mut();
spawned.join(&raw mut ret)?;The FreeRTOS implementation called vTaskDelete on the target task - the opposite of both the trait documentation ("blocks the calling thread until this thread terminates") and the POSIX implementation, which forwards to pthread_join. Since both FreeRTOS task wrappers already delete themselves once their callback returns, the old code was additionally a use-after-free whenever the task had already finished. Every spawned thread now carries an exit latch, so join waits for the thread and collects its return value on both backends. Code that used join() as a way to kill a FreeRTOS task must call delete() instead.
EventGroup::wait and EventGroup::wait_with_to_tick now take an explicit wait_for_all_bits: bool, matching FreeRTOS's xEventGroupWaitBits:
// Before
let bits = event_group.wait(mask, timeout_ticks);
// After
let bits = event_group.wait(mask, true, timeout_ticks); // AND: block until every bit in `mask` is set
let bits = event_group.wait(mask, false, timeout_ticks); // OR: block until any bit in `mask` is set
let bits = event_group.wait_with_to_tick(mask, true, timeout);Previously the two backends silently disagreed: POSIX only ever implemented AND-wait, while FreeRTOS hardcoded OR-wait (xWaitForAllBits = pdFALSE) regardless of what the trait docs said. Code that waited on multiple independent bits (e.g. "unblock when either of these two mutually exclusive events fires") could hang forever on POSIX while working by accident on FreeRTOS. Both backends now implement the same, explicit semantics.
OSAL-RS selects its implementation at compile time via Cargo features. There is no default backend - exactly one of freertos / posix must be enabled explicitly, or the crate fails to build. Enabling neither trips a compile_error!; enabling both is equally unsupported, since the two backends are mutually exclusive by design.
For bare-metal embedded targets (no_std). FreeRTOS provides preemptive multitasking with priority-based scheduling, mutexes with priority inheritance, semaphores, queues and software timers.
Requirements:
- FreeRTOS kernel properly configured and linked into your project
- C porting layer from
osal-rs-porting/freeretos/compiled and linked (an FFI bridge between Rust and FreeRTOS) - CMake build system set up for your embedded project (see CMake Integration)
- Rust toolchain with the appropriate embedded target installed
Configuration - ensure your FreeRTOSConfig.h includes:
#define configTICK_RATE_HZ 1000
#define configUSE_MUTEXES 1
#define configUSE_RECURSIVE_MUTEXES 1
#define configUSE_COUNTING_SEMAPHORES 1
#define configUSE_TIMERS 1
#define configUSE_QUEUE_SETS 1
#define configSUPPORT_DYNAMIC_ALLOCATION 1Build:
# Install Rust target (example for ARM Cortex-M33)
rustup target add thumbv8m.main-none-eabi
# Build with FreeRTOS support
cargo build --release --target thumbv8m.main-none-eabi --features freertosRuns on any POSIX/pthreads host so OSAL-RS applications - and their tests and doc examples - can execute for real on Linux/macOS without embedded hardware or a cross toolchain. Enabling posix disables no_std and builds the crate against std.
Requirements:
- A glibc/Linux host (see note below)
- No special build steps: unlike
freertos, the small POSIX porting shim inosal-rs-porting/posix/is compiled and linked automatically byosal-rs-build- no CMake, cross toolchain or RTOS kernel sources required
Build:
# Native development / host testing
cargo build --features posixThe posix backend links directly against glibc (the GNU C Library), not just any C compiler. It relies on glibc-specific internals - struct layouts (pthread_attr_t, pthread_mutex_t, pthread_cond_t, sigset_t, etc.) and the __libc_current_sigrtmin() extension used to implement thread suspend/resume via real-time signals.
This means:
- The compiler doesn't matter - gcc or clang both work fine.
- The C library does matter - targets linking against musl (e.g.
x86_64-unknown-linux-musl) or non-glibc platforms (e.g. macOS/BSD libc) are not supported by theposixbackend.
Threads spawned by the posix backend normally inherit the creating thread's scheduling policy/priority. The real_time feature switches them to the real-time SCHED_FIFO policy instead.
You don't need to request this feature yourself: osal-rs-build's build script probes the host at compile time and automatically turns real_time on whenever the OS/kernel supports SCHED_FIFO. It's a plain Cargo feature only so it can be inspected via cfg(feature = "real_time"); a plain cargo build --features posix is enough to get it on a capable host.
System::start()simply spins until [System::stop()] is called from another thread - there is no scheduler to hand control to, unlike FreeRTOS where it never returns.- Timers each spawn their own background thread and permanently block
SIGALRMon the thread that creates them; create a newTimerrather than reusing one that already fired as a one-shot.
use osal_rs::os::*;
fn main() {
// Create a thread
let mut thread = Thread::new(
"worker",
4096, // stack size
5, // priority
);
thread.spawn_simple(|| {
loop {
println!("Working...");
System::delay(1000);
}
}).unwrap();
// Start the scheduler (never returns on FreeRTOS; spins until `System::stop()` on POSIX)
System::start();
}use osal_rs::os::*;
use std::sync::Arc;
let counter = Arc::new(Mutex::new(0));
let counter_clone = counter.clone();
let mut thread = Thread::new("incrementer", 2048, 5);
thread.spawn_simple(move || {
let mut guard = counter_clone.lock().unwrap();
*guard += 1;
Ok(Arc::new(()))
}).unwrap();use osal_rs::os::*;
let queue = Queue::new(10, 4).unwrap();
// Send data
let data = [1u8, 2, 3, 4];
queue.post(&data, 100).unwrap();
// Receive data
let mut buffer = [0u8; 4];
queue.fetch(&mut buffer, 100).unwrap();use osal_rs::os::*;
use osal_rs::os::types::{EventBits, TickType};
use std::sync::Arc;
const READY: EventBits = 1 << 0;
const ERROR: EventBits = 1 << 1;
let events = Arc::new(EventGroup::new().unwrap());
let events_clone = events.clone();
let mut thread = Thread::new("waiter", 2048, 5);
thread.spawn_simple(move || {
// OR-wait (`false`): unblocks as soon as either bit is set - e.g. by a
// callback running on another thread - not only when both are set.
let bits = events_clone.wait(READY | ERROR, false, TickType::MAX);
assert!(bits & (READY | ERROR) != 0);
Ok(Arc::new(()))
}).unwrap();
events.set(READY); // wakes the waiting threadThe same code compiles and runs unchanged against either backend - just switch the freertos/posix feature flags.
- Thread Management: Create, manage, and synchronize threads with priorities
- Synchronization Primitives: Mutexes (recursive & non-recursive), binary & counting semaphores, event groups
- Message Queues: Type-safe inter-thread communication with blocking/non-blocking operations
- Software Timers: Periodic and one-shot timers with callbacks
- Memory Allocation: Custom allocator integration for heap management (
freertos) or the system allocator (posix) - Time Management: Duration handling and tick-based timing
- System Control: Scheduler control, task notifications, and system information
- No-std Support: Fully compatible with bare-metal embedded systems (
freertosbackend) - Host Testing: Native
stdexecution for tests, examples and simulation (posixbackend) - 🧪 EXPERIMENTAL Async/Await: Backend-agnostic
async/awaitsupport without Tokio (see below)
OSAL-RS provides several Cargo features to customize the build configuration for different platforms and use cases:
| Feature | Default | Description |
|---|---|---|
freertos |
❌ | Enable the FreeRTOS backend implementation for embedded RTOS development. Mutually exclusive with posix - exactly one of the two is required. |
posix |
❌ | Enable the POSIX/native backend implementation for host environments. Requires glibc (see note above). Mutually exclusive with freertos - exactly one of the two is required. |
real_time |
❌ | POSIX only: schedules spawned threads with the real-time SCHED_FIFO policy instead of inheriting the creating thread's policy/priority. Not meant to be requested by hand - osal-rs-build's build script enables it automatically when the host OS/kernel supports SCHED_FIFO. |
async |
❌ | Enable backend-agnostic async/await support (block_on, AsyncQueue, AsyncSemaphore, AsyncMutex). Works with both freertos and posix. No Tokio required. |
serde |
❌ | Enable serialization/deserialization support via osal-rs-serde. Includes derive macros for automatic implementation. |
There is no default feature set: you must explicitly pick freertos or posix or the build fails.
# FreeRTOS embedded development
cargo build --target thumbv8m.main-none-eabi --features freertos
# FreeRTOS with async support
cargo build --target thumbv8m.main-none-eabi --features freertos,async
# FreeRTOS with serialization support
cargo build --target thumbv8m.main-none-eabi --features freertos,serde
# Native development (POSIX) - real_time is auto-detected, no need to request it
cargo build --features posix
# Native development with async support
cargo build --features posix,async
# Native development with serialization
cargo build --features posix,serdeTo use OSAL-RS in your project with specific features (exactly one of freertos/posix is required):
[dependencies]
osal-rs = { version = "1.0", features = ["freertos"] }
# Or for host development/testing
osal-rs = { version = "1.0", features = ["posix"] }
# Or with serialization support
osal-rs = { version = "1.0", features = ["freertos", "serde"] }OSAL-RS includes a backend-agnostic async runtime that works on both FreeRTOS and POSIX without Tokio or any external async runtime.
| Component | Description |
|---|---|
block_on(future) |
Drives a Future to completion on the calling RTOS task |
AsyncQueue |
Queue with fetch_async / post_async methods |
AsyncSemaphore |
Semaphore with wait_async |
AsyncMutex<T> |
Mutex whose lock() returns a Future |
- No Tokio, no
std: built oncore::future::Future+ OSAL semaphores as the blocking primitive. - Per-task executor:
block_onruns on the calling RTOS task; no thread pool is needed. - Lock-free waker storage:
WakerSlotusesAtomicPtr<Waker>- no RTOS overhead for waker updates. - Race-condition safe: the classic store-waker-then-retry double-check pattern is used in every
pollimplementation.
use osal_rs::os::{block_on, AsyncMutex, AsyncQueue, AsyncSemaphore};
// Run async code inside any RTOS task — no runtime setup required
block_on(async {
// Async mutex
let mutex = AsyncMutex::new(0u32);
{
let mut guard = mutex.lock().await;
*guard += 1;
}
// Async semaphore (signal from another task or ISR)
let sem = AsyncSemaphore::new(1, 0).unwrap();
sem.signal();
sem.wait_async().await;
// Async queue
let queue = AsyncQueue::new(8, 4).unwrap();
queue.post_async(&[1, 2, 3, 4]).await.unwrap();
let mut buf = [0u8; 4];
queue.fetch_async(&mut buf).await.unwrap();
});# Cargo.toml
[dependencies]
osal-rs = { version = "1.0", features = ["freertos", "async"] }
# or for host development
osal-rs = { version = "1.0", features = ["posix", "async"] }# FreeRTOS embedded target
cargo build --release --target thumbv8m.main-none-eabi --features freertos,async
# POSIX host (for tests / simulation)
cargo build --features posix,asyncA complete serialization framework designed specifically for embedded systems:
- No-std Compatible: Works in bare-metal environments without standard library
- Zero-Copy: Direct buffer operations with no intermediate allocations
- Derive Macros: Automatic
#[derive(Serialize, Deserialize)]implementation - Rich Type Support: Primitives, arrays, tuples, Option, Vec, nested structs
- Extensible Architecture: Create custom serializers for any format (JSON, MessagePack, CBOR, etc.)
- Memory Efficient: Little-endian binary format with predictable sizes
- Compile-Time Guarantees: Type-safe serialization with static checks
- Standalone: Can be used independently in any Rust project
use osal_rs_serde::{Serialize, Deserialize, to_bytes, from_bytes};
#[derive(Serialize, Deserialize, Debug, PartialEq)]
struct SensorData {
temperature: i16,
humidity: u8,
pressure: u32,
status: Option<u8>,
}
let data = SensorData {
temperature: 25,
humidity: 60,
pressure: 1013,
status: Some(0xFF),
};
// Serialize to stack buffer
let mut buffer = [0u8; 32];
let len = to_bytes(&data, &mut buffer).unwrap();
// Deserialize from buffer
let restored: SensorData = from_bytes(&buffer[..len]).unwrap();
assert_eq!(data, restored);Perfect for inter-task communication:
use osal_rs::os::{Queue, QueueFn};
use osal_rs_serde::{Serialize, Deserialize, to_bytes, from_bytes};
#[derive(Serialize, Deserialize)]
struct Command {
id: u32,
params: [u16; 4],
}
fn sender_task(queue: &Queue) {
let cmd = Command { id: 42, params: [1, 2, 3, 4] };
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();
}For comprehensive documentation, examples, and advanced features, see:
- osal-rs-serde README - Complete feature documentation
- osal-rs-serde/derive README - Derive macro guide
osal-rs-serde/examples/- Working code examples
CMake integration is only needed for the FreeRTOS backend, since it must link against your project's FreeRTOS kernel and C porting layer. The POSIX backend needs no CMake step - cargo build --features posix is enough (see Supported Backends).
Important: Always ensure that the C porting layer files from osal-rs-porting/freeretos/ are compiled and linked to your project, as they provide the necessary FFI bridge between Rust and FreeRTOS.
Add OSAL-RS to your existing CMake project:
cmake_minimum_required(VERSION 3.20)
project(my_embedded_project C CXX)
# Configure FreeRTOS (assuming it's already in your project)
add_subdirectory(freertos)
# Add OSAL-RS porting layer
add_library(osal_rs_porting STATIC
osal-rs-porting/freeretos/src/osal_rs.c
)
target_include_directories(osal_rs_porting PUBLIC
osal-rs-porting/freeretos/inc
${FREERTOS_INCLUDE_DIRS}
)
target_link_libraries(osal_rs_porting PUBLIC
freertos
)
# Configure Rust library
set(RUST_TARGET "thumbv8m.main-none-eabi") # Adjust for your target
set(OSAL_RS_LIB "${CMAKE_CURRENT_SOURCE_DIR}/osal-rs/target/${RUST_TARGET}/release/libosal_rs.a")
# Custom command to build Rust library
add_custom_command(
OUTPUT ${OSAL_RS_LIB}
COMMAND cargo build --release --target ${RUST_TARGET} --features freertos
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/osal-rs
COMMENT "Building OSAL-RS library"
)
add_custom_target(osal_rs_build DEPENDS ${OSAL_RS_LIB})
# Create imported library for OSAL-RS
add_library(osal_rs STATIC IMPORTED GLOBAL)
set_target_properties(osal_rs PROPERTIES
IMPORTED_LOCATION ${OSAL_RS_LIB}
)
add_dependencies(osal_rs osal_rs_build)
# Your main application
add_executable(my_app
src/main.c
)
target_link_libraries(my_app PRIVATE
osal_rs
osal_rs_porting
freertos
)# Function to build OSAL-RS for different configurations
function(add_osal_rs_library TARGET_NAME RUST_TARGET CARGO_PROFILE)
set(PROFILE_DIR ${CARGO_PROFILE})
if(CARGO_PROFILE STREQUAL "release")
set(CARGO_FLAGS "--release")
else()
set(CARGO_FLAGS "")
endif()
set(LIB_PATH "${CMAKE_CURRENT_SOURCE_DIR}/osal-rs/target/${RUST_TARGET}/${PROFILE_DIR}/libosal_rs.a")
add_custom_command(
OUTPUT ${LIB_PATH}
COMMAND cargo build ${CARGO_FLAGS} --target ${RUST_TARGET} --features freertos
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/osal-rs
COMMENT "Building OSAL-RS (${CARGO_PROFILE}) for ${RUST_TARGET}"
)
add_custom_target(${TARGET_NAME}_build DEPENDS ${LIB_PATH})
add_library(${TARGET_NAME} STATIC IMPORTED GLOBAL)
set_target_properties(${TARGET_NAME} PROPERTIES
IMPORTED_LOCATION ${LIB_PATH}
)
add_dependencies(${TARGET_NAME} ${TARGET_NAME}_build)
endfunction()
# Use it in your project
add_osal_rs_library(osal_rs "thumbv8m.main-none-eabi" "release")Example CMake toolchain file for ARM Cortex-M:
# toolchain-arm-none-eabi.cmake
set(CMAKE_SYSTEM_NAME Generic)
set(CMAKE_SYSTEM_PROCESSOR arm)
set(CMAKE_C_COMPILER arm-none-eabi-gcc)
set(CMAKE_CXX_COMPILER arm-none-eabi-g++)
set(CMAKE_ASM_COMPILER arm-none-eabi-gcc)
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
# Rust target
set(RUST_TARGET "thumbv8m.main-none-eabi")Use it with:
cmake -DCMAKE_TOOLCHAIN_FILE=toolchain-arm-none-eabi.cmake -B build
cmake --build buildBy default, OSAL-RS looks for FreeRTOSConfig.h at <workspace_root>/inc/FreeRTOSConfig.h. You can override this path using the FREERTOS_CONFIG_PATH environment variable.
# Set custom path to FreeRTOSConfig.h
set(FREERTOS_CONFIG_PATH "${CMAKE_SOURCE_DIR}/inc/hhg-config/pico/FreeRTOSConfig.h")
# Pass to Cargo build via environment variable
add_custom_command(
OUTPUT ${OSAL_RS_LIB}
COMMAND ${CMAKE_COMMAND} -E env FREERTOS_CONFIG_PATH=${FREERTOS_CONFIG_PATH}
cargo build --release --target ${RUST_TARGET} --features freertos
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/osal-rs
COMMENT "Building OSAL-RS library"
)# Set environment variable before building
export FREERTOS_CONFIG_PATH="/path/to/your/FreeRTOSConfig.h"
cargo build --release --target thumbv8m.main-none-eabi --features freertosNote: The build system will automatically regenerate Rust type bindings from the specified FreeRTOSConfig.h during the build process.
osal-rs/
├── osal-rs/ # Main library crate (freertos + posix backends)
├── osal-rs-build/ # Build utilities
├── osal-rs-tests/ # Test suite
├── osal-rs-serde/ # Serialization framework
└── osal-rs-porting/ # Platform-specific C/C++ code
├── freeretos/ # FreeRTOS porting layer
│ ├── inc/ # Header files
│ └── src/ # Implementation
└── posix/ # POSIX porting layer (glibc shim, built automatically)
├── inc/ # Header files
└── src/ # Implementation
This project is licensed under the LGPL-2.1-or-later License - see the LICENSE file for details.
Contributions are welcome! Please feel free to submit pull requests or open issues for bugs and feature requests.
Antonio Salsi - passy.linux@zresa.it