Skip to content
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

Revisit on mutable self references #39

Closed
kellerkindt opened this issue Dec 31, 2020 · 21 comments · Fixed by #42
Closed

Revisit on mutable self references #39

kellerkindt opened this issue Dec 31, 2020 · 21 comments · Fixed by #42

Comments

@kellerkindt
Copy link

I don't understand the reason for immutable self references that clearly do mutate the state of self or require exclusive access to the underlying implementation or bus. I think this design decision only causes hacks to circumvent it, unnecessary overhead in the driver implementation (which is the opposite of 'zero cost'/'you only pay for what you need') and does not reflect the strategy of the rust ecosystem, such as embedded-hal, std-Read, std-Write, tokio-AsyncRead or tokio-AsyncWrite.

I don't follow the argument of @thejpster here as it seems more of an special case scenario and could be solved otherwise by implementing Clone on what seems to be an FD or raw-pointer(?) internally.

Feel free to convince me otherwise.

We stumbled over this in a discussion here where @jonahd-g (thanky you!) proposes an implementation of embedded-nal for the w5500 driver.

@thejpster
Copy link
Member

I think my example stands. If you can show me a functioning example which has at least two users of embedded-nal (e.g. an HTTP client and something else - CoAP, SMTP, DNS, MQTT, or whatever), and that the burden of juggling the mut-ref between two entirely independent protocol implementations isn't onerous, I'm happy to be persuaded. I come at this as a protocol and wireless systems engineer and so separation of concerns is important to me. Plus I prefer to place complexity in the nal impl rather than on the nal user, so it only has to be done once. I understand though, that others may have different priorities and if your model is, say, the Arduino HTTP library, I can see why embedded-nal might grate a little.

I also think my example of the BSD socket library is valid, as it's what almost all stacks are based on (certainly Linux, Windows and macOS are). There, the mutable state is entirely an issue for your OS, and not your application or library - you do not have to lock anything, you just call 'open' or 'write', etc. This is what I have tried to follow, but using an immutable reference to alleviate the need for a global static state variable, which makes unit testing harder.

@kellerkindt
Copy link
Author

In my opinion, the comparison with the BSD Socket API or POSIX (connect) API is not fair or completely correct. There is no equivalent argument for &self (which would be the ~kernel, I guess?). To some extend, the functions are called on a global static mut instance via sys-calls.

embedded-nal seems to build driver abstractions for devices (if I get this correctly?), but you compare it to the features of an full fledged Kernel/OS?

In my understanding, if I need an instance to be mutable from two or more sources, I would then wrap it in a RefCell or Mutex or whatever fits. Enforcing this on the driver will either cause some overhead (RefCell), more overhead (Mutex) - regardless of whether this is actually required - or lacking support for either/some use case scenarios at the driver level.

I see your point that it might become awkward managing and passing along wrapped driver impls. But I still think, writing a little (user-)wrapper is more flexible than enforcing support for all scenarios on the driver level.

pub trait UdpClientStack {
	fn send(&mut self, socket: &mut Self::UdpSocket, buffer: &[u8]) -> nb::Result<(), Self::Error>;
}

pub struct MyDriver { ... };
impl UdpClientStack for MyDriver {
	fn send(&mut self, socket: &mut Self::UdpSocket, buffer: &[u8]) -> nb::Result<(), Self::Error> {
            // do the stuff without overhead
        }
}

pub struct RefWrapper<'a>(&'a RefCell<MyDriver>);
// pub struct MutexWrapper<'a>(&'a Mutex<MyDriver>); // as alternative in threaded environments
// pub struct RcWrapper(Rc<RefCell<MyDriver>>); // for alloc environments

impl UdpClientStack for RefWrapper<'_> {
	fn send(&mut self, socket: &mut Self::UdpSocket, buffer: &[u8]) -> nb::Result<(), Self::Error> {
            self.0.borrow_mut().send(socket, buffer)
        }
}

