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

Possible to dynamic dispatch on Device trait? #957

Open
Dr-TSNG opened this issue Jul 21, 2024 · 3 comments
Open

Possible to dynamic dispatch on Device trait? #957

Dr-TSNG opened this issue Jul 21, 2024 · 3 comments

Comments

@Dr-TSNG
Copy link

Dr-TSNG commented Jul 21, 2024

I am trying to put different Device implementations into something like Box<dyn Device>. Moreover, I want to have something looks like

struct NetInterface {
    device: Box<dyn Device>,
    iface: Interface,
    sockets: SocketSet<'static>,
}

Without which, I have to write a lot of duplicate codes to support multiple physical network interfaces. However the trait doesn't meet object safe requirements. I wonder if this can be done with some tricks.

@rvbcldud
Copy link

Is doing something like the following forcing you to write a lot of duplicate code?

struct NetInterface<D> {
    device: D,
    iface: Interface,
    sockets: SocketSet<'static>,
}

@mammothbane
Copy link

I have a similar problem — I'm working on a virtual ethernet switch that operates over a variable number of unknown phy::Devices:

struct VSwitch<'a, const MAX_DEVICES: usize> {
	devices: heapless::Vec<MAX_DEVICES, &'a mut dyn smoltcp::phy::Device>,
}

This isn't possible with the current phy::Device trait as it's not object-safe.

I'm considering a wrapper/"trampoline" trait that impls phy::Device with TxToken and RxToken specialized to my crate, something like:

// These need to wrap the {Rx,Tx}Token traits, which I *think* (hope) can be done 
// with a closure -- might be impossible.
struct MyCrateTxToken;
struct MyCrateRxToken;

trait DeviceTrampoline {
	fn receive(&mut self, timestamp: Instant) -> Option<(MyCrateRxToken, MyCrateTxToken)>;
	fn transmit(&mut self, timestamp: Instant) -> Option<MyCrateTxToken>;
	fn capabilities(&self) -> DeviceCapabilities;
}

impl<T> DeviceTrampoline for T 
where T: phy::Device { 
	/* delegate */ 
}

// Now (I think) I can have:
struct VSwitch<'a, const MAX_DEVICES: usize> {
	devices: heapless::Vec<MAX_DEVICES, &'a mut dyn DeviceTrampoline>,
}

This feels unfortunate in that I think dynamic dispatch for phy::Device is generally useful, yet there isn't a canonical object-safe wrapper. That said, I'm not sure there's anything that can be done in smoltcp while maintaining the flexibility of TxToken and RxToken — I just wanted to provide another data point.

@mammothbane
Copy link

I wrote a proof-of-concept crate that enables this -- dyn_phy. Uses tiny_fn to erase the *Token concrete types (and hence isn't guaranteed to be zero-alloc, so I don't think suitable in its current form for upstreaming).

My observation in looking at this problem is that the token traits are the obstacle to making Device object-safe, and the tokens aren't object safe because a) they have a type parameter in consume and b) take an owned self:

trait TxToken {
    fn consume<R, F>(self, f: F) -> R
    where 
        F: FnOnce(&mut [u8]) -> R {
        // ...
    }
}

fn use_token(tok: impl TxToken) {
    let result = tok.consume(move |buf| {
        // ... process frame
        buf.len() // some computation over the buffer
    });
}

The type parameter is easily eliminated if you accept allocation for the sake of argument -- the same functionality can be expressed like this:

trait TxToken {
    // I want "dyn FnOnce" by value here, but that needs unsized_fn_params.
    // FnMut isn't suitable because it unduly restricts the function implementation 
    // (e.g. we need FnOnce for tx-from-rx semantics in Device::receive)
    fn consume(self, f: Box<dyn FnOnce(&mut [u8])>)
        // ...
    }
}

// Ditto re: unsized_fn_params here
fn use_token(tok: Box<dyn TxToken>) {
    let mut result = None;

    {
        let result = &mut result;

        tok.consume(move |buf| {
            // ... process frame
            *mut result = Some(buf.len()); // some computation over the buffer
        });
    }

    let result = result.unwrap();
}

The point being that you can write TxToken as a trait object and still get a value out of the FnOnce without the type param to return it -- that's just a convenience. But this doesn't solve the self parameter/allocation problem — obviously, I'm heap allocating here, which smoltcp understandably doesn't want to do. I think we'd have a path to get around this if unsized_fn_params and unsized_locals were making progress, but they seem to be chronically stalled.

My crate uses tiny_fn to address this -- it stack-allocates the closure storage if it's small enough and so emulates unsized/allocaed dyn Trait that I'm looking for -- but as I mentioned, it unfortunately has to fall back on heap-allocation if the closure is too large.

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

No branches or pull requests

3 participants