Skip to content

phase 19f: SocketHandle abstraction over Server's socket storage#102

Draft
JustinKovacich wants to merge 1 commit into
feature/phase19e_loopback_integration_testfrom
feature/phase19f_server_local_bounds
Draft

phase 19f: SocketHandle abstraction over Server's socket storage#102
JustinKovacich wants to merge 1 commit into
feature/phase19e_loopback_integration_testfrom
feature/phase19f_server_local_bounds

Conversation

@JustinKovacich
Copy link
Copy Markdown
Contributor

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)

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>
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

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 + WrappableSocketHandle traits and provide handle implementations for Arc<T> and StaticSocketHandle<T>.
  • Make Server and EventPublisher generic over H: SocketHandle (defaulting Server’s H to Arc<F::Socket>), and route socket operations through H::socket().
  • Add Server::announcement_loop_local (non-Send) and move Send bounds for announcement_loop to 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.

Comment thread src/transport.rs
Comment on lines +1040 to +1044
/// 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
Copy link

Copilot AI Apr 29, 2026

Choose a reason for hiding this comment

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

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.

Copilot uses AI. Check for mistakes.
Comment thread src/transport.rs
Comment on lines 869 to 873
#[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};
Copy link

Copilot AI Apr 29, 2026

Choose a reason for hiding this comment

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

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.

Copilot uses AI. Check for mistakes.
Comment thread src/server/mod.rs
) -> Result<impl core::future::Future<Output = ()> + Send + 'static, Error> {
) -> Result<impl core::future::Future<Output = ()> + Send + 'static, Error>
where
F: Send + Sync,
Copy link

Copilot AI Apr 29, 2026

Choose a reason for hiding this comment

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

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).

Suggested change
F: Send + Sync,

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants