Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 1 addition & 8 deletions simple-someip-embassy-net/src/factory.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ use embassy_net::Stack;
use embassy_net::driver::Driver;
use embassy_net::udp::{PacketMetadata, UdpSocket};

use simple_someip::transport::{IoErrorKind, SocketOptions, TransportError, TransportFactory};
use simple_someip::transport::{SocketOptions, TransportError, TransportFactory};

use crate::socket::{EmbassyNetSocket, SlotReclaim};

Expand Down Expand Up @@ -316,10 +316,3 @@ where
}
}
}

/// Internal: unused-import guard so `IoErrorKind` stays threaded
/// through for use in the upcoming 19c socket-level error mapping.
#[allow(dead_code)]
fn _phantom_io_error_kind_use() -> IoErrorKind {
IoErrorKind::Other
}
190 changes: 147 additions & 43 deletions simple-someip-embassy-net/src/socket.rs
Original file line number Diff line number Diff line change
@@ -1,17 +1,18 @@
//! `TransportSocket` impl wrapping `embassy_net::udp::UdpSocket`.
//!
//! Phase 19b: the type is constructed by [`crate::factory::EmbassyNetFactory::bind`]
//! and carries the slot-reclamation hook so its `Drop` impl returns
//! the buffer pool slot to the free list. The `TransportSocket`
//! trait impl (named `send_to` / `recv_from` futures driving
//! `poll_send_to` / `poll_recv_from`, plus the multicast / local-addr
//! shims) lands in 19c.

use core::future::Ready;
//! Phase 19c lands the real send/recv I/O — named future structs
//! drive `embassy_net`'s `poll_send_to` / `poll_recv_from` directly,
//! so each datagram costs zero heap allocations on the hot path.

use core::future::Future;
use core::net::{Ipv4Addr, SocketAddrV4};
use core::pin::Pin;
use core::task::{Context, Poll};

use embassy_net::udp::{RecvError, SendError, UdpSocket};
use embassy_net::{IpAddress, IpEndpoint};

use embassy_net::udp::UdpSocket;
use simple_someip::transport::{ReceivedDatagram, TransportError, TransportSocket};
use simple_someip::transport::{IoErrorKind, ReceivedDatagram, TransportError, TransportSocket};

/// Hook implemented by [`crate::SocketPool`] for releasing a
/// claimed slot back to the free list when an
Expand Down Expand Up @@ -62,19 +63,6 @@ impl EmbassyNetSocket {
reclaim,
}
}

/// Borrow the inner `UdpSocket` for the upcoming
/// `TransportSocket` send/recv impl in 19c.
#[allow(dead_code)] // wired in 19c
pub(crate) fn inner(&self) -> &UdpSocket<'static> {
&self.inner
}

/// Local address recorded at bind time.
#[allow(dead_code)] // wired in 19c
pub(crate) fn local(&self) -> SocketAddrV4 {
self.local
}
}

impl Drop for EmbassyNetSocket {
Expand All @@ -87,32 +75,112 @@ impl Drop for EmbassyNetSocket {
}
}

// ── TransportSocket impl (stub) ──────────────────────────────────────
// ── Named send / recv futures ────────────────────────────────────────
//
// Phase 19b ships a minimum-viable impl so the
// `EmbassyNetFactory::TransportFactory` impl typechecks (the trait
// requires `Self::Socket: TransportSocket`). Every method here
// returns `Err(TransportError::Unsupported)`. Phase 19c replaces
// them with real `poll_send_to` / `poll_recv_from`-driven named
// futures.
//
// Until 19c, attempting to use a bound `EmbassyNetSocket` for actual
// I/O will fail at runtime with `Unsupported`. This is intentional:
// the 19b commit verifies the factory + pool + Drop wiring without
// requiring the full I/O bring-up, which is its own scoped work.
// Hand-rolled `Future` types over embassy-net's `poll_send_to` /
// `poll_recv_from` rather than wrapping the async `send_to` /
// `recv_from` in `Box::pin(async move { ... })`. The named-struct
// shape is what makes the adapter zero-alloc on the hot path —
// every datagram incurs no allocator traffic.

/// Future returned by [`EmbassyNetSocket::send_to`]. Drives
/// `embassy_net::udp::UdpSocket::poll_send_to` directly.
pub struct EmbassyNetSendFut<'a> {
socket: &'a UdpSocket<'static>,
buf: &'a [u8],
target: IpEndpoint,
}

impl Future for EmbassyNetSendFut<'_> {
type Output = Result<(), TransportError>;

fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
// EmbassyNetSendFut has no self-referential fields; the
// underlying `UdpSocket::poll_send_to` only borrows
// through `&self`, and `me.buf` is a fresh reborrow every
// poll. Safe to project to `&mut Self`.
let me = self.get_mut();
match me.socket.poll_send_to(me.buf, me.target, cx) {
Poll::Pending => Poll::Pending,
Poll::Ready(Ok(())) => Poll::Ready(Ok(())),
Poll::Ready(Err(SendError::NoRoute)) => {
Poll::Ready(Err(TransportError::Io(IoErrorKind::NetworkUnreachable)))
}
Poll::Ready(Err(SendError::SocketNotBound)) => {
// Programming error — we always bind before
// returning the socket from `EmbassyNetFactory::bind`.
// Surface as `Other` so it shows up in operator
// logs distinctly from a routing failure.
Poll::Ready(Err(TransportError::Io(IoErrorKind::Other)))
}
}
}
}

