From c72c3cf960a9151d4ad73684f2a333e78f262e90 Mon Sep 17 00:00:00 2001 From: Justin Kovacich Date: Wed, 29 Apr 2026 10:00:02 -0400 Subject: [PATCH] phase 20c: EventPublisherHandle trait + drop Arc requirement MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Mirrors phase 20b's `SdStateHandle` work for the second of the two production-path Arc usages flagged by the 20-pre alloc audit (finding E.1). Adds: - `EventPublisherHandle: Clone + 'static` trait in `src/server/event_publisher.rs`. Method: `fn publisher(&self) -> &EventPublisher`. - `WrappableEventPublisherHandle: EventPublisherHandle<...>` extension trait with `fn wrap(EventPublisher) -> Self`. - `Arc>: EventPublisherHandle + WrappableEventPublisherHandle` (alloc-using std-side default; preserves existing behavior). - `&'static EventPublisher: EventPublisherHandle` (no-alloc bare-metal path; user declares the static, supplies the reference into a future `Server::new_with_handles`). Server gains a third type parameter `Hep = Arc>` (default), threaded through the struct decl and all three impl blocks. Field `publisher: Arc>` becomes `publisher: Hep`. `Arc::new(EventPublisher::new(...))` in constructors becomes `Hep::wrap(EventPublisher::new(...))`. `Server::publisher()` accessor return type changes from `Arc>` to `Hep` — non-breaking for std users who pick up the default; bare-metal users get their chosen handle type back. Existing call sites pick up the `Hep` default; no churn in `tests/`, `examples/`, or any caller. All three Arc impls (SocketHandle, SdStateHandle, EventPublisherHandle) follow the same pattern: `Arc` for std (alloc), `&'static T` for bare-metal (no-alloc), `Wrappable*` extension for the inline- construction path. Three of four remediation items in the 20-pre alloc audit are now done: the recv buffer (20a), `Arc` (20b), and `Arc` (20c). The last item — combining all three handle types into a single `Server::new_with_handles` constructor that accepts pre-built handles directly without the `wrap` step — lands in 20d. Type-signature width: Server now reads `Server, Hsd = Arc, Hep = Arc>>`. Three defaults preserve every existing call site. After 20d, a bare-metal caller will spell out all three explicitly via the new constructor, and a std caller will keep accepting all three defaults via `Server::new_with_deps`. Gates green: - cargo fmt --check - cargo clippy --tests (2 pre-existing warnings, unrelated) - cargo build --workspace --all-targets - cargo build --no-default-features --features client,server,bare_metal - cargo build -p simple-someip-embassy-net --target thumbv7em-none-eabihf - cargo test --features client-tokio,server-tokio --test client_server -- --test-threads=1 (11/11) - cargo test --features client,server,bare_metal --test bare_metal_e2e (2/2) - cargo test -p simple-someip-embassy-net --test loopback (3/3) Co-Authored-By: Claude Opus 4.7 (1M context) --- src/server/event_publisher.rs | 96 ++++++++++++++++++++++++++++++++++- src/server/mod.rs | 43 +++++++++++----- 2 files changed, 125 insertions(+), 14 deletions(-) diff --git a/src/server/event_publisher.rs b/src/server/event_publisher.rs index 8d05dcf..8a85f20 100644 --- a/src/server/event_publisher.rs +++ b/src/server/event_publisher.rs @@ -7,7 +7,7 @@ use crate::e2e::E2EKey; use crate::protocol::{Header, Message}; use crate::traits::{PayloadWireFormat, WireFormat}; use crate::transport::{E2ERegistryHandle, SocketHandle, TransportSocket}; -#[cfg(test)] +#[cfg(any(feature = "embassy_channels", feature = "server"))] use alloc::sync::Arc; use core::net::SocketAddrV4; use heapless::Vec as HeaplessVec; @@ -451,6 +451,100 @@ where } } +/// Shared handle to the [`EventPublisher`] backing a +/// [`Server`](super::Server). +/// +/// Abstracts how the event publisher is shared between the Server's +/// run loop and any external task that wants to publish events +/// (`server.publisher().publish_event(...)`). Two impls ship out +/// of the box, mirroring the pattern established by +/// [`crate::transport::SocketHandle`] and +/// [`super::SdStateHandle`]: +/// +/// - `Arc>` on alloc-using builds — the +/// default for `Server::new_with_deps` / `new_passive_with_deps`. +/// - `&'static EventPublisher` on bare-metal-no-alloc +/// — caller declares a `static` somewhere and supplies the +/// reference into a future `Server::new_with_handles` +/// constructor. +/// +/// `Clone + 'static` only — neither `Send` nor `Sync` at the trait +/// level. Method-level `where` clauses on Server add Send bounds +/// at use sites that need them (`announcement_loop`'s +/// `+ Send`-bounded return type, etc.). +pub trait EventPublisherHandle: Clone + 'static +where + R: E2ERegistryHandle, + S: SubscriptionHandle, + H: SocketHandle, +{ + /// Borrow the underlying [`EventPublisher`] for read-only + /// access. Used by Server's run loop and accessor. + fn publisher(&self) -> &EventPublisher; +} + +// `&'static EventPublisher<...>`: trivially `Copy + Clone + 'static` +// for any 'static publisher. Caller arranges the static storage. +impl EventPublisherHandle for &'static EventPublisher +where + R: E2ERegistryHandle, + S: SubscriptionHandle, + H: SocketHandle, +{ + fn publisher(&self) -> &EventPublisher { + self + } +} + +#[cfg(any(feature = "embassy_channels", feature = "server"))] +impl EventPublisherHandle for Arc> +where + R: E2ERegistryHandle, + S: SubscriptionHandle, + H: SocketHandle, +{ + fn publisher(&self) -> &EventPublisher { + self + } +} + +/// Extension of [`EventPublisherHandle`] for handles that can be +/// constructed inline from an owned [`EventPublisher`]. +/// +/// Required by `Server` constructors that build the publisher +/// internally (the alloc-using path). The future +/// `Server::new_with_handles` will accept a pre-built `Hep: +/// EventPublisherHandle` directly and won't need this trait — +/// callers using `&'static EventPublisher<...>` declare their +/// `static` storage themselves and pass the reference in. +/// +/// Mirrors the [`crate::transport::WrappableSocketHandle`] / +/// [`super::WrappableSdStateHandle`] split: the basic `Handle` +/// trait gives read access; the `Wrappable*` extension adds the +/// inline-construction path that requires an allocator. +pub trait WrappableEventPublisherHandle: EventPublisherHandle +where + R: E2ERegistryHandle, + S: SubscriptionHandle, + H: SocketHandle, +{ + /// Place an owned [`EventPublisher`] behind this handle's + /// shared storage. + fn wrap(publisher: EventPublisher) -> Self; +} + +#[cfg(any(feature = "embassy_channels", feature = "server"))] +impl WrappableEventPublisherHandle for Arc> +where + R: E2ERegistryHandle, + S: SubscriptionHandle, + H: SocketHandle, +{ + fn wrap(publisher: EventPublisher) -> Self { + Arc::new(publisher) + } +} + #[cfg(all(test, feature = "server-tokio"))] mod tests { use super::*; diff --git a/src/server/mod.rs b/src/server/mod.rs index f77d40e..ae9bb2d 100644 --- a/src/server/mod.rs +++ b/src/server/mod.rs @@ -13,7 +13,7 @@ mod service_info; mod subscription_manager; pub use error::Error; -pub use event_publisher::EventPublisher; +pub use event_publisher::{EventPublisher, EventPublisherHandle, WrappableEventPublisherHandle}; pub use service_info::Subscriber; #[cfg(feature = "std")] pub use service_info::{EventGroupInfo, ServiceInfo}; @@ -143,8 +143,15 @@ where /// these as `Arc>` / `Arc>` /// / `TokioTransport` / `TokioTimer`. Bare-metal callers use /// [`Self::new_with_deps`] (under `server`) and supply their own. -pub struct Server::Socket>, Hsd = Arc> -where +pub struct Server< + R, + S, + F, + Tm, + H = Arc<::Socket>, + Hsd = Arc, + Hep = Arc>, +> where R: E2ERegistryHandle, S: SubscriptionHandle, F: TransportFactory + 'static, @@ -152,6 +159,7 @@ where Tm: Timer + Clone + 'static, H: SocketHandle, Hsd: SdStateHandle, + Hep: EventPublisherHandle, { config: ServerConfig, /// Socket for receiving subscription requests, behind whatever @@ -163,8 +171,10 @@ where sd_socket: H, /// Subscription manager subscriptions: S, - /// Event publisher - publisher: Arc>, + /// Event publisher, behind whatever shared-storage `Hep` chose + /// (`Arc>` on std, + /// `&'static EventPublisher` on bare-metal-no-alloc). + publisher: Hep, /// SD session-ID counter and announcement emitter, behind whatever /// shared-storage `Hsd` chose (`Arc` on std, /// `&'static SdStateManager` on bare-metal-no-alloc). @@ -282,7 +292,7 @@ impl } } -impl Server +impl Server where R: E2ERegistryHandle, S: SubscriptionHandle, @@ -291,6 +301,7 @@ where Tm: Timer + Clone + 'static, H: WrappableSocketHandle, Hsd: WrappableSdStateHandle, + Hep: WrappableEventPublisherHandle, { /// Bare-metal-friendly constructor that takes every dependency /// explicitly via a [`ServerDeps`] bundle. The `server-tokio` @@ -358,7 +369,7 @@ where sd::MULTICAST_IP ); - let publisher = Arc::new(EventPublisher::new( + let publisher = Hep::wrap(EventPublisher::new( subscriptions.clone(), unicast_socket.clone(), e2e_registry.clone(), @@ -428,7 +439,7 @@ where sd_placeholder_addr ); - let publisher = Arc::new(EventPublisher::new( + let publisher = Hep::wrap(EventPublisher::new( subscriptions.clone(), unicast_socket.clone(), e2e_registry.clone(), @@ -450,7 +461,7 @@ where } } -impl Server +impl Server where R: E2ERegistryHandle, S: SubscriptionHandle, @@ -459,6 +470,7 @@ where Tm: Timer + Clone + 'static, H: SocketHandle, Hsd: SdStateHandle, + Hep: EventPublisherHandle, { /// Build the periodic-SD-announcement future. /// @@ -694,10 +706,14 @@ where Ok(()) } - /// Get the event publisher for sending events + /// Get a clone of the event-publisher handle for sending events. + /// + /// Returns the [`EventPublisherHandle`] type — `Arc>` for std users (the default `Hep`), + /// `&'static EventPublisher` for bare-metal-no-alloc. #[must_use] - pub fn publisher(&self) -> Arc> { - Arc::clone(&self.publisher) + pub fn publisher(&self) -> Hep { + self.publisher.clone() } /// Get the local address of the unicast socket. @@ -1219,7 +1235,7 @@ fn extract_subscriber_endpoint( } } -impl Server +impl Server where R: E2ERegistryHandle, S: SubscriptionHandle, @@ -1228,6 +1244,7 @@ where Tm: Timer + Clone + 'static, H: SocketHandle, Hsd: SdStateHandle, + Hep: EventPublisherHandle, { /// Send `SubscribeAck` from an entry view async fn send_subscribe_ack_from_view(