[SOLVED] Transmute between trait objects


#1

I’ve been backed into a corner where I need to transmute a boxed trait object to the correct trait type. I know for certain that in this match arm the boxed item is this trait object, I’m just transmuting it because the type can’t be written in the signature, yet transmute fails:

error[E0512]: transmute called with types of different sizes
   --> src/datatype/mod.rs:168:32
    |
168 |                 let cast_val = std::mem::transmute::<Box<T>, Box<interface::PartitioningController>>(val);
    |                                ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    |
    = note: source type: std::boxed::Box<T> (pointer to T)
    = note: target type: std::boxed::Box<datatype::interface::PartitioningController + 'static> (128 bits)

Fake method with extraneous types removed:

    fn from_box<T: ?Sized>(name: &str, val: Box<T>) -> Option<Self> {
        match name {
            "Partitioning" => unsafe {
                let cast_val = std::mem::transmute::<Box<T>, Box<interface::PartitioningController>>(val);
                Some(DefaultInterfaceControllers::Partitioning(cast_val))
            },
            _ => None,
        }
    }

I understand boxes can be one of two sizes (depending on whether they point to a struct or a trait object), but have no idea how to express this in the T type constraint. This fn will only ever be taking boxed trait objects.

And, yes, I know this is major code stench, but when dealing with user/macro defined types/traits this is sometimes the only way out. I’m asking how to remove the safety on a footgun.


#2

Could you possibly downcast by using Any?

Please don't open this
I'm warning you, this is horribly unsafe
You still have a chance! Walk away before you are exposed to such dark magics!
unsafe fn cast_unsized(a: &A) -> &B
{
    let p_a = a as *const A;
    let pp_a = &a as *const *const A;
    let pp_b = pp_a as *const *const B;
    // this line is undefined behavior if A and B differ in sizedness,
    // because *const A and *const B will differ in fatness
    let p_b = *pp_b as *const B;
    p_b.as_ref().unwrap()
}

#3

By the way, I’m almost 100% certain that this will invoke undefined behavior in your code, because the two types actually DO differ in sizedness. (I’ve used such code to go from unsized to unsized, and from sized to sized, but you’re going from unsized to sized!)

okay, wait, you’re actually going from unsized to unsized.

Uh… How on earth did you manage to paint yourself into this situation anyways?


#4

Unless I’ve vastly misunderstand Any (having used it elsewhere), I can’t use it here because I can’t write/don’t want the concrete type (ThingThatImplsRelevantTrait) of the thing that was originally boxed, just the trait it was boxed as before the fn was called (RelevantTrait).

Or are you saying I could take an Any of the box whose concrete type is Box<RelevantTrait>, and recover that? I didn’t think that was possible.


#5

yes, I entirely misread, and was just in the middle of scribbling out part of my post (see the new edit)

The question I added stands.


#6

I think it’s best to take a step back and look at what you want to do and to find the right way to do it.

Transmute doesn’t do conversions, per se. The output is identical to the input, it just has a different type. The compiler is saving you here – your Box<T> does not have a representation that is compatible with the type you are trying to transmute into.

What you need is a conversion, something that creates a value with the right representation, the right data in that representation, and then possibly transmute can help you make it into a value that the compiler thinks is the desired type.

  1. The final boxed trait object must contain the correct pointer to the vtable that corresponds to T’s implementation of the particular trait in question. How to aquire that vtable pointer?

#7

@bluss I think you’ve misread the question the same way I have (or similar at least; my initial reading was even further off!).

It is known that T = dyn TheTrait, so it contains the vtable pointer already.


#8

You can use transmute_copy (be extra careful and don’t forget to mem::forget) if you are absolutely certain that T = PartitioningController. Put an assert_eq!(size_of::<Box<T>>(), size_of::<Box<PartitioningController>>()); just to be on the safe side.


#9

Can probably do something terrible like this:

fn from_box<T: ?Sized>(val: Box<T>) -> Box<PartitioningController> {
    let raw = &val as *const _ as *const (*mut (), *mut());
    // or let raw = &val as *const _ as *mut std::raw::TraitObject if nightly is OK
    std::mem::forget(val);
    unsafe {Box::from_raw(std::mem::transmute(*raw))}
}

#10

And if you don’t want to consume the box, it’s similar:

