1

I have an struct that implements the trait A which has the function fn consume. I want to pass a callback to this struct, to be called by fn consume. Something like this:

pub type OnVirtualTunWrite = Arc<dyn Fn(?, usize) -> Result<(), VirtualTunWriteError> + Send + Sync>;

It's on an Arc because it's shared between threads.

struct A {
    on_virtual_tun_write: OnVirtualTunWrite
}

impl S for A {
    fn consume<R, F>(self, _timestamp: Instant, len: usize, f: F) -> smoltcp::Result<R>
    where
        F: FnOnce(&mut [u8]) -> smoltcp::Result<R>,
    {
        let mut lower = self.lower.as_ref().borrow_mut();
        //I should send this f to self.on_virtual_tun_write
        (self.on_virtual_tun_write)(f, len);
        //return the appropriate result here

OnVirtualTunWrite is a closure that should receive the f,len from fn consume and then use it like this:

let my_on_virtual_tun_write = Arc::new(|?, len| -> ?{
    let mut buffer = Vec::new(len);
    buffer.resize(len);
    //fills buffer with data
    f(buffer);
})

How can I make my OnVirtualTunWrite?

I tried Arc<dyn Fn(dyn FnOnce(&mut [u8]), usize) -> Result<(), ()> + Send + Sync> but it won't work because dyn Fn must have arguments with size know at compile time.

Also, there's still a small problem: how do I return -> smoltcp::Result<R> in OnVirtualTunWrite if OnVirtualTunWrite can't possibly know R?

Gatonito
  • 1,662
  • 5
  • 26
  • 55
  • Also does this help? https://stackoverflow.com/questions/26577070/how-to-use-the-fn-traits-closures-in-rust-function-signatures – hellow Mar 11 '21 at 06:05
  • what about this one? https://stackoverflow.com/questions/32618872/sized-is-not-implemented-for-the-type-fn – hellow Mar 11 '21 at 07:01

1 Answers1

2

I tried Arc<dyn Fn(dyn FnOnce(&mut [u8]), usize) -> Result<(), ()> + Send + Sync>

That should be &dyn FnOnce(...), but that won't work either because calling FnOnce automatically moves it, so it can't be called from behind a reference. The simplest solution is to introduce an extra allocation in consume, because Box<dyn FnOnce> implements FnOnce itself since Rust 1.35. For example (playground):

pub type OnVirtualTunWrite = Arc<
    dyn Fn(Box<dyn FnOnce(&mut [u8])>, usize) -> Result<(), VirtualTunWriteError> + Send + Sync,
>;

pub struct A {
    pub on_virtual_tun_write: OnVirtualTunWrite,
}

impl A {
    pub fn consume<F>(&self, f: F)
    where
        F: FnOnce(&mut [u8]) + 'static,
    {
        (self.on_virtual_tun_write)(Box::new(f), 0).unwrap();
    }
}

To avoid the allocation, you can use the technique described here to invoke FnOnce from an FnMut. It uses Option rather than Box, so it's zero-cost, or at least allocation-free. For example (full code in the playground):

pub type OnVirtualTunWrite = Arc<
    dyn Fn(&mut dyn FnMut(&mut [u8]), usize) -> Result<(), VirtualTunWriteError> + Send + Sync,
>;

trait CallOnceSafe {
    fn call_once_safe(&mut self, x: &mut [u8]);
}

impl<F: FnOnce(&mut [u8])> CallOnceSafe for Option<F> {
    fn call_once_safe(&mut self, x: &mut [u8]) {
        // panics if called more than once - but A::consume() calls it
        // only once
        let func = self.take().unwrap();
        func(x)
    }
}

impl A {
    pub fn consume<F>(&self, f: F)
    where
        F: FnOnce(&mut [u8]) + 'static,
    {
        let mut f = Some(f);
        (self.on_virtual_tun_write)(&mut move |x| f.call_once_safe(x), 0).unwrap();
    }
}

The above works by first moving the FnMut into an Option, and calling it through call_once_safe, a method on a private CallOnceSafe trait with a blanket impl for Option<T: FnOnce(...)>. The blanket implementation moves the closure out of the Option and invokes it. This is allowed because the size of the closure is known in the blanket implementation, which is generic over T.

The blanket impl can get away with mutating rather than consuming self because it uses Option::take to move the content out of the option, while leaving it empty and otherwise usable. Not consuming the option allows call_once_safe to be called from an FnMut closure, such as the one created in consume and passed as argument to OnVirtualTunWrite. call_once_safe does consume the actual FnOnce closure contained in the Option, thus preserving the invariant that the closure is called no more than once. If consume were to call call_once_safe on the same Option<F> twice (or if the outer closure called its first argument more than once), it would result in a panic.

user4815162342
  • 141,790
  • 18
  • 296
  • 355
  • Thanks, very good workaround to prevent Box allocation. However, your consume ignores the `` generic. I'm forced to use it in the trait implementation. And I don't know how to use the return type from `f`, which is `smoltcp::Result`, in its caller, which is `OnVirtualTunWrite` – Gatonito Mar 11 '21 at 22:52
  • @Gatonito Can `OnVirtualTunWrite` and `A` be generic over `R`? I really don't see how it could work otherwise. – user4815162342 Mar 11 '21 at 23:23
  • That would force the struct that holds the `OnVirtualTunWrite` to be generic over R too, rigth? I'm thinking of a way out of this. Maybe return a generic Ok(R) if ok and Err(R) if not, but I think even this is not possible, as R might not be default constructible? – Gatonito Mar 11 '21 at 23:41
  • I thought of something like this: `let r; let result = (lower.on_virtual_tun_write)(Box::new(|b: &mut [u8]| { r = f(b); }), len);` but it appears that I cannot modify the outside `r` from the Box closure – Gatonito Mar 12 '21 at 01:02
  • @Gatonito Yes, both the closure and `A` need to be generic over `R`, I don't see a way around that - given the lack of any trait bounds on `R` (no `Default`, no `From`) there is no way you could invent your own `R` value inside `consume`. You could return an `Err(smoltcp::Error(Exhausted))` or something, and it would compile, but it's unlikely to work correctly. I'm not sure where you're going with `let r`, but perhaps that will work if you make the closure `move`. – user4815162342 Mar 12 '21 at 07:37