From bf8f77411971c7be68cd2a3ec68f31a355a3e03b Mon Sep 17 00:00:00 2001 From: Justin Kovacich Date: Wed, 29 Apr 2026 10:03:23 -0400 Subject: [PATCH] phase 20d: Server::new_with_handles + new_passive_with_handles MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds the no-alloc-friendly counterparts to `Server::new_with_deps` and `Server::new_passive_with_deps`. Caller supplies all four storage handles (`H` for sockets, `Hsd` for SD state, `Hep` for EventPublisher) pre-built; Server stores them directly without calling `factory.bind(...)` internally and without invoking any `Wrappable*Handle::wrap` step. This is the constructor path bare-metal-no-alloc consumers need: they own their UDP transport (lwIP, vendor IP stack, etc.), bind sockets externally, and wrap them via `StaticSocketHandle` / `&'static SdStateManager` / `&'static EventPublisher<...>` — materializing the static storage themselves at boot. Surface additions: - `pub struct ServerHandles` — bundle of factory + timer + e2e_registry + subscriptions + the four pre-built handles. Mirrors `ServerDeps` for the same caller ergonomics. - `Server::new_with_handles(deps, config) -> Result` — constructs an active server (announcement loop runnable, run loop runnable). Back-fills `config.local_port` from `unicast_socket.local_addr()` so SD offers advertise the bound port. - `Server::new_passive_with_handles(deps, config) -> Result` — same shape, marks `is_passive = true`. - Re-exported via `simple_someip::ServerHandles`. Both constructors live in the existing `impl@521` block whose bounds (`H: SocketHandle`, `Hsd: SdStateHandle`, `Hep: EventPublisherHandle` — all without `Wrappable*`) match what the no-alloc path requires. Both are synchronous (`pub fn`, not `pub async fn`) — no `factory.bind()` to await. Std users who prefer the async- ergonomic path keep using the existing `new_with_deps` / `new_passive_with_deps`. Combined with phases 20a-c, this completes the four-item "no-alloc Server completion" remediation surfaced by the 20-pre alloc audit: - 20a: `run_with_buffers` — caller-provided recv buffer (clears audit category-D recv-buffer item). - 20b: `SdStateHandle` — drops `Arc` (clears audit E.2). - 20c: `EventPublisherHandle` — drops `Arc` (clears audit E.1). - 20d: this commit — `new_with_handles` + `new_passive_with_handles` (clears audit category-D socket-Arc item by exposing the pre-built-handle path). A consumer building TC4D firmware with `default-features = false, features = ["client", "server", "bare_metal"]` and banning `extern crate alloc` can now construct a Server via `Server::new_with_handles(...)` using `&'static`-backed handles, drive it via `run_with_buffers(&mut [u8; N], &mut [u8; N])` over `static`-declared receive buffers, and emit SD via `announcement_loop_local`. Zero `alloc::*` surfaces in any production code path under that feature combo. What this leaves: an actual no-alloc bare-metal example / integration test against `simple-someip-embassy-net` (or a future `simple-someip-lwip` adapter) using these constructors. That's a separate "validation" commit — 20d ships the API; the witness comes when the lwip adapter exists. 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/lib.rs | 2 +- src/server/mod.rs | 155 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 156 insertions(+), 1 deletion(-) diff --git a/src/lib.rs b/src/lib.rs index c429cdd8..832c0030 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -215,7 +215,7 @@ pub use client::{ }; pub use e2e::{E2ECheckStatus, E2EKey, E2EProfile}; #[cfg(feature = "server")] -pub use server::{Server, ServerDeps, SubscriptionHandle}; +pub use server::{Server, ServerDeps, ServerHandles, SubscriptionHandle}; #[cfg(any(feature = "client-tokio", feature = "server-tokio"))] pub use tokio_transport::{TokioChannels, TokioSocket, TokioSpawner, TokioTimer, TokioTransport}; #[cfg(feature = "bare_metal")] diff --git a/src/server/mod.rs b/src/server/mod.rs index ae9bb2d1..4df9f618 100644 --- a/src/server/mod.rs +++ b/src/server/mod.rs @@ -128,6 +128,63 @@ where pub subscriptions: S, } +/// Bundle of pre-built dependencies + storage handles for +/// [`Server::new_with_handles`] / [`Server::new_passive_with_handles`]. +/// +/// Variant of [`ServerDeps`] for callers who have already bound +/// their sockets externally and assembled storage handles +/// themselves — the bare-metal-no-alloc path. Each +/// `Wrappable*Handle`-using constructor on the alloc path +/// (`Server::new_with_deps`, `Server::new_passive_with_deps`) has a +/// counterpart here that takes pre-built handles directly, +/// skipping the internal `wrap` step. That lets a no-alloc consumer +/// supply `StaticSocketHandle` / +/// `&'static SdStateManager` / `&'static EventPublisher<...>` +/// instances they materialized via their preferred static-storage +/// pattern. +/// +/// All eight fields are public so the struct can be assembled +/// inline. +pub struct ServerHandles +where + F: TransportFactory + 'static, + Tm: Timer, + R: E2ERegistryHandle, + S: SubscriptionHandle, + H: SocketHandle, + Hsd: SdStateHandle, + Hep: EventPublisherHandle, +{ + /// Transport factory. Retained on the `Server` for any + /// post-construction state the backend needs to keep alive + /// (e.g., embassy-net `Stack` handle); the new-with-handles + /// constructor does NOT call `factory.bind()`. + pub factory: F, + /// Async sleep primitive used by the announcement loop's + /// 1-second tick. + pub timer: Tm, + /// Shared E2E registry handle for runtime E2E configuration. + pub e2e_registry: R, + /// Shared subscription manager handle. + pub subscriptions: S, + /// Pre-built unicast socket handle. Caller has already bound + /// the underlying socket to the desired interface + port. + pub unicast_socket: H, + /// Pre-built SD socket handle. For active servers, caller has + /// bound to the SD multicast port (30490) and joined the SD + /// multicast group; for passive servers, this is whatever + /// placeholder socket the caller chose (will not be driven). + pub sd_socket: H, + /// Pre-built SD-state handle (`&'static SdStateManager` for + /// no-alloc, `Arc` for alloc). + pub sd_state: Hsd, + /// Pre-built `EventPublisher` handle. For std users this is + /// typically `Arc`; for no-alloc, a `&'static EventPublisher<...>` + /// declared externally. + pub publisher: Hep, +} + /// SOME/IP Server that can offer services and publish events. /// /// Generic over the four pluggable infrastructure types bundled in @@ -472,6 +529,104 @@ where Hsd: SdStateHandle, Hep: EventPublisherHandle, { + /// Construct a `Server` from pre-built dependencies + storage + /// handles. The bare-metal-no-alloc counterpart to + /// [`Self::new_with_deps`]. + /// + /// Unlike `new_with_deps`, this constructor does NOT call + /// `factory.bind(...)` and does NOT join any multicast group. + /// The caller has already bound their unicast and SD sockets + /// (typically against an externally-managed UDP stack — lwIP, + /// vendor IP, etc.) and joined the SOME/IP-SD multicast group + /// (`224.0.23.0`) on the SD socket externally. The caller has + /// also assembled the `EventPublisher` and `SdStateManager` + /// handles into whatever shared-storage their target uses + /// (`Arc<...>` on alloc, `&'static ...` on no-alloc). + /// + /// `config.local_port` is back-filled from + /// `unicast_socket.local_addr()?.port()` so SD offers and + /// event publishers advertise the actual bound port. + /// + /// # Errors + /// + /// Returns an error if querying `unicast_socket.local_addr()` + /// fails on the underlying transport. + pub fn new_with_handles( + deps: ServerHandles, + mut config: ServerConfig, + ) -> Result { + let bound_port = deps.unicast_socket.socket().local_addr()?.port(); + config.local_port = bound_port; + tracing::info!( + "Server (handles) bound to {}:{} for service 0x{:04X}", + config.interface, + bound_port, + config.service_id + ); + + Ok(Self { + config, + unicast_socket: deps.unicast_socket, + sd_socket: deps.sd_socket, + subscriptions: deps.subscriptions, + publisher: deps.publisher, + sd_state: deps.sd_state, + e2e_registry: deps.e2e_registry, + factory: deps.factory, + timer: deps.timer, + is_passive: false, + announcement_loop_started: AtomicBool::new(false), + }) + } + + /// Passive-server counterpart to [`Self::new_with_handles`]. + /// + /// Same shape; the resulting server is marked + /// `is_passive = true` so [`Self::announcement_loop`] / + /// [`Self::announcement_loop_local`] / [`Self::run`] / + /// [`Self::run_with_buffers`] return + /// `Err(Error::InvalidUsage(...))` rather than driving the SD + /// loop. The caller is expected to handle SD externally + /// (typically via a `Client::sd_announcements_loop` on the + /// same host). + /// + /// The `sd_socket` field is retained but never driven; pass + /// any pre-built handle the caller can spare (a placeholder + /// socket bound to an ephemeral port is fine, mirroring + /// `Server::new_passive_with_deps`). + /// + /// # Errors + /// + /// Returns an error if querying `unicast_socket.local_addr()` + /// fails on the underlying transport. + pub fn new_passive_with_handles( + deps: ServerHandles, + mut config: ServerConfig, + ) -> Result { + let bound_port = deps.unicast_socket.socket().local_addr()?.port(); + config.local_port = bound_port; + tracing::info!( + "Passive server (handles) bound to {}:{} for service 0x{:04X}", + config.interface, + bound_port, + config.service_id + ); + + Ok(Self { + config, + unicast_socket: deps.unicast_socket, + sd_socket: deps.sd_socket, + subscriptions: deps.subscriptions, + publisher: deps.publisher, + sd_state: deps.sd_state, + e2e_registry: deps.e2e_registry, + factory: deps.factory, + timer: deps.timer, + is_passive: true, + announcement_loop_started: AtomicBool::new(false), + }) + } + /// Build the periodic-SD-announcement future. /// /// Returns a future that sends an `OfferService` message to the SD