Non understandable error while upcasting then downcasting trait object using Any

#1

Hi,

I’m trying to implement an event bus where we can register handler to react to events.

For that I upcast a struct to a Box<Any> to store it it the subscriptions list and then downcast_ref to access the underlying trait object to dispatch the events.

The problem is that downcast_ref().unwrap() panic and I don’t understand why :confused: as I’m only trying to ‘retrieve’ the trait object I’ve stored earlier.

here is the code:

use std::any::{Any, TypeId};
use std::collections::HashMap;

#[derive(Debug)]
pub enum MyError {
    A,
}

pub trait Subscription {
    type Error;
    type Message;
    type Context;

    fn handle(&self, ctx: &Self::Context, msg: &Self::Message) -> Result<(), Self::Error>;
}


struct CreateProfile;
struct AccountCreatedV1{
    id: String,
}

#[derive(Debug)]
struct Context{}

impl Subscription for CreateProfile {
    type Error = MyError;
    type Message = AccountCreatedV1;
    type Context = Context;

    fn handle(&self, _ctx: &Self::Context, msg: &Self::Message) -> Result<(), Self::Error> {
        println!("account created: {}", msg.id);
        return Ok(());
    }
}


type TypeMap<A> = HashMap<TypeId, A>;

struct Broker{
    pub subscriptions: TypeMap<Vec<Box<Any>>>,
}


fn print_typeid<T: ?Sized + Any>(_s: &T) {
    println!("{:?}", TypeId::of::<T>());
}

impl Broker {
    fn new() -> Self {
        return Broker{
            subscriptions: HashMap::new(),
        };
    }

    fn subscribe<S: Any, C, M: Any, E>(&mut self, subscription: S)
    where S: Subscription<Context = C, Message = M, Error = E> {
        let msg_id = TypeId::of::<M>();
        let boxed = Box::new(subscription);
        print_typeid(&boxed);
        self.subscriptions.insert(msg_id, vec![boxed]);
    }

    fn publish<C: Any, M: Any, E: Any>(&mut self, ctx: &C, message: &M) -> Result<(), E> {
        let msg_id = TypeId::of::<M>();
        if let Some(subscriptions) = self.subscriptions.get_mut(&msg_id) {
            for subscription in subscriptions {
                // the line where it fails
                let subscription: &Box<Subscription<Context = C, Message = M, Error = E>> = subscription.downcast_ref().expect("error downcasting");
                subscription.handle(ctx, message)?;
            }
        }
        return Ok(());
    }
}


static mut EVENT_BUS: Option<Broker> = None;

fn event_bus() -> &'static mut Broker {
    unsafe {
        if EVENT_BUS.is_none() {
            EVENT_BUS = Some(Broker::new());
        }

        return EVENT_BUS.as_mut().unwrap();
    }
}



fn subscribe<S: Any, C: Any, M: Any, E>(subscription: S)
where S: Subscription<Context = C, Message = M, Error = E> {
    event_bus().subscribe(subscription);
}

fn publish<C: Any, M: Any, E: Any>(ctx: &C, message: &M) -> Result<(), E> {
    return event_bus().publish(ctx, message);
}


fn main() {
    let ctx = Context{};
    subscribe::<_, _, AccountCreatedV1, _>(CreateProfile{});
    publish::<_, _, MyError>(&ctx, &AccountCreatedV1{
        id: "123".to_string(),
    }).unwrap();

    println!("{:?}", ctx);
}

https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=eae5137e93d96b509d43abdb7e3dddb8

Best regards,
Sylvain

#2

Your Any type is a concrete structure, likely your after (with a few more changes);

let boxed = Box::new(subscription) as Box<dyn Subscription<Context = C, Message = M, Error = E>>;
self.subscriptions.insert(msg_id, vec![Box::new(boxed) /* as Box<dyn Any> */]);

event_bus() should be unsafe fn.

1 Like
#3

I’m afraid you may not be able to do the exact thing you’re trying. From the module documentation for Any:

Note that &Any is limited to testing whether a value is of a specified concrete type, and cannot be used to test whether a type implements a trait.

Meaning, given a Box<S> that is upcase to a Box<dyn Any>, you can’t then downcast it to a Box<dyn Subscription<...>> even if S: Subscription. This is a limitation in how Any is implemented.

What @jonh is suggesting is that you double-Box the reference: Any can only downcast to a concrete type, and while dyn Subscription<...> is not concrete, it just so happens that Box<dyn Subscription<...>> is a concrete type.

Here is a version of your playground code implementing that change. I also had to specify that the E and C parameter types were 'static, because of requirements on Any.

1 Like
#4

Hi,
That’s it!
Thank you!

I was mistaken the trait bound S: Subscription with ‘generic parameters’.

#5

As @jonh said, that event_bus is unsafe: if you call it and ever coerce the returned reference to a &'static Broker any ulterior call to event_bus() becomes UB; static mut is very likely to end up removed from the language altogether because of the difficulty to handle it soundly.

For a safe mutable global, you may look at ::lazy_static, or, given your API, try the lighter ::once_cell:

use ::std::sync::Mutex;

fn event_bus () -> &'static Mutex<Broker>
{
    use ::once_cell::sync::OnceCell;
    static EVENT_BUS: OnceCell<Mutex<Broker>> = OnceCell::INIT;
    EVENT_BUS.get_or_init(|| Mutex::new(Broker::new()))
}
1 Like