fn main() {
    let mut driver = MyDriver::new();
    send(&mut driver); // no overhead

    let shared_driver = RefCell::new(driver);
    let mut http_ref = RefWrapper(&shared_driver);
    let mut smtp_ref = RefWrapper(&shared_driver);
    let mut mqtt_ref = smtp_ref.clone(); // or if clone is derived

    send(&mut http_ref); // opt-in overhead

    let mut client = http_connect(http_ref, "https://www.google.de/search?q=what+is+my+ip");
    let ip = client.read(..);
}

fn send(udp: &mut impl UdpClientStack) {
    // ...
}

// for the sake of typing-simplicity of this example, assuming HTTP travels over UDP
fn http_connect<T: UdpClientStack>(my_owned_stack: T, url: &str) -> HttpClient { ... }

If I am not mistaken, the lifetimes of *_ref in the example above should behave the same as with direct immutable references to the driver.

@thejpster
Copy link
Member

embedded-nal seems to build driver abstractions for devices (if I get this correctly?), but you compare it to the features of an full fledged Kernel/OS?

embedded-nal exists because I was annoyed it was so difficult to write an HTTP library that worked on both a Quectel BC68 (TCP over AT over UART) and a Nordic nRF9160 (BSDish sockets). I then didn't have time to finish the idea, so it ended up here.

Your argument that we should bring the choice of which mutex (if any) to use up to the application layer, as opposed to putting it at the TCP/UDP layer is persuasive. It does make assembling an application more complicated (outside of the naive case of using a single NAL protocol), but perhaps all the tedious .borrow_mut() wrappers can be generated with macros.

@chrysn
Copy link
Contributor

chrysn commented Jan 11, 2021

I think we can have a best-of-both-worlds situation if we do require a &mut, but encourage stack authors to provide clonable, runtime-mutexed version of it.

Thus, it would be up to the high-level application author whether they want runtime efficiency (passing the single &mut stack to whichever part of their software just needs it, taking it back from them when that part is dormant) or whether they just take the runtime overhead of needlessly going through the mutex several times in code that could really do without, and just pass everyone an owned object that performs a lock/unlock on every single operation.

@Sympatron
Copy link
Contributor

@kellerkindt I think only MutexWrapper is the only example you gave that makes sense in practice, because the other ones panic if the stack is already borrowed.
In general I would argue for flexibility, too. But I think in this case putting the burden to add synchronization on every implementer of embedded-nal is much less painful than putting it on every user. The only way forward for this idea I can see is providing some kind of MutexWrapper in embedded-nal which allows users to make their stack concurrent without the need for implementers to care about synchronization. If somebody can come up with how this might look, this could be useful.

In general I would say that a simple, single threaded application, where you don't need mutexes is probably not the majority and should probably not be the focus of embedded-nal.

@jonahbron
Copy link

jonahbron commented Jan 14, 2021

I find @kellerkindt's comment about zero-cost-abstractions to be compelling. Requiring all driver implementations to be internally thread-safe puts a wasted cost on applications that don't need it. I also think he's right that it's unfair to compare an embedded driver to the interface provided by an operating system.

IMO the NAL should dictate only the most simple common abstraction possible. The whole point of an abstraction layer is that higher level features (like convenient interfaces) can be implemented once for all abstraction implementations. A feature like thread safety (however important to whatever potential majority) falls outside of that simple abstraction.

