-
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
[Proposal] Socket-directed API #53
Comments
This would indeed make the sockets easier to use. But it would make writing a network stack a lot more difficult, because of all the synchronization that would be necessary. Also each socket would have to contain a reference to the stack internally, which would complicate lifetimes. I think the current approach with Python's API is nice, but Pythen the GIL, a garbage collector (i.e. no lifetimes) and uses the OS for socket I/O which does all the heavy lifting. It is therefore a lot easier to write an easy to use API in Python than it is in Embedded Rust. |
Synchronization is only required for stacks that don't have socket buffers (e.g. the W5500). No synchronization is required for something like I would also note that this synchronization is already required, and we're not meeting it (see below).
This is not true for all cases, but is relevant for some use cases.
The While I agree this complicates things slightly, it's because things have to get more complicated if the network stack supports multiple execution priorities (e.g. RTIC tasks with different priority levels). The alternative here would be to implement something like |
Also, after thinking about this more, let's take an example where you have a network stack off-chip (e.g. the W5500, a TCP/UDP stack that communicates via SPI). Instead of having a single |
One could easily write a variant of I wouldn't see W5500 and similar as an edge case. Currently it's one of the two main stacks for
I am pretty sure this would be almost impossible to get right, because all stacks could mess with the state of all other stacks by for example issuing a soft reset or by writing to any other global register. |
Yes, but that requires essentially duplicating
I do not consider it an edge case. I am actively using it for one of our projects. Both
This is very much not impossible to get right and is quite easy to do when the driver is written within a single crate. You simply have to ensure that individual sockets do not modify other socket registers. Given Rust's ownership semantics, this is quite feasible and easily doable in my opinion. |
I suppose it could be done. In my opinion it's just way more complicated than adding a thread-safe I just had a look at Maybe trying to write an implementation first (especially for the W5500 case) would be helpful to see how difficult it actually is. |
I'm working on a proof-of-concept that I hope will clarify how everything will work together. I'll post some details on this once I finish, but I do think this will make a much cleaner usage and API. I don't think there's any reason that we need to force objects to refer to the whole network stack when they could just refer to a part of it (e.g. the socket buffer) when possible. |
I agree that the api should be socket focused, but I think that trait UdpClient {
type Socket: Write + Read; // Dropping a socket closes it.
type Error;
fn connect(&mut self, remote: SocketAddr) -> Result<Self::Socket, Self::Error>;
} Or, in the case of async: trait AsyncUdpClient {
type Socket: AsyncWrite + AsyncRead; // Dropping a socket closes it.
type ConnectFuture<'a>: Future<Output = Result<Self::Socket, Self::Error>> + 'a;
type Error;
fn connect<'a>(&'a mut self, remote: SocketAddr) -> Self::ConnectFuture<'a>;
} |
Bear in mind that we don't have |
There is a no_std io::Read, io::Write crate, and I believe that they'll eventually make their way to core. |
While I agree the ultimate goal here should be async, I think there's still some work to stabilize things in In any case, lets keep this issue related to the main topic and keep async APIs in #47 |
Would it make sense to keep the to-be-implemented API as it is, but provide a shim that allows combining a socket with an owned stack (which is easy if you only ever have one socket, or if your implementation is synchronized anyway so your stack is clonable, or you've created a practically-sharedly-usable stack through shared-bus or similar) into something that has the proposed API? Downside would be that it might create a split in the ecosystem (anything built on the simple API would only be usable under one of the above conditions), or need very strong warning words that any library should use the (self, stack) API and that only leaf crates can use the simple (self) form for simplicity. |
Correct me if I am wrong but as far as I see,
Because an embedded-nal stack is not a global static (like in operating systems), the network stack needs ownership or a |
I have experimented today with how a socket-directed API could look like. In the beginning, I took the result I ended up with in #62 and generalized it so that part of the implementation could be moved out of the driver into Afterwards, I tried to get rid of the hard-coded This approach would allow a socket-directed API, implement
Altogether, we want to mutably share resources (at least share access to the hardware between multiple sockets) so I think the interior mutability pattern and some of the implementation details will always pop up. On the other hand, I just realized that what I called Finally, I do not think that reusing anything from |
I really like the result you ended up with! So from my end i have no objections against working in that direction for the future. I do not have the time to drive the development, though i think it feels much more rust-like & comparable to how the std-net does it. |
Note that this is being used (or experimented on, not sure of the maturity) in the TcpConnect trait of embedded-nal-async: The result of TcpConnect can be used through a mutable reference alone (along the embedded_io) traits, making it effectively a socket directed API. |
I took a stab at implementing this as part of #85, but ran into issues around lifetime specification of the TCP connection object, as that needs to mutably borrow the stack. See #85 (comment). I talked to @MathiasKoch about this and he pointed out that one workaround for this is to enforce that the |
With my colleague @embediver, I stumbled upon this in a different but similar scenario so we discussed the In the following, I try to outline my thoughts about that topic: Prerequisites / Assumptions
Mutable access requirements in Rust normally translate to having ownership or requiring (exclusive) Since we want multiple sockets and we can not hold Current Trait Design and LimitationsAt the time of writing, the most recent released version of But at a closer look, this does not work as desired:
In summary, the approach to sharing which is reflected in the current trait design does not seem to work well in the general case. So I am convinced that interior mutability using Socket-Oriented APIFor a socket-oriented API, I would roughly follow the pub trait TcpStack {
type Socket<'a>: TcpSocket
where
Self: 'a;
type Error: embedded_io::Error;
fn socket<'a>(&'a self) -> Result<Self::Socket<'a>, Self::Error>;
}
pub trait TcpSocket: embedded_io::Write + embedded_io::Read {
fn connect(&mut self, remote: embedded_nal::SocketAddr) -> nb::Result<(), Self::Error>;
fn close(&mut self) -> nb::Result<(), Self::Error>;
} I would prefer having
In total, any socket-oriented API seems cleaner. For the implementation side, it can be clearly communicated how to implement this. On the user side, it is easier and more convenient to use. So in my opinion, this is the way to go. If you plan to still support the non-async version, it should definitely move to such a design, too. |
@DrTobe Thanks for the comprehensive writeup, your conclusions about the design are largely correct. I think the one aspect that is missing is the fact that a The most important aspect here is that a Given this, I think it may make the most sense to copy the design of The primary problem is that the actual synchronization mechanism depends on the end user application. Whatever design we take needs to be able to support a user-specified synchronization mechanism similar to how |
To me, it was totally clear that a
I was trying to express the idea that in any case, we need a method to "upgrade" the shared references to
Actually, I do not think that this is true anymore. Bus sharing for I2C or SPI can be done (single-threaded example) by just moving a bus implementation which requires I have the impression that we are discussing two related but distinct problems:
Again, in the bus example, these questions are more easily answered:
Regarding network stacks, my statements are primarily concerned with the first question. So in a first step, I would switch to a socket-oriented network stack trait which is defined with sharing in mind. Afterwards, I would work on the second question and see if we can work out a solution to support different sharing requirements with generic implementations. |
I was discussing the design of the NAL with @jordens the other day and an interesting point was brought up.
Why not change the API of the
embedded-nal
to more closely follow python/std::net
?Sample proposal:
This allows stack implementations to freely hand out sockets without having to worry about coherency between multiple threads when the network stack assigns a unique buffer to each socket. In other cases, the network stack can implement some means of synchronizing access (e.g. for off-chip network stacks controlled via serial communications).
This makes it much easier to use multiple sockets in different threads/contexts with a single network stack.
What are some of your thoughts on this @jonahd-g / @MathiasKoch ?
The text was updated successfully, but these errors were encountered: