Add casting between two unrelated traits

Since trait upcasting is now stable, I think it now makes sense to start thinking about casting between two unrelated traits.

How would the compiler enable this to happen?

There will be a trait, let's call it Castable. One of it's roles will be to serve as a marker to indicate that it can be used to cast to another trait. There will be a single method, which will look like this

trait Castable {
    fn get_vtable(&self, dyn_trait_type_id: TypeId) -> Option<&'static ()>;
}

This method's implementation would be generated by the compiler. it would return the vtable to the specified dyn, EG

some_struct.get_vtable(TypeId::of::<dyn Any>())

gets the vtable of dyn Any for type some_struct

How will the compiler generate this method to get all the types that would be casted to?

the compiler will ONLY keep track of all dyn Traits that supertraits Castable, that have been used in the program

trait SomeTrait : Castable{}
let foo : &dyn SomeTrait;

now the compiler will ONLY take note of dyn SomeTrait, because its the only dyn Trait used somewhere in the program. this way, the compiler will not have to look for how many possible dyn Foo<T> exists, where T could be anything

for every struct that implements SomeTrait, the compiler will include it in their get_vtables method

impl SomeTrait for i32{}

Castable for i32 would look like

impl Castable for i32{
   fn get_vtable(&self, dyn_trait_type_id: TypeId) -> Option<&'static ()>{
       if dyn_trait_type_id == TypeId::of::<dyn SomeTrait>(){
           return Some(/**compiler returns the vtable somehow*/);
       }
       return None
   }
}

with that, rusts std could now make a method that would look like this

fn cast<T: Castable + ?Sized + 'static>(input: &impl Castable + ?Sized)
 -> Option<&T>;

and use the vtable to return the appropriate cast

Possible issues

Some ways of implementing traits that supertrait Castable could cause issues. for example

trait MyTrait : Castable {}

struct Wrap<T>(T);

impl<T> MyTrait for T where Wrap<T>: MyTrait {}

and then, calling a method like this

fn require_trait(_: impl MyTrait) {}

fn main() {
    require_trait(1i32);
}

would result in an infinite recursion, making the compiler hit the recursion limit.

if a trait that can be castable, in this case, MyTrait, were to be implemented that way, merely upcasting a struct to dyn Castable, or any other trait that supertraits it, would cause the compiler to do the thing that makes it hit the recursion limit, since it would have to check if the struct implements Wrap<Self> to see if it can implement MyTrait(hope im making sense)

This isn't good, because if a crate dependency were to implement a trait that supertraits Castable this way, upcasting structs to dyn Castable would be an automatic error.

How to prevent this?

By making the compiler reject where clauses when implementing a trait that supertraits Castable, so it wont be able to happen in the first place.

so merely doing

impl<T> MyTrait for T where Wrap<T>: MyTrait {}

would cause the rust compiler to give a compiler error, saying something like:

Cannot add where clause when implementing MyTrait, since it supertraits Castable

This prevents the recursion thing from happening when casting to dyn Castable (or any of its subtraits), since you cannot even implement the trait that way without the Where clause in the first place.

If there is any possible situation that this could mess up the compiler, feel free to comment! (and ill try to provide a solution)

This breaks separate compilation, since it requires knowing the full set of dyn Traits that are mentioned in order to codegen <_ as Castable>::get_vtable. This isn't an impossible blocker, as the function codegen could be delayed until the root binary artifact is compiled, but it's a relevant impact to the compilation model either way, and fundamentally prevents inlining.

It also runs into the specialization problem with lifetime dependent impls; whether a particular impl can be used (eg included in the downcast options) must not depend on lifetime relationships that aren't immediately true given the context's bounds, as the lifetimes don't participate in codegen or the impl selection, only the impl validity checks. And this is fundamental, due to for<'a> fn pointers.

2 Likes

How would something like this be handled?

trait Foo<T>: Castable {}
impl<T> Foo<T> for Bar {}

Bar implements Foo<T> for every possible type T, which is an unbounded amount, so it's not possible to generate code for all of them.


Another issue I can see is with dylibs. In a dylib you can implement traits for existing types, so the implementation of get_vtable should be codegenned in it. But what if you have multiple dylibs?

3 Likes

For the lifetime issue I think that would be simple to resolve. Only types with a static lifetime will be able to implement Castable.

trait Castable : 'static {
    fn get_vtable(&self, trait_type_id : TypeId);
}

Any has this requirement as well, and it enables downcasting. I am assuming it’s to prevent the same issues you are mentioning.

As for the codegen, that is a good point. How about, Instead of generating the get_vtable after keeping track of all possible dyn T, it uses some kind of global variable (eg HashMap) that keeps track of the vtables?

struct TraitVTables{
    HashMap<TypeId , &’static ()>

// where TypeId is the dyn Ts type id
}