/// Future returned by [`EmbassyNetSocket::recv_from`]. Drives
/// `embassy_net::udp::UdpSocket::poll_recv_from` directly.
pub struct EmbassyNetRecvFut<'a> {
socket: &'a UdpSocket<'static>,
buf: &'a mut [u8],
}

impl Future for EmbassyNetRecvFut<'_> {
type Output = Result<ReceivedDatagram, TransportError>;

fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
let me = self.get_mut();
match me.socket.poll_recv_from(me.buf, cx) {
Poll::Pending => Poll::Pending,
Poll::Ready(Ok((n, endpoint))) => match endpoint_to_socket_addr_v4(endpoint) {
Some(source) => Poll::Ready(Ok(ReceivedDatagram {
bytes_received: n,
source,
// embassy-net's `recv_slice` returns
// `Truncated` (mapped to `Err` below) when the
// datagram doesn't fit; on the success path it
// delivered the whole thing.
Comment on lines +138 to +141

Copilot AI Apr 29, 2026

Copy link

Choose a reason for hiding this comment

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

The inline comment refers to embassy-net's recv_slice, but this implementation is driving UdpSocket::poll_recv_from. If truncation semantics differ between the APIs (or if recv_slice no longer exists in embassy-net 0.4), this is confusing/misleading. Consider updating the comment to reference the specific API used here (poll_recv_from) and how it signals truncation.

Suggested change
// embassy-net's `recv_slice` returns
// `Truncated` (mapped to `Err` below) when the
// datagram doesn't fit; on the success path it
// delivered the whole thing.
// `poll_recv_from` reports an oversized datagram
// as `RecvError::Truncated` (handled below), so
// reaching this success path means the datagram
// fit in `buf` and was not truncated.

Copilot uses AI. Check for mistakes.
truncated: false,
})),
None => {
// IPv6 source on a v4-bound SOME/IP socket is a
// misconfiguration upstream — surface as
// `Unsupported` for the same reason
// `tokio_transport::recv_from` does.
Poll::Ready(Err(TransportError::Unsupported))
}
},
Poll::Ready(Err(RecvError::Truncated)) => {
// Caller's buffer was smaller than the datagram.
// simple-someip uses `UDP_BUFFER_SIZE = 1500` for
// its recv buffers, which exceeds typical UDP
// payloads — hitting this branch indicates either
// an undersized SocketPool RX_BUF or an
// unexpectedly large incoming datagram. Either way
// the application has a sizing problem worth
// logging through the operator pipeline.
Poll::Ready(Err(TransportError::Io(IoErrorKind::Other)))
}
}
Comment on lines +127 to +163

Copilot AI Apr 29, 2026

Copy link

Choose a reason for hiding this comment

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

RecvError::Truncated is currently mapped to Err(TransportError::Io(IoErrorKind::Other)). That violates the TransportSocket::recv_from contract (truncation should be reported via ReceivedDatagram { truncated: true, .. }, not as an error) and will cause existing socket loops to treat oversized datagrams as fatal-class recv errors (incrementing the consecutive-error counter and potentially tearing down the loop). Please map embassy-net truncation into a successful ReceivedDatagram with truncated: true (and a sensible bytes_received), using whatever embassy-net API/state is needed to also retain the source endpoint.

Suggested change
impl Future for EmbassyNetRecvFut<'_> {
type Output = Result<ReceivedDatagram, TransportError>;
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
let me = self.get_mut();
match me.socket.poll_recv_from(me.buf, cx) {
Poll::Pending => Poll::Pending,
Poll::Ready(Ok((n, endpoint))) => match endpoint_to_socket_addr_v4(endpoint) {
Some(source) => Poll::Ready(Ok(ReceivedDatagram {
bytes_received: n,
source,
// embassy-net's `recv_slice` returns
// `Truncated` (mapped to `Err` below) when the
// datagram doesn't fit; on the success path it
// delivered the whole thing.
truncated: false,
})),
None => {
// IPv6 source on a v4-bound SOME/IP socket is a
// misconfiguration upstream — surface as
// `Unsupported` for the same reason
// `tokio_transport::recv_from` does.
Poll::Ready(Err(TransportError::Unsupported))
}
},
Poll::Ready(Err(RecvError::Truncated)) => {
// Caller's buffer was smaller than the datagram.
// simple-someip uses `UDP_BUFFER_SIZE = 1500` for
// its recv buffers, which exceeds typical UDP
// payloads — hitting this branch indicates either
// an undersized SocketPool RX_BUF or an
// unexpectedly large incoming datagram. Either way
// the application has a sizing problem worth
// logging through the operator pipeline.
Poll::Ready(Err(TransportError::Io(IoErrorKind::Other)))
}
}
fn received_datagram_from_packet(
buf: &mut [u8],
packet: &[u8],
endpoint: IpEndpoint,
) -> Result<ReceivedDatagram, TransportError> {
match endpoint_to_socket_addr_v4(endpoint) {
Some(source) => {
let bytes_received = core::cmp::min(packet.len(), buf.len());
buf[..bytes_received].copy_from_slice(&packet[..bytes_received]);
Ok(ReceivedDatagram {
bytes_received,
source,
truncated: packet.len() > buf.len(),
})
}
None => {
// IPv6 source on a v4-bound SOME/IP socket is a
// misconfiguration upstream — surface as
// `Unsupported` for the same reason
// `tokio_transport::recv_from` does.
Err(TransportError::Unsupported)
}
}
}
impl Future for EmbassyNetRecvFut<'_> {
type Output = Result<ReceivedDatagram, TransportError>;
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
let me = self.get_mut();
me.socket
.poll_recv_from_with(cx, |packet, endpoint| {
received_datagram_from_packet(me.buf, packet, endpoint)
})