My thoughts on this align with what @Sympatron suggested: providing a thread-safe wrapper. It would need the following attributes:

  • Take ownership of (or mutably borrow) a NAL implementation
  • Implement the NAL traits itself
  • Implement the Sync trait (? I'm not well versed on multithreading in Rust; or maybe the Copy trait).

Applications that need to share the driver would simply use the type constraint UdpClientStack + Sync (for example). I'd also suggest that perhaps this wrapper may belong in a different crate, since it would technically be in implementation of embedded-nal.

If this is a direction that is acceptable to the maintainers, I'd be willing to start experimenting into how this could be done. I don't expect it would be particularly complicated, but I'm still a junior rustacean at this point (especially when it comes to thread safety); it's possible there are issues I haven't yet considered.

@MathiasKoch
Copy link
Collaborator

I would have no issue going in that direction, and would definitely be very keen on seeing the result. Can any of you think of other abstration crates that does it in this way? or the other way for that sake?

Thoughts @ryan-summers @eldruin ?

@ryan-summers
Copy link
Member

ryan-summers commented Jan 14, 2021

I personally am more aligned with @chrysn's opinion. Specifically, I do see a lot of applications that will only use a single protocol on top of the embedded-nal, where the NAL is intended just to be an abstract API so that a protocol isn't tied to a particular stack.

However, as noted, some users may need to use it with multiple protocols, and forcing them to handle the synchronization is quite an ask (it's a complex, hard topic).

I figure it may be worth stating here, but in the shared-bus crate, it comes to light that creating a generic mutex is a very non-trivial ask. Thus, it may not even make sense for embedded-nal implementers to provide a default-safe, sharable stack for all types of architectures.

In Summary

If someone can persuade me that a generic implementer of embedded-nal can implement a safe, sharable stack for all (or at least, multiple) architectures, then I see the benefit of the immutable self reference. However, if that's not feasible, it doesn't make sense to force this design decision on implementers (it does add a lot of overhead, having written two implementations myself).

@Sympatron
Copy link
Contributor

Replicating something like shared-bus sounds like a good idea. If somebody is able to pull this off the synchronization would be contained in one place. But I don't know how complicated this would be.

@thejpster
Copy link
Member

thejpster commented Jan 16, 2021

I think the object which hands out mutable access to the stack is necessarily going to depend on the RTOS (or not) that you are running.

  • Using RTIC, the exclusivity is given by the framework and the Interrupt controller, so you need an impl UDPStack for RTIC_UDP_Proxy.
  • Using FreeRTOS, the exclusivity will be through a FreeRTOS mutex that suspends the task until the stack is available, so you need a impl UDPStack for FreeRTOS_UDP_Proxy
  • Using Mentor Nucleus you will need to use the Nucleus mutex...
  • Using mbedOS you will need to use the mbedOS / CMSIS Nucleus...
  • If you have none of the above, you can just use the stack implementation directly.

Tangentially, this doesn't solve the issue that if the reference isn't immutable, you can't store the stack reference in the socket object. This means you need to be very careful to pass the correct socket object to the correct stack object.

let mut buffer = [0u8; 1500];
let mut stack_one = QuectelTcpStack(uart1);
let mut stack_two = QuectelTcpStack(uart2);
let mut stack_three = PosixTcpStack();
let mut socket_one = stack_one.open("google.com", 80);
let results = stack_two.read(&mut socket_one, &mut buffer); /* boom - and even type checking can't help here */
let results = stack_three.read(&mut socket_one, &mut buffer); /* also boom - but type checking could catch this */

In an ideal world you could do:

let results = socket_one.read(&mut buffer);

But, this is incompatible with the decision around using &mut that I think we kind of agreed on above.

@jonahbron
Copy link

jonahbron commented Jan 17, 2021

@thejpster While the current embedded-nal traits require internal mutability, they don't enforce thread safety. Right now whether a driver is thread-safe is entirely up to the implementation. Since that's the status quo, I think we should solve and release a single-thread solution, then come up with a thread-safe solution after. What do you think?

Here's what I have so far on a single-threaded sharable wrapper, similar to shared-bus.

Edit: stop, don't real all this code. Read the later revision: #39 (comment)

use core::cell::RefCell;
use embedded_nal::{SocketAddr, UdpClient};

pub struct SharedUdpClientManagerSimple<T: UdpClient> {
    driver: RefCell<T>,
}

impl<T: UdpClient> SharedUdpClientManagerSimple<T> {
    pub fn new(driver: T) -> Self {
        SharedUdpClientManagerSimple {
            driver: RefCell::new(driver),
        }
    }

    pub fn acquire(&self) -> SharedUdpClientSimple<T> {
        SharedUdpClientSimple {
            driver: &self.driver,
        }
    }
}

pub struct SharedUdpClientSimple<'a, T: UdpClient> {
    driver: &'a RefCell<T>,
}

impl<'a, T: UdpClient> UdpClient for SharedUdpClientSimple<'a, T> {
    type Error = T::Error;
    type UdpSocket = T::UdpSocket;

    fn connect(&mut self, address: SocketAddr) -> Result<Self::UdpSocket, Self::Error> {
        self.driver.borrow_mut().connect(address)
    }

    fn send(
        &mut self,
        socket: &mut Self::UdpSocket,
        data: &[u8],
    ) -> Result<(), embedded_nal::nb::Error<<T as embedded_nal::UdpClient>::Error>> {
        self.driver.borrow_mut().send(socket, data)
    }

    fn receive(
        &mut self,
        socket: &mut Self::UdpSocket,
        data: &mut [u8],
    ) -> Result<(usize, SocketAddr), embedded_nal::nb::Error<<T as embedded_nal::UdpClient>::Error>>
    {
        self.driver.borrow_mut().receive(socket, data)
    }

    fn close(&mut self, socket: Self::UdpSocket) -> Result<(), Self::Error> {
        self.driver.borrow_mut().close(socket)
    }
}

This compiles nicely. This depends on an embedded-nal patched to use mutable references. Obviously this isn't super useful yet, since you can only share a single trait from the NAL at a time. I need to think more about how to permit sharing as many stack traits as are available.

@thejpster
Copy link
Member

Makes sense.

Should it be in this crate though? Or should it be in embedded-nal-sharing (name TBD)?

@jonahbron
Copy link

jonahbron commented Jan 17, 2021

@thejpster Different crate IMO. I'm using that same name for my testing crate.

Took me a little while of lying in bed to realize how to share all the different traits. This wraps and shares all three traits nicely.

Usage:

let mut manager = SharedNalManager::new(/* initialize driver*/);
let driver1 = manager.acquire();
let driver2 = manager.acquire();

let tcp_socket = driver1.open();
let udp_server_socket = driver2.bind(/* ... */);
// ...

Code:

Edit: warning again, don't read all this. There's a better one below: #39 (comment)

use core::cell::RefCell;
use embedded_nal::{nb, SocketAddr, TcpStack, UdpClient, UdpServer};

pub struct SharedNalManager<T> {
    driver: RefCell<T>,
}

impl<T> SharedNalManager<T> {
    pub fn new(driver: T) -> Self {
        SharedNalManager {
            driver: RefCell::new(driver),
        }
    }

    pub fn acquire(&self) -> SharedNal<T> {
        SharedNal {
            driver: &self.driver,
        }
    }
}

pub struct SharedNal<'a, T> {
    driver: &'a RefCell<T>,
}

