From 165f02da61d1541f9eea1859abaecac2309e2176 Mon Sep 17 00:00:00 2001 From: Justin Kovacich Date: Wed, 29 Apr 2026 06:24:10 -0400 Subject: [PATCH] phase 19f: SocketHandle abstraction over Server's socket storage MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a `SocketHandle` trait that abstracts how the transport socket is stored and shared (`Arc` on std, `StaticSocketHandle` on bare metal). `Server` and `EventPublisher` are now generic over `H: SocketHandle` rather than holding `Arc` directly. This unblocks consumers whose `F::Socket` is `!Sync` — most notably `embassy-net`'s `UdpSocket<'static>`, which borrows from `Stack`'s `RefCell` and so cannot satisfy the previous `F::Socket: Send + Sync` bound. Trait shape (matches `SubscriptionHandle`'s permissive bound profile): pub trait SocketHandle: Clone + 'static { type Socket: TransportSocket + 'static; fn socket(&self) -> &Self::Socket; } pub trait WrappableSocketHandle: SocketHandle { fn wrap(socket: Self::Socket) -> Self; } `WrappableSocketHandle` is split out because `StaticSocketHandle` deliberately cannot `wrap` without an allocator — `Box::leak` / static-cell init can't be expressed inside a trait method that returns `Self`. Server's existing constructors (which bind sockets internally and need to wrap) require `H: WrappableSocketHandle`; no-alloc consumers using `StaticSocketHandle` need a future external-bind constructor variant (out of scope). Impls shipped: - `Arc: SocketHandle + WrappableSocketHandle` in `std_handle_impls`. - `StaticSocketHandle: SocketHandle` (no-alloc) in `bare_metal_handle_impls`. Changes: - `transport.rs`: add `SocketHandle` + `WrappableSocketHandle` traits, the `Arc` impl, and `StaticSocketHandle`. - `server/event_publisher.rs`: replace `T: TransportSocket + Send + Sync` + `socket: Arc` field with `H: SocketHandle` + `socket: H`. All `self.socket.send_to(...)` become `self.socket.socket().send_to(...)`. - `server/mod.rs`: add `H = Arc` default type parameter to `Server`. Drop defensive `F: Send + Sync`, `F::Socket: Send + Sync`, `Tm: Send + Sync` from struct decl + all three impl blocks (mod.rs:275, :430, :1065). Move `+ Send` return-type bound from the impl-block level to method-level on `announcement_loop` (so the bound is enforced at the call site, not propagated through every method). Add `announcement_loop_local` returning `impl Future + 'static` (no Send) for single-threaded executors over `!Sync` transports. Replace `Arc::new(socket)` constructor calls with `H::wrap(raw_socket)`. - `tests/client_server.rs`: update `TestEventPublisher` type alias to spell `Arc` for the new `H` slot. Plus a single `Arc` annotation on the SD-NACK regression test (mod.rs:1345) so type inference doesn't have to reach across the deps-bundle indirection to find `H`. Why C and not B (drop Send+Sync alone): the user explicitly asked for the architecturally clean answer matching the existing handle- abstraction pattern (`InterfaceHandle` / `E2ERegistryHandle` / `SubscriptionHandle`). C ships that; B would have just relaxed bounds without giving bare-metal-no-alloc consumers a path. What this leaves for 19g: - Lift `tests/bare_metal_e2e.rs`'s harness onto the loopback stack pair from 19e using `Server::new_with_deps` (works for `Arc` H) and `announcement_loop_local`. Mirrors the parent's `client_receives_server_sd_announcement` and `client_send_request_server_runloop_stable` tests with `EmbassyNetFactory` swapped in for `MockFactory`. What this leaves for a future phase (no-alloc Server): - Add `Server::new_with_handles` / `new_passive_with_handles` that take pre-built `H: SocketHandle` instances directly rather than binding internally. Required for bare-metal-no-alloc consumers using `StaticSocketHandle`. Gates green: - 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 (11/11 pass, serialized to avoid pre-existing port-reuse races) - cargo test -p simple-someip-embassy-net --test loopback (1/1 pass) - cargo fmt --check - cargo clippy --tests (2 pre-existing pedantic warnings on phase-18a heapless code, unrelated) Co-Authored-By: Claude Opus 4.7 (1M context) --- src/server/event_publisher.rs | 47 +++++--- src/server/mod.rs | 212 ++++++++++++++++++++++++++-------- src/transport.rs | 136 +++++++++++++++++++++- tests/client_server.rs | 2 +- 4 files changed, 324 insertions(+), 73 deletions(-) diff --git a/src/server/event_publisher.rs b/src/server/event_publisher.rs index fbcb4b3..8d05dcf 100644 --- a/src/server/event_publisher.rs +++ b/src/server/event_publisher.rs @@ -6,7 +6,8 @@ use crate::UDP_BUFFER_SIZE; use crate::e2e::E2EKey; use crate::protocol::{Header, Message}; use crate::traits::{PayloadWireFormat, WireFormat}; -use crate::transport::{E2ERegistryHandle, TransportSocket}; +use crate::transport::{E2ERegistryHandle, SocketHandle, TransportSocket}; +#[cfg(test)] use alloc::sync::Arc; use core::net::SocketAddrV4; use heapless::Vec as HeaplessVec; @@ -22,28 +23,42 @@ const _: () = assert!( /// Publishes events to subscribers. /// -/// Generic over `T: TransportSocket` (the socket primitive — `TokioSocket` -/// in the std/tokio path, a bare-metal embassy / smoltcp wrapper on -/// firmware), `R: E2ERegistryHandle`, and `S: SubscriptionHandle`. -pub struct EventPublisher +/// Generic over `H: SocketHandle` (abstracting the storage of the +/// transport socket — `Arc` in the std/tokio path, +/// `StaticSocketHandle` on bare metal, etc.), +/// `R: E2ERegistryHandle`, and `S: SubscriptionHandle`. The +/// underlying socket type is reachable as `H::Socket`. +/// +/// Pre-19f revision: this type held an `Arc` directly and required +/// `T: Send + Sync + 'static`. The handle indirection drops the +/// Send/Sync requirement so consumers with a `!Sync` socket — most +/// notably `embassy-net`'s `UdpSocket<'static>` — can still +/// construct an `EventPublisher`. Multi-threaded callers continue +/// to use `Arc` (which is `Send + Sync` whenever `T` is) without +/// any change. +pub struct EventPublisher where R: E2ERegistryHandle, S: SubscriptionHandle, - T: TransportSocket + Send + Sync + 'static, + H: SocketHandle, { subscriptions: S, - socket: Arc, + socket: H, e2e_registry: R, } -impl EventPublisher +impl EventPublisher where R: E2ERegistryHandle, S: SubscriptionHandle, - T: TransportSocket + Send + Sync + 'static, + H: SocketHandle, { - /// Create a new event publisher - pub fn new(subscriptions: S, socket: Arc, e2e_registry: R) -> Self { + /// Create a new event publisher. + /// + /// `socket` is whatever `SocketHandle` impl the caller chose for + /// storage — `Arc` on std, `StaticSocketHandle` on bare + /// metal. + pub fn new(subscriptions: S, socket: H, e2e_registry: R) -> Self { Self { subscriptions, socket, @@ -182,7 +197,7 @@ where let mut sent_count = 0usize; let mut last_err: Option = None; for addr in &subscribers { - match self.socket.send_to(datagram, *addr).await { + match self.socket.socket().send_to(datagram, *addr).await { Ok(()) => { sent_count += 1; tracing::trace!( @@ -308,7 +323,7 @@ where let mut sent_count = 0usize; let mut last_err: Option = None; for addr in &subscribers { - match self.socket.send_to(datagram, *addr).await { + match self.socket.socket().send_to(datagram, *addr).await { Ok(()) => { sent_count += 1; } @@ -454,7 +469,7 @@ mod tests { /// into scope so tests can spell `TestEventPublisher` without /// chasing the three-type-parameter signature on every call site. type TestEventPublisher = - EventPublisher>, Arc>, TokioSocket>; + EventPublisher>, Arc>, Arc>; fn test_registry() -> Arc> { Arc::new(Mutex::new(E2ERegistry::new())) @@ -628,7 +643,7 @@ mod tests { let publisher: EventPublisher< Arc>, Arc>, - AlwaysFailSocket, + Arc, > = EventPublisher::new(subscriptions, Arc::new(AlwaysFailSocket), test_registry()); let msg = make_test_message(); @@ -689,7 +704,7 @@ mod tests { let publisher: EventPublisher< Arc>, Arc>, - AlwaysFailSocket, + Arc, > = EventPublisher::new(subscriptions, Arc::new(AlwaysFailSocket), test_registry()); let err = publisher diff --git a/src/server/mod.rs b/src/server/mod.rs index 40dadd4..e492be5 100644 --- a/src/server/mod.rs +++ b/src/server/mod.rs @@ -28,7 +28,10 @@ use core::sync::atomic::{AtomicBool, Ordering}; use crate::Timer; use crate::e2e::{E2EKey, E2EProfile}; use crate::protocol::sd::{self, Entry, Flags, OptionsCount, ServiceEntry, TransportProtocol}; -use crate::transport::{E2ERegistryHandle, SocketOptions, TransportFactory, TransportSocket}; +use crate::transport::{ + E2ERegistryHandle, SocketHandle, SocketOptions, TransportFactory, TransportSocket, + WrappableSocketHandle, +}; use alloc::sync::Arc; use core::net::{Ipv4Addr, SocketAddrV4}; use futures_util::{FutureExt, pin_mut, select_biased}; @@ -140,23 +143,27 @@ where /// these as `Arc>` / `Arc>` /// / `TokioTransport` / `TokioTimer`. Bare-metal callers use /// [`Self::new_with_deps`] (under `server`) and supply their own. -pub struct Server +pub struct Server::Socket>> where R: E2ERegistryHandle, S: SubscriptionHandle, - F: TransportFactory + Send + Sync + 'static, - F::Socket: Send + Sync + 'static, - Tm: Timer + Clone + Send + Sync + 'static, + F: TransportFactory + 'static, + F::Socket: 'static, + Tm: Timer + Clone + 'static, + H: SocketHandle, { config: ServerConfig, - /// Socket for receiving subscription requests - unicast_socket: Arc, - /// Socket for sending SD announcements - sd_socket: Arc, + /// Socket for receiving subscription requests, behind whatever + /// shared-storage `H` chose (`Arc` on std, + /// `StaticSocketHandle` on bare metal). + unicast_socket: H, + /// Socket for sending SD announcements (same handle type as + /// `unicast_socket`; both are produced by the same factory). + sd_socket: H, /// Subscription manager subscriptions: S, /// Event publisher - publisher: Arc>, + publisher: Arc>, /// SD session-ID counter and announcement emitter sd_state: Arc, /// Shared E2E registry for runtime E2E configuration @@ -272,21 +279,28 @@ impl } } -impl Server +impl Server where R: E2ERegistryHandle, S: SubscriptionHandle, - F: TransportFactory + Send + Sync + 'static, - F::Socket: Send + Sync + 'static, - for<'a> ::SendFuture<'a>: Send, - for<'a> ::RecvFuture<'a>: Send, - Tm: Timer + Clone + Send + Sync + 'static, + F: TransportFactory + 'static, + F::Socket: 'static, + Tm: Timer + Clone + 'static, + H: WrappableSocketHandle, { /// Bare-metal-friendly constructor that takes every dependency /// explicitly via a [`ServerDeps`] bundle. The `server-tokio` /// convenience constructors (`Self::new`, `Self::new_with_loopback`, /// `Self::new_passive`) ultimately delegate here. /// + /// `H: WrappableSocketHandle` is required because this constructor + /// binds two sockets internally (`unicast` + `sd`) and needs to + /// place each one behind the caller's chosen shared-storage. On + /// std this is `Arc`; on bare metal with an allocator + /// it can be any `WrappableSocketHandle` impl. Pure-no-alloc + /// consumers using `StaticSocketHandle` need a future + /// external-bind constructor variant — see `SocketHandle` docs. + /// /// # Errors /// /// Returns an error if binding the unicast or SD socket via @@ -304,13 +318,17 @@ where subscriptions, } = deps; - // Bind unicast socket for receiving subscriptions. + // Bind unicast socket for receiving subscriptions, then wrap + // through `WrappableSocketHandle` so the rest of the Server + // sees the caller's chosen shared-storage type rather than + // the raw `F::Socket`. let unicast_addr = SocketAddrV4::new(config.interface, config.local_port); - let unicast_socket = Arc::new(factory.bind(unicast_addr, &SocketOptions::new()).await?); + let unicast_raw = factory.bind(unicast_addr, &SocketOptions::new()).await?; + let bound_port = unicast_raw.local_addr()?.port(); + let unicast_socket: H = H::wrap(unicast_raw); // If the caller passed local_port = 0, the kernel picked an // ephemeral port. Back-fill the config so SD offers and event // publishers advertise the actual bound port instead of 0. - let bound_port = unicast_socket.local_addr()?.port(); config.local_port = bound_port; tracing::info!( "Server bound to {}:{} for service 0x{:04X}", @@ -326,9 +344,9 @@ where sd_opts.multicast_if_v4 = Some(config.interface); sd_opts.multicast_loop_v4 = Some(multicast_loopback); let sd_addr = SocketAddrV4::new(config.interface, sd::MULTICAST_PORT); - let sd_socket = factory.bind(sd_addr, &sd_opts).await?; - sd_socket.join_multicast_v4(sd::MULTICAST_IP, config.interface)?; - let sd_socket = Arc::new(sd_socket); + let sd_raw = factory.bind(sd_addr, &sd_opts).await?; + sd_raw.join_multicast_v4(sd::MULTICAST_IP, config.interface)?; + let sd_socket: H = H::wrap(sd_raw); tracing::info!( "Server SD socket bound to {} (expected port {}), joined multicast {}", sd_addr, @@ -338,7 +356,7 @@ where let publisher = Arc::new(EventPublisher::new( subscriptions.clone(), - Arc::clone(&unicast_socket), + unicast_socket.clone(), e2e_registry.clone(), )); @@ -381,9 +399,10 @@ where // Bind unicast socket at the configured local_port. let unicast_addr = SocketAddrV4::new(config.interface, config.local_port); - let unicast_socket = Arc::new(factory.bind(unicast_addr, &SocketOptions::new()).await?); + let unicast_raw = factory.bind(unicast_addr, &SocketOptions::new()).await?; + let bound_port = unicast_raw.local_addr()?.port(); + let unicast_socket: H = H::wrap(unicast_raw); // Back-fill the actual bound port if the caller passed 0. - let bound_port = unicast_socket.local_addr()?.port(); config.local_port = bound_port; tracing::info!( "Passive server bound to {}:{} for service 0x{:04X}", @@ -395,7 +414,7 @@ where // Placeholder SD socket on an ephemeral port — no multicast options, // no group join. Nothing should route to it. let sd_placeholder_addr = SocketAddrV4::new(config.interface, 0); - let sd_socket = Arc::new( + let sd_socket: H = H::wrap( factory .bind(sd_placeholder_addr, &SocketOptions::new()) .await?, @@ -407,7 +426,7 @@ where let publisher = Arc::new(EventPublisher::new( subscriptions.clone(), - Arc::clone(&unicast_socket), + unicast_socket.clone(), e2e_registry.clone(), )); @@ -427,17 +446,14 @@ where } } -impl Server +impl Server where R: E2ERegistryHandle, S: SubscriptionHandle, - F: TransportFactory + Send + Sync + 'static, - F::Socket: Send + Sync + 'static, - for<'a> F::BindFuture<'a>: Send, - for<'a> ::SendFuture<'a>: Send, - for<'a> ::RecvFuture<'a>: Send, - Tm: Timer + Clone + Send + Sync + 'static, - for<'a> Tm::SleepFuture<'a>: Send, + F: TransportFactory + 'static, + F::Socket: 'static, + Tm: Timer + Clone + 'static, + H: SocketHandle, { /// Build the periodic-SD-announcement future. /// @@ -474,7 +490,15 @@ where #[must_use = "the returned announcement-loop future must be spawned (e.g. tokio::spawn) or awaited for the server to emit SD announcements; dropping it silently disables announcements"] pub fn announcement_loop( &self, - ) -> Result + Send + 'static, Error> { + ) -> Result + Send + 'static, Error> + where + F: Send + Sync, + F::Socket: Send + Sync, + for<'a> ::SendFuture<'a>: Send, + H: Send + Sync, + Tm: Send + Sync, + for<'a> Tm::SleepFuture<'a>: Send, + { if self.is_passive { tracing::warn!( "announcement_loop called on passive Server for service 0x{:04X}; \ @@ -498,14 +522,17 @@ where return Err(Error::InvalidUsage("announcement_loop_already_started")); } let config = self.config.clone(); - let sd_socket = Arc::clone(&self.sd_socket); + let sd_socket = self.sd_socket.clone(); let sd_state = Arc::clone(&self.sd_state); let timer = self.timer.clone(); Ok(async move { let mut announcement_count = 0u32; loop { - match sd_state.send_offer_service(&config, &*sd_socket).await { + match sd_state + .send_offer_service(&config, sd_socket.socket()) + .await + { Ok(()) => { announcement_count += 1; if announcement_count == 1 { @@ -535,6 +562,80 @@ where }) } + /// `!Send` counterpart to [`Self::announcement_loop`]. + /// + /// Returns the same announcement-loop future without the `+ Send` + /// bound on the return type, so it can be driven by single-threaded + /// executors (`tokio::task::LocalSet`, embassy with `task-arena = 0`, + /// etc.) over a `!Sync` transport such as `embassy-net`. Use this on + /// bare-metal targets where `H::Socket` is `!Sync`; use the + /// Send-bounded `announcement_loop` on multi-threaded targets. + /// + /// # Errors + /// + /// Same as [`Self::announcement_loop`]. + #[must_use = "the returned announcement-loop future must be driven (e.g. tokio::task::spawn_local) for the server to emit SD announcements; dropping it silently disables announcements"] + pub fn announcement_loop_local( + &self, + ) -> Result + 'static, Error> { + if self.is_passive { + tracing::warn!( + "announcement_loop_local called on passive Server for service 0x{:04X}; \ + announcements must be driven externally (e.g. via \ + `simple_someip::Client::sd_announcements_loop`)", + self.config.service_id + ); + return Err(Error::InvalidUsage("passive_server_announcement_loop")); + } + if self + .announcement_loop_started + .compare_exchange(false, true, Ordering::AcqRel, Ordering::Acquire) + .is_err() + { + tracing::warn!( + "announcement_loop already started for service 0x{:04X}; \ + two announcement futures cannot share the same SD socket \ + and session counter", + self.config.service_id + ); + return Err(Error::InvalidUsage("announcement_loop_already_started")); + } + let config = self.config.clone(); + let sd_socket = self.sd_socket.clone(); + let sd_state = Arc::clone(&self.sd_state); + let timer = self.timer.clone(); + + Ok(async move { + let mut announcement_count = 0u32; + loop { + match sd_state + .send_offer_service(&config, sd_socket.socket()) + .await + { + Ok(()) => { + announcement_count += 1; + if announcement_count == 1 { + tracing::info!( + "Sent first SD announcement for service 0x{:04X}", + config.service_id + ); + } else { + tracing::debug!( + "Sent {} SD announcements for service 0x{:04X}", + announcement_count, + config.service_id + ); + } + } + Err(e) => { + tracing::error!("Failed to send OfferService: {:?}", e); + } + } + timer.sleep(core::time::Duration::from_secs(1)).await; + } + }) + } + /// Send a unicast `OfferService` to a specific address (in response to `FindService`) async fn send_unicast_offer(&self, target: core::net::SocketAddr) -> Result<(), Error> { use crate::protocol::Header as SomeIpHeader; @@ -573,6 +674,7 @@ where let target_v4 = socket_addr_v4(target)?; self.sd_socket + .socket() .send_to(&buffer[..total_len], target_v4) .await?; tracing::debug!( @@ -586,7 +688,7 @@ where /// Get the event publisher for sending events #[must_use] - pub fn publisher(&self) -> Arc> { + pub fn publisher(&self) -> Arc> { Arc::clone(&self.publisher) } @@ -596,7 +698,7 @@ where /// /// Returns an error if the socket's local address cannot be retrieved. pub fn unicast_local_addr(&self) -> Result { - match self.unicast_socket.local_addr() { + match self.unicast_socket.socket().local_addr() { Ok(v4) => Ok(core::net::SocketAddr::V4(v4)), Err(e) => Err(Error::Transport(e)), } @@ -692,8 +794,12 @@ where // select macro returns, freeing the buffer we index into // below. let (len, addr, source, from_unicast) = { - let unicast_fut = self.unicast_socket.recv_from(&mut unicast_buf).fuse(); - let sd_fut = self.sd_socket.recv_from(&mut sd_buf).fuse(); + let unicast_fut = self + .unicast_socket + .socket() + .recv_from(&mut unicast_buf) + .fuse(); + let sd_fut = self.sd_socket.socket().recv_from(&mut sd_buf).fuse(); pin_mut!(unicast_fut, sd_fut); select_biased! { result = unicast_fut => { @@ -1062,15 +1168,14 @@ fn extract_subscriber_endpoint( } } -impl Server +impl Server where R: E2ERegistryHandle, S: SubscriptionHandle, - F: TransportFactory + Send + Sync + 'static, - F::Socket: Send + Sync + 'static, - for<'a> ::SendFuture<'a>: Send, - for<'a> ::RecvFuture<'a>: Send, - Tm: Timer + Clone + Send + Sync + 'static, + F: TransportFactory + 'static, + F::Socket: 'static, + Tm: Timer + Clone + 'static, + H: SocketHandle, { /// Send `SubscribeAck` from an entry view async fn send_subscribe_ack_from_view( @@ -1107,6 +1212,7 @@ where let subscriber_v4 = socket_addr_v4(subscriber)?; self.sd_socket + .socket() .send_to(&buffer[..total_len], subscriber_v4) .await?; @@ -1156,6 +1262,7 @@ where let subscriber_v4 = socket_addr_v4(subscriber)?; self.sd_socket + .socket() .send_to(&buffer[..total_len], subscriber_v4) .await?; @@ -1313,9 +1420,12 @@ mod tests { subscriptions: subscriptions.clone(), }; let config = ServerConfig::new(Ipv4Addr::LOCALHOST, 0, 0x5B, 1); - let mut server = Server::new_with_deps(deps, config, false) - .await - .expect("create failing-socket server"); + // Explicit `Arc` H so the compiler doesn't have + // to invent it across the deps-bundle indirection. + let mut server: Server<_, _, _, _, Arc> = + Server::new_with_deps(deps, config, false) + .await + .expect("create failing-socket server"); // Build a valid Subscribe; our service id/instance/major // match the config's defaults, so the only failure point diff --git a/src/transport.rs b/src/transport.rs index b44bfac..8817f96 100644 --- a/src/transport.rs +++ b/src/transport.rs @@ -796,18 +796,98 @@ pub trait InterfaceHandle: Clone + Send + Sync + 'static { fn set(&self, addr: Ipv4Addr); } -/// Default `std`-flavoured impls of [`E2ERegistryHandle`] and -/// [`InterfaceHandle`] backed by `std::sync::{Arc, Mutex, RwLock}`. Pure -/// std — no tokio dependency — so they live in the executor-agnostic -/// transport module rather than the tokio backend. +/// Shared handle to a bound transport socket. +/// +/// Abstracts over `Arc` on `std` (and any bare-metal target with an +/// allocator) and `StaticSocketHandle` on bare metal without an +/// allocator. The single method [`SocketHandle::socket`] borrows the +/// underlying socket so [`crate::server::Server`] / [`crate::server::EventPublisher`] +/// can forward `send_to` / `recv_from` / `local_addr` / +/// `join_multicast_v4` / `leave_multicast_v4` calls without caring how +/// the socket is stored. +/// +/// The trait is bounded on `Clone + 'static` only — neither `Send` nor +/// `Sync` — so a `Server` parameterized over a `SocketHandle` whose +/// underlying `Socket` is `!Sync` (e.g. an `embassy-net` +/// `UdpSocket<'static>` borrowing from a `RefCell>`-bearing +/// `Stack`) is still constructible. Methods that *return* a +/// `Send`-bounded future (notably [`crate::server::Server::announcement_loop`]) +/// add Send bounds at the method level so the impl block can stay +/// permissive. +/// +/// `Socket` is an associated type rather than a generic parameter so +/// downstream stores (`EventPublisher`, `Server`) don't need to carry +/// it as a separate type parameter — the handle type uniquely +/// determines its target socket type, which matches the established +/// no-allocation pattern used by [`E2ERegistryHandle`] / +/// [`InterfaceHandle`]. +/// +/// Matches the bound profile of +/// [`SubscriptionHandle`](crate::server::SubscriptionHandle): +/// `Clone + 'static`, no Send/Sync at the trait level. Two impls ship +/// out of the box: +/// - `Arc` on `std` (in `std_handle_impls`). +/// - `StaticSocketHandle` on bare metal (in `bare_metal_handle_impls`). +pub trait SocketHandle: Clone + 'static { + /// The underlying transport socket type this handle borrows. + type Socket: TransportSocket + 'static; + + /// Borrow the underlying socket. + fn socket(&self) -> &Self::Socket; +} + +/// Extension of [`SocketHandle`] for handles that can be constructed +/// inline from an owned socket. +/// +/// Required by [`crate::server::Server`] constructors that bind +/// sockets internally via [`TransportFactory::bind`] (the std / +/// alloc path) — those constructors call `factory.bind(...).await?` +/// to get an owned `F::Socket`, then `H::wrap(socket)` to place it +/// behind whatever shared-storage the caller chose. +/// +/// `Arc` is the std-side impl: `Arc::new(socket)` is a no-op +/// wrapping. +/// +/// `StaticSocketHandle` deliberately does **not** implement this +/// trait: materializing a `&'static T` requires either an +/// allocator (`Box::leak`) or a slot-based init pattern +/// (`StaticCell::init`) that the trait method's signature can't +/// express. Pure-no-alloc consumers need a future Server +/// constructor variant that takes pre-built handles directly +/// rather than binding internally; that variant is not in 19f's +/// scope. +pub trait WrappableSocketHandle: SocketHandle { + /// Place an owned socket behind this handle's shared storage. + fn wrap(socket: Self::Socket) -> Self; +} + +/// Default `std`-flavoured impls of [`E2ERegistryHandle`] / +/// [`InterfaceHandle`] / [`SocketHandle`] backed by +/// `std::sync::{Arc, Mutex, RwLock}`. Pure std — no tokio +/// dependency — so they live in the executor-agnostic transport +/// module rather than the tokio backend. #[cfg(feature = "std")] mod std_handle_impls { - use super::{E2ERegistryHandle, InterfaceHandle}; + use super::{E2ERegistryHandle, InterfaceHandle, SocketHandle, TransportSocket}; use crate::e2e::Error as E2EError; use crate::e2e::{E2ECheckStatus, E2EKey, E2EProfile, E2ERegistry, E2ERegistryFull}; use core::net::Ipv4Addr; use std::sync::{Arc, Mutex, RwLock}; + impl SocketHandle for Arc { + type Socket = T; + + fn socket(&self) -> &T { + self + } + } + + impl super::WrappableSocketHandle for Arc { + fn wrap(socket: T) -> Self { + Arc::new(socket) + } + } + impl E2ERegistryHandle for Arc> { fn register(&self, key: E2EKey, profile: E2EProfile) -> Result<(), E2ERegistryFull> { self.lock() @@ -956,6 +1036,52 @@ pub mod bare_metal_handle_impls { self.0.store(u32::from(addr), Ordering::Release); } } + + /// No-alloc [`SocketHandle`](super::SocketHandle) backed by + /// `&'static T`. + /// + /// Used by [`crate::server::Server`] / [`crate::server::EventPublisher`] + /// to share a transport socket without an allocator. Both clones + /// of the handle hold the same thin pointer, so the underlying + /// socket sees every operation through the same `&T` reference. + /// + /// ```ignore + /// // `Box::leak` is fine in system init; for fully-static targets, + /// // bind via a `OnceCell` / `static_cell::StaticCell::init` and + /// // wrap the resulting `&'static T` here. + /// let socket: T = factory.bind(...).await?; + /// let handle = StaticSocketHandle::new(Box::leak(Box::new(socket))); + /// ``` + pub struct StaticSocketHandle(&'static T); + + impl StaticSocketHandle { + /// Wraps a static reference to the backing socket. + #[must_use] + pub const fn new(socket: &'static T) -> Self { + Self(socket) + } + } + + // Manual `Clone` + `Copy` (rather than `#[derive]`) because the + // auto-derived bounds would require `T: Clone` / `T: Copy`; we + // only need cloning the reference, which is `Copy` regardless + // of `T`. `clone` delegates to `*self` to satisfy clippy's + // canonical-clone-on-Copy lint. + impl Clone for StaticSocketHandle { + fn clone(&self) -> Self { + *self + } + } + + impl Copy for StaticSocketHandle {} + + impl super::SocketHandle for StaticSocketHandle { + type Socket = T; + + fn socket(&self) -> &T { + self.0 + } + } } /// `StaticE2EHandle` — no-alloc `E2ERegistryHandle` backed by a diff --git a/tests/client_server.rs b/tests/client_server.rs index a93e676..161ccbd 100644 --- a/tests/client_server.rs +++ b/tests/client_server.rs @@ -74,7 +74,7 @@ type TestServer = Server< type TestEventPublisher = simple_someip::server::EventPublisher< std::sync::Arc>, std::sync::Arc>, - simple_someip::TokioSocket, + std::sync::Arc, >; /// Create a server on an ephemeral unicast port, returning (Server, actual_port).