Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,8 @@ jobs:
with:
tool: cargo-llvm-cov, cargo-nextest
- run: cargo test --no-default-features
- name: No-alloc witness (explicit gate)
run: cargo test --features client,bare_metal --test no_alloc_witness
- run: cargo llvm-cov nextest --all-features --lcov --output-path ./target/lcov.info
- name: Upload Coverage report
uses: codecov/codecov-action@v5
Expand Down
5 changes: 5 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,11 @@ required-features = ["client", "bare_metal"]
name = "static_channels_alloc_witness"
required-features = ["client", "bare_metal"]

[[test]]
name = "no_alloc_witness"
required-features = ["client", "bare_metal"]
harness = false

[[test]]
name = "bare_metal_server"
required-features = ["server", "bare_metal"]
2 changes: 2 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -209,3 +209,5 @@ pub use transport::{
OneshotCancelled, OneshotRecv, OneshotSend, ReceivedDatagram, SocketOptions, Spawner, Timer,
TransportError, TransportFactory, TransportSocket, UnboundedRecv, UnboundedSend,
};
#[cfg(feature = "bare_metal")]

Copilot AI Apr 28, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These re-exports are gated only on feature = "bare_metal", but the newly added handle implementations currently rely on std-only E2ERegistry. If the implementation is updated to cfg(all(feature = "bare_metal", feature = "std")) (or otherwise made std-free), this pub use should match to avoid build failures under --no-default-features --features bare_metal.

Suggested change
#[cfg(feature = "bare_metal")]
#[cfg(all(feature = "bare_metal", feature = "std"))]

Copilot uses AI. Check for mistakes.
pub use transport::{AtomicInterfaceHandle, StaticE2EHandle, StaticE2EStorage};
150 changes: 150 additions & 0 deletions src/transport.rs
Original file line number Diff line number Diff line change
Expand Up @@ -776,6 +776,156 @@ mod std_handle_impls {
}
}

/// Bare-metal no-alloc impls of [`E2ERegistryHandle`] and [`InterfaceHandle`].
///
/// These types satisfy `Clone + Send + Sync + 'static` without any heap
/// allocation. The backing storage lives in a caller-owned `static`; the
/// handles are thin `&'static` pointers that are trivially `Copy`.
///
/// # Production pattern
///
/// ```ignore
/// use core::cell::RefCell;
/// use core::sync::atomic::{AtomicU32, Ordering};
/// use embassy_sync::blocking_mutex::Mutex;
/// use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex;
/// use simple_someip::e2e::E2ERegistry;
/// use simple_someip::transport::{StaticE2EHandle, AtomicInterfaceHandle};
///
/// // Initialize once in main() before spawning tasks.
/// fn init() -> (StaticE2EHandle, AtomicInterfaceHandle) {
/// static IFACE_ADDR: AtomicU32 = AtomicU32::new(0);
/// // E2ERegistry::new() is not const so the storage is heap-placed once.
/// let registry_storage: &'static _ = Box::leak(Box::new(
/// Mutex::<CriticalSectionRawMutex, RefCell<E2ERegistry>>::new(
/// RefCell::new(E2ERegistry::new()),
/// ),
/// ));
/// (StaticE2EHandle::new(registry_storage), AtomicInterfaceHandle::new(&IFACE_ADDR))
/// }
/// ```
///
/// # No-allocator targets
///
/// The example above uses `Box::leak` because [`E2ERegistry::new`] is not
/// currently `const`. On a target with no allocator, swap that for a
/// `static`-cell pattern (e.g. `static_cell::StaticCell::init`) once the
/// registry constructor becomes `const`-friendly. The handle layer itself
/// never allocates — only the one-time storage materialization does.
#[cfg(feature = "bare_metal")]

Copilot AI Apr 28, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

bare_metal_handle_impls depends on crate::e2e::E2ERegistry, but E2ERegistry is only exported when feature = "std" (see src/e2e/mod.rs). With --no-default-features --features bare_metal, this module will fail to compile even if these handles aren’t used. Consider gating this module (and its re-exports) behind cfg(all(feature = "bare_metal", feature = "std")), or redesigning the storage alias/handle to not require the std-only E2ERegistry type.

Suggested change
#[cfg(feature = "bare_metal")]
#[cfg(all(feature = "bare_metal", feature = "std"))]

