Make associated consts object safe

Summary

Allow associated consts in object-safe traits, by adding the const in the trait vtable.

Motivation

Today, we can emulate having an associated const in an object-safe manner by wrapping it in a function:

trait ObjectSafe {
    fn get_constant(&self) -> &'static str;
}

However, this has several limitations:

  • Getting the constant from a dyn ObjectSafe requires a vcall, which has a runtime cost
  • The vcall is opaque to the compiler, so it removes many optimization opportunity (like knowing that this is actually a pure function)
  • The function could actually be impure, which would be misleading for a programmer

Adding the constant (or a reference to it) directly in the vtable would remove those limitations. Additionally, this limitation feels a bit arbitrary, so removing it would make trait object feel more first-class and easier to teach.

Design

Associated constants are allowed in object-safe traits. The constant in included in vtables so it can be retrieved at runtime from a dyn reference.

For example, the trait Any could be changed as:

trait Any {
    const TYPE_ID: TypeId = TypeId::of::<Self>();

    // Kept for backward compatibity
    fn type_id(&self) -> TypeId {
        TypeId::of::<Self>()
    }
}

impl dyn Any + 'static {
    fn is::<T: Any>(&self) -> bool {
        // Placeholder syntax to retrieve a constant from a value
        // This does not involve a vcall, only a vtable lookup
        self@@TYPE_ID == T::TYPE_ID
    }

    fn downcast_ref::<T: Any>(&self) -> Option<&T> {
        self.is::<T>.then(|| unsafe { /* ... */ })
    }
}

Its vtable would look like this:

#[repr(C)]
struct Vtable {
    drop_in_place: unsafe fn(*mut ()),
    size: usize,
    align: usize,
    TYPE_ID: TypeId, // This is new
    type_id: unsafe fn(*const ()) -> TypeId,
}

If the constant is too large, a reference to it could be stored in the vtable instead, which can avoid bloating the program if it is shared by several concrete types.

Interaction with non-copy types

I see two possibilities here:

  • Using a constant with (placeholder) @@ syntax create a new byte-copied value, as does using a "real" const now.
  • Allow only Copy types, others can only be used through a ref. This is more conservative, and forward compatible with the former proposition, but feels somewhat arbitrary and harder to teach.

I think the former possibility is the best, and it feels coherent with how const values work today.

Drawbacks

Syntax

I could not find any syntax which feels nice and inline with what we have today, and clear about the fact that we're just copying a constant (eventually from a vtable).

Vtable bloat

A common criticism of trait object is the size they take in the final binary. This proposition could increase it even more.

However, this proposition does not affect the size of vtables if not used, and for large values a pointer can be stored in the vtable instead of the value itself.

9 Likes

This seems problematic. The current way that trait objects work is in that they actually implement the trait. So you can work with a dyn MyTrait trait object like you can with any generic T: ?Sized + MyTrait parameter, and there’s no way to force any usage some new/special/different-syntax stuff.

As you noticed yourself, retrieving a “constant” from a vtable requires a value of the type, not just the type itself, so this interaction is fundamentally different from how associated consts work. Those are more simular to fn foo() -> ... methods, rather than fn foo(&self) -> ... methods; and methods without self-parameter make traits non-object-safe, too, for the same underlying reason.

Code like

trait MyTrait {
    const CONST: i32;
}

fn foo() -> i32 { bar::<dyn MyTrait>() }

fn bar<T: ?Sized + MyTrait>() -> i32 { T::CONST }

would still need to fail, because there’s no way it could work. Currently it fails because MyTrait isn't object safe. Under your proposed feature, it’s unclear what should happen. Perhaps you assume that dyn MyTrait doesn’t implement MyTrait after all in this case?

9 Likes

If that were the case then the proposal would be equivalent to making all associated consts be const NAME: Ty = {value} where Self: Sized;, which you can do for associated functions but can't currently do for associated consts.

There are two issues to address here: the coherence of the language by allowing you to do more things the way you expect them to work, and the performance impacts of having to go through a vtable. The former could be addressed by making associated consts lower to a const fn in trait objects. This would remove the restriction in a way that is suboptimal, but that doesn't preclude a better future solution.

2 Likes

This could work with a new syntax to mark associated constants as object-safe:

trait Foo {
    // placeholder syntax
    objectsafe const BAR: i32;
}

Object-safe constants can only be accessed through a value, so foo@@BAR works, but Foo::BAR does not.

1 Like

This is adding a lot of syntax for something I don't think people need that much. Do you have examples of places you need that and using functions isn't performant enough?

1 Like

The syntax is not yet set in stone, this is just a Pre-RFC.

I'm not the one who proposed the feature, and I don't have any compelling use cases for this. I was merely trying to improve the design of this Pre-RFC. Even if it is ultimately rejected for being too complex or not useful enough, I think it is valuable to explore the design space and look at all the options. We might still find a simpler, more intuitive syntax.

1 Like

Performance isn't the only reason to want associated consts.

I have some traits I am working on (in particular a Widget trait in a GUI framework) where I have to specify associated values as methods for dyn-safety, even though it would make a lot more sense in the interface for them to be consts; eg, because changing the value returned by those methods would be a logical error.

4 Likes

An alternative plan might be to make it possible to declare const fns in traits. Const functions without parameters are nearly constants, and it's well defined what calling a function pointer to a const fn does (runs it as a normal function), so allowing a const fn in a vtable would allow the semantics of object-safe associated constants while not introducing new concepts or syntax.

3 Likes

But would the function have an &self parameter or literally no parameters? If the former, it would be difficult to use in const context. If the latter, how would you call it on a trait object?

Oops, you're right, the idea doesn't actually work without new semantics. You would need some sort of "calling the vtable" where there is a fat pointer available to specify the vtable, but the function being called doesn't get a &self reference (which in static dispatch would just be calling a non-method associated function).

I wonder we could allow you to declare const fn foo(_: &Self) :thinking:

That is already allowed. It will not add it to the vtable though. It will be a regular associated function with a single &Self argument. You can't call it as val.foo();.

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