Skip to content

Repository files navigation

OSAL-RS

Operating System Abstraction Layer for Rust - A cross-platform compatibility layer for embedded and real-time systems development.

Crates.io Documentation License: LGPL-2.1 Tests

Overview

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.

Workspace Components

  • 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

Current Implementation Status

  • 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

Breaking Changes

Timer clones share one timer, and the last handle destroys it (from version 1.2.0)

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 here

Timer 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.

ThreadFn::join blocks on FreeRTOS instead of killing the thread (from version 1.2.0)

// 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.

EventGroupFn::wait gained a wait_for_all_bits parameter (from version 1.1.0)

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.

Supported Backends

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.

FreeRTOS Backend (freertos)

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:

  1. FreeRTOS kernel properly configured and linked into your project
  2. C porting layer from osal-rs-porting/freeretos/ compiled and linked (an FFI bridge between Rust and FreeRTOS)
  3. CMake build system set up for your embedded project (see CMake Integration)
  4. 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 1

Build:

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

POSIX Backend (posix)

Runs 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 in osal-rs-porting/posix/ is compiled and linked automatically by osal-rs-build - no CMake, cross toolchain or RTOS kernel sources required

Build:

# Native development / host testing
cargo build --features posix

POSIX Backend: glibc Requirement

The 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 the posix backend.

Real-Time Scheduling (real_time)

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.

Caveats of the POSIX Backend

  • 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 SIGALRM on the thread that creates them; create a new Timer rather than reusing one that already fired as a one-shot.

Quick Start

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 thread

The same code compiles and runs unchanged against either backend - just switch the freertos/posix feature flags.

Core OSAL Features

  • 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 (freertos backend)
  • Host Testing: Native std execution for tests, examples and simulation (posix backend)
  • 🧪 EXPERIMENTAL Async/Await: Backend-agnostic async/await support without Tokio (see below)

Cargo Features

OSAL-RS provides several Cargo features to customize the build configuration for different platforms and use cases:

Available Features

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.

Feature Combinations

# 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,serde

Using Features in Cargo.toml

To 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"] }

🧪 EXPERIMENTAL Async/Await Support (feature async)

OSAL-RS includes a backend-agnostic async runtime that works on both FreeRTOS and POSIX without Tokio or any external async runtime.

Design

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 on core::future::Future + OSAL semaphores as the blocking primitive.
  • Per-task executor: block_on runs on the calling RTOS task; no thread pool is needed.
  • Lock-free waker storage: WakerSlot uses AtomicPtr<Waker> - no RTOS overhead for waker updates.
  • Race-condition safe: the classic store-waker-then-retry double-check pattern is used in every poll implementation.

Quick Example

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();
});

Enable the feature

# 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,async

osal-rs-serde Features

A 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

osal-rs-serde Quick Example

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

Integration with OSAL Queues

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:

CMake Integration

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.

Basic CMake Integration

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
)

Advanced CMake Integration with Multiple Configurations

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

Cross-Compilation Setup

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 build

Custom FreeRTOS Configuration Path

By 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.

Setting via CMake

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

Setting via Environment Variable

# Set environment variable before building
export FREERTOS_CONFIG_PATH="/path/to/your/FreeRTOSConfig.h"
cargo build --release --target thumbv8m.main-none-eabi --features freertos

Note: The build system will automatically regenerate Rust type bindings from the specified FreeRTOSConfig.h during the build process.

Project Structure

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

License

This project is licensed under the LGPL-2.1-or-later License - see the LICENSE file for details.

Contributing

Contributions are welcome! Please feel free to submit pull requests or open issues for bugs and feature requests.

Author

Antonio Salsi - passy.linux@zresa.it

Links

Example implementation

https://github.com/HiHappyGarden/hi-happy-garden-rs

Releases

Packages

Used by

Contributors

Languages