simple_someip/transport.rs
1//! Executor-agnostic transport abstraction.
2//!
3//! [`TransportSocket`] is the minimum UDP surface `simple-someip` needs from
4//! its networking backend: unicast and multicast send/recv plus a few
5//! socket-level knobs. [`TransportFactory`] constructs bound and configured
6//! sockets at startup. [`Timer`] provides async sleep.
7//!
8//! # Why a trait, and why like this
9//!
10//! The crate's `client` and `server` modules today use a tokio-based UDP
11//! backend, with sockets created/configured via `socket2` (for reuse /
12//! multicast-interface / multicast-loop options) and then handed off as
13//! `tokio::net::UdpSocket` for the async I/O loop. That works on
14//! `std + tokio` but makes no-`std` / non-tokio embedded use impossible.
15//! These traits are the integration point for alternative backends (lwIP,
16//! smoltcp, etc.).
17//!
18//! Three explicit design choices:
19//!
20//! 1. **Executor-agnostic for socket / timer I/O.** [`TransportSocket`]
21//! and [`Timer`] methods return `impl Future`, not `async fn`, and
22//! those traits make no statement about `Send` or `'static` bounds on
23//! their returned futures. Callers that need those bounds (e.g. to
24//! `tokio::spawn`) require them at the consumer site. Bare-metal
25//! callers driving the future on a single executor task pay no `Send`
26//! tax for socket I/O. **[`Spawner::spawn`] is the deliberate
27//! exception:** it is a multi-task abstraction by definition, so it
28//! requires `Send + 'static` on its argument. Single-core executors
29//! that need a `!Send` variant (embassy with `task_arena_size = 0`,
30//! `LocalSet`-style models) need either a future `spawn_local` shim
31//! or a hand-rolled adapter; the `Send + 'static` bound is documented
32//! on the trait method itself.
33//! 2. **IPv4-only address type.** This transport abstraction currently
34//! uses [`core::net::SocketAddrV4`] directly rather than `SocketAddr`,
35//! matching the crate's present transport-layer reach for unicast and
36//! the standard SD IPv4 multicast address
37//! ([`crate::protocol::sd::MULTICAST_IP`], `239.255.0.255`). This
38//! saves every backend from writing a `SocketAddr::V6(_) =>
39//! Unsupported` arm, and documents the crate's actual reach at this
40//! layer. (The protocol layer parses IPv6 SD option endpoints too;
41//! only the transport bind / send is IPv4-today.)
42//! 3. **No object safety.** Because `impl Future` is used in method return
43//! positions, the traits cannot be made into trait objects
44//! (`Box<dyn TransportSocket>` will not compile). This is intentional:
45//! there is exactly one transport implementation per build, selected at
46//! compile time, and monomorphization eliminates any dispatch overhead.
47//! Consumers carry a generic `<T: TransportSocket>`.
48//!
49//! # `Send` and multithreaded executors
50//!
51//! Neither [`TransportSocket`] nor [`Timer`] method signatures require
52//! their returned futures to be `Send`. This is on purpose: single-threaded
53//! executors (embassy, smol's `LocalSet`, and any bare-metal task loop)
54//! benefit from the relaxation and can hold `!Send` state across yield
55//! points.
56//!
57//! Implementations targeting multithreaded executors such as `tokio::spawn`
58//! are expected to produce `Send + 'static` futures in practice. Consumers
59//! that require `Send` should enforce it through how they use the
60//! transport, not by naming the hidden future type returned by the trait
61//! methods — with RPITIT that type is anonymous and cannot be named, and
62//! there is no `TransportSocketSendFut`-style associated-type escape
63//! hatch here. Instead, wrap the call in an `async move` block and
64//! require `T: Send + 'static` on the captured state:
65//!
66//! ```ignore
67//! fn spawn_loop<T>(sock: T)
68//! where
69//! T: TransportSocket + Send + 'static,
70//! {
71//! tokio::spawn(async move {
72//! let mut sock = sock;
73//! /* use sock here */
74//! });
75//! }
76//! ```
77//!
78//! A tokio-backed implementation where the underlying `UdpSocket` is
79//! already `Send + Sync` will produce `Send` futures automatically via
80//! `async` block capture inference, so the pattern above works without
81//! any extra trait-level future bound. Implementations that hold
82//! `!Send` state internally simply won't satisfy the `T: Send` bound
83//! — the compiler catches the mismatch at the `tokio::spawn` call
84//! site rather than inside the trait definition.
85//!
86//! # Status
87//!
88//! A default `std + tokio` implementation
89//! (`crate::tokio_transport::TokioTransport`,
90//! `crate::tokio_transport::TokioSocket`, `crate::tokio_transport::TokioTimer`)
91//! ships under the `client` and `server` features and is re-exported at the
92//! crate root. The paths are rendered as code literals rather than
93//! intra-doc links because the `tokio_transport` module is feature-gated,
94//! and links would otherwise break default-feature rustdoc builds. Other
95//! backends (for example `smoltcp::UdpSocket` + `embassy-time` on embedded)
96//! are the consumer's responsibility — the traits here are the integration
97//! point.
98//!
99//! # Minimal adapter sketch
100//!
101//! ```
102//! # #[cfg(feature = "client-tokio")]
103//! # fn wrapper() {
104//! use core::future::Future;
105//! use core::net::{Ipv4Addr, SocketAddrV4};
106//! use core::pin::Pin;
107//! use core::time::Duration;
108//! use simple_someip::transport::{
109//! IoErrorKind, ReceivedDatagram, SocketOptions, Timer, TransportError,
110//! TransportFactory, TransportSocket,
111//! };
112//!
113//! // A boxed future alias keeps this sketch short without pulling in the
114//! // `futures` crate (the engine itself depends only on `futures-util`).
115//! type BoxFuture<'a, T> = Pin<Box<dyn Future<Output = T> + Send + 'a>>;
116//!
117//! struct TokioTransport;
118//!
119//! struct TokioSocket {
120//! inner: tokio::net::UdpSocket,
121//! }
122//!
123//! impl TransportFactory for TokioTransport {
124//! type Socket = TokioSocket;
125//! type BindFuture<'a> = BoxFuture<'a, Result<Self::Socket, TransportError>>;
126//! fn bind<'a>(
127//! &'a self,
128//! addr: SocketAddrV4,
129//! _options: &'a SocketOptions,
130//! ) -> Self::BindFuture<'a> {
131//! Box::pin(async move {
132//! let inner = tokio::net::UdpSocket::bind(addr)
133//! .await
134//! .map_err(|_| TransportError::Io(IoErrorKind::Other))?;
135//! Ok(TokioSocket { inner })
136//! })
137//! }
138//! }
139//!
140//! impl TransportSocket for TokioSocket {
141//! // `BoxFuture` keeps this sketch short. The real `TokioSocket`
142//! // shipped under the `client` / `server` features uses named
143//! // future structs that wrap `poll_send_to` / `poll_recv_from`
144//! // for zero-allocation per datagram — see `tokio_transport.rs`.
145//! type SendFuture<'a> = BoxFuture<'a, Result<(), TransportError>>;
146//! type RecvFuture<'a> = BoxFuture<'a, Result<ReceivedDatagram, TransportError>>;
147//!
148//! fn send_to<'a>(
149//! &'a self,
150//! buf: &'a [u8],
151//! target: SocketAddrV4,
152//! ) -> Self::SendFuture<'a> {
153//! Box::pin(async move {
154//! self.inner
155//! .send_to(buf, target)
156//! .await
157//! .map(|_| ())
158//! .map_err(|_| TransportError::Io(IoErrorKind::Other))
159//! })
160//! }
161//! fn recv_from<'a>(
162//! &'a self,
163//! buf: &'a mut [u8],
164//! ) -> Self::RecvFuture<'a> {
165//! Box::pin(async move {
166//! let (n, src) = self
167//! .inner
168//! .recv_from(buf)
169//! .await
170//! .map_err(|_| TransportError::Io(IoErrorKind::Other))?;
171//! let source = match src {
172//! std::net::SocketAddr::V4(v4) => v4,
173//! std::net::SocketAddr::V6(_) => return Err(TransportError::Unsupported),
174//! };
175//! Ok(ReceivedDatagram {
176//! bytes_received: n,
177//! source,
178//! truncated: false,
179//! })
180//! })
181//! }
182//! fn local_addr(&self) -> Result<SocketAddrV4, TransportError> {
183//! match self.inner.local_addr() {
184//! Ok(std::net::SocketAddr::V4(v4)) => Ok(v4),
185//! Ok(_) => Err(TransportError::Unsupported),
186//! Err(_) => Err(TransportError::Io(IoErrorKind::Other)),
187//! }
188//! }
189//! fn join_multicast_v4(
190//! &self,
191//! group: Ipv4Addr,
192//! iface: Ipv4Addr,
193//! ) -> Result<(), TransportError> {
194//! self.inner
195//! .join_multicast_v4(group, iface)
196//! .map_err(|_| TransportError::Io(IoErrorKind::Other))
197//! }
198//! fn leave_multicast_v4(
199//! &self,
200//! group: Ipv4Addr,
201//! iface: Ipv4Addr,
202//! ) -> Result<(), TransportError> {
203//! self.inner
204//! .leave_multicast_v4(group, iface)
205//! .map_err(|_| TransportError::Io(IoErrorKind::Other))
206//! }
207//! }
208//!
209//! struct TokioTimer;
210//! impl Timer for TokioTimer {
211//! // `tokio::time::Sleep` is `!Send`; box it behind a non-`Send`
212//! // future so this sketch stays backend-agnostic.
213//! type SleepFuture<'a> = Pin<Box<dyn Future<Output = ()> + 'a>>;
214//! fn sleep(&self, duration: Duration) -> Self::SleepFuture<'_> {
215//! Box::pin(tokio::time::sleep(duration))
216//! }
217//! }
218//! # }
219//! ```
220//!
221//! # Lifecycle
222//!
223//! Sockets are dropped to close. There is no explicit `shutdown` method —
224//! implementations should release kernel / stack resources in `Drop`.
225//! Implementations that need graceful shutdown (flushing an outgoing queue,
226//! for example) should perform it in `Drop` or expose an inherent method
227//! outside this trait.
228
229use core::future::Future;
230use core::net::{IpAddr, Ipv4Addr, SocketAddrV4};
231use core::time::Duration;
232
233use crate::e2e::Error as E2EError;
234use crate::e2e::{E2ECheckStatus, E2EKey, E2EProfile};
235
236/// Portable I/O error kinds surfaced by transport implementations.
237///
238/// This is a deliberately small vocabulary — anything that does not fit
239/// maps to [`IoErrorKind::Other`]. The enum is `#[non_exhaustive]` so new
240/// kinds can be added without a breaking change. Kept local to this crate
241/// (rather than re-exporting `embedded_io::ErrorKind`) so our public API
242/// does not move when `embedded_io` bumps major versions.
243#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
244#[non_exhaustive]
245pub enum IoErrorKind {
246 /// The operation timed out.
247 #[error("operation timed out")]
248 TimedOut,
249 /// The operation was interrupted and can be retried.
250 #[error("operation interrupted")]
251 Interrupted,
252 /// The caller lacks permission for the operation.
253 #[error("permission denied")]
254 PermissionDenied,
255 /// A remote peer actively refused the connection / destination was
256 /// unreachable.
257 #[error("connection refused")]
258 ConnectionRefused,
259 /// The network layer rejected the operation (routing, MTU, etc.).
260 #[error("network unreachable")]
261 NetworkUnreachable,
262 /// A non-blocking call would have blocked. Transient — caller
263 /// should retry or wait for readiness rather than treating as
264 /// fatal.
265 #[error("would block")]
266 WouldBlock,
267 /// An inbound datagram was truncated because it exceeded the receive
268 /// buffer. The datagram is discarded; the socket loop survives.
269 ///
270 /// Backends that receive this signal MUST drop the datagram and continue
271 /// polling — it does NOT count toward the consecutive-error kill cap.
272 /// This variant is distinct from [`Self::Other`] so that genuine I/O
273 /// errors are still counted as potentially-fatal.
274 #[error("inbound datagram truncated (exceeded buffer)")]
275 Truncated,
276 /// Any error that does not fit a more specific variant.
277 #[error("i/o error")]
278 Other,
279}
280
281impl IoErrorKind {
282 /// Returns `true` if a recv-loop error of this kind is a transient
283 /// condition that should not count toward a "kill the loop after N
284 /// consecutive errors" cap. Includes:
285 /// - [`Self::ConnectionRefused`] — a peer's ICMP port-unreachable
286 /// reply is normal noise on a SOME/IP host that probes services
287 /// that are not yet available;
288 /// - [`Self::NetworkUnreachable`] — a routing blip during
289 /// interface migration is recoverable;
290 /// - [`Self::WouldBlock`] — by definition, retry-on-readiness;
291 /// - [`Self::Interrupted`] — a signal interrupted the syscall;
292 /// - [`Self::TimedOut`] — caller-driven timeout, not a socket
293 /// failure;
294 /// - [`Self::Truncated`] — an inbound datagram was truncated because
295 /// it exceeded the receive buffer; the datagram is dropped and the
296 /// loop continues (distinct from [`Self::Other`] so genuine I/O
297 /// errors are still counted as potentially-fatal).
298 ///
299 /// All other kinds (including [`Self::Other`]) are treated as
300 /// potentially-fatal and DO count toward the cap.
301 #[must_use]
302 pub fn is_transient_recv(self) -> bool {
303 matches!(
304 self,
305 Self::ConnectionRefused
306 | Self::NetworkUnreachable
307 | Self::WouldBlock
308 | Self::Interrupted
309 | Self::TimedOut
310 | Self::Truncated,
311 )
312 }
313}
314
315/// Errors returned by [`TransportSocket`] and [`TransportFactory`]
316/// operations.
317///
318/// `#[non_exhaustive]` so that backend-specific conditions can be added in
319/// future releases without a breaking change. Implementations map their
320/// native error types into one of these variants; anything that does not
321/// fit a specific variant should use [`TransportError::Io`] with an
322/// appropriate [`IoErrorKind`].
323#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
324#[non_exhaustive]
325pub enum TransportError {
326 /// Bind failed because the address or port is already in use.
327 #[error("address in use")]
328 AddressInUse,
329 /// The operation is not supported by this transport (for example,
330 /// multicast on a backend that has none, or an IPv6 address on an
331 /// IPv4-only stack).
332 #[error("unsupported transport operation")]
333 Unsupported,
334 /// A generic I/O error, classified by a portable [`IoErrorKind`].
335 #[error("transport i/o: {0}")]
336 Io(IoErrorKind),
337}
338
339/// Socket-level options applied by [`TransportFactory::bind`].
340///
341/// The fields mirror the BSD / `socket2` options that `simple-someip`
342/// needs for its Service Discovery socket layout. A default-constructed
343/// [`SocketOptions`] requests a plain unicast socket.
344///
345/// `#[non_exhaustive]` so additional knobs (TTL, buffer sizes) can be
346/// introduced later without breaking downstream construction.
347#[derive(Debug, Clone, Copy)]
348#[non_exhaustive]
349pub struct SocketOptions {
350 /// Enable `SO_REUSEADDR`. Required on the SD port 30490 when more
351 /// than one SOME/IP endpoint runs on the same interface; on Linux,
352 /// callers binding 30490 should set BOTH this and [`Self::reuse_port`]
353 /// because Linux ties multicast-group membership to the
354 /// `SO_REUSEPORT` group rather than `SO_REUSEADDR` alone — without
355 /// REUSEPORT a second binder may fail or silently steal datagrams.
356 pub reuse_address: bool,
357 /// Enable `SO_REUSEPORT` where supported (Linux, BSD). Ignored on
358 /// platforms that do not expose it. See [`Self::reuse_address`] for
359 /// the Linux-specific reason both are required on the SD socket.
360 pub reuse_port: bool,
361 /// Outbound multicast interface (`IP_MULTICAST_IF`). `None` lets the
362 /// backend choose.
363 pub multicast_if_v4: Option<Ipv4Addr>,
364 /// Loop multicast traffic back to sockets on the same host
365 /// (`IP_MULTICAST_LOOP`). Tri-state:
366 /// - `None` — the OS default applies (Linux: enabled by default).
367 /// Use this when you have no opinion on loopback.
368 /// - `Some(true)` — explicitly enable. Required when running a
369 /// SOME/IP server and client on the same machine for testing.
370 /// - `Some(false)` — explicitly disable.
371 ///
372 /// Backends call `setsockopt(IP_MULTICAST_LOOP)` only for
373 /// `Some(_)`. A previous bool-typed field caused
374 /// `multicast_if_v4: Some(_), multicast_loop_v4: false` to silently
375 /// turn loopback OFF on hosts where the OS default was ON, even
376 /// when the caller had no opinion on loopback.
377 pub multicast_loop_v4: Option<bool>,
378}
379
380impl SocketOptions {
381 /// A plain unicast socket with no multicast configuration.
382 #[must_use]
383 pub const fn new() -> Self {
384 Self {
385 reuse_address: false,
386 reuse_port: false,
387 multicast_if_v4: None,
388 multicast_loop_v4: None,
389 }
390 }
391}
392
393impl Default for SocketOptions {
394 fn default() -> Self {
395 Self::new()
396 }
397}
398
399/// The result of a successful [`TransportSocket::recv_from`].
400///
401/// `truncated` is set if the backend delivered only a prefix of the
402/// incoming datagram because it did not fit in the caller's buffer. If
403/// callers use a buffer sized to [`crate::UDP_BUFFER_SIZE`], truncation is
404/// generally not expected on backends whose delivered datagrams are
405/// bounded by that configured application-level cap. Backends that may
406/// deliver larger datagrams should surface this explicitly instead of
407/// silently dropping the fact that data was discarded.
408///
409/// Note: the default Tokio backend currently always reports
410/// `truncated: false` because `tokio::net::UdpSocket::recv_from` does not
411/// expose `MSG_TRUNC` (or equivalent). Reliable truncation detection
412/// requires a backend that does — e.g. a `recvmsg`-based backend, or a
413/// `no_std` stack like smoltcp / embassy-net that surfaces the original
414/// datagram length.
415#[derive(Debug, Clone, Copy, PartialEq, Eq)]
416pub struct ReceivedDatagram {
417 /// Number of bytes written to the caller's buffer.
418 pub bytes_received: usize,
419 /// Source address of the datagram.
420 pub source: SocketAddrV4,
421 /// `true` if the incoming datagram was larger than the caller's
422 /// buffer and the tail was discarded. See the type-level docs for
423 /// the default Tokio backend's caveat.
424 pub truncated: bool,
425}
426
427/// A bound, configured UDP socket usable for SOME/IP message exchange.
428///
429/// Implementations are obtained via [`TransportFactory::bind`]. The
430/// send/receive methods return associated future types so callers can
431/// require `Send` bounds when spawning socket loops on multithreaded
432/// executors. The smaller socket-level queries ([`Self::local_addr`],
433/// [`Self::join_multicast_v4`], [`Self::leave_multicast_v4`]) are
434/// synchronous because they are typically O(1) lookups on a backend's
435/// internal handle and do not benefit from yielding to the executor.
436///
437/// Multicast group membership is joined *after* bind via
438/// [`TransportSocket::join_multicast_v4`]; the bind-time
439/// [`SocketOptions::multicast_if_v4`] only selects the *outbound*
440/// multicast interface.
441///
442/// # Associated future types
443///
444/// The [`SendFuture`](Self::SendFuture) and [`RecvFuture`](Self::RecvFuture)
445/// associated types let consumers express `Send` bounds on the futures
446/// returned by `send_to` and `recv_from` without requiring nightly-only
447/// Return-Type Notation (RTN, RFC 3654). This enables:
448///
449/// ```ignore
450/// fn spawn_loop<T: TransportSocket>(sock: T, spawner: impl Spawner)
451/// where
452/// T: Send + Sync + 'static,
453/// for<'a> T::SendFuture<'a>: Send,
454/// for<'a> T::RecvFuture<'a>: Send,
455/// {
456/// spawner.spawn(async move { /* use sock */ });
457/// }
458/// ```
459///
460/// `TokioSocket` implements these with `Send` futures; bare-metal
461/// implementations must do the same if they want to be used with
462/// multithreaded spawners.
463pub trait TransportSocket {
464 /// Future returned by [`Self::send_to`].
465 type SendFuture<'a>: Future<Output = Result<(), TransportError>>
466 where
467 Self: 'a;
468
469 /// Future returned by [`Self::recv_from`].
470 type RecvFuture<'a>: Future<Output = Result<ReceivedDatagram, TransportError>>
471 where
472 Self: 'a;
473
474 /// Send `buf` to `target`. UDP is atomic — either the whole datagram
475 /// is transmitted or an error is returned; there is no short-write
476 /// case, which is why this method returns `()` on success rather than
477 /// a byte count.
478 ///
479 /// Takes `&self` so a single-task socket loop can hold a pending
480 /// [`Self::recv_from`] future and still call `send_to` in another
481 /// `select!` branch. Backends that need to mutate their socket
482 /// handle on send — e.g. direct smoltcp — must provide interior
483 /// mutability (typically `RefCell<_>` on single-threaded `no_std`, or
484 /// `critical_section::Mutex<RefCell<_>>` on multi-core HAL). The
485 /// `tokio::net::UdpSocket` and `embassy_net::udp::UdpSocket` APIs
486 /// are already `&self`, so adapters over those backends need no
487 /// extra wrapping.
488 ///
489 /// # Errors
490 ///
491 /// Returns:
492 /// - [`TransportError::Io`] with the appropriate [`IoErrorKind`] for
493 /// transport-level send failures (e.g. the peer is unreachable,
494 /// the interface is down, the datagram exceeds the link MTU, or a
495 /// platform-level send error).
496 /// - [`TransportError::Unsupported`] if `target` is not representable
497 /// on a backend that only speaks a subset of IPv4 (rare; most
498 /// backends surface addressing issues as [`TransportError::Io`]).
499 fn send_to<'a>(&'a self, buf: &'a [u8], target: SocketAddrV4) -> Self::SendFuture<'a>;
500
501 /// Receive the next datagram into `buf`, returning a
502 /// [`ReceivedDatagram`] carrying byte count, source, and a truncation
503 /// flag.
504 ///
505 /// Takes `&self` for the same reason as [`Self::send_to`]: the
506 /// pending receive future must not hold an exclusive borrow of the
507 /// socket, or the concurrent send branch of a `select!` cannot
508 /// compile.
509 ///
510 /// # Cancel safety
511 ///
512 /// The returned [`Self::RecvFuture`] **must be cancel-safe**:
513 /// dropping it before completion (the typical outcome inside a
514 /// `select!` / `select_biased!` where another arm wins) must not
515 /// lose any datagram that the kernel had already delivered to the
516 /// socket. The server run-loop and the client socket-manager both
517 /// race this future against other arms and rely on the
518 /// drop-and-retry pattern; a backend whose recv-future commits
519 /// kernel state before yielding (and loses it on drop) would
520 /// silently drop datagrams. The default `TokioSocket` impl
521 /// satisfies this via tokio's documented cancel-safety on
522 /// `UdpSocket::recv_from`.
523 ///
524 /// # Errors
525 ///
526 /// Returns:
527 /// - [`TransportError::Io`] with the appropriate [`IoErrorKind`] for
528 /// transport-level receive failures (e.g. the socket was closed,
529 /// the interface went down, or a platform-level recv error).
530 /// - [`TransportError::Unsupported`] if the backend surfaces a
531 /// non-IPv4 source address that cannot be represented as
532 /// [`SocketAddrV4`].
533 ///
534 /// A datagram whose payload exceeds `buf` is **not** an error; it is
535 /// returned with [`ReceivedDatagram::truncated`] set to `true`. The
536 /// caller decides whether to treat truncation as fatal.
537 fn recv_from<'a>(&'a self, buf: &'a mut [u8]) -> Self::RecvFuture<'a>;
538
539 /// Return the local address this socket is bound to. Useful for
540 /// discovering the ephemeral port chosen by `bind(port: 0, ..)`.
541 ///
542 /// # Errors
543 ///
544 /// Returns [`TransportError`] if the backend cannot report the address.
545 fn local_addr(&self) -> Result<SocketAddrV4, TransportError>;
546
547 /// Join IPv4 multicast group `group` on interface `iface`. Required
548 /// before the socket will receive multicast traffic for that group.
549 ///
550 /// Called once per group per socket; joining twice is allowed and a
551 /// no-op on most backends.
552 ///
553 /// # Errors
554 ///
555 /// Returns [`TransportError::Unsupported`] if the backend has no
556 /// multicast support; otherwise [`TransportError::Io`] with an
557 /// appropriate kind.
558 fn join_multicast_v4(&self, group: Ipv4Addr, iface: Ipv4Addr) -> Result<(), TransportError>;
559
560 /// Leave IPv4 multicast group `group` on interface `iface`. Symmetric
561 /// to [`Self::join_multicast_v4`]. Most backends implicitly leave on
562 /// drop, so this is optional for simple lifetimes but required for
563 /// long-lived sockets that rotate group membership.
564 ///
565 /// # Errors
566 ///
567 /// Returns [`TransportError::Unsupported`] if the backend has no
568 /// multicast support; otherwise [`TransportError::Io`] with an
569 /// appropriate kind.
570 fn leave_multicast_v4(&self, group: Ipv4Addr, iface: Ipv4Addr) -> Result<(), TransportError>;
571
572 /// Upper bound, in bytes, on datagrams this socket will successfully
573 /// accept in `send_to` or return via `recv_from`. The default returns
574 /// [`crate::UDP_BUFFER_SIZE`], the crate's default application-level
575 /// UDP payload cap (currently 1500 bytes — note that this is *not*
576 /// MTU-safe; see [`crate::UDP_BUFFER_SIZE`]'s own docs for the
577 /// IPv4/IPv6 header overhead).
578 ///
579 /// Backends with a smaller effective MTU (for example, some
580 /// resource-constrained embedded stacks) should override this to
581 /// advertise the real limit so callers can size buffers accordingly.
582 #[must_use]
583 fn max_datagram_size(&self) -> usize {
584 crate::UDP_BUFFER_SIZE
585 }
586}
587
588/// Constructs [`TransportSocket`] instances from a bind address and
589/// [`SocketOptions`]. The factory carries whatever state the backend needs
590/// (for example, an lwIP network-interface handle) so that `bind` itself
591/// is a pure data operation.
592///
593/// On `std + tokio`, a unit-struct `TokioTransport;` factory is all that's
594/// needed — the runtime is implicit.
595pub trait TransportFactory {
596 /// The socket type produced by this factory.
597 type Socket: TransportSocket;
598
599 /// Future returned by [`Self::bind`].
600 ///
601 /// As an associated GAT (matching [`TransportSocket::SendFuture`] /
602 /// [`TransportSocket::RecvFuture`]), consumers can express a `Send`
603 /// bound at use sites that need it without forcing every backend
604 /// to produce a `Send` bind future. Multi-threaded callers add
605 /// `where for<'a> F::BindFuture<'a>: Send`; single-threaded callers
606 /// (`Client::new_with_deps_local`) drop that bound and accept a
607 /// `!Send` bind future from a backend like embassy-net.
608 type BindFuture<'a>: Future<Output = Result<Self::Socket, TransportError>>
609 where
610 Self: 'a;
611
612 /// Bind a new socket to `addr` with the requested `options`.
613 ///
614 /// `addr.port() == 0` requests an ephemeral port; call
615 /// [`TransportSocket::local_addr`] afterwards to discover what was
616 /// assigned.
617 ///
618 /// # Errors
619 ///
620 /// Returns [`TransportError::AddressInUse`] if the requested address
621 /// and port pair is already bound (and `reuse_*` was not enabled).
622 /// Other backend-level failures surface as [`TransportError::Io`].
623 fn bind<'a>(&'a self, addr: SocketAddrV4, options: &'a SocketOptions) -> Self::BindFuture<'a>;
624}
625
626/// Executor-agnostic sleep primitive.
627///
628/// `simple-someip` needs timed waits in two places: the Service Discovery
629/// announcement tick (1 s) and the client event-loop idle timeout
630/// (125 ms). Consumers provide a `Timer` at startup; on `std + tokio` this
631/// is a one-line wrapper around `tokio::time::sleep`, on embedded it is a
632/// one-line wrapper around `embassy_time::Timer::after` or similar.
633pub trait Timer {
634 /// Future returned by [`Self::sleep`].
635 ///
636 /// As an associated GAT, consumers can require `Send` at use sites
637 /// (`where for<'a> Tm::SleepFuture<'a>: Send`) without forcing every
638 /// backend's sleep future to be `Send`. Multi-threaded callers
639 /// (`Server::announcement_loop`, the tokio Client) add the bound;
640 /// single-threaded callers do not, accepting a `!Send` future from
641 /// a backend like `embassy_time`.
642 type SleepFuture<'a>: Future<Output = ()>
643 where
644 Self: 'a;
645
646 /// Wait for at least `duration` before resolving. Implementations MAY
647 /// overshoot but MUST NOT undershoot.
648 fn sleep(&self, duration: Duration) -> Self::SleepFuture<'_>;
649}
650
651/// Executor-agnostic task-spawning primitive.
652///
653/// `simple-someip`'s per-socket I/O loops need to run concurrently with
654/// the client's main event loop — otherwise `SocketManager::send`'s
655/// internal oneshot wait deadlocks (the send future parks the main
656/// loop, which is the only thing that would drive the socket loop to
657/// produce its response). The `Spawner` trait lets std+tokio callers
658/// pass a one-line `TokioSpawner` and bare-metal callers wrap their own
659/// executor's task-spawning primitive.
660///
661/// # Design rationale
662///
663/// The transport-trait design deliberately avoided wrapping spawn to
664/// prevent "reinventing embassy" and trait-object dispatch in the hot
665/// path. However, without a spawn abstraction, `Inner::bind_*` has to
666/// call `tokio::spawn` directly — making the whole crate tokio-only.
667/// The revised rule: spawn DOES need a trait, but we avoid the
668/// concerns by (1) keeping the trait generic (monomorphized, no
669/// `dyn Spawner`) and (2) scoping it narrowly — just spawn, not
670/// select/sleep which have other solutions.
671///
672/// # Usage
673///
674/// On `std + tokio`, use `crate::tokio_transport::TokioSpawner`
675/// (available when the `client` or `server` feature is enabled) —
676/// a zero-size unit struct whose `spawn` is a thin wrapper around
677/// `tokio::spawn`. The path is rendered as a code literal rather
678/// than an intra-doc link because the target module is feature-gated
679/// and would break default-feature rustdoc builds. On embedded:
680///
681/// ```ignore
682/// struct EmbassySpawner(embassy_executor::Spawner);
683/// impl simple_someip::Spawner for EmbassySpawner {
684/// fn spawn(&self, fut: impl core::future::Future<Output = ()> + Send + 'static) {
685/// // embassy's Spawner has its own task-registration model;
686/// // the adapter layer depends on how the user defined their tasks
687/// todo!("call self.0.spawn(...)");
688/// }
689/// }
690/// ```
691/// Local-executor counterpart to [`Spawner`].
692///
693/// Where [`Spawner::spawn`] requires its future to be `Send + 'static`
694/// (matching multi-threaded executors like tokio), `LocalSpawner::spawn_local`
695/// drops the `Send` bound and is the trait that single-threaded
696/// executors — embassy with `task-arena = 0`, tokio's `LocalSet`, async-std
697/// `LocalExecutor`, etc. — implement directly.
698///
699/// The two traits are independent: an executor MAY implement both
700/// (`current_thread` tokio with `LocalSet`), only [`Spawner`]
701/// (multi-threaded tokio default), or only [`LocalSpawner`]
702/// (single-task embassy).
703///
704/// Use `crate::client::Client::new_with_deps_local` (under `client`) to
705/// construct a Client whose run-loop and per-socket loops are submitted
706/// through a
707/// `LocalSpawner` (and whose `TransportFactory::Socket` is therefore
708/// allowed to be `!Send`).
709pub trait LocalSpawner {
710 /// Submit `future` to the local executor. Must not block; must
711 /// arrange for the future to be polled to completion on some
712 /// single-threaded task.
713 ///
714 /// The future is **not** required to be `Send` — it may capture
715 /// `Rc`, `RefCell`, raw `*mut` pointers, etc.
716 fn spawn_local(&self, future: impl Future<Output = ()> + 'static);
717}
718
719pub trait Spawner {
720 /// Submit `future` to the executor. Must not block; must arrange
721 /// for the future to be polled to completion on some task.
722 ///
723 /// # Correctness requirement
724 ///
725 /// Implementations MUST poll the submitted future. Dropping it
726 /// without polling — or holding it in a queue that never drains —
727 /// will deadlock `crate::client::Client` (available when the
728 /// `client` feature is enabled): `SocketManager::send`
729 /// `await`s an internal mpsc→oneshot round-trip whose only driver
730 /// is the per-socket loop future submitted here. No poll, no
731 /// progress, no oneshot resolution; the caller's `send` hangs
732 /// forever.
733 ///
734 /// The mock spawners in `tests/bare_metal_*.rs` demonstrate
735 /// correct integration patterns; callers that simply drop the
736 /// future will deadlock on any operation that requires a socket
737 /// round-trip.
738 ///
739 /// # Fire-and-forget by design
740 ///
741 /// `spawn` returns `()`, not a join-handle. The rest of the crate
742 /// observes `tokio::JoinHandle`s wherever it spawns work directly
743 /// (commit `d92c5a3`); this trait is the deliberate exception. The
744 /// per-socket loops have no observable result — they run forever and
745 /// only exit when their owning `SocketManager` drops its channel
746 /// ends — so a join-handle would just be storage with no callers.
747 /// A future revision MAY add an associated `Handle` type if a
748 /// concrete shutdown / cancellation use case appears; today there is
749 /// none.
750 ///
751 /// # Bound rationale
752 ///
753 /// The `Send + 'static` bound matches multi-threaded executors like
754 /// tokio, async-std, and smol — the captured per-socket loop is
755 /// already `Send + 'static` because its underlying `TokioSocket` is.
756 /// Embassy and other `no_alloc` / single-core executors typically need
757 /// additional adapter scaffolding (a typed `SpawnToken`, a static
758 /// task arena, hardware-specific waker plumbing) to satisfy
759 /// `Send + 'static`; the example at the top of this docstring has a
760 /// `todo!()` precisely because the adapter is not one-line. A future
761 /// release MAY add a `spawn_local`-style variant gated on a cargo
762 /// feature for those targets.
763 fn spawn(&self, future: impl Future<Output = ()> + Send + 'static);
764}
765
766/// Shared handle to the runtime E2E configuration registry.
767///
768/// Abstracts over `Arc<Mutex<E2ERegistry>>` on `std` and over
769/// critical-section-backed primitives (e.g. `embassy_sync::blocking_mutex`)
770/// on bare metal. All methods take `&self` and provide interior-mutable
771/// access. Implementations are required to be `Clone` so the handle can be
772/// cheaply shared between the `Client` (or `Server`) handle and its inner
773/// event loop.
774pub trait E2ERegistryHandle: Clone + Send + Sync + 'static {
775 /// Register an E2E profile for the given key, replacing any prior entry.
776 ///
777 /// # Errors
778 ///
779 /// Returns [`crate::e2e::E2ERegistryFull`] when the underlying registry has no
780 /// capacity for a new key. Replacing an already-registered key
781 /// always succeeds (the existing slot is reused). Implementations
782 /// that wrap [`crate::e2e::E2ERegistry`] forward this error
783 /// directly; backends with their own storage should pick an
784 /// equivalent overflow contract.
785 fn register(&self, key: E2EKey, profile: E2EProfile)
786 -> Result<(), crate::e2e::E2ERegistryFull>;
787
788 /// Remove the E2E configuration for the given key. No-op if absent.
789 fn unregister(&self, key: &E2EKey);
790
791 /// Returns `true` if a profile is registered for `key`.
792 fn contains_key(&self, key: &E2EKey) -> bool;
793
794 /// Run E2E protect for `key` if configured, writing to `output`.
795 ///
796 /// Returns `None` if no profile is registered for `key`.
797 /// Returns `Some(Err(_))` if protection fails (e.g. buffer too small).
798 /// Returns `Some(Ok(len))` on success; `len` is the number of bytes
799 /// written to `output`.
800 fn protect(
801 &self,
802 key: E2EKey,
803 payload: &[u8],
804 upper_header: [u8; 8],
805 output: &mut [u8],
806 ) -> Option<Result<usize, E2EError>>;
807
808 /// Run E2E check for `key` against `source`'s receive counter state,
809 /// if configured.
810 ///
811 /// Returns `None` if no profile is registered for `key`. Otherwise
812 /// returns the check status and the effective payload slice — the
813 /// E2E header is stripped on success; the original bytes are returned
814 /// on check failure so the caller can decide how to handle it.
815 ///
816 /// `source` keys the receive counter state: on a shared subnet several
817 /// devices send the same `(service, method)` under one instance id, so
818 /// each sender's sequence counter must be tracked independently. See
819 /// [`crate::e2e::E2ERegistry`].
820 ///
821 /// The returned slice borrows from `payload`, not from this handle.
822 fn check<'a>(
823 &self,
824 source: IpAddr,
825 key: E2EKey,
826 payload: &'a [u8],
827 upper_header: [u8; 8],
828 ) -> Option<(E2ECheckStatus, &'a [u8])>;
829
830 /// Drop all per-source receive counter state for `source` (e.g. when
831 /// its reboot is detected via Service Discovery), so its next frame
832 /// starts a fresh sequence. Configuration and transmit state are
833 /// untouched.
834 fn reset_source(&self, source: IpAddr);
835}
836
837/// Shared handle to the local interface address.
838///
839/// Abstracts over `Arc<RwLock<Ipv4Addr>>` on `std`. All clones of a
840/// `Client` share the same handle, so writes from one clone (e.g.
841/// `Client::set_interface`) are visible to all others.
842///
843/// On bare metal, where `Client` is not `Clone`, a trivial implementation
844/// wrapping a `core::cell::Cell<Ipv4Addr>` suffices.
845pub trait InterfaceHandle: Clone + Send + Sync + 'static {
846 /// Returns the current interface address.
847 fn get(&self) -> Ipv4Addr;
848
849 /// Updates the stored interface address.
850 fn set(&self, addr: Ipv4Addr);
851}
852
853/// Shared handle to a single owned-or-borrowed `T`.
854///
855/// One trait covering every "Server holds an `Arc<T>` for sharing
856/// between its run loop and consumer-side tasks" pattern in this
857/// crate. Replaces the three separate handle traits this crate
858/// shipped earlier (`SocketHandle`, `SdStateHandle`,
859/// `EventPublisherHandle`), each of which had the same shape with
860/// a different concrete `T`.
861///
862/// Two impls ship out of the box, both via blanket impls so any
863/// consumer-defined type wrapped in `Arc<T>` or `&'static T`
864/// satisfies the bound automatically:
865///
866/// - `Arc<T>: SharedHandle<T>` on alloc-using builds (`std` or
867/// `bare_metal`-with-alloc). `Arc::clone` increments the
868/// refcount; `get` returns the inner reference.
869/// - `&'static T: SharedHandle<T>` on bare-metal-no-alloc. The
870/// reference is `Copy + Clone + 'static`; the user declares the
871/// underlying `static` storage at boot.
872///
873/// `Clone + 'static` only — neither `Send` nor `Sync` at the
874/// trait level. Method-level `where` clauses on `Server` add
875/// Send bounds at the use sites that need them
876/// (`announcement_loop`'s `+ Send` return type, etc.).
877///
878/// `T: 'static` because both blanket impls require it: an `Arc<T>`
879/// is `'static` only when `T: 'static`, and `&'static T` requires
880/// `T: 'static` by definition.
881///
882/// `?Sized` is intentionally NOT supported — the inline-construction
883/// path ([`WrappableSharedHandle::wrap`]) needs an owned `T`, which
884/// requires `Sized`.
885pub trait SharedHandle<T: 'static>: Clone + 'static {
886 /// Borrow the underlying `T`. Both blanket impls return a
887 /// reference into the underlying storage; consumers should
888 /// not assume more than a fresh borrow's worth of lifetime.
889 fn get(&self) -> &T;
890}
891
892/// Extension of [`SharedHandle`] for handles that can be
893/// constructed inline from an owned `T`.
894///
895/// Required by `Server` constructors that build the underlying
896/// `T` internally (the alloc-using path —
897/// e.g., `Server::new_with_deps` calls `factory.bind(...).await?`
898/// to get an `F::Socket`, then `H::wrap(socket)` to place it
899/// behind the caller's chosen shared-storage). The no-alloc
900/// counterpart constructors (`Server::new_with_handles`) take
901/// pre-built handles directly and don't need this trait.
902///
903/// `&'static T` deliberately does NOT implement this trait —
904/// materializing a `&'static T` from an owned `T` inside a trait
905/// method's body requires an allocator (`Box::leak`) or a
906/// slot-based init pattern (`StaticCell::init`) that the trait
907/// method's signature can't express. No-alloc consumers declare
908/// their `static` storage themselves and pass `&STATIC` into the
909/// no-wrap constructor.
910pub trait WrappableSharedHandle<T: 'static>: SharedHandle<T> {
911 /// Place an owned `T` behind this handle's shared storage.
912 fn wrap(value: T) -> Self;
913}
914
915// `&'static T` is the no-alloc handle. `&'static T: Copy + Clone +
916// 'static` for any `T: 'static`, so the trait bounds are met
917// without further work.
918impl<T: 'static> SharedHandle<T> for &'static T {
919 fn get(&self) -> &T {
920 self
921 }
922}
923
924// `Arc<T>` is the alloc-using handle. `Arc::clone` is the
925// reference-count increment; `wrap` is `Arc::new`. Gated on the
926// internal `_alloc` feature, which is also what gates the
927// crate-root `extern crate alloc` declaration — server,
928// embassy_channels, and std all imply it.
929#[cfg(feature = "_alloc")]
930impl<T: 'static> SharedHandle<T> for alloc::sync::Arc<T> {
931 fn get(&self) -> &T {
932 self
933 }
934}
935
936#[cfg(feature = "_alloc")]
937impl<T: 'static> WrappableSharedHandle<T> for alloc::sync::Arc<T> {
938 fn wrap(value: T) -> Self {
939 alloc::sync::Arc::new(value)
940 }
941}
942
943/// Default `std`-flavoured impls of [`E2ERegistryHandle`] /
944/// [`InterfaceHandle`] / [`SocketHandle`] backed by
945/// `std::sync::{Arc, Mutex, RwLock}`. Pure std — no tokio
946/// dependency — so they live in the executor-agnostic transport
947/// module rather than the tokio backend.
948#[cfg(feature = "std")]
949mod std_handle_impls {
950 use super::{E2ERegistryHandle, InterfaceHandle};
951 use crate::e2e::Error as E2EError;
952 use crate::e2e::{E2ECheckStatus, E2EKey, E2EProfile, E2ERegistry, E2ERegistryFull};
953 use core::net::{IpAddr, Ipv4Addr};
954 use std::sync::{Arc, Mutex, RwLock};
955
956 impl E2ERegistryHandle for Arc<Mutex<E2ERegistry>> {
957 fn register(&self, key: E2EKey, profile: E2EProfile) -> Result<(), E2ERegistryFull> {
958 self.lock()
959 .expect("e2e registry lock poisoned")
960 .register(key, profile)
961 }
962
963 fn unregister(&self, key: &E2EKey) {
964 self.lock()
965 .expect("e2e registry lock poisoned")
966 .unregister(key);
967 }
968
969 fn contains_key(&self, key: &E2EKey) -> bool {
970 self.lock()
971 .expect("e2e registry lock poisoned")
972 .contains_key(key)
973 }
974
975 fn protect(
976 &self,
977 key: E2EKey,
978 payload: &[u8],
979 upper_header: [u8; 8],
980 output: &mut [u8],
981 ) -> Option<Result<usize, E2EError>> {
982 self.lock().expect("e2e registry lock poisoned").protect(
983 key,
984 payload,
985 upper_header,
986 output,
987 )
988 }
989
990 fn check<'a>(
991 &self,
992 source: IpAddr,
993 key: E2EKey,
994 payload: &'a [u8],
995 upper_header: [u8; 8],
996 ) -> Option<(E2ECheckStatus, &'a [u8])> {
997 self.lock().expect("e2e registry lock poisoned").check(
998 source,
999 key,
1000 payload,
1001 upper_header,
1002 )
1003 }
1004
1005 fn reset_source(&self, source: IpAddr) {
1006 self.lock()
1007 .expect("e2e registry lock poisoned")
1008 .reset_source(source);
1009 }
1010 }
1011
1012 impl InterfaceHandle for Arc<RwLock<Ipv4Addr>> {
1013 fn get(&self) -> Ipv4Addr {
1014 *self.read().expect("interface lock poisoned")
1015 }
1016
1017 fn set(&self, addr: Ipv4Addr) {
1018 *self.write().expect("interface lock poisoned") = addr;
1019 }
1020 }
1021}
1022
1023/// Bare-metal no-alloc impls of [`E2ERegistryHandle`] and [`InterfaceHandle`].
1024///
1025/// These types satisfy `Clone + Send + Sync + 'static` without any heap
1026/// allocation. The backing storage lives in a caller-owned `static`; the
1027/// handles are thin `&'static` pointers that are trivially `Copy`.
1028///
1029/// # Production pattern
1030///
1031/// ```ignore
1032/// use core::cell::RefCell;
1033/// use core::sync::atomic::{AtomicU32, Ordering};
1034/// use embassy_sync::blocking_mutex::Mutex;
1035/// use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex;
1036/// use simple_someip::e2e::E2ERegistry;
1037/// use simple_someip::transport::{StaticE2EHandle, AtomicInterfaceHandle};
1038///
1039/// // Initialize once in main() before spawning tasks.
1040/// fn init() -> (StaticE2EHandle, AtomicInterfaceHandle) {
1041/// static IFACE_ADDR: AtomicU32 = AtomicU32::new(0);
1042/// // E2ERegistry::new() is not const so the storage is heap-placed once.
1043/// let registry_storage: &'static _ = Box::leak(Box::new(
1044/// Mutex::<CriticalSectionRawMutex, RefCell<E2ERegistry>>::new(
1045/// RefCell::new(E2ERegistry::new()),
1046/// ),
1047/// ));
1048/// (StaticE2EHandle::new(registry_storage), AtomicInterfaceHandle::new(&IFACE_ADDR))
1049/// }
1050/// ```
1051///
1052/// # No-allocator targets
1053///
1054/// The example above uses `Box::leak` because [`crate::e2e::E2ERegistry::new()`] is not
1055/// currently `const`. On a target with no allocator, swap that for a
1056/// `static`-cell pattern (e.g. `static_cell::StaticCell::init`) once the
1057/// registry constructor becomes `const`-friendly. The handle layer itself
1058/// never allocates — only the one-time storage materialization does.
1059#[cfg(feature = "bare_metal")]
1060pub mod bare_metal_handle_impls {
1061 use super::InterfaceHandle;
1062 use core::net::Ipv4Addr;
1063 use core::sync::atomic::{AtomicU32, Ordering};
1064
1065 // `StaticE2EHandle` wraps `E2ERegistry`, which currently requires
1066 // `feature = "std"` because its backing storage is `HashMap`. Ported
1067 // separately below so the rest of this module — in particular
1068 // `AtomicInterfaceHandle` — is available in pure `no_std` bare-metal
1069 // builds.
1070
1071 /// No-alloc [`InterfaceHandle`] backed by a `&'static AtomicU32`.
1072 ///
1073 /// IPv4 addresses are encoded as big-endian `u32` (`Ipv4Addr::into::<u32>`).
1074 /// All clones are the same thin pointer. Declare the backing storage in a
1075 /// `static`:
1076 ///
1077 /// ```ignore
1078 /// static IFACE_ADDR: AtomicU32 = AtomicU32::new(0);
1079 /// let handle = AtomicInterfaceHandle::new(&IFACE_ADDR);
1080 /// ```
1081 ///
1082 /// # Memory ordering
1083 ///
1084 /// `set` uses [`Ordering::Release`] and `get` uses
1085 /// [`Ordering::Acquire`] so a reader on a weakly-ordered core sees
1086 /// updates promptly. Cheap on x86-TSO (free) and inexpensive on
1087 /// aarch64 (one `dmb ish`).
1088 #[derive(Clone, Copy)]
1089 pub struct AtomicInterfaceHandle(&'static AtomicU32);
1090
1091 impl AtomicInterfaceHandle {
1092 /// Wraps a static reference to the backing atomic.
1093 pub const fn new(addr: &'static AtomicU32) -> Self {
1094 Self(addr)
1095 }
1096 }
1097
1098 // Send + Sync are derived automatically: `&'static AtomicU32` is
1099 // `Send + Sync` because `AtomicU32` is `Sync`.
1100
1101 impl InterfaceHandle for AtomicInterfaceHandle {
1102 fn get(&self) -> Ipv4Addr {
1103 // `Acquire` ordering pairs with the `Release` store below
1104 // so a reader sees the most recent address promptly even
1105 // on weakly-ordered hardware. The cost over `Relaxed` is
1106 // a `dmb ish` on aarch64; on x86-TSO it is free.
1107 Ipv4Addr::from(self.0.load(Ordering::Acquire))
1108 }
1109
1110 fn set(&self, addr: Ipv4Addr) {
1111 self.0.store(u32::from(addr), Ordering::Release);
1112 }
1113 }
1114 // `StaticSocketHandle<T>(&'static T)` was collapsed into a
1115 // direct `impl SharedHandle<T> for &'static T` blanket — the
1116 // wrapper type's only role was carrying the `'static` lifetime,
1117 // which the blanket impl achieves without a wrapper. Consumers
1118 // pass `&SOCKET` directly into Server's no-wrap constructors.
1119}
1120
1121/// `StaticE2EHandle` — no-alloc `E2ERegistryHandle` backed by a
1122/// `&'static` critical-section mutex.
1123///
1124/// Available in pure `no_std` builds: [`crate::e2e::E2ERegistry`] is
1125/// backed by [`heapless::index_map::FnvIndexMap`], so no allocator is
1126/// required.
1127#[cfg(feature = "bare_metal")]
1128pub mod bare_metal_e2e_impl {
1129 use super::E2ERegistryHandle;
1130 use crate::e2e::{
1131 E2ECheckStatus, E2EKey, E2EProfile, E2ERegistry, E2ERegistryFull, Error as E2EError,
1132 };
1133 use core::cell::RefCell;
1134 use core::net::IpAddr;
1135 use embassy_sync::blocking_mutex::Mutex;
1136 use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex;
1137
1138 /// Convenience type alias for the embassy-sync critical-section mutex
1139 /// backing [`StaticE2EHandle`].
1140 pub type StaticE2EStorage = Mutex<CriticalSectionRawMutex, RefCell<E2ERegistry>>;
1141
1142 /// No-alloc [`E2ERegistryHandle`] backed by a `&'static` critical-section
1143 /// mutex.
1144 ///
1145 /// All clones are the same thin pointer. Construct via [`StaticE2EHandle::new`]
1146 /// and supply a `&'static StaticE2EStorage` (typically obtained via
1147 /// `Box::leak` during system init, since [`E2ERegistry::new`] is not const).
1148 #[derive(Clone, Copy)]
1149 pub struct StaticE2EHandle(&'static StaticE2EStorage);
1150
1151 impl StaticE2EHandle {
1152 /// Wraps a static reference to the backing mutex.
1153 pub const fn new(storage: &'static StaticE2EStorage) -> Self {
1154 Self(storage)
1155 }
1156 }
1157
1158 impl E2ERegistryHandle for StaticE2EHandle {
1159 fn register(&self, key: E2EKey, profile: E2EProfile) -> Result<(), E2ERegistryFull> {
1160 self.0.lock(|cell| cell.borrow_mut().register(key, profile))
1161 }
1162
1163 fn unregister(&self, key: &E2EKey) {
1164 self.0.lock(|cell| cell.borrow_mut().unregister(key));
1165 }
1166
1167 fn contains_key(&self, key: &E2EKey) -> bool {
1168 self.0.lock(|cell| cell.borrow().contains_key(key))
1169 }
1170
1171 fn protect(
1172 &self,
1173 key: E2EKey,
1174 payload: &[u8],
1175 upper_header: [u8; 8],
1176 output: &mut [u8],
1177 ) -> Option<Result<usize, E2EError>> {
1178 self.0.lock(|cell| {
1179 cell.borrow_mut()
1180 .protect(key, payload, upper_header, output)
1181 })
1182 }
1183
1184 fn check<'a>(
1185 &self,
1186 source: IpAddr,
1187 key: E2EKey,
1188 payload: &'a [u8],
1189 upper_header: [u8; 8],
1190 ) -> Option<(E2ECheckStatus, &'a [u8])> {
1191 self.0
1192 .lock(|cell| cell.borrow_mut().check(source, key, payload, upper_header))
1193 }
1194
1195 fn reset_source(&self, source: IpAddr) {
1196 self.0.lock(|cell| cell.borrow_mut().reset_source(source));
1197 }
1198 }
1199}
1200
1201#[cfg(feature = "bare_metal")]
1202pub use bare_metal_handle_impls::AtomicInterfaceHandle;
1203
1204#[cfg(feature = "bare_metal")]
1205pub use bare_metal_e2e_impl::{StaticE2EHandle, StaticE2EStorage};
1206
1207// ── Channel-handle abstraction ────────────────────────────────────────────
1208//
1209// `ChannelFactory` and its associated sender / receiver traits abstract over
1210// the channel primitive used by the client. `TokioChannels` (in
1211// `tokio_transport`) is the default for `std + tokio` builds;
1212// `EmbassySyncChannels` (in `crate::embassy_channels`, gated behind
1213// `embassy_channels` feature) is a heap-backed alternative for no-tokio builds;
1214// `static_channels` (gated behind `bare_metal`) is the no-alloc alternative.
1215
1216/// Returned by [`OneshotRecv::recv`] when the sender was dropped before
1217/// sending a value.
1218#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1219pub struct OneshotCancelled;
1220
1221impl core::fmt::Display for OneshotCancelled {
1222 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
1223 f.write_str("oneshot sender dropped before sending a value")
1224 }
1225}
1226
1227/// The send half of a oneshot channel. Consuming: a value can be sent exactly
1228/// once.
1229pub trait OneshotSend<T: Send + 'static>: Send + 'static {
1230 /// Send `value` through the channel.
1231 ///
1232 /// # Errors
1233 ///
1234 /// Returns `Err(value)` if the receiver was already dropped.
1235 fn send(self, value: T) -> Result<(), T>;
1236}
1237
1238/// The receive half of a oneshot channel. Resolves once the sender delivers a
1239/// value, or returns [`OneshotCancelled`] if the sender is dropped first.
1240pub trait OneshotRecv<T: Send + 'static>: Send + 'static {
1241 /// Await the value. Consumes self — a oneshot receiver can only be awaited
1242 /// once.
1243 fn recv(self) -> impl core::future::Future<Output = Result<T, OneshotCancelled>> + Send;
1244}
1245
1246/// The send half of a bounded MPSC channel.
1247///
1248/// Implementations must be [`Clone`] so that multiple producers can share the
1249/// same channel (e.g. the `Client` handle is `Clone` and every clone must be
1250/// able to send control messages to `Inner`).
1251pub trait MpscSend<T: Send + 'static>: Clone + Send + 'static {
1252 /// Send `value`, waiting if the channel is full. Returns `Err(())` if the
1253 /// receiver was dropped.
1254 fn send(&self, value: T) -> impl core::future::Future<Output = Result<(), ()>> + Send + '_;
1255}
1256
1257/// The receive half of a bounded MPSC channel.
1258pub trait MpscRecv<T: Send + 'static>: Send + 'static {
1259 /// Receive the next value, waiting if the channel is empty. Returns `None`
1260 /// if all senders were dropped and the channel is empty.
1261 fn recv(&mut self) -> impl core::future::Future<Output = Option<T>> + Send + '_;
1262
1263 /// Poll the channel without blocking. Used by `receive_any_unicast` to
1264 /// multiplex across several socket channels in a single `poll_fn` pass.
1265 fn poll_recv(&mut self, cx: &mut core::task::Context<'_>) -> core::task::Poll<Option<T>>;
1266}
1267
1268/// The send half of an unbounded MPSC channel.
1269///
1270/// Unlike [`MpscSend`], sending never blocks — the implementation must buffer
1271/// arbitrarily many values (or, for embassy-sync, use a large finite capacity
1272/// that is treated as effectively unbounded).
1273pub trait UnboundedSend<T: Send + 'static>: Clone + Send + 'static {
1274 /// Send `value` without blocking.
1275 ///
1276 /// # Errors
1277 ///
1278 /// Returns `Err(value)` if the receiver was dropped.
1279 fn send_now(&self, value: T) -> Result<(), T>;
1280}
1281
1282/// The receive half of an unbounded MPSC channel.
1283pub trait UnboundedRecv<T: Send + 'static>: Send + 'static {
1284 /// Receive the next value, waiting if the channel is empty. Returns `None`
1285 /// if all senders were dropped and the channel is empty.
1286 fn recv(&mut self) -> impl core::future::Future<Output = Option<T>> + Send + '_;
1287}
1288
1289/// A zero-sized factory that creates channel pairs used by the client's
1290/// internal transport.
1291///
1292/// Abstracting over both `tokio::sync::mpsc` / `oneshot` (std path) and
1293/// `embassy-sync::channel::Channel` (bare-metal path) behind a single trait
1294/// lets `Client` / `Inner` / `SocketManager` compile without a tokio
1295/// dependency when `bare_metal` is active and `tokio` is not.
1296///
1297/// The three channel families:
1298/// - **oneshot** — single-shot rendezvous, capacity 1. Used for command
1299/// completion callbacks inside `crate::client::ControlMessage`.
1300/// - **bounded** — finite-capacity MPSC queue. Used for the control channel
1301/// and per-socket send / receive queues.
1302/// - **unbounded** — notionally unbounded MPSC queue (embassy-sync
1303/// implementations use a large-capacity channel). Used for the
1304/// `ClientUpdate` stream from `Inner` to `Client`.
1305///
1306/// # Per-`T` opt-in via the `*Pooled<Self>` traits
1307///
1308/// The three constructor methods are generic over the channeled type
1309/// `T`, but a heap-free static-pool implementation needs to map each `T`
1310/// to a pre-declared `static` storage area. To make that mapping
1311/// type-safe — and to surface "you forgot to declare a pool for this
1312/// type" as a compile error rather than a runtime panic — each method
1313/// requires the channeled type to implement the corresponding
1314/// `*Pooled<Self>` trait and delegates the actual construction to it:
1315///
1316/// ```ignore
1317/// fn oneshot<T>() -> (...) where T: OneshotPooled<Self> { T::oneshot_pair() }
1318/// ```
1319///
1320/// Backends that have a single shared allocator (Tokio, embassy-sync)
1321/// publish a blanket `impl<T: Send + 'static> OneshotPooled<Self> for T`
1322/// (and its bounded / unbounded peers), so existing user code does not
1323/// notice the change. A static-pool backend instead publishes per-`T`
1324/// impls (typically generated by a `define_static_channels!` macro) that wire
1325/// each `T` to its declared pool. Calling `oneshot::<NotDeclared>()`
1326/// against such a backend fails at the call site with
1327/// `OneshotPooled<MyChannels> is not implemented for NotDeclared`.
1328pub trait ChannelFactory: Clone + Send + Sync + 'static {
1329 /// Oneshot sender type.
1330 type OneshotSender<T: Send + 'static>: OneshotSend<T>;
1331 /// Oneshot receiver type.
1332 type OneshotReceiver<T: Send + 'static>: OneshotRecv<T>;
1333 /// Create a oneshot channel pair.
1334 ///
1335 /// Default body delegates to [`OneshotPooled::oneshot_pair`]; impls
1336 /// rarely need to override this, they just publish the appropriate
1337 /// `OneshotPooled<Self>` impls for the types they support.
1338 #[must_use]
1339 fn oneshot<T>() -> (Self::OneshotSender<T>, Self::OneshotReceiver<T>)
1340 where
1341 T: OneshotPooled<Self>,
1342 {
1343 T::oneshot_pair()
1344 }
1345
1346 /// Bounded-channel sender type. The `const N: usize` parameter is
1347 /// the channel capacity; it must match the `N` passed to
1348 /// [`Self::bounded`]. Backends that store the capacity at
1349 /// construction time (`tokio::sync::mpsc`) ignore it for storage
1350 /// purposes; backends that bake it into the type (`embassy-sync`)
1351 /// use it directly.
1352 type BoundedSender<T: Send + 'static, const N: usize>: MpscSend<T>;
1353 /// Bounded-channel receiver type. See [`Self::BoundedSender`].
1354 type BoundedReceiver<T: Send + 'static, const N: usize>: MpscRecv<T>;
1355 /// Create a bounded channel with capacity `N`.
1356 ///
1357 /// Default body delegates to [`BoundedPooled::bounded_pair`].
1358 #[must_use]
1359 fn bounded<T, const N: usize>() -> (Self::BoundedSender<T, N>, Self::BoundedReceiver<T, N>)
1360 where
1361 T: BoundedPooled<Self, N>,
1362 {
1363 T::bounded_pair()
1364 }
1365
1366 /// Unbounded-channel sender type.
1367 type UnboundedSender<T: Send + 'static>: UnboundedSend<T>;
1368 /// Unbounded-channel receiver type.
1369 type UnboundedReceiver<T: Send + 'static>: UnboundedRecv<T>;
1370 /// Create an unbounded channel.
1371 ///
1372 /// Default body delegates to [`UnboundedPooled::unbounded_pair`].
1373 #[must_use]
1374 fn unbounded<T>() -> (Self::UnboundedSender<T>, Self::UnboundedReceiver<T>)
1375 where
1376 T: UnboundedPooled<Self>,
1377 {
1378 T::unbounded_pair()
1379 }
1380}
1381
1382/// Per-`T` opt-in for [`ChannelFactory::oneshot`].
1383///
1384/// Implementors declare "this `T` may be channeled through `C`'s oneshot
1385/// family" and provide the construction. Backends with a single shared
1386/// allocator (Tokio, embassy-sync) publish a blanket
1387/// `impl<T: Send + 'static> OneshotPooled<Self> for T`. Static-pool
1388/// backends publish per-`T` impls — typically via a macro — each
1389/// pointing at a declared `static` pool slot.
1390///
1391/// The trait is parameterized over the channel factory `C` so a single
1392/// `T` may participate in multiple backends without conflicting impls.
1393pub trait OneshotPooled<C: ChannelFactory>: Send + Sized + 'static {
1394 /// Build a `(sender, receiver)` pair through `C`'s oneshot family.
1395 fn oneshot_pair() -> (C::OneshotSender<Self>, C::OneshotReceiver<Self>);
1396}
1397
1398/// Per-`(T, N)` opt-in for [`ChannelFactory::bounded`]. See
1399/// [`OneshotPooled`] for the design rationale; this is the bounded peer
1400/// with capacity baked into the type.
1401pub trait BoundedPooled<C: ChannelFactory, const N: usize>: Send + Sized + 'static {
1402 /// Build a `(sender, receiver)` pair through `C`'s bounded family
1403 /// with capacity `N`.
1404 fn bounded_pair() -> (C::BoundedSender<Self, N>, C::BoundedReceiver<Self, N>);
1405}
1406
1407/// Per-`T` opt-in for [`ChannelFactory::unbounded`]. See
1408/// [`OneshotPooled`] for the design rationale.
1409pub trait UnboundedPooled<C: ChannelFactory>: Send + Sized + 'static {
1410 /// Build a `(sender, receiver)` pair through `C`'s unbounded family.
1411 fn unbounded_pair() -> (C::UnboundedSender<Self>, C::UnboundedReceiver<Self>);
1412}
1413
1414// ── BufferProvider ────────────────────────────────────────────────────────
1415
1416use crate::buffer_pool::{BufferLease, BufferPool};
1417
1418/// Source of `&'static mut [u8]` receive/scratch buffers for the client's
1419/// socket loops. Mirrors [`ChannelFactory`]'s role for channels: the
1420/// bare-metal path is backed by a consumer-declared `static BufferPool`;
1421/// the tokio path is heap-backed and provisioned internally.
1422pub trait BufferProvider: Clone + Send + Sync + 'static {
1423 /// Claim one buffer, or `None` when the pool is exhausted.
1424 fn claim(&self) -> Option<BufferLease>;
1425}
1426
1427/// `BufferProvider` backed by a `'static` [`BufferPool`] (bare-metal path).
1428#[derive(Clone, Copy, Debug)]
1429pub struct StaticBufferProvider<const SLOTS: usize, const LEN: usize>(
1430 pub &'static BufferPool<SLOTS, LEN>,
1431);
1432
1433impl<const SLOTS: usize, const LEN: usize> BufferProvider for StaticBufferProvider<SLOTS, LEN> {
1434 fn claim(&self) -> Option<BufferLease> {
1435 self.0.claim()
1436 }
1437}
1438
1439/// Zero-behavior implementations of the client- and server-side
1440/// dependency traits. Two uses: (1) compile-time proof the trait
1441/// signatures are implementable without async machinery, (2)
1442/// **layout probing** — `tools/size_probe` instantiates `Client`
1443/// with these on `thumbv7em-none-eabihf` so `-Zprint-type-sizes`
1444/// reports the real on-target future layouts (see
1445/// `docs/simple_someip/plans/2026-06-09-phase22-125-memory-reduction-design.md`).
1446///
1447/// NOT for production use: sockets error, and the spawner panics
1448/// outright — probe code is compiled, never executed, and a loud
1449/// failure beats the silent deadlock a future-dropping spawner
1450/// would cause in a driven `Client`.
1451#[cfg(any(test, feature = "bare_metal"))]
1452pub mod probe {
1453 use super::{
1454 E2ERegistryHandle, InterfaceHandle, ReceivedDatagram, SocketOptions, Spawner, Timer,
1455 TransportError, TransportFactory, TransportSocket,
1456 };
1457 use crate::e2e::{E2ECheckStatus, E2EKey, E2EProfile, Error as E2EError};
1458 use core::future::Future;
1459 use core::net::{IpAddr, Ipv4Addr, SocketAddrV4};
1460 use core::time::Duration;
1461
1462 /// Socket whose I/O futures resolve immediately with
1463 /// `TransportError::Unsupported`.
1464 pub struct NullSocket {
1465 addr: SocketAddrV4,
1466 }
1467
1468 impl NullSocket {
1469 #[must_use]
1470 pub const fn new(addr: SocketAddrV4) -> Self {
1471 Self { addr }
1472 }
1473 }
1474
1475 impl TransportSocket for NullSocket {
1476 type SendFuture<'a> = core::future::Ready<Result<(), TransportError>>;
1477 type RecvFuture<'a> = core::future::Ready<Result<ReceivedDatagram, TransportError>>;
1478
1479 fn send_to<'a>(&'a self, _buf: &'a [u8], _target: SocketAddrV4) -> Self::SendFuture<'a> {
1480 core::future::ready(Err(TransportError::Unsupported))
1481 }
1482
1483 fn recv_from<'a>(&'a self, _buf: &'a mut [u8]) -> Self::RecvFuture<'a> {
1484 core::future::ready(Err(TransportError::Unsupported))
1485 }
1486
1487 fn local_addr(&self) -> Result<SocketAddrV4, TransportError> {
1488 Ok(self.addr)
1489 }
1490
1491 fn join_multicast_v4(
1492 &self,
1493 _group: Ipv4Addr,
1494 _iface: Ipv4Addr,
1495 ) -> Result<(), TransportError> {
1496 Err(TransportError::Unsupported)
1497 }
1498
1499 fn leave_multicast_v4(
1500 &self,
1501 _group: Ipv4Addr,
1502 _iface: Ipv4Addr,
1503 ) -> Result<(), TransportError> {
1504 Err(TransportError::Unsupported)
1505 }
1506 }
1507
1508 /// Factory that "binds" a [`NullSocket`] at the requested addr.
1509 pub struct NullFactory;
1510
1511 impl TransportFactory for NullFactory {
1512 type Socket = NullSocket;
1513 type BindFuture<'a> = core::future::Ready<Result<Self::Socket, TransportError>>;
1514
1515 fn bind<'a>(
1516 &'a self,
1517 addr: SocketAddrV4,
1518 _options: &'a SocketOptions,
1519 ) -> Self::BindFuture<'a> {
1520 core::future::ready(Ok(NullSocket::new(addr)))
1521 }
1522 }
1523
1524 /// Timer whose sleeps resolve immediately.
1525 pub struct NullTimer;
1526
1527 impl Timer for NullTimer {
1528 type SleepFuture<'a> = core::future::Ready<()>;
1529
1530 fn sleep(&self, _duration: Duration) -> Self::SleepFuture<'_> {
1531 core::future::ready(())
1532 }
1533 }
1534
1535 /// E2E registry handle that registers nothing and checks nothing.
1536 #[derive(Clone)]
1537 pub struct NullE2ERegistry;
1538
1539 impl E2ERegistryHandle for NullE2ERegistry {
1540 fn register(
1541 &self,
1542 _key: E2EKey,
1543 _profile: E2EProfile,
1544 ) -> Result<(), crate::e2e::E2ERegistryFull> {
1545 Ok(())
1546 }
1547 fn unregister(&self, _key: &E2EKey) {}
1548 fn contains_key(&self, _key: &E2EKey) -> bool {
1549 false
1550 }
1551 fn protect(
1552 &self,
1553 _key: E2EKey,
1554 _payload: &[u8],
1555 _upper_header: [u8; 8],
1556 _output: &mut [u8],
1557 ) -> Option<Result<usize, E2EError>> {
1558 None
1559 }
1560 fn check<'a>(
1561 &self,
1562 _source: IpAddr,
1563 _key: E2EKey,
1564 _payload: &'a [u8],
1565 _upper_header: [u8; 8],
1566 ) -> Option<(E2ECheckStatus, &'a [u8])> {
1567 None
1568 }
1569 fn reset_source(&self, _source: IpAddr) {}
1570 }
1571
1572 /// Interface handle pinned to a fixed address.
1573 #[derive(Clone)]
1574 pub struct NullInterface(pub Ipv4Addr);
1575
1576 impl InterfaceHandle for NullInterface {
1577 fn get(&self) -> Ipv4Addr {
1578 self.0
1579 }
1580 fn set(&self, _addr: Ipv4Addr) {}
1581 }
1582
1583 /// Spawner that PANICS if asked to spawn. Probe code only
1584 /// constructs futures, never drives them — failing loudly beats
1585 /// violating [`Spawner`]'s poll-to-completion contract by
1586 /// silently dropping the future.
1587 pub struct NullSpawner;
1588
1589 impl Spawner for NullSpawner {
1590 fn spawn(&self, _future: impl Future<Output = ()> + Send + 'static) {
1591 panic!("NullSpawner is layout-probe-only; it never polls");
1592 }
1593 }
1594}
1595
1596#[cfg(test)]
1597mod tests {
1598 //! The traits are pure interfaces — these tests only verify that
1599 //! trivial mock implementations compile and that defaults behave as
1600 //! documented.
1601
1602 use super::probe::{NullE2ERegistry, NullFactory, NullInterface, NullSocket, NullTimer};
1603 use super::*;
1604
1605 /// `IoErrorKind::is_transient_recv` must classify the well-known
1606 /// transient kinds as `true` (so they do not count toward
1607 /// `MAX_CONSECUTIVE_RECV_ERRORS` in the per-socket loop) and
1608 /// everything else — including the catch-all `Other` — as `false`.
1609 /// Regression for H10: an inbound ICMP storm
1610 /// (`ConnectionRefused`) was wrongly counted as fatal and tore
1611 /// down healthy sockets after 16 transient blips.
1612 #[test]
1613 fn io_error_kind_transient_classification() {
1614 // Transient kinds — must NOT count toward fatal-error cap.
1615 assert!(IoErrorKind::ConnectionRefused.is_transient_recv());
1616 assert!(IoErrorKind::NetworkUnreachable.is_transient_recv());
1617 assert!(IoErrorKind::WouldBlock.is_transient_recv());
1618 assert!(IoErrorKind::Interrupted.is_transient_recv());
1619 assert!(IoErrorKind::TimedOut.is_transient_recv());
1620
1621 // Fatal-class kinds — DO count toward the cap.
1622 assert!(!IoErrorKind::PermissionDenied.is_transient_recv());
1623 assert!(!IoErrorKind::Other.is_transient_recv());
1624 }
1625
1626 /// Drive a Future to completion on the test thread, assuming it never
1627 /// yields (as with [`core::future::ready`] and its sync-in-disguise
1628 /// peers). Panics if the future returns `Poll::Pending`.
1629 fn block_on_ready<F: Future>(fut: F) -> F::Output {
1630 use core::pin::pin;
1631 use core::task::{Context, Poll, Waker};
1632 let waker = Waker::noop();
1633 let mut cx = Context::from_waker(waker);
1634 let mut fut = pin!(fut);
1635 match fut.as_mut().poll(&mut cx) {
1636 Poll::Ready(v) => v,
1637 Poll::Pending => panic!("future yielded Pending; use a real executor"),
1638 }
1639 }
1640
1641 #[test]
1642 fn socket_options_default_is_plain_unicast() {
1643 let opts = SocketOptions::default();
1644 assert!(!opts.reuse_address);
1645 assert!(!opts.reuse_port);
1646 assert!(opts.multicast_if_v4.is_none());
1647 assert!(opts.multicast_loop_v4.is_none());
1648 }
1649
1650 #[test]
1651 fn socket_options_new_matches_default() {
1652 let a = SocketOptions::new();
1653 let b = SocketOptions::default();
1654 assert_eq!(a.reuse_address, b.reuse_address);
1655 assert_eq!(a.reuse_port, b.reuse_port);
1656 assert_eq!(a.multicast_if_v4, b.multicast_if_v4);
1657 assert_eq!(a.multicast_loop_v4, b.multicast_loop_v4);
1658 }
1659
1660 #[test]
1661 fn null_factory_bind_resolves_with_addr() {
1662 let factory = NullFactory;
1663 let addr = SocketAddrV4::new(Ipv4Addr::LOCALHOST, 0);
1664 let options = SocketOptions::default();
1665 let sock = block_on_ready(factory.bind(addr, &options)).expect("bind");
1666 assert_eq!(sock.local_addr().unwrap(), addr);
1667 }
1668
1669 #[test]
1670 fn max_datagram_size_default_is_udp_buffer_size() {
1671 let sock = NullSocket::new(SocketAddrV4::new(Ipv4Addr::LOCALHOST, 0));
1672 assert_eq!(sock.max_datagram_size(), crate::UDP_BUFFER_SIZE);
1673 }
1674
1675 #[test]
1676 fn null_timer_sleep_resolves_immediately() {
1677 let timer = NullTimer;
1678 block_on_ready(timer.sleep(Duration::from_secs(1)));
1679 }
1680
1681 #[test]
1682 fn received_datagram_construct_and_field_access() {
1683 let d = ReceivedDatagram {
1684 bytes_received: 42,
1685 source: SocketAddrV4::new(Ipv4Addr::LOCALHOST, 9999),
1686 truncated: false,
1687 };
1688 assert_eq!(d.bytes_received, 42);
1689 assert!(!d.truncated);
1690 }
1691
1692 #[test]
1693 fn io_error_kind_variants_are_distinct() {
1694 // Compile-time check that all variants are constructible and
1695 // distinguishable — Eq is derived, so assert some inequalities.
1696 assert_ne!(IoErrorKind::TimedOut, IoErrorKind::Interrupted);
1697 assert_ne!(IoErrorKind::PermissionDenied, IoErrorKind::Other);
1698 assert_ne!(
1699 IoErrorKind::ConnectionRefused,
1700 IoErrorKind::NetworkUnreachable
1701 );
1702 }
1703
1704 #[test]
1705 fn transport_error_io_wraps_kind() {
1706 let e = TransportError::Io(IoErrorKind::TimedOut);
1707 assert_eq!(e, TransportError::Io(IoErrorKind::TimedOut));
1708 assert_ne!(e, TransportError::AddressInUse);
1709 }
1710
1711 #[test]
1712 fn null_e2e_registry_compiles() {
1713 let r = NullE2ERegistry;
1714 let key = E2EKey::new(0, 0);
1715 r.register(
1716 key,
1717 crate::e2e::E2EProfile::Profile4(crate::e2e::Profile4Config::new(0, 8)),
1718 )
1719 .expect("NullE2ERegistry::register is infallible");
1720 assert!(!r.contains_key(&key));
1721 assert!(
1722 r.check(Ipv4Addr::LOCALHOST.into(), key, b"hello", [0; 8])
1723 .is_none()
1724 );
1725 r.reset_source(Ipv4Addr::LOCALHOST.into()); // no-op in null impl
1726 }
1727
1728 #[test]
1729 fn null_interface_get_set() {
1730 let h = NullInterface(Ipv4Addr::LOCALHOST);
1731 assert_eq!(h.get(), Ipv4Addr::LOCALHOST);
1732 h.set(Ipv4Addr::UNSPECIFIED); // no-op in null impl
1733 assert_eq!(h.get(), Ipv4Addr::LOCALHOST); // unchanged
1734 }
1735}