struct VTableRegistry {
    HashMap<TypeId, TraitVTables>

    // where TypeId is the structs type id
}

The compiler will add the vtables to this global var of type VTableRegistry, and the get_vtables method will look through it to get the valid vtable . For that, Castable would require to supertrait Any

As I said, the compiler will only include dyn Traits that are actually used in the program

It won’t try to look for the vtables of all the possible

dyn Foo<T>. It will only count all variants of

dyn Foo<T> that are used in the program

If there’s only dyn Foo<i32> and dyn Foo<bool> used in the program, it will ONLY consider those variants of dyn Foo<T>

As for dylibs idk what that is. I’ll look into it and get back to you

The important thing about dylibs in this context is that they mean the list of "dyn traits that are actually used in the program" is not known, even at runtime, because the running program could, at any time, call dlopen and splat some more code into the process. And programs actually do this! It's one common way of implementing "plugins".

I see. CAD97 brought up something else, and it made me change from “compiler generates get_vtable” to “get_vtable uses a global var like a HashMap to return the appropriate vtable”

With that, won’t it be possible for dylib to add the new vtables to that global variable?

Without knowing any more than what you've posted, all I can say is "maybe" and "it could depend on the operating system".

But before we even get to worrying about how to implement this, we have to figure out if it even makes sense. What if two different plugins try to add different implementations of the same vtable? At compile time this can be an error; at runtime what do you do?

Can that even happen? can't you only implement a trait for a struct once? or does dynlibs do something differently?

With plugins implemented using dylibs, it's not just possible, it's likely.

Suppose the main program defines a trait. Plugins A and B each depend on a library crate C which impl's that trait for one of its public types. The main program does not depend on C. The orphan rules are fine with this. If A, B, C, and main were all being compiled and linked together, rustc would compile C only once and would only emit one copy of the trait impl. However, with dylibs, the main program and A and B are each compiled and linked separately. When compiling the main program, rustc has no information about either plugin. When compiling each plugin, rustc also has access to information about the main program, but not about any other plugin. So what would happen is, the trait impl would be emitted twice, once for C-in-A and once for C-in-B.

Presumably, the trait impl is functionally identical between C-in-A and C-in-B, or we'd have bigger problems. But the compiler may well have specialized C-in-A for how A uses C, and C-in-B for how B uses C, causing the machine code of the C-in-A trait impl to not work correctly for B and vice versa. This is fine as long as the C-in-A impl is only ever called from A and the C-in-B impl is only ever called from B. But your global table of trait impls has only one slot for each trait; as described, it can't distinguish C-in-A from C-in-B.

Worse, we may not even be able to detect that there is a problem, because the dynamic loader, which is the first piece of software that gets to see all three of main, A, and B at the same time, knows nothing about Rust.

3 Likes

As a concrete way this can manifest, imagine C-in-A has devirtualization applied to it at some codepath (perhaps because of LTO, but that's not required). When a C-in-B is passed through here, it will end up calling C-in-A's function even though the vtable points to the C-in-B copy.

1 Like

wait a sec, if each plugin is compiled seperately, won't it also make each plugin to have their own seperate hashmap? So there won't be a situation where they are using a vtable generated elsewhere

Don't you need a single hashmap for the entire process for this scheme to work at all?

The only way i could think that this wont work is in this scenario.

say the main program has a struct Bar. Bar implements Castable, and is upcasted to &dyn Foo, then is passed from the main program to a plugin by calling one of the plugins functions somehow.

The plugin doesnt know about the concrete type Bar, so it does not have it's vtables registered in its own hashmap, even though the main program does. Currently thinking of a way that prevents this issue

Ok I realized that automatically it would check its sources hashmap (eg if a &dyn Foo comes from plugin A, it will check with the hashmap of plugin A) so that wont be an issue.

but an issue would be if, plugin A depends on crate C, plugin B also depends on crate C, and in crate C, theres a struct called Bar.

plugin A has a trait called Foo, and makes Bar implement it.

somehow, plugin B upcasts Bar to &dyn Castable, and is passed into some code in plugin A. (so this &dyn Castable uses plugin Bs hashmap)

now plugin A wants to check if this &dyn Castable is a &dyn Foo (it is since plugin A made its comcrete type, Bar, implement it). But since plugin B does not know that dyn Foo exists, its hashmap doesnt have its vtable, resulting in a failed cast since the &dyn Castable comes from plugin B, not A. that wouldn't be good.

a fix would be, to get the vtable, first use the hashmap of the &dyn Castable's source (eg if it was made in plugin A it will use plugin A's hashmap). If the vtable cannot be found there, then it will check it's own hashmap. but for this to work,

typeid of dyn T in plugin A will have to be equal to typeid of dyn T in plugin B. Is that the case?

If that's not the case, then downcasting to concrete type with dyn Any would also have issues. Saw a post on the discord reddit of someone having this issue actually