fn from_box<T:?Sized>(b: &Box<T>) -> Option<&PartitioningController> {
   let raw = b as *const _ as *const (*mut (), *mut ());
   Some(unsafe {std::mem::transmute(*raw)})
}

#11

Uh… How on earth did you manage to paint yourself into this situation anyways?

Full justification for such a nasty hack would require a few K SLOC, but I think a brief motivation would be this:

A library (let’s call it ‘Base’) defines a set of types that all have trait A. These types must be heterogeneous because they behave differently. Some of these types may also have traits B, C, D, etc (or are able to create other types that do). These types have relations to each other so the library has structs, functions, etc. that store and perform computation on those relations. Whatever.

A library using/extending this library also may define some of these types implementing A. These externally defined types may also have traits B, C, D, as well as M, N, O, etc. However it still wants to use the relational storage and computation from Base.

To handle this much of Base is generic on macro-generated enum types that are created by the user library. However most of Base is still interacting with things as As. When you need to get, e.g., an M interface, it’s easy to ask the type via A if it impls M, and it’s easy for the type to create a Box<M>, but for A to return things that may be Box<T> where T in A, B, …, M, N etc. requires another macro-generated enum user type X (several, actually), which, again, much of Base is generic over. To get a Box<M> out of this type, the enum has to unpack itself. So, a type has to create the X::M(Box<M>), but can’t write X::M because there’s no…“enum trait”(?)…in Rust, but it knows for sure it needs to create a Box<M> and X knows for sure it can construct a variant around a Box<M>. But the trait Y which X is generic over in ‘Base’ can’t write M or really any definite trait types, so it just says “things that impl me will know if they have a variant for a Box<T>, and will know for sure what T is by an identifier” (which I substituted with a &str in the example for clarity).

Of course, as I’ve described the simplified problem here there are obvious, more elegant workarounds, but in the real problem setting all that I’ve tried end up being impossible. I’m not saying there isn’t an elegant, safe solution, I’m saying I haven’t found it after several weeks and at this point just need something functional.

Transmute doesn’t do conversions, per se.

I’m not converting anything. I know the Box<T> is exactly Box<PartitioningController> (not just something that impls that trait or trait object of a subtrait), it simply can’t be written in the trait fn signature.

I know it’s a terrible sign a design has gone wrong, but it is what it is.

@vitalyd Thanks, something base on your nightly solution (hadn’t heard about TraitObject!) compiled and works correctly. Thanks for helping me make the world a little less safe. :slight_smile:


#12

(gulp) :slight_smile: Hopefully you can change your code - this is one of those that will come back to bite you in time.


#13

Have you thought about a landing pad approach: Inherited Traits / Implementations (Methods)??


#14

Landing pad doesn’t quite work because I can’t write all the trait methods in a landing pad trait because some of the traits will be defined by user libraries. For example, in the sketch I described, a landing pad trait in ‘Base’ wouldn’t know about M, N, O. I could have a landing pad per lib, but then it’s harder to mix and match the A impl types and B,M,etc. traits you actually want to use for a particular project.

It did inspire a way to remove the unsafe hack, though, by using yet more macros. I can have a trait like

pub trait InterfaceController<T: ?Sized> {
    fn from_controller(controller: Box<T>) -> Self;
    fn to_controller(self) -> Box<T>;
}

that the X user lib macro-defined enum implements for each of the T: A,B,…M,…, etc. it knows about, then each of the A implementations just need to be generic on this trait for the controller traits they care about, like A<T: InterfaceController<B + M>>, and as long as the user lib defined X type impls all the right trait this works. It’s a lot of macros and generated code (that monomorphizes to yet more), but there’s typically only one such X type defined in an actual binary, so it’s reasonable code bloat. So in this sense X is a landing pad, but mostly macro created with implementations (via macro defs) split between multiple libs. (Also in the real version there are reasons this can’t just be based on From, as it might look like here.)

Still, the unsafe hack was useful to briefly test other parts of the system and convince myself they were fine, so even though everything is safe now I appreciate Rust giving the unsafety freedom to briefly work around a wart. Thanks everyone for the help.

e: from a Rust learning standpoint, I would say what I learned from this process was refining my ideas about how to split something I would do in C++ templates between Rust generics and macros. I often forget about macros or go to them as a last resort, which ends up lending weight to wrong predisposition against code size/verbosity when trying to tackle something through generics alone.