Downcasting covariantly associated types

⚓ Rust    📅 2025-09-02    👤 surdeus    👁️ 5      

surdeus

Ok, this is a bit convoluted by you're familiar with the yoke crate it might be easier.

In this playground you can find a CovariantDowncast trait thats make it possible to downcast a 'static reference to a type that has a lifetime that is 'static to a reference with lifetime 'a to the same type with lifetime 'a. Downcasting covariantly references is trivial for owned types (hence the blanket implementation that followes immediately), but the situation is more complicated if you have internal lifetimes.

Specifically, this problem happens when you want to pair a zero-copy data structure referring to some memory and the memory it refers to. You put in the a structure Case (not shown here, it's Yoke in the yoke crate) both the zero-copy data structure with lifetime 'static (remember that it refers to some memory), and the associated memory. The important thing is then when you get the data structure from Case you need not only to get a &'a to the data structure for some 'a—also the lifetime of the data structure must be downcast from 'static to 'a, or you could throw away the Case, access the reference and crash. The purpose of CovariantDowncast is to code (unsafely) this constraint (it is a simplified version of the Yokeable trait, but the idea comes from there). Basically, it says "this lifetime is covariant and can be downcast from 'static".

Now, epserde is a kind-of zero-copy (de)serialization framework. Each type has an associated deserialization type: for example, Vec<usize> has deserialization type &[usize] (and so on recursively for more complex types). We need to implement CovariantDowncast for all associated deserialization types.

If there are no type parameters, everything is fine, as you can see in the second half of the code (the associated types are irrelevant for this example).

The problem is that as soon as there are type parameters, the compiler complains that they are not used. This is of course correct—different types might have the same deserialization type and that would create conflicting implementations. The problem is that we do not see how to get out of this except by adding a parameter T to CovariantDowncast the represent the original type, and which thus disambiguate the implementation. However, then T gets propagated everywhere, creating a really awkward and cumbersome environment.

T is not necessary: it is just there to tell the compiler "this implementation is unique". We were wondering whether there's any other way to convince the compiler of this fact.

Note that it is possible that in some basic cases associated deserialization types are the same: for example, both String and Box<std> have the same deserialization type—&str. In this specific case, we can just write a single implementation of CovariantDowncast that covers both cases.

In the derive code, we define deserialization types recursively: U<A> has deserialization type U<A::DeserType<'a>>. Even replacing U<A>::DeserType<a'> with U<A::DeserType<'a>> in the CovariantDowncast implementation we do not solve the problem, as, again, the compiler does not consider A as used when in appears in U<A::DeserType<'a>>.

In principle, if we could unroll the recursion at derive time and write the actual deserialization type, we would be fine, but there's no way to access this information at derive time. We might create a utility generating a CovariantDowncast implementation for the actual deserialization type, but it would be very unnatural and cumbersome.

Any suggestion would be welcome!

1 post - 1 participant

Read full topic

🏷️ Rust_feed