Skip to content

Traits for asynchronous UDP #73

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 13 commits into from
Jan 17, 2023
Merged
Show file tree
Hide file tree
Changes from 7 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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/target
/embedded-nal-async/target
**/*.rs.bk
Cargo.lock
1 change: 1 addition & 0 deletions embedded-nal-async/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,4 @@ pub use no_std_net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr, SocketAddrV4, Socke
pub use dns::Dns;
pub use embedded_nal::AddrType;
pub use stack::TcpConnect;
pub use stack::{ConnectedUdp, UdpStack, UnconnectedUdp};
2 changes: 2 additions & 0 deletions embedded-nal-async/src/stack/mod.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
mod tcp;
mod udp;

pub use tcp::TcpConnect;
pub use udp::{ConnectedUdp, UdpStack, UnconnectedUdp};
232 changes: 232 additions & 0 deletions embedded-nal-async/src/stack/udp.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,232 @@
//! Traits for using UDP on embedded devices
//!
//! ## Notes for implementers
//!
//! * At several places, the APIs expect to provide a local address. Backends that can not obtain
//! it, such as some AT-command based stacks, <!-- should question whether they may really call
//! themselves UDP and --> may pretend to have performed some form of network address
//! translation, and present invalid addresses as the local address.
//!
//! * Implementing [`UdpStack::UniquelyBound`] and [`UdpStack::MultiplyBound`] unconnected sockets
//! separately allows discarding the local addresses in the bound case. With LTO enabled, all the
//! overhead compared with a third trait variant between [ConnectedUdp] and [UnconnectedUdp] (in
//! which the local address is static but the remote address is flexible) should optimized out.
//! Implementing `UniquelyBound` and `MultiplyBound` with the same type is expected to be a
//! common choice.

use core::future::Future;
use no_std_net::SocketAddr;

/// This trait is implemented by UDP sockets.
///
/// The socket it represents is both bound (has a local IP address, port and interface) and
/// connected (has a remote IP address and port).
///
/// The term "connected" here refers to the semantics of POSIX datagram sockets, through which datagrams
/// are sent and received without having a remote address per call. It does not imply any process
/// of establishing a connection (which is absent in UDP). While there is typically no POSIX
/// `bind()` call in the creation of such sockets, these are implicitly bound to a suitable local
/// address at connect time.
pub trait ConnectedUdp {
/// Error type returned by send and receive operations.
type Error: embedded_io::Error;

/// Send the provided data to the connected peer
fn send<'a>(&'a mut self, data: &'a [u8]) -> Self::SendFuture<'a>;
/// Return type of the [`.send()`] method
type SendFuture<'a>: Future<Output = Result<(), Self::Error>>
where
Self: 'a;

/// Receive a datagram into the provided buffer.
///
/// If the received datagram exceeds the buffer's length, it is received regardless, and the
/// remaining bytes are discarded. The full datagram size is still indicated in the result,
/// allowing the recipient to detect that truncation.
///
/// ## Compatibility note
///
/// This deviates from the sync/nb equivalent trait in that it describes the overflow behavior
/// (a possibility not considered there). The name deviates from the original `receive()` to
/// make room for a version that is more zero-copy friendly.
fn receive_into<'a>(&'a mut self, buffer: &'a mut [u8]) -> Self::ReceiveIntoFuture<'a>;
/// Return type of the [`.receive_into()`] method
type ReceiveIntoFuture<'a>: Future<Output = Result<usize, Self::Error>>
where
Self: 'a;

// WIP to allow zero-copy operation
// The plain receive is simple and can be provided -- implementations that don't populate
// receive calls from scatter-gather can just return a slice of the raw data instead, and rely
// on the socket still being exclusively owned. receive_oned is harder as providing it requires
// alloc.
//
// fn receive(&mut self, buffer: &mut [u8]) -> impl Future<Output = Result<impl AsRef<u8> + '_, Self::Error>>;
// fn receive_owned(&mut self) -> impl Future<Output = Result<impl AsRef<u8> + 'static, Self::Error>>;
}

/// This trait is implemented by UDP sockets.
///
/// The socket it represents is not necessarily bound (may not have a single local IP address, port
/// and interface), and is typically not connected (has no remote IP address and port). Both are
/// addresses are explicitly given in every call.
///
/// If there were constraints in place at socket creation time (typically on the local side), the
/// caller MUST pass in the same (or compatible) values, MAY and pass in unspecified values where
/// applicable. The implementer MAY check them for compatibility, and SHOULD do that in debug mode.
pub trait UnconnectedUdp {
/// Error type returned by send and receive operations.
type Error: embedded_io::Error;

/// Send the provided data to a peer
///
/// ## Sending initial messages
///
/// The local address can be left unspecified by leaving any of its component zero -- that
/// gives the "any" address (`[::]` / `0.0.0.0`), the uncspecified port (0) or the unspecified
/// zone identifier (0). Unless the operating system provides facilities exceeding this crate's traits for
/// enumerating local interfaces and addresses, this is the only way to initiate outbound
/// traffic.
///
/// ## Responding to messages
///
/// Users who have previously received data from a peer and want to respond have a choice of
/// sending from the address to which the original datagram was addressed, or from an unbound
/// address. Both are valid choices in some situations, and the right choice depends on the
/// protocol used.
///
/// Note that users of sockets created through [`UdpStack::bind_single()`] should always pass
/// in that single address -- even though they've made their intention clear at construction.
/// They can pass either the one obtained at socket creation time, or the one obtained at
/// receive time; these should be equal. This allows implementations of the trait to use a
/// single kind of socket for both sockets bound to a single and sockets bound to multiple
/// addresses.
fn send<'a>(
&'a mut self,
local: SocketAddr,
remote: SocketAddr,
data: &'a [u8],
) -> Self::SendFuture<'a>;
/// Return type of the [`.send()`] method
type SendFuture<'a>: Future<Output = Result<(), Self::Error>>
where
Self: 'a;

/// Receive a datagram into the provided buffer.
///
/// If the received datagram exceeds the buffer's length, it is received regardless, and the
/// remaining bytes are discarded. The full datagram size is still indicated in the result,
/// allowing the recipient to detect that truncation.
///
/// The local and remote address are given, in that order, in the result along with the number
/// of bytes.
fn receive_into<'a>(&'a mut self, buffer: &'a mut [u8]) -> Self::ReceiveIntoFuture<'a>;
/// Return type of the [`.receive_into()`] method
type ReceiveIntoFuture<'a>: Future<
Output = Result<(usize, SocketAddr, SocketAddr), Self::Error>,
> where
Self: 'a;
}

/// This trait is implemented by UDP/IP stacks. The trait allows the underlying driver to
/// construct multiple connections that implement the I/O traits from embedded-io.
///
/// Note that stacks with exotic connection creation methods may still not implement this, yet have
/// objects that implement [`ConnectedUdp`] or similar.
pub trait UdpStack {
/// Error type returned on socket creation failure.
type Error: embedded_io::Error;

/// Eventual socket return type of the [`.connect()`] method
type Connected<'m>: ConnectedUdp
where
Self: 'm;
/// Eventual socket return type of the [`.bind_single()`] method
type UniquelyBound<'m>: UnconnectedUdp
where
Self: 'm;
/// Eventual return type of the [`.bind_multiple()`] method
type MultiplyBound<'m>: UnconnectedUdp
where
Self: 'm;

/// Create a socket that has a fixed remote address.
///
/// The local address is chosen automatically.
///
/// While asynchronous traits implemented through GAT can not have provided default methods,
/// implementers are encouraged to use the hidden `.connect_default()` method if all they would
/// do is delegating to [`.connect_from`] with a suitable unspecified local address.
fn connect(&self, remote: SocketAddr) -> Self::ConnectFuture<'_>;
/// Future return type of the [`.connect()`] method
type ConnectFuture<'a>: Future<Output = Result<(SocketAddr, Self::Connected<'a>), Self::Error>>
where
Self: 'a;

/// Create a socket that has a fixed remote address.
///
/// The local address is given explicitly, but may be partially unspecified; it is fixed by the
/// network stack at connection time. The full local address is returned along with the
/// connected socket, primarily for debugging purposes.
fn connect_from(&self, local: SocketAddr, remote: SocketAddr) -> Self::ConnectFromFuture<'_>;
/// Future return type of the [`.connect_from()`] method
type ConnectFromFuture<'a>: Future<
Output = Result<(SocketAddr, Self::Connected<'a>), Self::Error>,
> where
Self: 'a;

/// Helper that implements [`connect()`] using [`connect_from()`].
#[doc(hidden)]
fn connect_default(&self, remote: SocketAddr) -> Self::ConnectFromFuture<'_> {
Copy link
Contributor

Choose a reason for hiding this comment

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

I don't understand how this differs from connect() in practice. If the local address is unspecified, isn't the only reasonable thing for the implementation to select one automatically like connect()? Unless it's a common thing to do, I would probably leave it out of the trait.

Copy link
Member Author

Choose a reason for hiding this comment

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

As (for lack of async fn in traits) the connect() method is paired with the ConnectFuture type, we can't have provided methods. Were that not the case, this could be the default impl for connect(), but as things are, implementers who want to use the simple default can just set type ConnectFuture<'a> = ConnectFromFuture<'a> and fn connect(&self, remote) -> ... { self.connect_default(remote) } in lieu of using the provided method.

It is a distinct method with its distinct return type because calling .connect() will be a common occurrence, and implementers might want to optimize this over the provided default. (In a POSIX implementation, this might be saving a syscall).

Copy link
Member Author

Choose a reason for hiding this comment

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

This is not documented here (because the item is hidden, because users should not rely on it), but in the documentation of the .connect() method.

use no_std_net::{Ipv4Addr, Ipv6Addr, SocketAddr::*, SocketAddrV4, SocketAddrV6};

let local = match remote {
V4(_) => V4(SocketAddrV4::new(Ipv4Addr::UNSPECIFIED, 0)),
V6(_) => V6(SocketAddrV6::new(Ipv6Addr::UNSPECIFIED, 0, 0, 0)),
};
self.connect_from(local, remote)
}

/// Create a socket that has a fixed local address.
///
/// Note that the giving an unspecified address here is *not* the same as a POSIX `bind()` --
/// if the underlying stack supports multiple local addresses, it will pick *one* of the
/// applicable addresses, rather than binding to all of them.
///
/// The full local address is returned along with the bound socket; it may then be passed on to
/// other protocols for advertising purposes.
fn bind_single(&self, local: SocketAddr) -> Self::BindSingleFuture<'_>;
/// Future return type of the [`.bind_single()`] method
type BindSingleFuture<'a>: Future<
Output = Result<(SocketAddr, Self::UniquelyBound<'a>), Self::Error>,
> where
Self: 'a;

/// Create a socket that has no single fixed local address.
///
/// The IP address part of the local address is typically left unspecified, and the port is
/// given. There are use cases for other constellations, and this interface does not rule out
/// that they can be used, but they are rare (e.g. using the same IP address on different
/// network interfaces, and listening to datagrams arriving at any of them) or not well
/// supported by operating systems (e.g., binding to all ports at the same is not possible on
/// POSIX systems, where giving port 0 to a bind makes the OS pick *some* suitable port).
///
/// Caveats:
///
/// * There is currently no way to pass in a local address that has an unspecified address
/// family (which would effectively create a single socket that servers both IPv4 and IPv6);
/// it is not specified whether stacks that use V6MAPPED IPv4 addresses could simply used
/// that mechanism.
///
/// * It is currently not specified whether this mechanism can be used to join multicast
/// groups.
///
/// * There is currently no hybrid binding that allows emulating what POSIX systems do when
/// binding to `[::]:0`, that is, picking some available port but then still leaving the
/// interface and IP address unspecified.
fn bind_multiple(&self, local: SocketAddr) -> Self::BindMultipleFuture<'_>;
/// Future return type of the [`.bind_multiple()`] method
type BindMultipleFuture<'a>: Future<Output = Result<Self::MultiplyBound<'a>, Self::Error>>
where
Self: 'a;
}