ove/lib.rs
1// Copyright (C) 2026 Kamil Lulko <kamil.lulko@gmail.com>
2//
3// SPDX-License-Identifier: GPL-3.0-or-later
4//
5// This file is part of oveRTOS.
6
7//! oveRTOS Rust SDK — safe wrappers for the oveRTOS embedded RTOS framework.
8//!
9//! Provides RAII types for threads, mutexes, semaphores, queues, timers, etc.
10//! and the `app!` macro that generates all FFI boilerplate so application code
11//! can be written in pure safe Rust.
12//!
13//! # Async (Embassy)
14//!
15//! Activated by the `async` Cargo feature combined with C-side
16//! `CONFIG_OVE_ASYNC=y`. Adds an [Embassy](https://embassy.dev)-based
17//! async runtime hosted on the oveRTOS substrate:
18//!
19//! ```ignore
20//! use embassy_executor::Spawner;
21//! use embassy_time::{Duration, Timer};
22//!
23//! #[embassy_executor::task]
24//! async fn blinker() {
25//! loop {
26//! log::info!("tick");
27//! Timer::after(Duration::from_millis(250)).await;
28//! }
29//! }
30//!
31//! #[ove::main]
32//! async fn app_main(spawner: Spawner) {
33//! spawner.must_spawn(blinker());
34//! }
35//! ```
36//!
37//! Under the hood:
38//!
39//! - [`async_runtime::Executor`] wraps `embassy_executor::raw::Executor`
40//! and blocks on `ove_event_wait` between polls — yields cleanly to
41//! the FreeRTOS / Zephyr / NuttX scheduler. No `WFE` busy-park, no
42//! host-thread parking.
43//! - [`async_runtime::AsyncStream`] / [`AsyncQueue`](async_runtime::AsyncQueue)
44//! / [`AsyncEventGroup`](async_runtime::AsyncEventGroup) /
45//! [`AsyncSemaphore`](async_runtime::AsyncSemaphore) /
46//! [`AsyncUart`](async_runtime::AsyncUart) /
47//! [`AsyncInput`](async_runtime::AsyncInput) wrap the corresponding
48//! synchronous primitives and bridge their C-level `_set_notify`
49//! hooks into `embassy_sync::waitqueue::AtomicWaker`.
50//! - Time driver: `embassy_time::Timer::after_*()` runs on a
51//! re-armable `ove_timer_*_ns` one-shot.
52//! - Critical-section impl: `ove_irq_lock` / `ove_irq_unlock` on every
53//! target.
54//!
55//! Cargo feature: enable `async`. The optional `embedded-io-async`
56//! feature adds `embedded_io_async::Read` impls on `&'static AsyncUart`
57//! and `&'static AsyncStream`.
58//!
59//! # Async networking (embassy-net)
60//!
61//! Layered on top of the async runtime, [`async_net`] adds an
62//! `embassy_net::Stack` backed by a per-board Ethernet Driver
63//! (`QemuShmDriver` for QEMU MPS2-AN500, `Stm32f7EthDriver` for the
64//! STM32F746G-Discovery). Open async TCP / UDP / DNS sockets via the
65//! standard `embassy_net` types; pair with crates.io community libraries
66//! ([`reqwless`](https://crates.io/crates/reqwless),
67//! [`rust-mqtt`](https://crates.io/crates/rust-mqtt),
68//! [`embedded-tls`](https://crates.io/crates/embedded-tls),
69//! [`picoserve`](https://crates.io/crates/picoserve)) for the protocol
70//! layers.
71//!
72//! Mutually exclusive with the blocking [`net`] stack at build time —
73//! both want to own the MAC. Cargo features: `async-net` plus a
74//! transport sub-feature (`async-net-qemu-shm`, `async-net-stm32f7-eth`)
75//! and C-side `CONFIG_OVE_ASYNC_NET=y`. See the [`async_net`] module
76//! docs for the full decision guide vs. blocking [`net`], the
77//! community-crate pairing recipes, and hardware-verified memory
78//! budgets on STM32F7.
79//!
80//! # Ergonomic extras
81//!
82//! A handful of small additions modelled on patterns from
83//! `zephyr-lang-rust` and `embassy-sync` that come up often enough to
84//! warrant first-class support:
85//!
86//! - [`channel`] — crossbeam-style MPMC `Sender` / `Receiver` over the
87//! existing [`Queue`] FFI. Cloneable halves, refcounted, with
88//! half-closed detection ([`Error::NetClosed`]). Heap mode uses an
89//! internal `Arc`; zero-heap uses [`channel::Sender::from_static`] /
90//! [`channel::Receiver::from_static`] against caller-supplied counters.
91//! - [`config`] — every `CONFIG_OVE_*` symbol from `ove_config.h`
92//! surfaces as a Rust `const` (`bool` / `i64` / `&str`, plus
93//! `_USIZE` for non-negative ints) and a `#[cfg(config_ove_*)]`
94//! flag. Lets apps reference build-time tunables (e.g.
95//! `CONFIG_OVE_PM_MAX_WAKE_SOURCES_USIZE`) without hard-coding the
96//! number twice.
97//! - [`printk!`] / [`printkln!`] / [`ove_print!`] — direct
98//! `ove_console_write` macros that bypass the `log` framework. Use
99//! for early-boot banners before [`log::try_init`] runs and for
100//! ISR-adjacent contexts.
101//! - [`sync::SpinMutex`] — IRQ-locking mutex (`ove_irq_lock`-backed)
102//! for sub-microsecond critical sections, ISR-shared state, and
103//! crossing `.await` points where a real [`sync::Mutex`] would
104//! deadlock. Requires the `async` feature (the `ove_irq_*` substrate
105//! lives in `irq.h` under `CONFIG_OVE_ASYNC`); not available on WASM.
106//! - [`ove_macros::thread`] (`#[ove::thread(stack_size = N, name = "...")]`)
107//! — attribute-macro form of static-storage thread spawn. The
108//! decorated `fn name()` becomes a spawn helper that, on first call,
109//! creates a thread with a `static ThreadStorage<N>`. Complements
110//! the existing declarative [`thread!`] macro.
111//!
112//! ## Optional ecosystem interop
113//!
114//! Two opt-in Cargo features add ecosystem-standard types alongside
115//! the defaults (neither is enabled by default — the existing API
116//! stays unchanged for callers who don't opt in):
117//!
118//! - `fugit` — exposes [`time::DurationUs`] / [`time::DurationNs`] /
119//! [`time::DurationMs`] / [`time::InstantUs`] from the
120//! [`fugit`](https://crates.io/crates/fugit) crate, with conversions
121//! to/from [`core::time::Duration`]. Catches `from_secs(N)` vs
122//! `from_millis(N)` mix-ups at compile time. Zero-heap friendly.
123//! - `portable-atomic-arc` — swaps [`heap::Arc`] from `alloc::sync::Arc`
124//! to [`portable_atomic_util::Arc`]. Same API; the alternative impl
125//! falls back to a `critical-section` path on targets without native
126//! CAS (Cortex-M0+, RV32 without A). Current oveRTOS targets all
127//! have CAS, so this is future-proofing.
128//!
129//! # Hot-path inline discipline
130//!
131//! Wrapper methods that are a thin `unsafe { ffi::ove_*(...) }` plus
132//! `Error::from_code(rc)` are marked `#[inline]` so the rustc/LLVM
133//! optimizer can fold the FFI call, the error-code match, and the
134//! `Result` construction into the caller's frame. Without this,
135//! `Mutex::lock` and friends compile as out-of-line `bl` targets that
136//! cost an extra ~20–60 ns per call on Cortex-M and show up as a 6–15%
137//! per-binding overhead on the shortest sync paths. The same
138//! discipline applies to any new wrapper added here — keep the body a
139//! one-liner and add `#[inline]`.
140//!
141//! Cross-language inlining between Rust and C is gated behind the
142//! `OVE_CROSS_LTO=ON` CMake option. It is off by default because it
143//! requires a bitcode-aware linker and a target-cpu/target-feature
144//! match that the embedded toolchains do not always provide; per
145//! Gale's "three quiet barriers" analysis, mismatched feature sets
146//! silently kill the inliner without warning.
147
148#![cfg_attr(not(feature = "std"), no_std)]
149// Pedantic/nursery lints intentionally relaxed for this crate:
150// - FFI surface (`bindings::*`) drives most pointer/cast patterns; the
151// .cast()/.cast_mut() rewrites add no safety, only churn.
152// - `must_use` discipline is applied selectively on outward APIs; blanket
153// #[must_use] on every getter would be noise.
154// - Documentation style follows ours, not clippy::doc_markdown's expected
155// backtick density.
156#![allow(
157 clippy::doc_markdown,
158 clippy::must_use_candidate,
159 clippy::return_self_not_must_use,
160 clippy::ptr_as_ptr,
161 clippy::use_self,
162 clippy::borrow_as_ptr,
163 clippy::ref_as_ptr,
164 clippy::missing_const_for_fn,
165 clippy::pub_underscore_fields,
166 clippy::assertions_on_constants,
167 clippy::cast_lossless,
168 clippy::cast_sign_loss,
169 clippy::cast_possible_truncation,
170 clippy::cast_possible_wrap,
171 clippy::cast_ptr_alignment,
172 clippy::ptr_cast_constness,
173 clippy::not_unsafe_ptr_arg_deref,
174 clippy::similar_names,
175 clippy::missing_panics_doc,
176 clippy::too_long_first_doc_paragraph,
177 clippy::elidable_lifetime_names,
178 clippy::needless_pass_by_value,
179 // bindgen emits different pointer types for the same C symbol
180 // depending on how LVGL's lv_obj_t is reached at parse time
181 // (typedef chain vs. forward decl). Casts that look unnecessary in
182 // one config are mandatory in another, so we tolerate the no-op
183 // form rather than gate every callsite on cfg(docsrs).
184 clippy::unnecessary_cast
185)]
186
187// Panic handler (only when no_std + feature enabled)
188#[cfg(all(not(feature = "std"), feature = "panic-handler"))]
189mod panic;
190
191// Bring in `alloc` so we can re-export Arc / Box / Vec / String for
192// users. On `std` builds this is already linked transitively; on
193// `no_std` the user must supply a `#[global_allocator]` (the
194// `ove-allocator` sub-crate provides one that wraps libc malloc/free).
195#[cfg(feature = "alloc")]
196extern crate alloc;
197
198/// Heap-allocating types re-exported from `alloc::*`. Available with
199/// the `alloc` (or `std`) feature. On `no_std` targets the consumer
200/// must register a `#[global_allocator]`; the `ove-allocator` crate
201/// provides a default that wraps libc malloc/free.
202#[cfg(feature = "alloc")]
203pub mod heap {
204 pub use alloc::{boxed::Box, string::String, vec::Vec};
205
206 /// `Arc` re-export. By default this is `alloc::sync::Arc`; enable
207 /// the `portable-atomic-arc` feature to swap in
208 /// `portable_atomic_util::Arc`, which works on targets without
209 /// native CAS (e.g. Cortex-M0+, RV32 without A) by falling through
210 /// to a `critical-section`-based path.
211 ///
212 /// The two implementations are API-compatible; downstream code
213 /// using `ove::heap::Arc` doesn't have to change.
214 #[cfg(not(feature = "portable-atomic-arc"))]
215 pub use alloc::sync::Arc;
216 #[cfg(feature = "portable-atomic-arc")]
217 pub use portable_atomic_util::Arc;
218}
219
220#[cfg(all(feature = "async", has_async))]
221pub mod async_runtime;
222// Async wrappers re-exported at the crate root for symmetry with the
223// blocking primitives (`ove::Queue` → `ove::AsyncQueue`, etc.). The
224// `Spawner` type stays at `async_runtime::Spawner` — an `embassy_executor`
225// re-export, not an ove-native type.
226//
227// Each `Async*` wrapper is gated on its primitive's `has_*` cfg, mirroring
228// the per-feature gates in `async_runtime/mod.rs`: a config that enables
229// CONFIG_OVE_ASYNC without (say) CONFIG_OVE_QUEUE compiles out the
230// `async_runtime::queue` module, so re-exporting it unconditionally here
231// would be an unresolved import (the example_async_rust defconfigs do
232// exactly this — async on, most comm primitives off).
233#[cfg(all(feature = "async", has_async, has_eventgroup))]
234pub use async_runtime::AsyncEventGroup;
235#[cfg(all(feature = "async", has_async, has_i2c))]
236pub use async_runtime::AsyncI2c;
237#[cfg(all(feature = "async", has_async, has_gpio))]
238pub use async_runtime::AsyncInput;
239#[cfg(all(feature = "async", has_async, has_queue))]
240pub use async_runtime::AsyncQueue;
241#[cfg(all(feature = "async", has_async, has_sync))]
242pub use async_runtime::AsyncSemaphore;
243#[cfg(all(feature = "async", has_async, has_spi))]
244pub use async_runtime::AsyncSpi;
245#[cfg(all(feature = "async", has_async, has_stream))]
246pub use async_runtime::AsyncStream;
247#[cfg(all(feature = "async", has_async, has_uart))]
248pub use async_runtime::AsyncUart;
249#[cfg(all(feature = "async", has_async))]
250pub use async_runtime::Executor;
251// A compile-time error to catch the common misconfiguration where the
252// Cargo `async` feature is on but the C side wasn't built with
253// CONFIG_OVE_ASYNC=y. Without this gate the binding would fail with
254// confusing linker errors about missing ove_irq_lock / ove_event_*.
255#[cfg(all(feature = "async", not(has_async)))]
256compile_error!(
257 "feature = \"async\" requires CONFIG_OVE_ASYNC=y on the C side. \
258 Enable it in your board's defconfig (or .config) and rebuild."
259);
260#[cfg(all(feature = "async-net", has_async_net))]
261pub mod async_net;
262#[cfg(all(feature = "async-net", not(has_async_net)))]
263compile_error!(
264 "feature = \"async-net\" requires CONFIG_OVE_ASYNC_NET=y on the C side. \
265 Enable it in your board's defconfig (or .config) and rebuild. \
266 Note CONFIG_OVE_NET (the blocking lwIP path) and CONFIG_OVE_ASYNC_NET \
267 are mutually exclusive."
268);
269#[cfg(has_audio)]
270pub mod audio;
271#[cfg(not(docsrs))]
272pub(crate) mod bindings;
273#[cfg(docsrs)]
274#[path = "bindings_stub.rs"]
275pub(crate) mod bindings;
276#[cfg(has_board)]
277pub mod board;
278#[cfg(has_bsp)]
279pub mod bsp;
280pub mod cell;
281#[cfg(has_queue)]
282pub mod channel;
283pub mod config;
284#[cfg(has_console)]
285pub mod console;
286pub mod containers;
287#[cfg(feature = "embedded-hal")]
288mod embedded_hal_impl;
289#[cfg(all(feature = "embedded-io-async", feature = "async", has_async))]
290mod embedded_io_async_impl;
291#[cfg(feature = "embedded-io")]
292mod embedded_io_impl;
293pub mod error;
294#[cfg(has_eventgroup)]
295pub mod eventgroup;
296pub mod fmt;
297#[cfg(has_fs)]
298pub mod fs;
299#[cfg(has_gpio)]
300pub mod gpio;
301#[cfg(has_i2c)]
302pub mod i2c;
303#[cfg(has_i2s)]
304pub mod i2s;
305#[cfg(has_infer)]
306pub mod infer;
307pub mod init_cell;
308#[cfg(has_led)]
309pub mod led;
310pub mod log;
311#[cfg(has_lvgl)]
312pub mod lvgl;
313#[cfg(has_net)]
314pub mod net;
315#[cfg(has_net_http)]
316pub mod net_http;
317#[cfg(has_net_httpd)]
318pub mod net_httpd;
319#[cfg(has_net_mqtt)]
320pub mod net_mqtt;
321#[cfg(has_net_sntp)]
322pub mod net_sntp;
323#[cfg(has_net_tls)]
324pub mod net_tls;
325#[cfg(has_nvs)]
326pub mod nvs;
327#[cfg(has_pm)]
328pub mod pm;
329#[cfg(has_queue)]
330pub mod queue;
331#[cfg(has_shell)]
332pub mod shell;
333#[cfg(has_spi)]
334pub mod spi;
335#[cfg(has_stream)]
336pub mod stream;
337#[cfg(has_sync)]
338pub mod sync;
339pub mod thread;
340#[cfg(has_time)]
341pub mod time;
342#[cfg(has_timer)]
343pub mod timer;
344#[cfg(has_uart)]
345pub mod uart;
346#[cfg(has_watchdog)]
347pub mod watchdog;
348#[cfg(has_workqueue)]
349pub mod workqueue;
350
351// All user-facing macros (`ove::mutex!`, `ove::thread!`, `log::info!`, etc.)
352// and the internal `ove_handle_impl!` boilerplate live in `macros.rs`.
353// `#[macro_export]` puts them at the crate root regardless of module.
354mod macros;
355
356/// Raw FFI bindings — **escape hatch** for narrow, app-private interop
357/// that the safe wrappers don't cover (e.g. custom `lv_subject_t` observers,
358/// `ove_work_fn` handlers linking against user C helpers).
359///
360/// **Do not use from normal app code.** App business logic should rely
361/// exclusively on the safe wrappers in sibling modules (`audio`, `lvgl`,
362/// `net`, `timer`, `thread`, …). Any use of `ove::ffi::*` requires the
363/// surrounding module to be marked `#[allow(unsafe_code)]`, which forms
364/// the de-facto auditability boundary for the app.
365#[doc(hidden)]
366pub mod ffi {
367 pub use crate::bindings::*;
368}
369
370// Public re-exports for convenience
371pub use cell::{LvCell, LvRefCell};
372pub use error::{Error, Result};
373#[cfg(has_eventgroup)]
374#[allow(deprecated)] // EG_* shims kept for compatibility; new code uses WaitFlags
375pub use eventgroup::{EG_CLEAR_ON_EXIT, EG_WAIT_ALL, EventGroup, EventGroupStorage, WaitFlags};
376pub use fmt::FmtBuf;
377#[cfg(has_i2c)]
378pub use i2c::I2c;
379pub use init_cell::{InitCell, InitMut};
380/// `#[ove::main]` proc-macro: marks the application entry point.
381/// Expands into the `extern "C" fn ove_main()` trampoline.
382pub use ove_macros::{main, thread as thread_attr};
383#[cfg(has_queue)]
384pub use queue::{Queue, QueueStorage};
385#[cfg(has_spi)]
386pub use spi::Spi;
387#[cfg(has_stream)]
388pub use stream::{Stream, StreamStorage};
389#[cfg(has_sync)]
390pub use sync::{
391 CondVar, CondVarStorage, Event, EventStorage, Mutex, MutexGuard, MutexStorage, RecursiveMutex,
392 RecursiveMutexGuard, RecursiveMutexStorage, Semaphore, SemaphoreStorage, WaitTimeoutResult,
393};
394pub use thread::Priority;
395#[cfg(zero_heap)]
396pub use thread::ThreadStorage;
397pub use thread::{
398 Builder, JoinHandle, JoinHandleBorrowed, MemStats, StopToken, Thread, ThreadInfo, ThreadState,
399 ThreadStats,
400};
401#[cfg(has_time)]
402pub use time::Delay;
403#[cfg(has_timer)]
404pub use timer::{Timer, TimerStorage};
405#[cfg(has_uart)]
406pub use uart::Uart;
407#[cfg(has_watchdog)]
408pub use watchdog::Watchdog;
409#[cfg(has_workqueue)]
410pub use workqueue::{Work, Workqueue};
411
412/// Write a message to the oveRTOS console.
413pub fn log(msg: &[u8]) {
414 // SAFETY: `msg.as_ptr()` is valid for `msg.len()` bytes. The console
415 // write is a synchronous one-shot that does not retain the pointer.
416 unsafe {
417 bindings::ove_console_write(msg.as_ptr() as *const _, msg.len() as u32);
418 }
419}
420
421/// Start audio (if enabled) and the RTOS scheduler. Blocks forever.
422///
423/// This function must be called at the end of `ove_main` (or the function
424/// annotated with [`#[ove::main]`](main)). It transfers control to the RTOS
425/// and never returns.
426pub fn run() {
427 // SAFETY: `ove_run` is the RTOS scheduler entry; it never returns.
428 unsafe {
429 bindings::ove_run();
430 }
431}
432
433/// Start the RTOS scheduler **without** engaging the zero-heap lock.
434///
435/// Used by apps whose runtime structurally requires post-init dynamic
436/// allocation — notably the benchmark suite, which spawns helper threads
437/// inside test setup paths after `ove_main` has returned. On NuttX zero-heap
438/// builds, the heap lock would cause `pthread_create`'s
439/// `kmm_zalloc(task_group_s)` to fail with `ENOMEM`. Calling this in place
440/// of [`run`] opts out of the lock.
441///
442/// Like [`run`], this never returns.
443pub fn start_scheduler() {
444 // SAFETY: kicks the scheduler entry; never returns on most platforms.
445 unsafe {
446 bindings::ove_thread_start_scheduler();
447 }
448}