Copilot uses AI. Check for mistakes.
}
}

impl TransportSocket for EmbassyNetSocket {
type SendFuture<'a> = Ready<Result<(), TransportError>>;
type RecvFuture<'a> = Ready<Result<ReceivedDatagram, TransportError>>;
type SendFuture<'a> = EmbassyNetSendFut<'a>;
type RecvFuture<'a> = EmbassyNetRecvFut<'a>;

fn send_to<'a>(&'a self, _buf: &'a [u8], _target: SocketAddrV4) -> Self::SendFuture<'a> {
// 19c: drive `inner.poll_send_to(buf, target.into(), cx)`.
core::future::ready(Err(TransportError::Unsupported))
fn send_to<'a>(&'a self, buf: &'a [u8], target: SocketAddrV4) -> Self::SendFuture<'a> {
EmbassyNetSendFut {
socket: &self.inner,
buf,
target: socket_addr_v4_to_endpoint(target),
}
}

fn recv_from<'a>(&'a self, _buf: &'a mut [u8]) -> Self::RecvFuture<'a> {
// 19c: drive `inner.poll_recv_from(buf, cx)`.
core::future::ready(Err(TransportError::Unsupported))
fn recv_from<'a>(&'a self, buf: &'a mut [u8]) -> Self::RecvFuture<'a> {
EmbassyNetRecvFut {
socket: &self.inner,
buf,
}
}

fn local_addr(&self) -> Result<SocketAddrV4, TransportError> {
Expand All @@ -137,3 +205,39 @@ impl TransportSocket for EmbassyNetSocket {
Ok(())
}
}

// ── Address conversions ──────────────────────────────────────────────

fn socket_addr_v4_to_endpoint(addr: SocketAddrV4) -> IpEndpoint {
let o = addr.ip().octets();
IpEndpoint {
addr: IpAddress::v4(o[0], o[1], o[2], o[3]),
port: addr.port(),
}
}

/// Convert an embassy-net `IpEndpoint` to `SocketAddrV4`. Returns
/// `None` for non-IPv4 endpoints (SOME/IP's transport layer is
/// IPv4-only at this layer; an IPv6 source on a v4-bound socket
/// indicates a misconfiguration upstream).
///
/// The wildcard arm covers the case where smoltcp's `proto-ipv6`
/// feature gets pulled in via cargo's feature unification (e.g.
/// another crate in the dep graph enables it). Without the arm
/// the match would silently become non-exhaustive in that build.
Comment on lines +224 to +227

Copilot AI Apr 29, 2026

Copy link

Choose a reason for hiding this comment

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

This comment says the match would “silently become non-exhaustive” if IPv6 support is pulled in via feature unification. In practice, a new IpAddress variant would make the match non-exhaustive and fail to compile (which is good), so the wording here is misleading. Consider rephrasing to reflect the real motivation (e.g. keeping the code forward-compatible / avoiding unreachable_patterns warnings under cfg’d enum variants).

Suggested change
/// The wildcard arm covers the case where smoltcp's `proto-ipv6`
/// feature gets pulled in via cargo's feature unification (e.g.
/// another crate in the dep graph enables it). Without the arm
/// the match would silently become non-exhaustive in that build.
/// The wildcard arm keeps this match forward-compatible across
/// builds where smoltcp's `proto-ipv6` feature is enabled via
/// cargo feature unification (for example by another crate in the
/// dependency graph), and avoids cfg-dependent
/// `unreachable_patterns`/exhaustiveness churn when additional
/// `IpAddress` variants are present.

Copilot uses AI. Check for mistakes.
fn endpoint_to_socket_addr_v4(endpoint: IpEndpoint) -> Option<SocketAddrV4> {
match endpoint.addr {
IpAddress::Ipv4(v4) => {
// smoltcp's `Ipv4Address` is `pub struct Address(pub [u8; 4])`
// — no `octets()` accessor; the public tuple field is the
// documented way in.
let o = v4.0;
Some(SocketAddrV4::new(
Ipv4Addr::new(o[0], o[1], o[2], o[3]),
endpoint.port,
))
}
#[allow(unreachable_patterns)]
_ => None,
}
}