From 4f9d36ed1d03f0f5273f6715ee18b2709c242fc8 Mon Sep 17 00:00:00 2001 From: Justin Kovacich Date: Wed, 29 Apr 2026 09:45:19 -0400 Subject: [PATCH] server: split run() into run_with_buffers + alloc shim MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Introduces `Server::run_with_buffers(&mut self, unicast_buf: &mut [u8], sd_buf: &mut [u8])` as the no-alloc-friendly entry point for the server event loop. The existing `Server::run` becomes a thin convenience shim that heap-allocates two 65535-byte buffers via `alloc::vec!` and delegates. Why: bare-metal consumers (TC4D + future no-alloc targets) cannot call `Server::run` because it pulls in `alloc::vec![0u8; 65535]` for the recv buffers. Splitting the buffer allocation out of the event loop body lets those consumers supply their own storage (typically `static`- declared `[u8; 65535]` arrays) while leaving std consumers' ergonomics unchanged. Pre-existing 64 KiB sizing rationale carries over verbatim to `run_with_buffers`'s docs: peer SD messages are bounded by link MTU, but the server is a sink for any peer datagram landing on its SD/unicast port — a smaller buffer would silently truncate larger-than-MTU peer messages instead of surfacing them. Caller picks an appropriate size for their target. Reborrow nuance: inside the loop, `recv_from(&mut *buf)` rather than `recv_from(&mut buf)` because `unicast_buf` / `sd_buf` are now `&mut [u8]` parameters, not owned `Vec` locals. Direct `&mut buf` would produce `&mut &mut [u8]`. Clears 20-pre alloc audit's category-D recv-buffer item without breaking any std-side caller. Existing tests pass: - 11 client_server tests (serialized to avoid pre-existing port races) - 2 bare_metal_e2e tests - 3 simple-someip-embassy-net loopback tests Doesn't yet eliminate the other 20-pre findings: - D / 19f H = Arc default — handled by separate `Server::new_with_handles` work - E.1 Arc — handled by separate `EventPublisherHandle` work - E.2 Arc — handled by separate `SdStateHandle` work What this leaves: the bare-metal consumer must still hold the 65535-byte buffers in static storage (or wherever the firmware can spare 128 KB total recv-buffer RAM). On TC4D specifically with a typical RAM budget the size may need to shrink to something like 1500 + a documented truncation caveat — that's a per-consumer decision now exposed via the new API surface. 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 - cargo test --features client,server,bare_metal --test bare_metal_e2e - cargo test -p simple-someip-embassy-net --test loopback Co-Authored-By: Claude Opus 4.7 (1M context) --- src/server/mod.rs | 75 +++++++++++++++++++++++++++++++++++++---------- 1 file changed, 59 insertions(+), 16 deletions(-) diff --git a/src/server/mod.rs b/src/server/mod.rs index e492be5..bf75281 100644 --- a/src/server/mod.rs +++ b/src/server/mod.rs @@ -732,12 +732,36 @@ where self.e2e_registry.unregister(key); } - /// Run the server event loop + /// Run the server event loop with caller-provided receive buffers. /// /// Handles incoming subscription requests and manages event groups. /// Listens on both the unicast socket (for direct requests) and the /// SD multicast socket (for `FindService` and `SubscribeEventGroup`). /// + /// `unicast_buf` and `sd_buf` are caller-supplied scratch buffers + /// for incoming datagrams. Each must be at least one MTU + /// (~1500 bytes) and ideally up to the IP datagram limit + /// (64 KiB - 1) — peer SD messages are bounded by the link MTU, + /// but a SOME/IP server should not silently cap at 1500 because + /// it is a sink for any peer datagram landing on its SD or + /// unicast port. Backends that surface truncation + /// (`ReceivedDatagram::truncated`) emit a `tracing::warn!` when + /// the caller's buffer was too small; backends that don't + /// (TokioSocket today) silently truncate at the OS level. + /// + /// On bare-metal, callers typically place the buffers in + /// `static` storage: + /// ```ignore + /// static mut UNICAST_BUF: [u8; 65535] = [0; 65535]; + /// static mut SD_BUF: [u8; 65535] = [0; 65535]; + /// // SAFETY: only one task drives `run_with_buffers` for a given Server. + /// unsafe { server.run_with_buffers(&mut UNICAST_BUF, &mut SD_BUF).await }?; + /// ``` + /// + /// On std (or any alloc-using target), [`Self::run`] is the + /// convenience shim that heap-allocates 64 KiB buffers and + /// delegates here. + /// /// # Errors /// /// Returns [`Error::Io`] with [`std::io::ErrorKind::InvalidInput`] if @@ -747,7 +771,11 @@ where /// /// Otherwise returns an error if receiving from a socket fails or /// handling an SD message fails. - pub async fn run(&mut self) -> Result<(), Error> { + pub async fn run_with_buffers( + &mut self, + unicast_buf: &mut [u8], + sd_buf: &mut [u8], + ) -> Result<(), Error> { use crate::protocol::MessageView; if self.is_passive { @@ -761,18 +789,6 @@ where return Err(Error::InvalidUsage("passive_server_run")); } - // Incoming-peer buffers sized to the IP datagram limit (64 KiB - 1). - // Do NOT shrink to `UDP_BUFFER_SIZE` (1500): peer SD messages are - // bounded by the link MTU but `recv_from` here is a server-side - // sink for any peer datagram landing on the SD/unicast port, and - // larger-than-MTU peer messages must surface (or be cleanly - // truncated by the kernel) rather than being silently capped at - // 1500 by an undersized buffer. Out-going `EventPublisher` paths - // do use the smaller `UDP_BUFFER_SIZE` because we control the - // wire size of what we emit; that asymmetry is intentional. - let mut unicast_buf = alloc::vec![0u8; 65535]; - let mut sd_buf = alloc::vec![0u8; 65535]; - loop { // `select!` (not `select_biased!`) gives pseudo-random fairness // across ready arms each poll — matches the prior @@ -794,12 +810,16 @@ where // select macro returns, freeing the buffer we index into // below. let (len, addr, source, from_unicast) = { + // Reborrow `&mut *foo` rather than `&mut foo` because + // `unicast_buf` / `sd_buf` are `&mut [u8]` parameters + // here (caller-owned), not owned `Vec` locals + // — direct `&mut foo` would produce `&mut &mut [u8]`. let unicast_fut = self .unicast_socket .socket() - .recv_from(&mut unicast_buf) + .recv_from(&mut *unicast_buf) .fuse(); - let sd_fut = self.sd_socket.socket().recv_from(&mut sd_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 => { @@ -879,6 +899,29 @@ where } } + /// Run the server event loop with heap-allocated 64 KiB recv buffers. + /// + /// Convenience wrapper over [`Self::run_with_buffers`] for callers + /// who have an allocator available — this is the simplest entry + /// point for std and bare-metal-with-alloc consumers. Bare-metal + /// callers without an allocator must use + /// [`Self::run_with_buffers`] directly with caller-supplied + /// buffers (e.g. `static`-declared `[u8; N]` arrays). + /// + /// The 64 KiB sizing matches the IP datagram limit so the server + /// surfaces (or cleanly truncates at the OS level) any peer + /// datagram that exceeds the link MTU. See + /// [`Self::run_with_buffers`] for the full sizing rationale. + /// + /// # Errors + /// + /// Same as [`Self::run_with_buffers`]. + pub async fn run(&mut self) -> Result<(), Error> { + let mut unicast_buf = alloc::vec![0u8; 65535]; + let mut sd_buf = alloc::vec![0u8; 65535]; + self.run_with_buffers(&mut unicast_buf, &mut sd_buf).await + } + /// Handle a Service Discovery message #[allow(clippy::too_many_lines)] async fn handle_sd_message(