Skip to content

phase 19c: EmbassyNetSocket send/recv via poll_send_to / poll_recv_from#100

Draft
JustinKovacich wants to merge 1 commit into
feature/phase19b_embassy_net_factoryfrom
feature/phase19c_embassy_net_socket_io
Draft

phase 19c: EmbassyNetSocket send/recv via poll_send_to / poll_recv_from#100
JustinKovacich wants to merge 1 commit into
feature/phase19b_embassy_net_factoryfrom
feature/phase19c_embassy_net_socket_io

Conversation

@JustinKovacich
Copy link
Copy Markdown
Contributor

Replaces the 19b stubs (Ready<Err(Unsupported)>) with named futures that drive embassy-net's poll_send_to / poll_recv_from directly:

  • EmbassyNetSendFut<'a> / EmbassyNetRecvFut<'a> are hand-rolled Future structs, not async-block wrappers. Each datagram costs zero allocations on the hot path.
  • Error mapping: SendError::NoRoute -> Io(NetworkUnreachable), SendError::SocketNotBound -> Io(Other) (programming error, bind always precedes return), RecvError::Truncated -> Io(Other), IPv6 source endpoint -> Unsupported.
  • Address conversions (socket_addr_v4_to_endpoint / endpoint_to_socket_addr_v4) include a defensive wildcard arm so cargo feature-unification pulling in smoltcp's proto-ipv6 doesn't silently break exhaustiveness.
  • factory.rs: drop the _phantom_io_error_kind_use placeholder; IoErrorKind is now actually used downstream in socket.rs.

Open question (per plan v3 phase 19c) resolved: embassy-net 0.4's UdpSocket exposes poll_send_to / poll_recv_from directly, so the named-future shape works without an intermediate shim.

Gates green:

  • cargo fmt --check
  • cargo clippy -p simple-someip-embassy-net --all-targets -D warnings
  • cargo build -p simple-someip-embassy-net --target thumbv7em-none-eabihf
  • cargo check --workspace --all-targets

What this leaves for 19d: kill heap Vec from SD-emission paths (Server::send_subscribe_ack_from_view, send_subscribe_nack_from_view, send_unicast_offer, SdStateManager::send_offer_service) in favor of stack [u8; UDP_BUFFER_SIZE] matching EventPublisher::publish_event.

Replaces the 19b stubs (Ready<Err(Unsupported)>) with named futures
that drive embassy-net's poll_send_to / poll_recv_from directly:

- EmbassyNetSendFut<'a> / EmbassyNetRecvFut<'a> are hand-rolled
  Future structs, not async-block wrappers. Each datagram costs
  zero allocations on the hot path.
- Error mapping: SendError::NoRoute -> Io(NetworkUnreachable),
  SendError::SocketNotBound -> Io(Other) (programming error,
  bind always precedes return), RecvError::Truncated -> Io(Other),
  IPv6 source endpoint -> Unsupported.
- Address conversions (socket_addr_v4_to_endpoint /
  endpoint_to_socket_addr_v4) include a defensive wildcard arm so
  cargo feature-unification pulling in smoltcp's proto-ipv6 doesn't
  silently break exhaustiveness.
- factory.rs: drop the _phantom_io_error_kind_use placeholder;
  IoErrorKind is now actually used downstream in socket.rs.

Open question (per plan v3 phase 19c) resolved: embassy-net 0.4's
UdpSocket exposes poll_send_to / poll_recv_from directly, so the
named-future shape works without an intermediate shim.

Gates green:
- cargo fmt --check
- cargo clippy -p simple-someip-embassy-net --all-targets -D warnings
- cargo build -p simple-someip-embassy-net --target thumbv7em-none-eabihf
- cargo check --workspace --all-targets

What this leaves for 19d: kill heap Vec<u8> from SD-emission paths
(Server::send_subscribe_ack_from_view, send_subscribe_nack_from_view,
send_unicast_offer, SdStateManager::send_offer_service) in favor of
stack [u8; UDP_BUFFER_SIZE] matching EventPublisher::publish_event.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Implements real UDP send/recv for the embassy-net transport backend by replacing phase-19b stubs with named, poll-driven futures over embassy_net::udp::UdpSocket.

Changes:

  • Added EmbassyNetSendFut / EmbassyNetRecvFut futures that drive poll_send_to / poll_recv_from directly.
  • Added IPv4 SocketAddrV4IpEndpoint conversion helpers and error mappings for embassy-net send/recv errors.
  • Removed the IoErrorKind unused-import guard from the factory now that error kinds are used in the socket adapter.

Reviewed changes

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

File Description
simple-someip-embassy-net/src/socket.rs Replaces stub TransportSocket impl with real send/recv futures, adds error mapping + address conversions.
simple-someip-embassy-net/src/factory.rs Removes the temporary IoErrorKind placeholder/guard now that the socket layer uses it.
Comments suppressed due to low confidence (1)

simple-someip-embassy-net/src/socket.rs:188

  • TransportSocket::local_addr is expected to report the actual bound port (notably when binding with port 0 for an ephemeral port). Here it just returns the bind-time self.local, which is currently populated from the requested port in EmbassyNetFactory::bind, so bind(..., port=0) will keep returning port 0 and break callers that back-fill the chosen port (e.g. passive server / other port-0 binds). Consider reading the real port from the underlying embassy-net socket (e.g. via an endpoint()/local_endpoint()-style accessor) and combining it with the stored IPv4 address intent, or storing the real port after bind() completes.
    fn local_addr(&self) -> Result<SocketAddrV4, TransportError> {
        Ok(self.local)
    }

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

Comment on lines +127 to +163
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)))
}
}
Copy link

Copilot AI Apr 29, 2026

Choose a reason for hiding this comment

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

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.
Comment on lines +224 to +227
/// 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.
Copy link

Copilot AI Apr 29, 2026

Choose a reason for hiding this comment

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

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.
Comment on lines +138 to +141
// 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.
Copy link

Copilot AI Apr 29, 2026

Choose a reason for hiding this comment

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

The 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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants