phase 19f: SocketHandle abstraction over Server's socket storage#102
Conversation
Adds a `SocketHandle` trait that abstracts how the transport socket
is stored and shared (`Arc<T>` on std, `StaticSocketHandle<T>` on
bare metal). `Server` and `EventPublisher` are now generic over
`H: SocketHandle<Socket = F::Socket>` rather than holding
`Arc<F::Socket>` directly. This unblocks consumers whose `F::Socket`
is `!Sync` — most notably `embassy-net`'s `UdpSocket<'static>`,
which borrows from `Stack`'s `RefCell<Inner>` 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<T>: SocketHandle + WrappableSocketHandle` in `std_handle_impls`.
- `StaticSocketHandle<T>: SocketHandle` (no-alloc) in
`bare_metal_handle_impls`.
Changes:
- `transport.rs`: add `SocketHandle` + `WrappableSocketHandle`
traits, the `Arc<T>` impl, and `StaticSocketHandle<T>`.
- `server/event_publisher.rs`: replace `T: TransportSocket + Send +
Sync` + `socket: Arc<T>` field with `H: SocketHandle` + `socket: H`.
All `self.socket.send_to(...)` become `self.socket.socket().send_to(...)`.
- `server/mod.rs`: add `H = Arc<F::Socket>` 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<TokioSocket>` for the new `H` slot.
Plus a single `Arc<FailingSocket>` 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<EmbassyNetSocket>` 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) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Introduces a SocketHandle abstraction to decouple Server/EventPublisher from Arc<F::Socket>, enabling support for !Sync socket implementations (notably embassy-net’s UdpSocket<'static>) while keeping a std-friendly Arc<T> path.
Changes:
- Add
SocketHandle+WrappableSocketHandletraits and provide handle implementations forArc<T>andStaticSocketHandle<T>. - Make
ServerandEventPublishergeneric overH: SocketHandle(defaultingServer’sHtoArc<F::Socket>), and route socket operations throughH::socket(). - Add
Server::announcement_loop_local(non-Send) and moveSendbounds forannouncement_loopto method-level constraints.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
src/transport.rs |
Adds the socket-handle traits and concrete handle implementations (Arc<T>, StaticSocketHandle<T>). |
src/server/mod.rs |
Makes Server generic over a socket handle type H, relaxes struct-level bounds, and adds announcement_loop_local. |
src/server/event_publisher.rs |
Switches EventPublisher from storing Arc<T> to storing a generic H: SocketHandle. |
tests/client_server.rs |
Updates integration test type aliases for the new EventPublisher<..., H> shape. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| /// 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 |
There was a problem hiding this comment.
The module-level doc comment for bare_metal_handle_impls says it provides no-alloc impls of E2ERegistryHandle and InterfaceHandle, but this PR also adds StaticSocketHandle (a SocketHandle impl) to the same module. Consider updating that module doc comment so it accurately reflects the public surface now living under bare_metal_handle_impls.
| #[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}; |
There was a problem hiding this comment.
SocketHandle/WrappableSocketHandle impls for Arc<T> are currently inside #[cfg(feature = "std")] mod std_handle_impls. In no_std + alloc builds (e.g. --no-default-features --features server), Arc still exists (alloc::sync::Arc) but these impls won’t be compiled, which makes the default H = Arc<F::Socket> on Server unusable and forces downstream users to write their own handle impls. Consider moving the Arc<T> impls into a separate module gated on cfg(any(feature = "std", feature = "server", feature = "embassy_channels")) (or similar alloc-availability gate) and implement against alloc::sync::Arc so it works both with and without std.
| ) -> Result<impl core::future::Future<Output = ()> + Send + 'static, Error> { | ||
| ) -> Result<impl core::future::Future<Output = ()> + Send + 'static, Error> | ||
| where | ||
| F: Send + Sync, |
There was a problem hiding this comment.
The announcement_loop Send-returning variant adds F: Send + Sync as a where-bound, but the returned async block doesn’t capture self.factory (or any F value). This introduces an unnecessary API restriction: users can construct Server<..., F, ...> with a !Send/!Sync factory (now allowed by the struct bounds) but then can’t call announcement_loop even if their H/socket/timer are Send-safe. Consider dropping the F: Send + Sync bound here (keeping the socket/timer/handle bounds that actually affect the future’s Send).
| F: Send + Sync, |
Adds a
SocketHandletrait that abstracts how the transport socket is stored and shared (Arc<T>on std,StaticSocketHandle<T>on bare metal).ServerandEventPublisherare now generic overH: SocketHandle<Socket = F::Socket>rather than holdingArc<F::Socket>directly. This unblocks consumers whoseF::Socketis!Sync— most notablyembassy-net'sUdpSocket<'static>, which borrows fromStack'sRefCell<Inner>and so cannot satisfy the previousF::Socket: Send + Syncbound.Trait shape (matches
SubscriptionHandle's permissive bound profile):WrappableSocketHandleis split out becauseStaticSocketHandledeliberately cannotwrapwithout an allocator —Box::leak/ static-cell init can't be expressed inside a trait method that returnsSelf. Server's existing constructors (which bind sockets internally and need to wrap) requireH: WrappableSocketHandle; no-alloc consumers usingStaticSocketHandleneed a future external-bind constructor variant (out of scope).Impls shipped:
Arc<T>: SocketHandle + WrappableSocketHandleinstd_handle_impls.StaticSocketHandle<T>: SocketHandle(no-alloc) inbare_metal_handle_impls.Changes:
transport.rs: addSocketHandle+WrappableSocketHandletraits, theArc<T>impl, andStaticSocketHandle<T>.server/event_publisher.rs: replaceT: TransportSocket + Send + Sync+socket: Arc<T>field withH: SocketHandle+socket: H. Allself.socket.send_to(...)becomeself.socket.socket().send_to(...).server/mod.rs: addH = Arc<F::Socket>default type parameter toServer. Drop defensiveF: Send + Sync,F::Socket: Send + Sync,Tm: Send + Syncfrom struct decl + all three impl blocks (mod.rs:275, :430, :1065). Move+ Sendreturn-type bound from the impl-block level to method-level onannouncement_loop(so the bound is enforced at the call site, not propagated through every method). Addannouncement_loop_localreturningimpl Future + 'static(no Send) for single-threaded executors over!Synctransports. ReplaceArc::new(socket)constructor calls withH::wrap(raw_socket).tests/client_server.rs: updateTestEventPublishertype alias to spellArc<TokioSocket>for the newHslot.Plus a single
Arc<FailingSocket>annotation on the SD-NACK regression test (mod.rs:1345) so type inference doesn't have to reach across the deps-bundle indirection to findH.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:
tests/bare_metal_e2e.rs's harness onto the loopback stack pair from 19e usingServer::new_with_deps(works forArc<EmbassyNetSocket>H) andannouncement_loop_local. Mirrors the parent'sclient_receives_server_sd_announcementandclient_send_request_server_runloop_stabletests withEmbassyNetFactoryswapped in forMockFactory.What this leaves for a future phase (no-alloc Server):
Server::new_with_handles/new_passive_with_handlesthat take pre-builtH: SocketHandleinstances directly rather than binding internally. Required for bare-metal-no-alloc consumers usingStaticSocketHandle.Gates green: