Constants in dyn traits not allowed - any workaround?

I'm trying to work around a breaking change in winit. Winit used to just a library, but now it has a "framework", a trait that the caller must implement.

I'm stuck maintaining rend3-hp, a fork of non-longer-maintained rend3. I'm trying to keep the same API as rend3 while adapting it to use the new winit. It almost works. One compile error.

Here's the problem. Rend3's "framework" trait starts out like this:

pub trait App<T: 'static = ()> {
    /// The handedness of the coordinate system of the renderer.
    const HANDEDNESS: Handedness;

    fn register_logger(&mut self) {
        #[cfg(target_arch = "wasm32")]
        console_log::init().unwrap();
...

Each application that uses rend3-framework has to provide a value for the const HANDEDNESS. This worked fine until the latest "refactoring" over at winit. Now, winit has its own framework trait that has to be implemented by its user, which in this case is rend3.

pub trait ApplicationHandler {
    /// Emitted when new events arrive from the OS to be processed.

Tying them together looks like this:

/// New Winit framework usage
type AppRef<'a> = &'a mut dyn App;
impl ApplicationHandler<AppRef<'static>> for Rend3ApplicationHandler<'_, AppRef<'_>> {
   ...

The trouble is that "dyn App". If a trait instantiation is dyn, it can't have constants. So the declaration of HANDEDNESS, above, won't work.

Making HANDEDNESS a const parameter to App would work nicely. But that breaks everything that calls rend3, which means I have to change three big programs plus about eight test cases.

So if there's any trick for working around this without changing App, it would help. I don't think there is one, but it's worth asking.

(This is why libraries shouldn't be made into "frameworks". Frameworks want to be in charge. They don't play well with other "frameworks")

1 Like

Does something like this work for your case? Create an object-safe wrapper/proxy trait.

trait NonObjectSafe {
    const VALUE: i32;
    fn some_method(&self) -> i32;
}

trait ObjectSafe {
    fn get_value(&self) -> i32;
    fn some_method(&self) -> i32;
}

impl<T: NonObjectSafe> ObjectSafe for T {
    fn get_value(&self) -> i32 {
        T::VALUE
    }

    fn some_method(&self) -> i32 {
        self.some_method()
    }
}

struct MyStruct;

impl NonObjectSafe for MyStruct {
    const VALUE: i32 = 42;
    
    fn some_method(&self) -> i32 {
        self.get_value() * 2
    }
}

fn use_as_trait_object(obj: &dyn ObjectSafe) {
    println!("Value: {}", obj.get_value());
}

fn main() {
    let obj: &dyn ObjectSafe = &MyStruct;
    use_as_trait_object(obj);
}
2 Likes

I'm not familiar with rend3, but this looks somewhat suspect to me-- The type argument to ApplicationHandler is the type that gets put into user events, and I don't see how you can avoid violating the aliasing guarantees of &'static mut in that role. It seems more natural to store the App (or a reference to it) inside the type that's implementing ApplicationHandler

Aside from that, can I ask why you want to use dyn here at all? There's likely to only be one App type in any given program, so monomorphization bloat shouldn't be an issue. My instinct here would be to write an implementation like this, but it's entirely possible that something I don't know about is preventing this approach.

impl<UEData, RendApp> ApplicationHandler<UEData> for Rend3ApplicationHandler<RendApp>
where
    UEData: 'static,
    RendApp: App<UEData>
1 Like

Aside from that, can I ask why you want to use dyn here at all? There's likely to only be one App type in any given program, so monomorphization bloat shouldn't be an issue. My instinct here would be to write an implementation like this, but it's entirely possible that something I don't know about is preventing this approach.

Because App is a trait. So ...

error[E0782]: expected a type, found a trait
   --> rend3-framework/src/lib.rs:336:71
    |
336 | ...r Rend3ApplicationHandler<'_, App<'_>> {
    |                                  ^^^^^^^
    |
help: you can add the `dyn` keyword if you want a trait object
    |
336 | impl ApplicationHandler<App<'static>> for Rend3ApplicationHandler<'_, dyn App<'_>> {
    |                                                                       +++

error[E0782]: expected a type, found a trait
   --> rend3-framework/src/lib.rs:336:25
    |
336 | impl ApplicationHandler<App<'static>> for Rend3ApplicationHandler<'_, A...
    |                         ^^^^^^^^^^^^
    |
help: you can add the `dyn` keyword if you want a trait object
    |
336 | impl ApplicationHandler<dyn App<'static>> for Rend3ApplicationHandler<'_, App<'_>> {

I didn't design this. I'm just trying to make it work without a breaking change for its users (its tests and three applications of my own, at least) as other crates change underneath.

Right. But you should be able to deal with that via a bound on a generic parameter in place of dyn for this case:

impl<'a, T: 'a+App> Something for Rend3ApplicationHandler<'a, T> { ... }
    ^^^^^^^^^^^^^^^

It is likely that this will not be 100% achievable because rend3 includes types defined by these crates in its public API; any breaking change in their usage will therefore necessarily pass through rend3 and break the downstream users. That's not to say that you shouldn't try to minimize the problems, of course, just that you shouldn't expect perfection here.

As a particular example, App::create_window can be overridden by downstream users and is defined to take an argument of type winit::WindowBuilder, which no longer exists in the most recent version of winit.

1 Like

Right. I wasn't able to fix this with zero API changes at the Rend3 API. One API change was necessary - something that was previously a trait-level constant had to be turned into a function, because the virtual function tables for a dyn trait can't dispatch a constant.

Getting the ownership and lifetimes right, while caught between two APIs defined by others, was the usual headache. Here are the diffs for the changes I needed to make. Too many.

I'm hitting a similar situation in a different context. In fact my trait itself has the metadata defined and doesn't expect anyone else to implement it, it is just "attached" to the trait.

I found a good answer to this question here: Why doesn't Rust support trait objects with associated constants? - Stack Overflow

Essentially from my understanding it boils down to the fact that functions are in the vtable and constants are not. Which raises the question: can't constants be added to the vtable too?

My thinking is the following: if I can define a default function that returns the value, but no the constant with the same value, then I should also be able to define a const method that returns the same value (currently unstable feature), at which point function a constant looks surprisingly similar to a const method with no arguments :thinking:

UPD: Someone already proposed that years ago: Make associated consts object safe - language design - Rust Internals

Make associated consts object safe - language design - Rust Internals hints that the problem would also apply to const methods that do not have &self, which was also hinted to me by the compiler when I removed constants, but turned out that I also didn't have &self in methods, hence it was still not object safe:

note: for a trait to be "dyn-compatible" it needs to allow building a vtable to allow the call to be resolvable dynamically; for more information visit <https://doc.rust-lang.org/reference/items/traits.html#object-safety>
  --> crates/contracts/ab-contracts-standards/src/lib.rs:29:8
   |
26 | pub trait Fungible {
   |           -------- this trait cannot be made into an object...
...
29 |     fn transfer(
   |        ^^^^^^^^ ...because associated function `transfer` has no `self` parameter
...
38 |     fn balance(env: &Env, address: &Address) -> Result<Balance, ContractError>;
   |        ^^^^^^^ ...because associated function `balance` has no `self` parameter
help: consider turning `transfer` into a method by giving it a `&self` argument
   |
29 |     fn transfer(&self, 
   |                 ++++++
help: alternatively, consider constraining `transfer` so it does not apply to trait objects
   |
34 |     ) -> Result<(), ContractError> where Self: Sized;

Then I thought: if I can add Self: Sized to methods, can I do the same with constants?

Turns out yes! But only if I use unstable incomplete feature generic_const_items: Tracking issue for generic const items · Issue #113521 · rust-lang/rust · GitHub

It allows writing things like this, which are object safe:

trait T {
    const C: &[u8] where Self: Sized;
}

With the caveat that it requires nightly compiler and the feature itself is not even complete yet.

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.