phase 19c: EmbassyNetSocket send/recv via poll_send_to / poll_recv_from#100
Conversation
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>
There was a problem hiding this comment.
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/EmbassyNetRecvFutfutures that drivepoll_send_to/poll_recv_fromdirectly. - Added IPv4
SocketAddrV4↔IpEndpointconversion helpers and error mappings for embassy-net send/recv errors. - Removed the
IoErrorKindunused-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_addris expected to report the actual bound port (notably when binding with port 0 for an ephemeral port). Here it just returns the bind-timeself.local, which is currently populated from the requested port inEmbassyNetFactory::bind, sobind(..., 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 anendpoint()/local_endpoint()-style accessor) and combining it with the stored IPv4 address intent, or storing the real port afterbind()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.
| 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))) | ||
| } | ||
| } |
There was a problem hiding this comment.
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.
| 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) | |
| }) |
| /// 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. |
There was a problem hiding this comment.
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).
| /// 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. |
| // 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. |
There was a problem hiding this comment.
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.
| // 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. |
Replaces the 19b stubs (Ready<Err(Unsupported)>) with named futures that drive embassy-net's poll_send_to / poll_recv_from directly:
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:
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.