Skip to content

How's the event downcasted? #13

@PatatasDelPapa

Description

@PatatasDelPapa

TLDR: This become larger than I intended but putting it short I don't understand how the Box<dyn ProcessEventEntry> (and the EventEntry) is able to tell to which event downcast into. I've put my understanding of the code bellow, feel free to skip it I just want to know how's able to.

I've been reading the article and source code a lot recently trying to gain inspiration for my own proyect and I can't understand how's the event downcasted in some places in the code. I think I understand the high level perspective on how things works but I can't seem to grasp the implementation details.
From what I understand, first a struct is defined then implement the trait Component
where the event is defined in the associated type

pub trait Component: ProcessEventEntry {
    type Event: Debug + 'static;
    pub fn process_event(
        &self, 
        self_id: ComponentId<Self::Event>, 
        event: &Self::Event, 
        scheduler: &mut Scheduler, 
        state: &mut State
    );
}

that struct is then inserted in the Scheduler which returns a ComponentId which is just a Key (id and phantondata) for components, the component is saved in a container named Components that is a HashMap<usize, Box<dyn Any> at that point the container forgets about the concrete components and their associated event from the Component trait.
The Scheduler is a a BinaryHeap<EventEntry> (as I'm only concentrating on figuring out how the downcasting works I'm going to ignore the clock), EventEntry is defined as

pub struct EventEntry {
    time: Reverse<Duration>,
    component: usize,
    inner: Box<dyn Any>,
}
impl EventEntry {
    /* Omited */
    #[must_use]
    pub(crate) fn downcast<E: fmt::Debug + 'static>(&self) -> Option<EventEntryTyped<'_, E>> {
        self.inner.downcast_ref::<E>().map(|event| EventEntryTyped {
            time: self.time.0,
            component_id: ComponentId::new(self.component),
            component_idx: self.component,
            event,
        })
    }

    #[must_use]
    pub(crate) fn component_idx(&self) -> usize {
        self.component
    }
}

plus all the necessary traits to work in a BinaryHeap.

If I look at how the Scheduler implements schedule I found this

pub fn schedule<E: fmt::Debug + 'static>(
    &mut self,
    time: Duration,
    component: ComponentId<E>,
    event: E,
) {
    let time = self.time() + time;
    self.events.push(EventEntry::new(time, component, event));
}

It takes the ComponentId from when the user struct implementing Component was inserted and creates a EventEntry with it then is inserted on the BinaryHeap but at this point the EventEntry forget about the concrete event. Looking at Simulation I find the step() method that takes the next event from the Scheduler and process it

pub fn step(&mut self) -> bool {
    self.scheduler.pop().map_or(false, |event| {
        self.components
            .process_event_entry(event, &mut self.scheduler, &mut self.state);
        true
    })
}

There's a method on Components called process_event_entry() that's called above and is defined as so

pub fn process_event_entry(
    &self,
    entry: EventEntry,
    scheduler: &mut Scheduler,
    state: &mut State,
) {
    self.components
        .get(&entry.component_idx())
        .unwrap()
        .downcast_ref::<Box<dyn ProcessEventEntry>>()
        .expect("Failed to downcast component.")
        .process_event_entry(entry, scheduler, state);
}

It first takes the corresponding component using the id from the EventEntry (which got it from the ComponentId) as a Box<dyn Any> then it downcast it to a Box<dyn ProcessEventEntry> a non documented but public trait defined as

pub trait ProcessEventEntry {
    fn process_event_entry(&self, entry: EventEntry, scheduler: &mut Scheduler, state: &mut State);
}

At this point the component first inserted is now a Box<dyn ProcessEventEntry> and then its only method process_event_entry() is called passing in the EventEntry, Scheduler and State.
This downcast is possible because of pub trait Component: ProcessEventEntry, only structs implementing Component are able to be inserted so all of them are able to be downcasted to a dyn ProcessEventEntry

The user didn't have to think of implementing ProcessEventEntry because of the following interesting Blanked Implementation

impl<E, C> ProcessEventEntry for C
where
    E: fmt::Debug + 'static,
    C: Component<Event = E>,
{
    fn process_event_entry(&self, entry: EventEntry, scheduler: &mut Scheduler, state: &mut State) {
        let entry = entry
            .downcast::<E>()
            .expect("Failed to downcast event entry.");
        self.process_event(entry.component_id, entry.event, scheduler, state);
    }
}

At this point I'm totally lost how is this process_event_entry() able to tell to which E it has to downcast on

let entry = entry
    .downcast::<E>()
    .expect("Failed to downcast event entry.");

Here we get a EventEntryTyped which was already seen in the downcast method of EventEntry and is defined as

pub struct EventEntryTyped<'e, E: fmt::Debug> {
    pub time: Duration,
    pub component_id: ComponentId<E>,
    pub component_idx: usize,
    pub event: &'e E,
}

now all the info has been get and the method defined by the user is called

self.process_event(entry.component_id, entry.event, scheduler, state);

How the Box<dyn ProcessEventEntry> able to tell to which event downcast into from what I've seen all the info at that point was lost, the EventEntry has a Box<dyn Any> as the event, the ComponentId which has the event concrete type is already lost by that point, the component itself only remembers that it implements a ProcessEventEntry trait. I've reading and studing the code source but I can't seem to understand.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions