Downcasting Arcs in Rust | Eshed Schacham
into an Arc, as long as MyTrait is dyn compatible. What about casting it back from Arc to Arc? Well, here the plot thickens a bit. We can’t use the as keyword here, because that operation is inherently unsafe. An arbitrary Arc might not hold a MyStruct in it, but some other object that implements MyTrait, and trying to cast it into the wrong type would result in undefined behaviour. But the fact that something is unsafe doesn’t mean it should be impossible, or even undesirable. An unsized coercion if you want the fancy term. ↩" /><br>into an Arc, as long as MyTrait is dyn compatible. What about casting it back from Arc to Arc? Well, here the plot thickens a bit. We can’t use the as keyword here, because that operation is inherently unsafe. An arbitrary Arc might not hold a MyStruct in it, but some other object that implements MyTrait, and trying to cast it into the wrong type would result in undefined behaviour. But the fact that something is unsafe doesn’t mean it should be impossible, or even undesirable. An unsized coercion if you want the fancy term. ↩" />
The Arc type is Rust’s thread-safe smart pointer, and like other pointer types in Rust (e.g., Box), one can use the as keyword1 to cast an Arc into an Arc, as long as MyTrait is dyn compatible.
What about casting it back from Arc to Arc? Well, here the plot thickens a bit. We can’t use the as keyword here, because that operation is inherently unsafe. An arbitrary Arc might not hold a MyStruct in it, but some other object that implements MyTrait, and trying to cast it into the wrong type would result in undefined behaviour. But the fact that something is unsafe doesn’t mean it should be impossible, or even undesirable.
" and after it's unmasked, its caption is "Arc"" />
Why would I want to do that?
Arc has a few nice functions such as into_inner that allow you to consume it if you’re the only owner and unwrap the contained value. This is not possible for unsized types (such as dyn MyTrait) since they can only reside on the heap. So if we have an Arc that we’re 100% sure was originally created as an Arc, and we want to extract that MyStruct out, we can’t do that unless we first convert the Arc back to an Arc.2
So I’d like to implement the following function:
/// Calling this is only safe if `arc` was originally created<br>/// as an `Arc`.<br>unsafe fn downcast(arc: Arcdyn MyTrait>) -> ArcMyStruct>
Is there no simple solution?
Currently, Arc has support for downcasting from dyn Any through the fallible downcast. There’s also an unconditional downcasting in nightly, in the form of downcast_unchecked.
But what if my trait isn’t a subtrait of Any? We obviously can’t implement the casting safely, but there should still be an unsafe way to achieve what we want. We just need to use the available API a bit more creatively.
Raw pointer conversions
The Arc type provides the functions into_raw and from_raw to convert Arcs to and from raw pointers (*const T).
What would happen if we convert an Arc into a pointer, and then convert that pointer to an Arc? Is this safe? Let’s check the docs.
The docs for Arc::from_raw have a lengthy list of safety requirements, the important ones for our case are:
The raw pointer must have been previously returned by a call to Arc::into_raw
Note that we have two types here, U and T, the two types aren’t required to be identical, so we’re good on this front.
If U is unsized, its data pointer must have the same size and alignment as T. This is trivially true if Arc was constructed through Arc and then converted to Arc through an unsized coercion.
The last sentence describes our situation exactly, so we’re good here as well!
All we need to do is implement our function:
/// Calling this is only safe if `arc` was originally created<br>/// as an `Arc`.<br>unsafe fn downcast(arc: Arcdyn MyTrait>) -> ArcMyStruct> {<br>let ptr = Arc::into_raw(arc);<br>unsafe { Arc::from_raw(ptr.cast()) }
Wait, why aren’t we done?
Before we go on our merry way, let’s check what the function actually compiles into, specifically in comparison to the upcast. We use Compiler Explorer with the following code:
use std::sync::Arc;
pub struct MyStruct;<br>pub trait MyTrait {}
impl MyTrait for MyStruct {}
#[unsafe(no_mangle)]<br>pub fn upcast(arc: ArcMyStruct>) -> Arcdyn MyTrait> {<br>arc as _
#[unsafe(no_mangle)]<br>pub unsafe fn downcast(arc: Arcdyn MyTrait>) -> ArcMyStruct> {<br>let ptr = Arc::into_raw(arc);<br>unsafe { Arc::from_raw(ptr.cast()) }
to get this assembly output (with my own comments)
upcast:<br>; Copy the input pointer into the first output register<br>mov rax, rdi<br>; Load a pointer to MyStruct's virtual table into the second<br>; output register<br>lea rdx, [rip + .Lanon.09a60105b64a7b096223e4c3559521b0.0]<br>ret
; MyStruct's virtual table<br>.Lanon.09a60105b64a7b096223e4c3559521b0.0:<br>.asciz "\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\001\000\000\000\000\000\000"
downcast:<br>; Uhhhh...<br>mov rax, qword ptr [rsi +...