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

rtic-sync: DynSender and DynReceiver #62

Open
wiktorwieclaw opened this issue Jun 8, 2023 · 5 comments
Open

rtic-sync: DynSender and DynReceiver #62

wiktorwieclaw opened this issue Jun 8, 2023 · 5 comments

Comments

@wiktorwieclaw
Copy link

wiktorwieclaw commented Jun 8, 2023

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
Copy link
Collaborator

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
Copy link

I'm interested in implementing this feature.

@wiktorwieclaw
Copy link
Author

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
Copy link

Sure, I'm looking forward to cooperating.

@0xf4lc0n
Copy link

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:

  • SendFuture: Contains code previously placed within Sender::send(). By implementing the Future and Drop traits, I was able to eliminate the need for poll_fn and dropper functions.
  • RecvFuture: Contains code previously placed within Receiver::recv().
  • DynSendFuture: Similar to SendFuture but utilizes dynamic dispatch.
  • DynRecvFuture: Similar to RecvFuture but uses dynamic dispatch.
  • DynSender: A size-agnostic sender.
  • DynReceiver: A size-agnostic receiver.

Additionally, I've introduced two traits:

  • InternalAccess: This trait provides functions presented by @wiktorwieclaw in his ChannelImpl example. Furthermore, it includes functions abstracting the logic required by send and receive operations. This abstraction is especially crucial for DynSendFuture and DynRecvFuture, as these structs contain &dyn DynChannel, which disallows direct access to the fields of the Channel struct.
  • DynChannel: This trait offers dynamic send and dynamic receive functions.

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

However, there are a couple of challenges I encountered:

  • Code Duplication: SendFuture and DynSendFuture share the same logic in their poll functions, but I faced difficulties in extracting this code into a separate function and sharing it between the future's poll functions. This issue arises because SendFuture depends on &Channel, while DynSendFuture depends on &dyn DynChannel. The same applies for RecvFuture and DynRecvFuture.
  • Substantial Code Introduced by Features and InternalAccess Trait: The introduction of features and the InternalAccess trait has added a significant amount of code to the project.

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

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

No branches or pull requests

3 participants