Erasing generic parameters on callbacks

First of all, note for readers: the code in question is not a production code, or even "wanna-be-production" code. This is purely an experiment, a self-education. In production, I'd probably just wrap everything in Box<dyn Fn[Mut/Once]> and call it a day.


Here's the description of the problem I'm trying to solve.

  • The library provides a way to process some byte sequences coming from user, to convert them into another byte sequences (think webserver).
  • The library also wants to provide a way for user to provide strongly-typed processor functions - in the simplified case, of the signature (T) -> U, where T: Deserialize, U: Serialize.
  • I can't make the processor itself generic, since it have to match on the part of data (think URL path) and call differently-typed callbacks in different cases.
  • I want (again, not for practical purposes, but for educational ones) to do this all with function pointers, avoiding boxing and trait objects.

Here's what I've come with (simplified in the non-essential places):

// this will be T: Deserialize and will return Result
fn input<T: Default>(_: &[u8]) -> T {
    println!("Building input of type {}", std::any::type_name::<T>());
    Default::default()
}

// this will be U: Serialize
fn output<U>(_: U) -> Vec<u8> {
    println!("Consuming output of type {}", std::any::type_name::<U>());
    vec![]
}

pub struct Action {
    wrapper: fn(&[u8], *const ()) -> Vec<u8>,
    action: *const (),
}

impl Action {
    pub fn new<T: Default, U>(action: fn(T) -> U) -> Action {
        Action {
            wrapper: Self::wrapper::<T, U>,
            action: action as *const (),
        }
    }
    fn wrapper<T: Default, U>(data: &[u8], action: *const ()) -> Vec<u8> {
        let data = input(data);
        // Safety:
        // - this function is called only from `Self::exec`, so `action` is `self.action`;
        // - `self.action` can be set only in `Self::new`, i.e. it was cast from `fn(T) -> U`.
        let action = unsafe { std::mem::transmute::<*const (), fn(T) -> U>(action) };
        let data = action(data);
        output(data)
    }
    pub fn exec(&self, data: &[u8]) -> Vec<u8> {
        (self.wrapper)(data, self.action)
    }
}

Playground with an example of the user code calling this.

I'd like to know:

  • mainly, is this approach sound? AFAIK, the safety rule I've mentioned in comment is enough, but I want to be sure that I'm not missing something.
  • is there any obvious pessimization in this code? In other words, is there any place which can be surely improved?
  • what do you think about the ergonomics for the user? In particular, can this lead to any cryptic error messages, if some types appear to not match, or if compiler is unable to infer all the types used?

Thanks in advance!