Doug
Doug

Reputation: 35216

Is there any way cast to a generic T, where T is an arbitrary trait type?

My question title is a little vague, but in essence I want to achieve the following:

This is broadly speaking possible by forcing Content to know explicitly about BehaviourA, etc. like:

fn as_a(&self) -> Option<&BehaviourA>

However, this requires that the Content type explicitly knows about all behaviour types, which is not possible if Content is exported as an api in a crate.

Ideally Content would have an api like:

fn behaviour<T>(&self) -> Option<T> where T: Any;

Which could be invoked at runtime as:

match content_item.behaviour::<&BehaviourA>() {
  Some(x) => { ... },
  None => {}
}

However, I cannot find any way to implement this function.

My best attempt reads like this:

fn behaviour<T>(&self) -> Option<T> where T: Any {
  let own_type = TypeId::of::<T>();
  if own_type == TypeId::of::<Behaviour>() { return Some(self as T); }
  return None;
}

But this results in:

src/foo.rs:21:62: 21:71 error: non-scalar cast: `&interface::test::Foo` as `T`
src/foo.rs:21       if own_type == TypeId::of::<Behaviour>() { return Some(self as T); }

I appreciate there is no way to introspect a list of traits on an object at runtime, but here I'm explicitly casting to a known type in the implementation; it's simply that the bound T is not always satisfied.

Is there any way of doing this at all, even unsafely?

An example playpen showing the error is here: http://is.gd/fZNFA8

(NB. I have seen Can you restrict a generic to T where T is a trait implemented by Self?, but my question is different; in a nutshell, is there any way, even unsafely to generate a trait reference at runtime?)

Upvotes: 1

Views: 881

Answers (1)

DK.
DK.

Reputation: 59145

The fundamental problem here is that traits aren't types. When you say &BehaviourA, you're not referring to the trait, you're referring to the object-safe type which is derived from the trait. What you need to do is tell Rust that T isn't a type, but a trait.

Tragically, as of 1.6, there's still no way of doing this.

The only way I know of to make something like this work is explicitly, a-la COM: you need a base interface that allows you to ask "cast to a reference to X", then implement that on each concrete type for all supported X.

If you're willing to dip into unsafe, messy code, you can get something vaguely like what you want with some macros (playpen):

// Requires nightly compiler, since it depends on implementation details.
#![feature(raw)]

use std::any::TypeId;
use std::mem::transmute;
use std::raw::TraitObject;

macro_rules! qi {
    ($e:expr, $t:ty) => {
        unsafe {
            let iid = TypeId::of::<$t>();
            let ptr = &$e;
            match ptr.query_interface(&iid) {
                Some(to) => {
                    let tmp: &$t = transmute(to);
                    Some(tmp)
                },
                None => None,
            }
        }
    };
}

macro_rules! impl_unknown_for {
    ($base:ty: $($ints:ty),*) => {
        unsafe impl Unknown for $base {
            fn query_interface(&self, iid: &TypeId) -> Option<TraitObject> {
                unsafe {
                    $(
                        if *iid == TypeId::of::<$ints>() {
                            let to = transmute::<&$ints, _>(self);
                            return Some(to);
                        }
                    )*
                    None
                }
            }
        }
    };
}

unsafe trait Unknown {
    fn query_interface(&self, iid: &TypeId) -> Option<TraitObject>;
}

trait BehaviourA { fn a(&self); }
trait BehaviourB { fn b(&self); }
trait BehaviourC { fn c(&self); }

struct Thingy;

impl_unknown_for!(Thingy: BehaviourA, BehaviourC);

impl BehaviourA for Thingy {
    fn a(&self) { println!("Thingy::a"); }
}

impl BehaviourC for Thingy {
    fn c(&self) { println!("Thingy::c"); }
}

fn main() {
    let obj: Box<Unknown> = Box::new(Thingy);
    if let Some(ba) = qi!(obj, BehaviourA) { ba.a(); }
    if let Some(bb) = qi!(obj, BehaviourB) { bb.b(); }
    if let Some(bc) = qi!(obj, BehaviourC) { bc.c(); }
}

Upvotes: 1

Related Questions