-
Notifications
You must be signed in to change notification settings - Fork 25
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
eldruin
merged 13 commits into
rust-embedded-community:master
from
chrysn-pull-requests:async-udp
Jan 17, 2023
Merged
Changes from 7 commits
Commits
Show all changes
13 commits
Select commit
Hold shift + click to select a range
4b5038e
gitignore: Also ignore target inside nested crate
chrysn d3443f6
async: Add UDP traits
chrysn e52dd7e
async UDP: Simplify to 2 rather than 3 traits
chrysn 3f716ac
async UDP: Documentation fixes; receive_into consistency
chrysn 776e6df
async UDP: Make accessible through lib.rs
chrysn f58f4fd
async UDP: Fix lifetimes of send and receive functions
chrysn 0c2e98b
async UDP: Better names for bound sockets
chrysn aa60811
UDP: use async-fn-in-trait instead of associated types that are Future
chrysn 5bf0063
async/UDP: Remove connect_default helper
chrysn b2f5949
Merge branch 'master' into async-fn-in-trait
chrysn 3d6c868
async/CHANGELOG: Add UDP
chrysn d9c68fe
async/UDP: rustfmt
chrysn 9117f5a
async/UDP: Tie socket's error types to stack's
chrysn File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,4 @@ | ||
/target | ||
/embedded-nal-async/target | ||
**/*.rs.bk | ||
Cargo.lock |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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}; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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<'_> { | ||
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; | ||
} |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 settype ConnectFuture<'a> = ConnectFromFuture<'a>
andfn 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).There was a problem hiding this comment.
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.