How do I use Any to disambiguate between multiple user-defined structs?


#1

Use-case

I’m writing a code-generator for an IDL in which users can define their own exception types with custom fields. The IDL does not require these user-defined exception types to share any common fields or methods. I want the rust representation of these types to be wrapped in my crate’s common Error type.

Here’s what I came up with:

pub enum Error {
    /// Predefined error type for protocol-level issues.
    Protocol(ProtocolError),
    /// Should hold any user-defined types.
    UserDefined(Box<error::Error + Sync + Send>),
}

Now, obviously, users want to unpack the value contained by UserDefined as the custom type they defined and evaluate its fields and execute its methods. I read that you can ‘downcast’ to a specific type by using Any.

Unfortunately I can’t figure out how exactly to use it with the definition above! Here’s some non-working example code demonstrating what I’m trying to achieve, as well as a playground link.

use std::any::Any;
use std::fmt;
use std::fmt::{Debug, Display, Formatter};
use std::error;

// predefined
pub enum Error {
    Protocol(ProtocolError),
    UserDefined(Box<error::Error + Sync + Send>),
}

// some standard protocol error type (known in advance)
pub struct ProtocolError {
    message: String,
}

// users will define these error types
// and impl. StdError, Display, Debug
#[derive(Debug)]
pub struct MyError {
    message: String,
}

impl error::Error for MyError {
    fn description(&self) -> &str {
        &self.message
    }
}

impl Display for MyError {
    fn fmt(&self, f: &mut Formatter) -> fmt::Result {
        write!(f, "this is my error and here's what caused it:{}", self.message)
    }
}

// attempt to figure out the exact type of
// the wrapped error and do something with it
fn main() {
    let e0 = Error::UserDefined("TEST".to_owned().into());
    let e1 = Error::UserDefined(Box::new(MyError { message: "oh noes!".to_owned() }));
    do_work(&e0);
    do_work(&e1);
}

fn do_work(e: &Error) {
    match *e {
        Error::UserDefined(ref e) => {
            let wrapped: &Box<error::Error + Send + Sync> = e;
            let wrapped = &**wrapped as &Any;
            if let Some(m) = wrapped.downcast_ref::<String>() {
                println!("was a String {}!", m);
            }
            if let Some(x) = wrapped.downcast_ref::<MyError>() {
                println!("was a MyError {}!", x);
            }
        }
        _ => (),
    }
}

This code fails with non-scalar cast: '&std::error::Error + Send + Sync' as '&std::any::Any + 'static', which kinda makes sense - I’ve explicitly cast it to the type contained by the Box, but I thought that all types implicitly implemented Any.

Is what I’m doing possible? And if so, is the approach above the best way to achieve it?


#2

Error defines its own downcast methods, because this kind of casting doesn’t currently work. You should be able to remove the two lines at the start of the match block.


#3

@sfackler: I tried your suggestion (thank you!) and got mixed results:

match *e {
        Error::UserDefined(ref e) => {
            if let Some(m) = e.downcast_ref::<String>() {
                println!("was a String {}!", m);
            }
            if let Some(x) = e.downcast_ref::<MyError>() {
                println!("was a MyError {}!", x);
            }
        }
        _ => (),
    }

fails with:

rustc 1.13.0 (2c6933acc 2016-11-07)
error[E0277]: the trait bound `std::string::String: std::error::Error` is not satisfied
  --> <anon>:47:32
   |
47 |             if let Some(m) = e.downcast_ref::<String>() {
   |                                ^^^^^^^^^^^^ trait `std::string::String: std::error::Error` not satisfied

error: aborting due to previous error

Removing the String match arm allows the above code to work perfectly. I’m not sure how I was able to box the String into an error and yet not be able to unpack it because it doesn’t impl the Error trait.

This leads to a few questions:

  1. What would I have to read/know to have come up with your suggestion myself?
  2. It feels like I’m still flailing when it comes to understanding casting, conversion, and types with automatic impls. How do I get over this knowledge hump?
  3. How do I deal with String above?

#4

I asked a similar question in Trying to fake downcasts with traits defined on traits. If your IDL gives you an exhaustive list of exceptions that client code can possibly encounter, then the enum-of-references approach is probably the easiest to implement.

I don’t think there is an option besides a bare box<Any> in the error arm. The main challenge is that the type ID inside an Any object is chosen at the time of conversion to Any, and if you only have trait object at this point, the type ID will be that of the trait object, and not that of the underlying concrete type.

You can implement custom variants of Any using unsafe code, and such variants could offer the additional traits you want.