Skip to content

Commit d8f82e0

Browse files
authored
Traits for asynchronous UDP (#73)
* gitignore: Also ignore target inside nested crate * async: Add UDP traits Closes: #71 * async UDP: Simplify to 2 rather than 3 traits * async UDP: Documentation fixes; receive_into consistency One trait's receive_into still used the old receive name. * async UDP: Make accessible through lib.rs * async UDP: Fix lifetimes of send and receive functions Elided lifetimes would have asked too little of the data / buffer references. * async UDP: Better names for bound sockets * UDP: use async-fn-in-trait instead of associated types that are Future This removes the lifetimes from the Connect associated types; leaving them would cause errors (possibly related to [12681]), but I think that stacks whose sockets are not 'static can work around this by implementing the stack traits on a &'short stack instead. [12681]: rust-lang/rust#102681 * async/UDP: Remove connect_default helper The connect method can now be provided properly. * async/CHANGELOG: Add UDP * async/UDP: rustfmt * async/UDP: Tie socket's error types to stack's
1 parent 7d262f0 commit d8f82e0

File tree

5 files changed

+205
-0
lines changed

5 files changed

+205
-0
lines changed

.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
/target
2+
/embedded-nal-async/target
23
**/*.rs.bk
34
Cargo.lock

embedded-nal-async/CHANGELOG.md

+2
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
77

88
## [Unreleased]
99

10+
- Add traits for UDP
11+
1012
## [0.3.0] - 2022-11-25
1113

1214
- Bump `embedded-io` dependency to `0.4`

embedded-nal-async/src/lib.rs

+1
Original file line numberDiff line numberDiff line change
@@ -16,3 +16,4 @@ pub use no_std_net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr, SocketAddrV4, Socke
1616
pub use dns::Dns;
1717
pub use embedded_nal::AddrType;
1818
pub use stack::TcpConnect;
19+
pub use stack::{ConnectedUdp, UdpStack, UnconnectedUdp};

embedded-nal-async/src/stack/mod.rs

+2
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
11
mod tcp;
2+
mod udp;
23

34
pub use tcp::TcpConnect;
5+
pub use udp::{ConnectedUdp, UdpStack, UnconnectedUdp};

embedded-nal-async/src/stack/udp.rs

+199
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,199 @@
1+
//! Traits for using UDP on embedded devices
2+
//!
3+
//! ## Notes for implementers
4+
//!
5+
//! * At several places, the APIs expect to provide a local address. Backends that can not obtain
6+
//! it, such as some AT-command based stacks, <!-- should question whether they may really call
7+
//! themselves UDP and --> may pretend to have performed some form of network address
8+
//! translation, and present invalid addresses as the local address.
9+
//!
10+
//! * Implementing [`UdpStack::UniquelyBound`] and [`UdpStack::MultiplyBound`] unconnected sockets
11+
//! separately allows discarding the local addresses in the bound case. With LTO enabled, all the
12+
//! overhead compared with a third trait variant between [ConnectedUdp] and [UnconnectedUdp] (in
13+
//! which the local address is static but the remote address is flexible) should optimized out.
14+
//! Implementing `UniquelyBound` and `MultiplyBound` with the same type is expected to be a
15+
//! common choice.
16+
17+
use no_std_net::SocketAddr;
18+
19+
/// This trait is implemented by UDP sockets.
20+
///
21+
/// The socket it represents is both bound (has a local IP address, port and interface) and
22+
/// connected (has a remote IP address and port).
23+
///
24+
/// The term "connected" here refers to the semantics of POSIX datagram sockets, through which datagrams
25+
/// are sent and received without having a remote address per call. It does not imply any process
26+
/// of establishing a connection (which is absent in UDP). While there is typically no POSIX
27+
/// `bind()` call in the creation of such sockets, these are implicitly bound to a suitable local
28+
/// address at connect time.
29+
pub trait ConnectedUdp {
30+
/// Error type returned by send and receive operations.
31+
type Error: embedded_io::Error;
32+
33+
/// Send the provided data to the connected peer
34+
async fn send(&mut self, data: &[u8]) -> Result<(), Self::Error>;
35+
36+
/// Receive a datagram into the provided buffer.
37+
///
38+
/// If the received datagram exceeds the buffer's length, it is received regardless, and the
39+
/// remaining bytes are discarded. The full datagram size is still indicated in the result,
40+
/// allowing the recipient to detect that truncation.
41+
///
42+
/// ## Compatibility note
43+
///
44+
/// This deviates from the sync/nb equivalent trait in that it describes the overflow behavior
45+
/// (a possibility not considered there). The name deviates from the original `receive()` to
46+
/// make room for a version that is more zero-copy friendly.
47+
async fn receive_into(&mut self, buffer: &mut [u8]) -> Result<usize, Self::Error>;
48+
49+
// WIP to allow zero-copy operation
50+
// The plain receive is simple and can be provided -- implementations that don't populate
51+
// receive calls from scatter-gather can just return a slice of the raw data instead, and rely
52+
// on the socket still being exclusively owned. receive_oned is harder as providing it requires
53+
// alloc.
54+
//
55+
// async fn receive(&mut self, buffer: &mut [u8]) -> utput = Result<impl AsRef<u8> + '_, Self::Error>;
56+
// async fn receive_owned(&mut self) -> Result<impl AsRef<u8> + 'static, Self::Error>;
57+
}
58+
59+
/// This trait is implemented by UDP sockets.
60+
///
61+
/// The socket it represents is not necessarily bound (may not have a single local IP address, port
62+
/// and interface), and is typically not connected (has no remote IP address and port). Both are
63+
/// addresses are explicitly given in every call.
64+
///
65+
/// If there were constraints in place at socket creation time (typically on the local side), the
66+
/// caller MUST pass in the same (or compatible) values, MAY and pass in unspecified values where
67+
/// applicable. The implementer MAY check them for compatibility, and SHOULD do that in debug mode.
68+
pub trait UnconnectedUdp {
69+
/// Error type returned by send and receive operations.
70+
type Error: embedded_io::Error;
71+
72+
/// Send the provided data to a peer
73+
///
74+
/// ## Sending initial messages
75+
///
76+
/// The local address can be left unspecified by leaving any of its component zero -- that
77+
/// gives the "any" address (`[::]` / `0.0.0.0`), the uncspecified port (0) or the unspecified
78+
/// zone identifier (0). Unless the operating system provides facilities exceeding this crate's traits for
79+
/// enumerating local interfaces and addresses, this is the only way to initiate outbound
80+
/// traffic.
81+
///
82+
/// ## Responding to messages
83+
///
84+
/// Users who have previously received data from a peer and want to respond have a choice of
85+
/// sending from the address to which the original datagram was addressed, or from an unbound
86+
/// address. Both are valid choices in some situations, and the right choice depends on the
87+
/// protocol used.
88+
///
89+
/// Note that users of sockets created through [`UdpStack::bind_single()`] should always pass
90+
/// in that single address -- even though they've made their intention clear at construction.
91+
/// They can pass either the one obtained at socket creation time, or the one obtained at
92+
/// receive time; these should be equal. This allows implementations of the trait to use a
93+
/// single kind of socket for both sockets bound to a single and sockets bound to multiple
94+
/// addresses.
95+
async fn send(
96+
&mut self,
97+
local: SocketAddr,
98+
remote: SocketAddr,
99+
data: &[u8],
100+
) -> Result<(), Self::Error>;
101+
102+
/// Receive a datagram into the provided buffer.
103+
///
104+
/// If the received datagram exceeds the buffer's length, it is received regardless, and the
105+
/// remaining bytes are discarded. The full datagram size is still indicated in the result,
106+
/// allowing the recipient to detect that truncation.
107+
///
108+
/// The local and remote address are given, in that order, in the result along with the number
109+
/// of bytes.
110+
async fn receive_into(
111+
&mut self,
112+
buffer: &mut [u8],
113+
) -> Result<(usize, SocketAddr, SocketAddr), Self::Error>;
114+
}
115+
116+
/// This trait is implemented by UDP/IP stacks. The trait allows the underlying driver to
117+
/// construct multiple connections that implement the I/O traits from embedded-io.
118+
///
119+
/// Note that stacks with exotic connection creation methods may still not implement this, yet have
120+
/// objects that implement [`ConnectedUdp`] or similar.
121+
pub trait UdpStack {
122+
/// Error type returned on socket creation failure.
123+
type Error: embedded_io::Error;
124+
125+
/// Eventual socket return type of the [`.connect()`] method
126+
type Connected: ConnectedUdp<Error = Self::Error>;
127+
/// Eventual socket return type of the [`.bind_single()`] method
128+
type UniquelyBound: UnconnectedUdp<Error = Self::Error>;
129+
/// Eventual return type of the [`.bind_multiple()`] method
130+
type MultiplyBound: UnconnectedUdp<Error = Self::Error>;
131+
132+
/// Create a socket that has a fixed remote address.
133+
///
134+
/// The local address is chosen automatically.
135+
///
136+
/// There is a provided implementation that implements this from the maximally unspecified
137+
/// local address and [`.connect_from()`], but may be provided more efficiently by
138+
/// implementers.
139+
async fn connect(
140+
&self,
141+
remote: SocketAddr,
142+
) -> Result<(SocketAddr, Self::Connected), Self::Error> {
143+
use no_std_net::{Ipv4Addr, Ipv6Addr, SocketAddr::*, SocketAddrV4, SocketAddrV6};
144+
145+
let local = match remote {
146+
V4(_) => V4(SocketAddrV4::new(Ipv4Addr::UNSPECIFIED, 0)),
147+
V6(_) => V6(SocketAddrV6::new(Ipv6Addr::UNSPECIFIED, 0, 0, 0)),
148+
};
149+
self.connect_from(local, remote).await
150+
}
151+
152+
/// Create a socket that has a fixed remote address.
153+
///
154+
/// The local address is given explicitly, but may be partially unspecified; it is fixed by the
155+
/// network stack at connection time. The full local address is returned along with the
156+
/// connected socket, primarily for debugging purposes.
157+
async fn connect_from(
158+
&self,
159+
local: SocketAddr,
160+
remote: SocketAddr,
161+
) -> Result<(SocketAddr, Self::Connected), Self::Error>;
162+
163+
/// Create a socket that has a fixed local address.
164+
///
165+
/// Note that the giving an unspecified address here is *not* the same as a POSIX `bind()` --
166+
/// if the underlying stack supports multiple local addresses, it will pick *one* of the
167+
/// applicable addresses, rather than binding to all of them.
168+
///
169+
/// The full local address is returned along with the bound socket; it may then be passed on to
170+
/// other protocols for advertising purposes.
171+
async fn bind_single(
172+
&self,
173+
local: SocketAddr,
174+
) -> Result<(SocketAddr, Self::UniquelyBound), Self::Error>;
175+
176+
/// Create a socket that has no single fixed local address.
177+
///
178+
/// The IP address part of the local address is typically left unspecified, and the port is
179+
/// given. There are use cases for other constellations, and this interface does not rule out
180+
/// that they can be used, but they are rare (e.g. using the same IP address on different
181+
/// network interfaces, and listening to datagrams arriving at any of them) or not well
182+
/// supported by operating systems (e.g., binding to all ports at the same is not possible on
183+
/// POSIX systems, where giving port 0 to a bind makes the OS pick *some* suitable port).
184+
///
185+
/// Caveats:
186+
///
187+
/// * There is currently no way to pass in a local address that has an unspecified address
188+
/// family (which would effectively create a single socket that servers both IPv4 and IPv6);
189+
/// it is not specified whether stacks that use V6MAPPED IPv4 addresses could simply used
190+
/// that mechanism.
191+
///
192+
/// * It is currently not specified whether this mechanism can be used to join multicast
193+
/// groups.
194+
///
195+
/// * There is currently no hybrid binding that allows emulating what POSIX systems do when
196+
/// binding to `[::]:0`, that is, picking some available port but then still leaving the
197+
/// interface and IP address unspecified.
198+
async fn bind_multiple(&self, local: SocketAddr) -> Result<Self::MultiplyBound, Self::Error>;
199+
}

0 commit comments

Comments
 (0)