diff --git a/Makefile b/Makefile index 8d66dc6b..c6e1cebc 100644 --- a/Makefile +++ b/Makefile @@ -86,6 +86,18 @@ build: cargo build --package aimdb-tokio-adapter --features "tokio-runtime,tracing,observability" @printf "$(YELLOW) → Building sync wrapper$(NC)\n" cargo build --package aimdb-sync + @printf "$(YELLOW) → Building sync wrapper (no_std)$(NC)\n" + cargo build --package aimdb-sync --no-default-features + @printf "$(YELLOW) → Asserting no tokio in sync wrapper (no_std)$(NC)\n" + @out=$$(cargo tree -p aimdb-sync --no-default-features -e features,no-dev 2>&1) || { \ + printf "$(RED)✗ cargo tree failed — refusing to pass vacuously:$(NC)\n"; \ + printf '%s\n' "$$out"; exit 1; \ + }; \ + if printf '%s\n' "$$out" | grep -qi tokio; then \ + printf "$(RED)✗ tokio leaked into the no_std build$(NC)\n"; \ + printf '%s\n' "$$out" | grep -i tokio; exit 1; \ + fi + @printf "$(BLUE)✓ no_std graph is tokio-free$(NC)\n" @printf "$(YELLOW) → Building codegen library$(NC)\n" cargo build --package aimdb-codegen @printf "$(YELLOW) → Building CLI tools$(NC)\n" @@ -157,6 +169,10 @@ test: cargo test --package aimdb-wasm-adapter --no-default-features --features observability --lib @printf "$(YELLOW) → Testing sync wrapper$(NC)\n" cargo test --package aimdb-sync + @printf "$(YELLOW) → Testing sync wrapper (no_std)$(NC)\n" + cargo test --package aimdb-sync --no-default-features + @printf "$(YELLOW) → Testing sync wrapper (data-contracts: set_value family)$(NC)\n" + cargo test --package aimdb-sync --features data-contracts @printf "$(YELLOW) → Testing codegen library$(NC)\n" cargo test --package aimdb-codegen @printf "$(YELLOW) → Testing CLI tools$(NC)\n" @@ -240,6 +256,10 @@ clippy: cargo clippy --package aimdb-embassy-adapter --target thumbv7em-none-eabihf --features "embassy-runtime,embassy-net-support" -- -D warnings @printf "$(YELLOW) → Clippy on sync wrapper$(NC)\n" cargo clippy --package aimdb-sync --all-targets -- -D warnings + @printf "$(YELLOW) → Clippy on sync wrapper (no_std)$(NC)\n" + cargo clippy --package aimdb-sync --no-default-features --all-targets -- -D warnings + @printf "$(YELLOW) → Clippy on sync wrapper (data-contracts)$(NC)\n" + cargo clippy --package aimdb-sync --features data-contracts --all-targets -- -D warnings @printf "$(YELLOW) → Clippy on client library$(NC)\n" cargo clippy --package aimdb-client --all-targets -- -D warnings @printf "$(YELLOW) → Clippy on client library (serial transport arm)$(NC)\n" @@ -392,6 +412,8 @@ test-embedded: cargo check --package aimdb-tcp-connector --target thumbv7em-none-eabihf --target-dir $(EMBEDDED_CHECK_TARGET_DIR) --no-default-features --features "embassy-runtime" @printf "$(YELLOW) → Checking aimdb-tcp-connector (Embassy TCP client + defmt) on thumbv7em-none-eabihf target$(NC)\n" cargo check --package aimdb-tcp-connector --target thumbv7em-none-eabihf --target-dir $(EMBEDDED_CHECK_TARGET_DIR) --no-default-features --features "embassy-runtime,defmt" + @printf "$(YELLOW) → Checking aimdb-sync (no_std) on thumbv7em-none-eabihf target$(NC)\n" + cargo check --package aimdb-sync --target thumbv7em-none-eabihf --target-dir $(EMBEDDED_CHECK_TARGET_DIR) --no-default-features ## Example projects examples: diff --git a/aimdb-sync/Cargo.toml b/aimdb-sync/Cargo.toml index a0c4dbec..429416ee 100644 --- a/aimdb-sync/Cargo.toml +++ b/aimdb-sync/Cargo.toml @@ -11,12 +11,18 @@ categories = ["database", "api-bindings"] [dependencies] # Core dependencies -aimdb-core = { path = "../aimdb-core", version = "1.1.0" } -aimdb-tokio-adapter = { path = "../aimdb-tokio-adapter", version = "0.6.0" } +# `default-features = false` is load-bearing: aimdb-core's default set is +# ["std", "alloc", "derive"], so inheriting it would pull anyhow/serde/remote +# into the `--no-default-features` build and silently un-no_std this crate. +# The `std` feature below forwards to `aimdb-core/std` for the std path. +aimdb-core = { path = "../aimdb-core", version = "1.1.0", default-features = false, features = [ + "alloc", +] } +aimdb-tokio-adapter = { path = "../aimdb-tokio-adapter", version = "0.6.0", optional = true } tracing = { workspace = true, optional = true } # Tokio for channels and runtime -tokio = { version = "1.40", features = ["sync", "rt", "time", "macros"] } +tokio = { version = "1.40", features = ["sync", "rt", "time", "macros"], optional = true } # Error handling thiserror = { version = "2.0.16", default-features = false } @@ -38,7 +44,7 @@ serde_json = "1.0" [features] default = ["std"] -std = [] +std = ["aimdb-core/std", "dep:tokio", "dep:aimdb-tokio-adapter"] # Enable tracing for debugging tracing = ["dep:tracing", "aimdb-core/tracing"] diff --git a/aimdb-sync/src/consumer.rs b/aimdb-sync/src/consumer.rs index 5d779089..2b519da3 100644 --- a/aimdb-sync/src/consumer.rs +++ b/aimdb-sync/src/consumer.rs @@ -1,10 +1,11 @@ //! Synchronous consumer for typed records. use crate::{SyncError, SyncResult}; -use std::fmt::Debug; +use alloc::sync::Arc; +use core::fmt::Debug; +use core::time::Duration; use std::sync::mpsc; -use std::sync::{Arc, Mutex}; -use std::time::Duration; +use std::sync::Mutex; /// Synchronous consumer for records of type `T`. /// @@ -49,7 +50,7 @@ where T: Send + Sync + 'static + Debug + Clone, { /// Channel receiver for consumer data - /// Wrapped in Arc so it can be shared but only one thread receives at a time + /// Wrapped in `Arc` so it can be shared but only one thread receives at a time rx: Arc>>, } @@ -87,7 +88,6 @@ where /// /// # #[derive(Debug, Clone)] /// # struct MyData { value: i32 } - /// # #[cfg(feature = "std")] /// # fn main() -> SyncResult<()> { /// let handle = AimDbBuilder::new() /// .runtime(Arc::new(TokioAdapter)) @@ -127,7 +127,6 @@ where /// /// # #[derive(Debug, Clone)] /// # struct MyData { value: i32 } - /// # #[cfg(feature = "std")] /// # fn main() -> SyncResult<()> { /// let handle = AimDbBuilder::new() /// .runtime(Arc::new(TokioAdapter)) @@ -168,7 +167,6 @@ where /// /// # #[derive(Debug, Clone)] /// # struct MyData { value: i32 } - /// # #[cfg(feature = "std")] /// # fn main() -> SyncResult<()> { /// let handle = AimDbBuilder::new() /// .runtime(Arc::new(TokioAdapter)) @@ -216,7 +214,6 @@ where /// /// # #[derive(Debug, Clone)] /// # struct MyData { value: i32 } - /// # #[cfg(feature = "std")] /// # fn main() -> SyncResult<()> { /// let handle = AimDbBuilder::new() /// .runtime(Arc::new(TokioAdapter)) @@ -269,7 +266,6 @@ where /// /// # #[derive(Debug, Clone)] /// # struct MyData { value: i32 } - /// # #[cfg(feature = "std")] /// # fn main() -> SyncResult<()> { /// let handle = AimDbBuilder::new() /// .runtime(Arc::new(TokioAdapter)) diff --git a/aimdb-sync/src/handle.rs b/aimdb-sync/src/handle.rs index 791df334..f9963f27 100644 --- a/aimdb-sync/src/handle.rs +++ b/aimdb-sync/src/handle.rs @@ -2,10 +2,10 @@ use crate::{SyncError, SyncResult}; use aimdb_core::{log_error, log_warn, AimDb, AimDbBuilder, DbError, DbResult}; -use std::fmt::Debug; -use std::sync::Arc; +use alloc::sync::Arc; +use core::fmt::Debug; +use core::time::Duration; use std::thread::{self, JoinHandle}; -use std::time::Duration; use tokio::sync::mpsc; /// Default channel capacity for sync producers and consumers. @@ -50,7 +50,6 @@ pub trait AimDbBuilderSyncExt { /// use std::sync::Arc; /// /// # #[derive(Debug, Clone)] struct MyData { value: f32 } - /// # #[cfg(feature = "std")] /// # fn main() -> SyncResult<()> { /// let mut builder = AimDbBuilder::new() /// .runtime(Arc::new(TokioAdapter::new()?)); diff --git a/aimdb-sync/src/lib.rs b/aimdb-sync/src/lib.rs index 20fd4508..82f00b1c 100644 --- a/aimdb-sync/src/lib.rs +++ b/aimdb-sync/src/lib.rs @@ -43,7 +43,8 @@ //! //! ## Quick Start //! -//! ```no_run +#![cfg_attr(feature = "std", doc = "```no_run")] +#![cfg_attr(not(feature = "std"), doc = "```ignore")] //! use aimdb_core::{AimDbBuilder, buffer::BufferCfg}; //! use aimdb_tokio_adapter::{TokioAdapter, TokioRecordRegistrarExt}; //! use aimdb_sync::{AimDbBuilderSyncExt, SyncResult}; @@ -53,8 +54,6 @@ //! struct Temperature { //! celsius: f32, //! } -//! // Guard againts the use of TokioAdapter in case of "std" -//! # #[cfg(feature = "std")] //! # fn main() -> SyncResult<()> { //! // Build and attach database (NO #[tokio::main] NEEDED!) //! let adapter = Arc::new(TokioAdapter::new()?); @@ -87,7 +86,8 @@ //! //! Both `SyncProducer` and `SyncConsumer` can be cloned and shared across threads: //! -//! ```no_run +#![cfg_attr(feature = "std", doc = "```no_run")] +#![cfg_attr(not(feature = "std"), doc = "```ignore")] //! use std::thread; //! # use aimdb_sync::{SyncConsumer, SyncProducer}; //! # #[derive(Debug, Clone)] struct Temperature { celsius: f32 } @@ -114,7 +114,8 @@ //! Note: Cloning a `SyncConsumer` shares the same channel, so only one thread //! will receive each value. For independent subscriptions, create multiple consumers: //! -//! ```no_run +#![cfg_attr(feature = "std", doc = "```no_run")] +#![cfg_attr(not(feature = "std"), doc = "```ignore")] //! # use aimdb_sync::{AimDbHandle, SyncResult}; //! # #[derive(Debug, Clone)] struct Temperature { celsius: f32 } //! # fn demo(handle: &AimDbHandle) -> SyncResult<()> { @@ -131,7 +132,8 @@ //! By default, both producers and consumers use a channel capacity of 100. //! You can customize this per record type using the `_with_capacity` methods: //! -//! ```no_run +#![cfg_attr(feature = "std", doc = "```no_run")] +#![cfg_attr(not(feature = "std"), doc = "```ignore")] //! # use aimdb_sync::{AimDbHandle, SyncResult}; //! # #[derive(Debug, Clone)] struct SensorData { value: f32 } //! # #[derive(Debug, Clone)] struct RareEvent { code: u8 } @@ -167,7 +169,8 @@ //! ### Solutions for SingleLatest Semantics //! //! 1. **Use `get_latest()`** - Drains the channel to get the most recent value: -//! ```no_run +#![cfg_attr(feature = "std", doc = "```no_run")] +#![cfg_attr(not(feature = "std"), doc = "```ignore")] //! # use aimdb_sync::SyncResult; //! # #[derive(Debug, Clone)] struct Temperature { celsius: f32 } //! # fn demo(consumer: &aimdb_sync::SyncConsumer) -> SyncResult<()> { @@ -178,7 +181,8 @@ //! ``` //! //! 2. **Use capacity=1** - Minimize queueing: -//! ```no_run +#![cfg_attr(feature = "std", doc = "```no_run")] +#![cfg_attr(not(feature = "std"), doc = "```ignore")] //! # #[derive(Debug, Clone)] struct Temperature { celsius: f32 } //! # fn demo(handle: &aimdb_sync::AimDbHandle) -> aimdb_sync::SyncResult<()> { //! let consumer = handle.consumer_with_capacity::("sensor.temp", 1)?; @@ -223,7 +227,8 @@ //! and return any errors that occur in the async context //! - `try_set()` sends immediately without waiting for the produce result (fire-and-forget) //! -//! ```no_run +#![cfg_attr(feature = "std", doc = "```no_run")] +#![cfg_attr(not(feature = "std"), doc = "```ignore")] //! # use aimdb_sync::{DbError, SyncError, SyncProducer}; //! # use aimdb_core::{log_error}; //! # #[derive(Debug, Clone)] struct Temperature { celsius: f32 } @@ -245,16 +250,23 @@ #![warn(missing_docs)] #![warn(clippy::all)] #![cfg_attr(docsrs, feature(doc_cfg))] +#![cfg_attr(not(feature = "std"), no_std)] extern crate alloc; +#[cfg(feature = "std")] mod consumer; mod error; +#[cfg(feature = "std")] mod handle; +#[cfg(feature = "std")] mod producer; +#[cfg(feature = "std")] pub use consumer::SyncConsumer; +#[cfg(feature = "std")] pub use handle::{AimDbBuilderSyncExt, AimDbHandle, AimDbSyncExt, DEFAULT_SYNC_CHANNEL_CAPACITY}; +#[cfg(feature = "std")] pub use producer::SyncProducer; pub use error::{SyncError, SyncResult}; diff --git a/aimdb-sync/src/producer.rs b/aimdb-sync/src/producer.rs index 08361bc7..033fc0b0 100644 --- a/aimdb-sync/src/producer.rs +++ b/aimdb-sync/src/producer.rs @@ -2,9 +2,9 @@ use crate::{SyncError, SyncResult}; use aimdb_core::DbResult; -use std::fmt::Debug; -use std::sync::Arc; -use std::time::Duration; +use alloc::sync::Arc; +use core::fmt::Debug; +use core::time::Duration; use tokio::sync::{mpsc, oneshot}; /// Synchronous producer for records of type `T`. @@ -124,7 +124,6 @@ where /// /// # #[derive(Debug, Clone)] /// # struct MyData { value: i32 } - /// # #[cfg(feature = "std")] /// # fn main() -> SyncResult<()> { /// let handle = AimDbBuilder::new() /// .runtime(Arc::new(TokioAdapter)) @@ -161,7 +160,6 @@ where /// /// # #[derive(Debug, Clone)] /// # struct MyData { value: i32 } - /// # #[cfg(feature = "std")] /// # fn main() -> SyncResult<()> { /// let handle = AimDbBuilder::new() /// .runtime(Arc::new(TokioAdapter)) @@ -199,7 +197,6 @@ where /// /// # #[derive(Debug, Clone)] /// # struct MyData { value: i32 } - /// # #[cfg(feature = "std")] /// # fn main() -> SyncResult<()> { /// let handle = AimDbBuilder::new() /// .runtime(Arc::new(TokioAdapter)) @@ -243,8 +240,6 @@ where /// # Example /// /// ```no_run - /// # #[cfg(feature = "data-contracts")] - /// # #[cfg(feature = "std")] /// # use aimdb_sync::SyncResult; /// # fn main() -> SyncResult<()> { /// use aimdb_core::AimDbBuilder; @@ -272,8 +267,6 @@ where /// producer.set_value(22.5)?; // constructs Temperature::set(22.5, now_ms) and sends /// # Ok(()) /// # } - /// # #[cfg(not(feature = "data-contracts"))] - /// # fn main() {} /// ``` pub fn set_value(&self, value: T::Value) -> SyncResult<()> { self.set(T::set(value, unix_now_ms())) diff --git a/aimdb-sync/tests/integration_test.rs b/aimdb-sync/tests/integration_test.rs index 9a9136a9..52dee837 100644 --- a/aimdb-sync/tests/integration_test.rs +++ b/aimdb-sync/tests/integration_test.rs @@ -1,7 +1,9 @@ //! Integration tests for aimdb-sync //! //! These tests verify end-to-end functionality of the synchronous API wrapper. - +// The whole file exercises `attach()` / `SyncProducer` / `SyncConsumer`, none of +// which exist without `std`. +#![cfg(feature = "std")] use aimdb_core::{buffer::BufferCfg, AimDbBuilder, DbError}; use aimdb_sync::AimDbBuilderSyncExt; use aimdb_sync::SyncError; diff --git a/aimdb-sync/tests/settable_integration.rs b/aimdb-sync/tests/settable_integration.rs index 815885b1..eb15052f 100644 --- a/aimdb-sync/tests/settable_integration.rs +++ b/aimdb-sync/tests/settable_integration.rs @@ -2,7 +2,7 @@ //! construct via `Settable::set`, produce, and consume end-to-end through the //! real sync bridge. -#![cfg(feature = "data-contracts")] +#![cfg(all(feature = "std", feature = "data-contracts"))] use aimdb_core::{buffer::BufferCfg, AimDbBuilder}; use aimdb_data_contracts::{SchemaType, Settable}; diff --git a/aimdb-wasm-adapter/src/buffer.rs b/aimdb-wasm-adapter/src/buffer.rs index bb96107f..1564afbb 100644 --- a/aimdb-wasm-adapter/src/buffer.rs +++ b/aimdb-wasm-adapter/src/buffer.rs @@ -19,7 +19,9 @@ use alloc::boxed::Box; use alloc::collections::VecDeque; use alloc::rc::Rc; use alloc::vec::Vec; -use core::cell::{Cell, RefCell}; +#[cfg(feature = "wasm-runtime")] +use core::cell::Cell; +use core::cell::RefCell; use core::task::{Context, Poll, Waker}; use aimdb_core::buffer::{Buffer, BufferCfg, BufferReader, DynBuffer}; @@ -389,6 +391,7 @@ fn wake_all(wakers: &mut Vec) { // ============================================================================ /// Shared state between [`CancelToken`] and [`CancelHandle`]. +#[cfg(feature = "wasm-runtime")] struct CancelInner { cancelled: Cell, waker: RefCell>, @@ -399,6 +402,7 @@ struct CancelInner { /// Polled in a `futures_util::future::select` alongside `reader.recv()`. /// When [`CancelHandle::cancel()`] fires, the stored waker is woken and /// `is_cancelled()` returns `true`, causing the select to resolve. +#[cfg(feature = "wasm-runtime")] pub(crate) struct CancelToken { inner: Rc, } @@ -407,17 +411,23 @@ pub(crate) struct CancelToken { /// /// Calling [`cancel()`](CancelHandle::cancel) sets the flag and wakes the /// subscription task so it exits immediately — even if `recv()` is blocked. +#[cfg(feature = "wasm-runtime")] pub(crate) struct CancelHandle { inner: Rc, } // SAFETY: wasm32 is single-threaded — no concurrent access possible +#[cfg(feature = "wasm-runtime")] unsafe impl Send for CancelToken {} +#[cfg(feature = "wasm-runtime")] unsafe impl Sync for CancelToken {} +#[cfg(feature = "wasm-runtime")] unsafe impl Send for CancelHandle {} +#[cfg(feature = "wasm-runtime")] unsafe impl Sync for CancelHandle {} /// Create a linked cancel token/handle pair. +#[cfg(feature = "wasm-runtime")] pub(crate) fn cancel_pair() -> (CancelToken, CancelHandle) { let inner = Rc::new(CancelInner { cancelled: Cell::new(false), @@ -431,6 +441,7 @@ pub(crate) fn cancel_pair() -> (CancelToken, CancelHandle) { ) } +#[cfg(feature = "wasm-runtime")] impl CancelToken { /// Returns `true` if [`CancelHandle::cancel()`] has been called. pub(crate) fn is_cancelled(&self) -> bool { @@ -443,6 +454,7 @@ impl CancelToken { } } +#[cfg(feature = "wasm-runtime")] impl CancelHandle { /// Signal cancellation and wake the subscription task. ///