impl<'a, T: UdpClient> UdpClient for SharedNal<'a, T> {
    type Error = T::Error;
    type UdpSocket = T::UdpSocket;

    fn connect(&mut self, address: SocketAddr) -> Result<Self::UdpSocket, Self::Error> {
        self.driver.borrow_mut().connect(address)
    }

    fn send(
        &mut self,
        socket: &mut Self::UdpSocket,
        data: &[u8],
    ) -> Result<(), nb::Error<<T as embedded_nal::UdpClient>::Error>> {
        self.driver.borrow_mut().send(socket, data)
    }

    fn receive(
        &mut self,
        socket: &mut Self::UdpSocket,
        data: &mut [u8],
    ) -> Result<(usize, SocketAddr), nb::Error<<T as embedded_nal::UdpClient>::Error>> {
        self.driver.borrow_mut().receive(socket, data)
    }

    fn close(&mut self, socket: Self::UdpSocket) -> Result<(), Self::Error> {
        self.driver.borrow_mut().close(socket)
    }
}

impl<'a, T: UdpServer> UdpServer for SharedNal<'a, T> {
    fn bind(&mut self, local_port: u16) -> Result<Self::UdpSocket, Self::Error> {
        self.driver.borrow_mut().bind(local_port)
    }

    fn send_to(
        &mut self,
        socket: &mut Self::UdpSocket,
        remote: SocketAddr,
        buffer: &[u8],
    ) -> nb::Result<(), Self::Error> {
        self.driver.borrow_mut().send_to(socket, remote, buffer)
    }
}

