Skip to content

phase 20d: Server::new_with_handles + new_passive_with_handles#107

Draft
JustinKovacich wants to merge 1 commit into
feature/phase20c_event_publisher_handlefrom
feature/phase20d_server_new_with_handles
Draft

phase 20d: Server::new_with_handles + new_passive_with_handles#107
JustinKovacich wants to merge 1 commit into
feature/phase20c_event_publisher_handlefrom
feature/phase20d_server_new_with_handles

Conversation

@JustinKovacich
Copy link
Copy Markdown
Contributor

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<F, Tm, R, S, H, Hsd, Hep> — 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<Self, Error> — 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<Self, Error> — 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<SdStateManager> (clears audit E.2).
  • 20c: EventPublisherHandle — drops Arc<EventPublisher> (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)

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<F, Tm, R, S, H, Hsd, Hep>` — 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<Self, Error>`
  — 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<Self, Error>`
  — 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<SdStateManager>` (clears
  audit E.2).
- 20c: `EventPublisherHandle` — drops `Arc<EventPublisher>`
  (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) <noreply@anthropic.com>
@JustinKovacich JustinKovacich requested a review from Copilot April 29, 2026 14:05
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

Adds a new constructor path for server::Server that accepts pre-built socket/SD/publisher/SD-state handles, enabling externally-managed transports (e.g., bare-metal stacks) to instantiate a Server without factory.bind(...) or Wrappable*Handle::wrap(...).

Changes:

  • Introduces ServerHandles<...> as a dependency bundle mirroring ServerDeps, but including pre-bound socket/state/publisher handles.
  • Adds Server::new_with_handles(...) and Server::new_passive_with_handles(...) synchronous constructors that store the supplied handles directly and back-fill config.local_port from unicast_socket.local_addr().
  • Re-exports ServerHandles at the crate root under feature = "server".

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 6 comments.

File Description
src/server/mod.rs Adds ServerHandles and the new handle-based active/passive constructors.
src/lib.rs Re-exports ServerHandles for public API access.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/server/mod.rs
Comment on lines +556 to +559
mut config: ServerConfig,
) -> Result<Self, Error> {
let bound_port = deps.unicast_socket.socket().local_addr()?.port();
config.local_port = bound_port;
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.

new_with_handles overwrites config.local_port unconditionally with the socket’s bound port. This differs from new_with_deps (which only back-fills when the caller passed local_port = 0) and can silently mask a misconfiguration where the caller thinks they bound a specific port but actually didn’t. Consider either (a) only overwriting when config.local_port == 0, or (b) returning Error::InvalidUsage(...) / logging a warning if config.local_port != 0 && config.local_port != bound_port.

Copilot uses AI. Check for mistakes.
Comment thread src/server/mod.rs
Comment on lines +558 to +565
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
);
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.

new_with_handles uses config.interface later when constructing IpV4Endpoint options (e.g. in send_unicast_offer), but here it only back-fills the port from unicast_socket.local_addr() and does not validate that the socket is actually bound to config.interface. If the caller bound to a different IP (or 0.0.0.0), the server may advertise the wrong endpoint address. Consider validating local_addr.ip() against config.interface (with an explicit exception if the socket is bound to INADDR_ANY) and returning Error::InvalidUsage(...) or at least emitting a warning on mismatch.

Copilot uses AI. Check for mistakes.
Comment thread src/server/mod.rs
Comment on lines +567 to +570
Ok(Self {
config,
unicast_socket: deps.unicast_socket,
sd_socket: deps.sd_socket,
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.

For an active server, the SD socket is expected to be bound to the SOME/IP-SD port (30490) so outgoing announcements and SubscribeAck/Nack responses have the correct source port. new_with_handles currently trusts the caller and never checks sd_socket.socket().local_addr()?.port() == sd::MULTICAST_PORT; if it’s wrong, SD traffic will be emitted from an unexpected port and many peers will ignore it. Consider validating the SD socket port here (and returning Error::InvalidUsage(...) on mismatch) to fail fast on misconfiguration.

Copilot uses AI. Check for mistakes.
Comment thread src/server/mod.rs
Comment on lines +554 to +557
pub fn new_with_handles(
deps: ServerHandles<F, Tm, R, S, H, Hsd, Hep>,
mut config: ServerConfig,
) -> Result<Self, Error> {
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.

There are extensive constructor-focused tests in this file, but the new public constructor new_with_handles isn’t covered. Adding a unit test that builds a ServerHandles with a minimal TransportSocket stub (local_addr returning a known port) would help ensure config.local_port back-fill and the resulting is_passive flag behave as intended.

Copilot uses AI. Check for mistakes.
Comment thread src/server/mod.rs
mut config: ServerConfig,
) -> Result<Self, Error> {
let bound_port = deps.unicast_socket.socket().local_addr()?.port();
config.local_port = bound_port;
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.

new_passive_with_handles overwrites config.local_port unconditionally with the socket’s bound port. For consistency with new_passive_with_deps (and to avoid masking mismatches), consider only back-filling when config.local_port == 0, or returning Error::InvalidUsage(...) / logging a warning if a non-zero config.local_port does not match the socket’s bound port.

Suggested change
config.local_port = bound_port;
if config.local_port == 0 {
config.local_port = bound_port;
} else if config.local_port != bound_port {
return Err(Error::InvalidUsage(format!(
"new_passive_with_handles local_port mismatch: config.local_port={} but unicast socket is bound to {}",
config.local_port, bound_port
)));
}

Copilot uses AI. Check for mistakes.
Comment thread src/server/mod.rs
Comment on lines +606 to +613
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
);
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.

Same as new_with_handles: new_passive_with_handles relies on config.interface for SD endpoint construction elsewhere, but doesn’t validate it against unicast_socket.local_addr().ip(). A mismatch can lead to incorrect endpoint IP being advertised in OfferService responses. Consider validating (or warning) on interface mismatch, with an explicit exception for sockets bound to 0.0.0.0 if you want to allow INADDR_ANY binds.

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