rtic-rs / rfcs

11 stars 6 forks source link

rtic-sync: DynSender and DynReceiver #62

Open wiktorwieclaw opened 1 year ago

wiktorwieclaw commented 1 year ago

I'd like to have dynamically dispatched versions of Sender and Receiver with type erased queue size.

struct Sender<'a, T, const N: usize>(&'a Channel<T, N>);

// no queue size in the type!
struct DynSender<'a, T>(&'a dyn ChannelImpl<T>);

Similar feature is already implemented in embassy_sync.

Motivation

I'm trying to implement an actor model around RTIC. Each actor is supposed to have an address that can be used to send messages to it.

struct Addr<A: Actor, const N: usize> {
    sender: Sender<'static, A::Msg, N>
}

Having the channel's size embedded in the Address type is inconvenient and makes some patterns impossible.

Implementation

trait ChannelImpl<T> {
    fn send_footer(&mut self, idx: u8, val: T);
    fn try_send(&mut self, val: T) -> Result<(), TrySendError<T>>;
    // oh no! #![feature(async_fn_in_trait)] required, more on that later...
    async fn send(&mut self, val: T) -> Result<(), NoReceiver<T>>;
    fn try_recv(&mut self) -> Result<T, ReceiveError>;
    async fn recv(&mut self) -> Result<T, ReceiveError>;
    fn is_closed(&self) -> bool;
    fn is_full(&self) -> bool;
    fn is_empty(&self) -> bool;
    fn handle_sender_clone(&mut self);
    fn handle_receiver_clone(&mut self);
    fn handle_sender_drop(&mut self);
    fn handle_receiver_drop(&mut self);
}

impl<T, const N: usize> ChannelImpl<T> for Channel<T, N> {
     // -- snip! --
}

struct Sender<'a, T, const N: usize>(&'a Channel<T, N>);

impl<'a T, const N: usize> Sender<'a, T, N> {
        fn into_dyn(self) -> DynSender<'a, T> {
            let sender = DynSender(self.0);
            core::mem::forget(self);
            sender
        }

        #[inline(always)]
        fn try_send(&mut self, val: T) -> Result<(), TrySendError<T>> {
             // forward
             self.0.try_send(val)
        }

        // forward implementation for the other methods
        // -- snip! --
}

struct DynSender<'a, T>(&'a dyn ChannelImpl<T>);

impl<'a T> DynSender<'a, T> {
        #[inline(always)]
        fn try_send(&mut self, val: T) -> Result<(), TrySendError<T>> {
             // forward
             self.0.try_send(val)
        }

        // forward implementation for the other methods
        // -- snip! --
}

Of course, we can't have async functions in traits, but we could work around that by manually implementing SendFuture and RecvFuture types and returning them from regular functions.

fn send(&mut self, val: T) -> SendFuture;

I'd be happy to write a PR if I get a green light, I may want to ask for some help implementing the futures as I've never done this before.

korken89 commented 1 year ago

Hi, sorry for missing this!

I think this is a great idea, a PR world be highly appreciated! If you get started, ping me in matrix and I'll give feedback early :)

0xf4lc0n commented 1 year ago

I'm interested in implementing this feature.

wiktorwieclaw commented 1 year ago

To clarify I'd also like to work on this, I just was a bit busy last month. @0xf4lc0n we can work together on it if you wish

0xf4lc0n commented 1 year ago

Sure, I'm looking forward to cooperating.

0xf4lc0n commented 1 year ago

I would like to present my current solution for this problem in rtic-rs/rtic. You can view the whole code in here.

I've introduced six new structs and two traits to enhance the functionality:

Additionally, I've introduced two traits:

I've also added tests to ensure the correctness of my implementation.

However, there are a couple of challenges I encountered:

Please feel free to review the code and provide feedback or suggestions for improvement.