impl<'a, T: TcpStack> TcpStack for SharedNal<'a, T> {
    type TcpSocket = T::TcpSocket;
    type Error = T::Error;

    fn open(&mut self) -> Result<Self::TcpSocket, Self::Error> {
        self.driver.borrow_mut().open()
    }

    fn connect(
        &mut self,
        socket: Self::TcpSocket,
        remote: SocketAddr,
    ) -> Result<Self::TcpSocket, Self::Error> {
        self.driver.borrow_mut().connect(socket, remote)
    }

    fn is_connected(&mut self, socket: &Self::TcpSocket) -> Result<bool, Self::Error> {
        self.driver.borrow_mut().is_connected(socket)
    }

    fn write(
        &mut self,
        socket: &mut Self::TcpSocket,
        buffer: &[u8],
    ) -> nb::Result<usize, Self::Error> {
        self.driver.borrow_mut().write(socket, buffer)
    }

    fn read(
        &mut self,
        socket: &mut Self::TcpSocket,
        buffer: &mut [u8],
    ) -> nb::Result<usize, Self::Error> {
        self.driver.borrow_mut().read(socket, buffer)
    }

    fn close(&mut self, socket: Self::TcpSocket) -> Result<(), Self::Error> {
        self.driver.borrow_mut().close(socket)
    }
}

@jonahbron
Copy link

@kellerkindt ping for your feedback on this

@jonahbron
Copy link

jonahbron commented Jan 17, 2021

Tried my hand at writing a macro for the first time, and wow, between the intuitive syntax and the clear error messages, that was really easy. Got rid of the infinite list of self.driver.borrow_mut()s.

Edit: realized I hadn't pulled in a while, so this is now up-to-date with jonahd-g/embedded-nal#no-internal-mutability

use core::cell::RefCell;
use embedded_nal::{nb, SocketAddr, TcpClientStack, TcpFullStack, UdpClientStack, UdpFullStack};

pub struct SharedNalManager<T> {
    driver: RefCell<T>,
}

impl<T> SharedNalManager<T> {
    pub fn new(driver: T) -> Self {
        SharedNalManager {
            driver: RefCell::new(driver),
        }
    }

    pub fn acquire(&self) -> SharedNal<T> {
        SharedNal {
            driver: &self.driver,
        }
    }
}

pub struct SharedNal<'a, T> {
    driver: &'a RefCell<T>,
}

macro_rules! forward {
    ($func:ident($($v:ident: $IT:ty),*) -> $T:ty) => {
        fn $func(&mut self, $($v: $IT),*) -> $T {
            self.driver.borrow_mut().$func($($v),*)
        }
    }
}

impl<'a, T: UdpClientStack> UdpClientStack for SharedNal<'a, T> {
    type Error = T::Error;
    type UdpSocket = T::UdpSocket;

    forward! {socket() -> Result<Self::UdpSocket, Self::Error>}
    forward! {connect(socket: &mut Self::UdpSocket, address: SocketAddr) -> Result<(), Self::Error>}
    forward! {send(socket: &mut Self::UdpSocket, data: &[u8]) -> Result<(), nb::Error<<T as embedded_nal::UdpClientStack>::Error>>}
    forward! {receive(socket: &mut Self::UdpSocket, data: &mut [u8]) -> Result<(usize, SocketAddr), nb::Error<<T as UdpClientStack>::Error>>}
    forward! {close(socket: Self::UdpSocket) -> Result<(), Self::Error>}
}

