-
Notifications
You must be signed in to change notification settings - Fork 25
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
Traits for asynchronous UDP #73
Traits for asynchronous UDP #73
Conversation
Based on feedback from @Dirbaio, reduced to two trait sets now. |
Updated with experience from implementing it for std. There is now a branch at https://gitlab.com/chrysn/std-embedded-nal/-/tree/embedded-nal-async-0.2-udp that implements this on std. (It's currently cheating on the local address part; that's being fixed). |
An aspect I expect to be confronted with during review is that it's hard to get local addresses on servers (basically the #40 cluster); std-embedded-nal cheating there is evidence of that hardship, and the currently proposed API has now way for implementations to easily chicken out. (They can always return an error on I claim that this is a feature: It contributes to the coherence of the ecosystem, as it won't fragment into different levels of UDP support. If we had a base level and then extension traits, we'd get a lot of incomplete UDP implementations, which do not suffice for "advanced" use cases like CoAP1, QUIC2, [edit: or anything with multicast], or subtly break in the presence of NAT or when first used on a sever with multiple addresses. Note that in constrained stacks it'll be actually easier to implement this than on std, which is limited by how that design flaw has been baked into POSIX' sockets API, which is only worked around by RFC 3542. I plan to demonstrate this by implementing the traits also for RIOT's sockets (which in term abstract over several network stacks). Footnotes |
One detail that came up during std-embedded-nal implementation is that the IETF/POSIX API doesn't report the port of the incoming package -- it only reports the IP address. The port can be queried extra (and is stored in this implementation). I still think that the API is easier if a SocketAddr is used consistently throughout, but it's a piece of possibly useless information that the compiler may not be able to remove. |
There is one change I'd be tempted to do, but may not fit with the general pattern of this library: At several places, rather than passing I mention it here and not in a separate issue because implementers of this API could benefit from it a lot. This is true both for the std-based implementation (which would pass around opaque PktInfo structs) and constrained ones (which might use a refcount into a neighbor table rather than the full address, and use that information again right away). |
One trait's receive_into still used the old receive name.
Elided lifetimes would have asked too little of the data / buffer references.
Rebased, edited to account for #76 (which needed the trivial change from IP addresses' |
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 think it looks pretty good! I left an inline comment as well.
I'm maybe a little worried that even though the UdpStack trait is supporting some advanced uses, it is hard to tell up front if implementations will end up with lots of todo!'s instead of supporting it all. I'm not sure what to do about that other than using multiple traits.
embedded-nal-async/src/stack/udp.rs
Outdated
|
||
/// Helper that implements [`connect()`] using [`connect_from()`]. | ||
#[doc(hidden)] | ||
fn connect_default(&self, remote: SocketAddr) -> Self::ConnectFromFuture<'_> { |
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 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).
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.
As for the concern about implementations with But in the end, that's an opinion coming from a place where having these traits is usually a necessity, and if the maintainers, reviewers and/or embedded-community prefer, I'll just split the trait. (I'd still keep the names distinct, though, to allow easy and turbofish-free operation on stacks that support all modes, and for users who just |
Thinking on on the That is unlike embedded-hal (where my impression is that we're pretty fine-grained in what the types advertise in terms of features). Given how network stacks are generally very runtime-ish compared to embedded hardware, I think that's OK. Even if we had a network stack were you'd take ownership of an address that's configured on the network interface, and then split that into 65k UDP ports to which access is handed out -- that IP address probably has a "lifetime" that is vastly distinct from our concept of lifetime. So to abstract from that: Network socket operations, even such as static as binding, are conceptually fallible; and to add to that, they may depend on runtime configuration (you think you could bind to (I'd be happy to be shown wrong here. Good thing is: Even if it turns out that far down the way we'd revisit this, API changes or splits would only concern the stack, whereas the connections would still be handled by the |
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 have any further comments, so I think this looks good!
Any opinions @MathiasKoch, @ryan-summers ? |
Looks good to me. No objections 👍 |
I have not had the opportunity to dive into async Rust yet, so I don't know if I can have much valuable input for async traits. I've been leaving the async stuff up to other async folk for now. That being said, I'm on board for moving fast and breaking things at this point, since these are all new concepts. We can fix them as we try them out - there's not an abundance of users on the embedded-nal-async yet. |
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.
Great, @chrysn could you add an entry to the changelog?
We probably want to merge the AFIT ones #77 now that TCP uses AFIT. |
With #78 done, yes, that's the way to go. I'll merge 77 into this PR so it's a single consistent change, and update the change log. |
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
The connect method can now be provided properly.
Updated to AFIT using the very content of #78 (and changelog added); probably warrants a final review. The tests using std-embedded-nal still work from its branch (after fixing the embedded-io version, which was bumped in 0.3). |
I think this is ready; anything else for me to do? (I can't push the merge button ;-) ). |
LGTM overall! Just 2 things.
It's confusing that you can call both with a non-unspecified addr, in which case I wonder if it can be made clearer using an enum? Also, if enum IPVersion {
V4V6,
V4,
V6,
}
enum LocalAddress {
/// The network stack picks one address out of all the local addresses,
/// and the socket listens to just that one.
PickOne(IPVersion),
/// The socket listens on all local addresses of the specified version.
All(IPVersion),
/// The socket listens on the given address. Unspecified address is not allowed.
Fixed(Address),
}
async fn bind(local_addr: LocalAddress, local_port: u16) -> ...
Receive methods always receive into a buffer, there's not much point in adding I think we should stay away from "connect/connected" terms for UDP, as they imply For the socket kinds I'd use "bound", which is what docs are already using:
For the associated types, I think it'd be simpler to name them after the
For the methods, perhaps do |
Also, I think the associated types should have |
Ad 1, maybe an enum:
Going through an enum won't make that aspect easier; but I can try to emphasize that in the documentation (gist is "Use _single unless you really intend to bind to multiple addresses"). Ad 1, The current interface manages to delegate all handling of different IP versions to SocketAddr (as does the TCP version), I'd prefer to keep it that way. Technically, you never "bind to IPv4 and IPv6", you only bind to IPv6, and allow the socket to also take requests from its legacy support. (It's just that some platforms implement that legacy support, and some don't). If there's a use case for expressing an unspecified address that matches all addresses except V6MAPPED IPv4 addresses, I suggest that be added to SocketAddr. (Which we might have to fork from no_std_net (or std later) unless the maintainers agree that this has a place in SocketAddr). Ad 1, "why distinct types for uniquely and multiply bound", that does have a good reason: An implementation can do different optimizations with addresses bound to one address (that is, eventually elide all comparisons on the local address), or even only implement one. That's also why there are two different associated types around (but requiring the same traits, so an implementation may chose to serve both with the same).
Well because they're forced through oversimplified APIs that don't differentiate ;-) (on POSIX, even TCP and UDP are of the same type, and we still use distinct types here as well) More seriously:
ad 2 bikeshedding, I'm less obstinate there ;-) -- I'd just like to explain the rationale about the choices made so far (and if you insist, can change things):
|
Thanks, good catch, fixed. |
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.
Thank you for your work and everybody else for the discussion.
Could you resolve the conflicts? Then I would merge it.
Closes: #71
This adds traits for UDP that were removed in #70, but built from scratch. These address, in one go:
accept()
and spin off a bound one might be added later).This is early WIP and only checked to the point where it compiles; no implementations or users have been written yet -- but I'd like to have it around early for discussion.