Way to obtain `Option<fn(...)>` from traits if the function is not implemented

Has there been any thought about a way to get a nullable function pointer if a trait member function is unimplemented?

This would be relevant for interfacing with C libraries, where structs of functions are used as vtables - especially relevant when one might want to write a plugin or module in Rust. As an example, a C interface might expect the following vtable for an interface:

#[repr(C)]
pub struct st_operation{
    pub init: Option<unsafe extern "C" fn(self_: *mut c_void) -> c_int>,
    pub calc: Option<unsafe extern "C" fn(self_: *mut c_void, val: c_int) -> c_int>,
    pub deinit: Option<unsafe extern "C" fn(self_: *mut c_void) -> c_int>,
    ...
}

Logically, these map well to a Rust trait, which might look like the following:

trait Operation {
    fn init() -> Result<Self, ()>;
    
    #[detectable_unimplemented]
    fn calc(&mut self, val: i32) -> Result<(), ()> {
        unimplemented!()
    }
    
    fn deinit(&mut self) -> Result<(), ()>;
}

The thought was that #[unimplemented_nullptr] would be used to indicate that if that member function is not overridden when impl'd, it should be possible to know this when trying to get a function pointer.

// Will be a function pointer to that method if it's implemented, None otherwise
let x: Option<_> somestruct.calc.as_option()

The crate vtable provides some of this functionality, but it isn't always suitable. Rust for linux uses a custom vtable macro (source, trait and usage example)

Is there any chance a smoother implementation possibility would be considered for Rust?

What are some shortcomings of the Linux kernel macro and how do you wish to address them with that proposed language attribute? I don't immediately see how it would be feasible to produce the value you desire. What is the exact type you would expect in let x: Option<_>? The function zst object, a function pointer of the concrete class, or a function pointer of &mut dyn Operation? The largest hurdle is that the usual unsized trait objects vtables only contain functions pointers and their attributes aren't accessible directly–only via dyn Operation values, which wouldn't exist in your example since fn init() make the trait non-object-safe as far as I can tell.

The more Rust-idiomatic interface would be to just provide a default implementation and the vtable always contain a valid pointer, rather than have a nullable pointer in the vtable and (presumably) branching on null to do some default handling.

There is unfortunately a slight code size penalty for this unless the optimizer manages to polymorphize the implementation.

If you do actually need properly optional methods, the ideal post-expansion interface would probably look something like

trait Operation: ErasePtr {
    fn new() -> my::Result<Self>;
    fn flush(&mut self) -> my::Result<()>;

    const fn#calc: Option<fn(&mut Self, i32) -> my::Result<()>> = {
        where
            const { Self::fn#calc.is_some() }
        {
            Some(Self::calc)
        } else {
            None
        }
    };

    fn calc(&mut self, val: i32) -> my::Result<()>
    where const { Self::fn#calc.is_some() };
}

This uses a bunch of ad-hoc imaginary features I made up just now, and contains a cycle, but I think illustrates the point.

Thanks for the replies - here's my followup:

  • For shortcomings of the Linux kernel macro: it does work, but there are some uncomfortable things. The macro adds a const USE_VTABLE_ATTR, and a const HAS_X for each trait to track what is implemented. Which is OK from a functionality standpoint, but leads to a somewhat confusing API (see e.g. file::Operations docs). This macro must also be used both on the trait definition and on any corresponding impl block, and there is no per-field control (not that you necessarily need that, and a stronger proc macro could sense this)

  • I was imagining it to be a optional function pointer to that class's implementation - so really, just a nullable function pointer. But it could be anything, even just a bool to indicate that it is or isn't implemented, as long as the result can be compile-time evaluated.

  • You're correct that this example trait wouldn't work in a 1:1 way, but I was intending to have a wrapper interface. Perhaps something like the following

    st_operation {
        init: get_init_fn<SomeStruct as Operation>(),
        calc: get_calc_fn<SomeStruct as Operation>(),
        deinit: get_deinit_fn<SomeStruct as Operation>()
    }
    

    with wrappers returning optional function pointers, as applicable. get_calc_fn would be able to leverage this feature in what it returns.

The more Rust-idiomatic interface would be to just provide a default implementation and the vtable always contain a valid pointer

That does sound like a good rusty way. My concern here is that having a non-null pointer may be defined to have different behavior within the C caller, e.g. start allocating things to pass to that function's arguments, so there might be a performance hit too (maybe LTO could optimize, but that wouldn't help dynamic modules)

In some sense trait methods are equivalent to associated const function pointers (some piece of code specified by some type when implementing some trait), so for optional methods maybe you could make the optional trait method an associated constant Option of a function pointer instead:

trait Operation {
    const CALC: Option<fn(&mut Self, i32) -> Result<(),()>> = None;
}
fn test<T: Operation>(mut x: T) {
    if let Some(f) = Operation::CALC {
        f(&mut x, 42);
    }
}

full example

Unfortunately this won't work for trait objects since associated constants aren't object safe, but I would hope that could change in the future. It looks like the vtable-crate does allow constants.

1 Like

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.