impl<'a, T: UdpFullStack> UdpFullStack for SharedNal<'a, T> {
    forward! {bind(socket: &mut Self::UdpSocket, local_port: u16) -> Result<(), Self::Error>}
    forward! {send_to(socket: &mut Self::UdpSocket, remote: SocketAddr, buffer: &[u8]) -> Result<(), nb::Error<<T as UdpClientStack>::Error>>}
}

impl<'a, T: TcpClientStack> TcpClientStack for SharedNal<'a, T> {
    type TcpSocket = T::TcpSocket;
    type Error = T::Error;

    forward! {socket() -> Result<Self::TcpSocket, Self::Error>}
    forward! {connect(socket: &mut Self::TcpSocket, address: SocketAddr) -> Result<(), nb::Error<<T as TcpClientStack>::Error>>}
    forward! {send(socket: &mut Self::TcpSocket, data: &[u8]) -> Result<usize, nb::Error<<T as embedded_nal::TcpClientStack>::Error>>}
    forward! {receive(socket: &mut Self::TcpSocket, data: &mut [u8]) -> Result<usize, nb::Error<<T as TcpClientStack>::Error>>}
    forward! {is_connected(socket: &Self::TcpSocket) -> Result<bool, Self::Error>}
    forward! {close(socket: Self::TcpSocket) -> Result<(), Self::Error>}
}

impl<'a, T: TcpFullStack> TcpFullStack for SharedNal<'a, T> {
    forward! {bind(socket: &mut Self::TcpSocket, port: u16) -> Result<(), <T as TcpClientStack>::Error>}
    forward! {listen(socket: &mut Self::TcpSocket) -> Result<(), <T as TcpClientStack>::Error>}
    forward! {accept(socket: &mut Self::TcpSocket) -> Result<(<T as TcpClientStack>::TcpSocket, SocketAddr), nb::Error<<T as TcpClientStack>::Error>>}
}

@ryan-summers
Copy link
Member

ryan-summers commented Jan 18, 2021 via email

@MathiasKoch
Copy link
Collaborator

A point worth considering in this, would also be that RefCell does not work out great with Futures and async/await once that becomes a thing in embedded contexts.. Not that it would be a main motivator, but might be worth considering as part of this discussion.

@ryan-summers
Copy link
Member

I think we've come to the realization that any thread-safety provided by embedded-nal has to be managed similarly to how shared-bus manages thread safety (e.g. via external library wrappers for creating mutex'd proxies in a platform-dependent manner). As such, given that these functions often do require internal mutability of the stack (and logically do as well), I would be happy to move forward with adding mut to self references here.

Thoughts?

@Sympatron
Copy link
Contributor

Having looked a little deeper into shared-bus, I think this is exactly what we would want for embedded-nal, too. I think all problems mentioned can be handled like that. It is also generic over it's Mutex to support all possible use cases.

I'm not convinced that it has to be in it's own crate though, because it is such a common requirement and shared-bus shows that it can be solved generically. Plus nobody would be forced to use what embedded-nal has to offer and can just as easily write their own StackManager. I think providing a sensible StackManager that can be used in most cases directly in embedded-nal would be convenient.

@ryan-summers
Copy link
Member

I'm not convinced that it has to be in it's own crate though, because it is such a common requirement and shared-bus shows that it can be solved generically. Plus nobody would be forced to use what embedded-nal has to offer and can just as easily write their own StackManager. I think providing a sensible StackManager that can be used in most cases directly in embedded-nal would be convenient.

As far as I'm concerned, that's out-of-scope for the discussion in this thread. We do need something like shared-bus if we want to share the stack, but whether or not that lives here can be discussed in a new issue (this one is pretty hefty as-is).

@jonahbron
Copy link

Am I understanding the consensus to be

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging a pull request may close this issue.

7 participants