-
Notifications
You must be signed in to change notification settings - Fork 2
Description
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.