Copilot uses AI. Check for mistakes.
pub mod bare_metal_handle_impls {
use super::{E2ERegistryHandle, InterfaceHandle};
use crate::e2e::{E2ECheckStatus, E2EKey, E2EProfile, E2ERegistry, Error as E2EError};
use core::cell::RefCell;
use core::net::Ipv4Addr;
use core::sync::atomic::{AtomicU32, Ordering};
use embassy_sync::blocking_mutex::Mutex;
use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex;

/// Convenience type alias for the embassy-sync critical-section mutex
/// backing [`StaticE2EHandle`].
pub type StaticE2EStorage = Mutex<CriticalSectionRawMutex, RefCell<E2ERegistry>>;

/// No-alloc [`E2ERegistryHandle`] backed by a `&'static` critical-section
/// mutex.
///
/// All clones are the same thin pointer. Construct via [`StaticE2EHandle::new`]
/// and supply a `&'static StaticE2EStorage` (typically obtained via
/// `Box::leak` during system init, since [`E2ERegistry::new`] is not const).
#[derive(Clone, Copy)]
pub struct StaticE2EHandle(&'static StaticE2EStorage);

impl StaticE2EHandle {
/// Wraps a static reference to the backing mutex.
pub const fn new(storage: &'static StaticE2EStorage) -> Self {
Comment on lines +837 to +840

Copilot AI Apr 28, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

unsafe impl Send/Sync is a significant safety escape hatch. It would be safer to rely on auto-traits (if StaticE2EStorage is actually Send + Sync) rather than forcing Send/Sync manually. If the underlying embassy_sync::blocking_mutex::Mutex<..., RefCell<_>> is intentionally not Send/Sync on some targets, these unsafe impls could be unsound; consider removing them or switching to a backing type that is explicitly Send + Sync.

Copilot uses AI. Check for mistakes.
Self(storage)
}
}

// Send + Sync are derived automatically: `&'static StaticE2EStorage`
// is `Send + Sync` because `BlockingMutex<CriticalSectionRawMutex,
// RefCell<E2ERegistry>>` is `Sync` (the embassy-sync mutex serializes
// access to the inner `RefCell`, which is itself `Send`).

impl E2ERegistryHandle for StaticE2EHandle {
fn register(&self, key: E2EKey, profile: E2EProfile) {
self.0.lock(|cell| cell.borrow_mut().register(key, profile));
}

fn unregister(&self, key: &E2EKey) {
self.0.lock(|cell| cell.borrow_mut().unregister(key));
}

fn contains_key(&self, key: &E2EKey) -> bool {
self.0.lock(|cell| cell.borrow().contains_key(key))
}

fn protect(
&self,
key: E2EKey,
payload: &[u8],
upper_header: [u8; 8],
output: &mut [u8],
) -> Option<Result<usize, E2EError>> {
self.0
.lock(|cell| cell.borrow_mut().protect(key, payload, upper_header, output))
}

fn check<'a>(
&self,
key: E2EKey,
payload: &'a [u8],
upper_header: [u8; 8],
) -> Option<(E2ECheckStatus, &'a [u8])> {
self.0.lock(|cell| cell.borrow_mut().check(key, payload, upper_header))
}
}

/// No-alloc [`InterfaceHandle`] backed by a `&'static AtomicU32`.
///
/// IPv4 addresses are encoded as big-endian `u32` (`Ipv4Addr::into::<u32>`).
/// All clones are the same thin pointer. Declare the backing storage in a
/// `static`:
///
/// ```ignore
/// static IFACE_ADDR: AtomicU32 = AtomicU32::new(0);
/// let handle = AtomicInterfaceHandle::new(&IFACE_ADDR);
/// ```
///
/// # Memory ordering
///
/// Both `get` and `set` use [`Ordering::Relaxed`]. The address is the
Comment on lines +895 to +897

Copilot AI Apr 28, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

unsafe impl Send/Sync here looks unnecessary and weakens the compiler’s thread-safety checks. AtomicInterfaceHandle contains only a &'static AtomicU32, which is already Send + Sync, so the auto-traits should apply without unsafe. Prefer removing these unsafe impls (or, if you keep them, add a stronger safety justification than “&'static … is already Send + Sync”).

Copilot uses AI. Check for mistakes.
/// only synchronized datum — no other memory state is published or
/// observed alongside it — so single-location atomicity is sufficient.
/// A reader will eventually observe the latest write; there is no
/// happens-before relationship to establish with surrounding memory.
#[derive(Clone, Copy)]
pub struct AtomicInterfaceHandle(&'static AtomicU32);

impl AtomicInterfaceHandle {
/// Wraps a static reference to the backing atomic.
pub const fn new(addr: &'static AtomicU32) -> Self {
Self(addr)
}
}

// Send + Sync are derived automatically: `&'static AtomicU32` is
// `Send + Sync` because `AtomicU32` is `Sync`.

impl InterfaceHandle for AtomicInterfaceHandle {
fn get(&self) -> Ipv4Addr {
Ipv4Addr::from(self.0.load(Ordering::Relaxed))
}

fn set(&self, addr: Ipv4Addr) {
self.0.store(u32::from(addr), Ordering::Relaxed);
}
}
}

#[cfg(feature = "bare_metal")]
pub use bare_metal_handle_impls::{AtomicInterfaceHandle, StaticE2EHandle, StaticE2EStorage};

// ── Channel-handle abstraction ────────────────────────────────────────────
//
// `ChannelFactory` and its associated sender / receiver traits abstract over
Expand Down
Loading