Custom vtables with integers

If I want to build a custom vtable containing function pointers, I can do this:

use std::fmt::Display;

struct Vtable {
    to_string: unsafe fn(*const ()) -> String,
}

fn vtable_for_type<T: Display>() -> &'static Vtable {
    &Vtable {
        to_string: to_string::<T>,
    }
}

unsafe fn to_string<T: Display>(the_value: *const ()) -> String {
    let my_ref = &*(the_value as *const T);
    my_ref.to_string()
}

And the above works great. Static promotion ensures that the vtable lives for the 'static lifetime.

I might also want to store some sort of computed value in the vtable, e.g. I could add a field with the struct size:

struct Vtable {
    to_string: unsafe fn(*const ()) -> String,
    value_size: usize,
}

fn vtable_for_type<T: Display>() -> &'static Vtable {
    &Vtable {
        to_string: to_string::<T>,
        value_size: compute_value_size::<T>(),
    }
}

const fn compute_value_size<T>() -> usize {
    std::mem::size_of::<T>()
}

playground

However, now static promotion now longer works!

error[E0515]: cannot return reference to temporary value
  --> src/lib.rs:9:5
   |
9  |        &Vtable {
   |  ______^-
   | | _____|
   | ||
10 | ||         to_string: to_string::<T>,
11 | ||         value_size: compute_value_size::<T>(),
12 | ||     }
   | ||     ^
   | ||_____|
   | |______returns a reference to data owned by the current function
   |        temporary value created here

Is there any way to get around static promotion no longer working in this case? Curiously, if you call std::mem::size_of directly rather than call my custom function, then it does work, but I don't understand why.

The actual value I want to put in my vtable is the offset of a field in the struct.

5 Likes

Interesting. The 1414-rvalue_static_promotion - The Rust RFC Book RFC claims

These rules above should be consistent with the existing rvalue promotions in const initializer expressions:

// If this compiles:
const X: &'static T = &<constexpr foo>;

// Then this should compile as well:
let x: &'static T = &<constexpr foo>;

yet

fn foo() {
    const X: &'static u32 = &f(); // <- works
    let x: &'static u32 = &f(); // <- doesn't work
}

const fn f() -> u32 {
    42
}

The typical workaround for generic consts is to use traits, and that works here as well

use std::fmt::Display;

struct Vtable {
    to_string: unsafe fn(*const ()) -> String,
    value_size: usize,
}

const fn vtable_for_type<T: Display>() -> &'static Vtable {
    trait HasVTable {
        const VTABLE: &'static Vtable;
    }
    impl<T: Display> HasVTable for T {
        const VTABLE: &'static Vtable = &Vtable {
            to_string: to_string::<T>,
            value_size: compute_value_size::<T>(),
        };
    }
    T::VTABLE
}

const fn compute_value_size<T>() -> usize {
    std::mem::size_of::<T>()
}

unsafe fn to_string<T: Display>(the_value: *const ()) -> String {
    let my_ref = &*(the_value as *const T);
    my_ref.to_string()
}
9 Likes

Ah, interesting. I didn't think of using traits.

If anyone knows why this happens, I would be interested in hearing the answer.

On the other hand it makes sense that there are limitations beyond what that RFC wrote. You probably wouldn't want

let x: &u32 = &(panic!() as _);

to panic at compile time, even though

static X: &'static u32 = &(panic!() as _);

compiles somewhat successfully (at least until it reports that compile-time panic).

And even more non-trivial than simple panics, non-terminating (or slow) computations would be hard to handle in non-confusing ways. (E. g.: Should static promotion be tried and then disregarding it becomes a silent fallback in case of panic or if evaluation is taking to long? But if hitting some evaluation limit turns into lifetime errors that would be weird and confusing.)

5 Likes

Yes, this last point from @steffahn hits the nail on the head: traditionally code used to be more aggressive with static promotion, but this incurred in problems and limitations, related to UB-in-function-or-computation being lifted to compile-time (:scream:), and the semver hazards Steffahn mentioned about introducing panics in the function-or-computation causing compile-errors.

To illustrate:

use ::core::num::NonZeroU8;

if N != 0 {
    let x: &u8 = 42 / N; // compile-error if lifted to compile-time!
    unsafe {
        let y: &NonZeroU8 = &NonZeroU8::new_unchecked(N); // UB if lifted to compile-time
    }
}

Using explicit consts thus makes it clear that the author intended for the calculation / function-call to occur at compile-time, which thus makes these branched / guarded scenarios impossible. Hence why a more ad-hoc solution to the OP would be to involve a helper trait just for the problematic value:

- value_size: compute_value_size::<T>(),
+ value_size: {
+     // Generic `const { … }` pattern:
+     struct GenericConst<T>(*mut Self);
+     impl<T> GenericConst<T> {
+         const VALUE: usize = compute_value_size::<T>();
+     }
+     GenericConst::<T>::SIZE
+ },

that being said, for the specific case of a vtable, using a "generic const" for the whole vtable value will scale better (e.g., imagine there being two value_size kind of vtable entries).


Aside: FWIW, I had always been using the generic const approach for vtables, since I had a fuzzy memory of inline versions of it not being static-promoted for one reason or another. It's only somewhat recently that somebody "proved me wrong" with vtables of only function pointers, which puzzled me quite a bit since I did remember some kind of failure about it. So this thread finally clarifies things: not all of the const-compatible values are eligible for static promotion after all! :upside_down_face:

12 Likes

Oh, you can use a struct, too!? That's nice, I was only aware of traits for some reason ^^

3 Likes

const-block also seems to make it compile! (playground)

        value_size: const { compute_value_size::<T>() },
7 Likes

For assoc types you do need a trait, so I imagine that was the reason :upside_down_face:

This is definitely the long-term answer.

Basically, the rules got too complicated. See the top of 2203-const-repeat-expr - The Rust RFC Book noting that we actually unaccepted it in favour of const{}.

With the explicit form, it fixes the problems mentioned above in Custom vtables with integers - #4 by steffahn -- if you want something more complex than something "obviously" fine like &10, you have to say that you need it done at compile-time, rather than leaving it up to the optimizer.

1 Like

Makes me wonder…

…as far as I understand, const (as in, “can be used as the value of a const”) is about two properties of a value:

  • can be evaluated at compile-time
  • can be duplicated using a shallow copy (the concrete value can be duplicated, not any value of the same type)

With this in mind, for the use-case of array expressions, I wonder if one couldn’t rely solely on this second property while leaving the evaluation to run-time. I.e. the same checks as for const expressions is used to determine whether its okay to duplicate the value, however the problems of non-termination or panics can be left to run-time. (const { … } would still be useful to force compiler-time evaluation.)


(Of course for rvalue promotion, compile-time evaluation is essential, so this wouldn’t matter for that use-case.)

I think it's ok to stick with from_fn in std::array - Rust for the cases where re-calculating the value is ok but it doesn't need to be compile-time.