Help fix interfaces and polymorphism for a discrete event simulator

Hi, i'm exercising rewriting this simple discrete event simulator in Rust. The source code is given in Java and from the get go author operates with interfaces and abstract classes that i tried to port to Rust.

Here's a playground link

I think most of my errors are from mixing the knowledge generics and trait objects. Can you please help me to fix it and understand what i did wrong?

Some specific questions i have are:

  • Why does compiler ask me use dyn, dont i know all objects that implement Event at compile time?
  • Should my events be put on heap using Box<> and why?

In Rust, containers like BTreeSet can only ever contain a single concrete type; the compiler will organize the structure in memory based on its layout. To allow polymorphism, Rust defines a special type, a trait object, for most traits that can represent any object which implements that trait.

In modern Rust, this is always spelled as dyn TraitName. Historically, just TraitName was allowed, but this syntax was causing lots of confusion: traits and types are two distinct but related concepts in Rust, and the appearance of a trait name where a type is expected proved confusing.

Trait objects are dynamically-sized: The concrete type that it's standing in for could be 1 byte or 4kB, and the trait object is hiding this detail from the compiler. This prevents using trait objects directly inside something like a BTreeMap, as there's no way for the compiler to know how large each row in the BTree nodes should be. Instead, they need to appear behind some kind of a pointer, like a Box, which will have the same size regardless of the size of object it's pointing to.

1 Like

I got your example to compile. Most of the changes were minimal, but I did need to make some significant changes around do_all_events. Rust doesn't generally allow you to modify anything about a collection while you're iterating over it, so I had to engineer a collect-changes-and-apply-them-later approach:

    fn do_all_events(&mut self) {
        let mut changes = PendingChanges {
            next_id: self.next_id,
            added: Default::default(),
            removed: Default::default()
        };
        for (_,e) in &mut self.events {
            e.execute(&mut changes)
        }
        self.next_id = changes.next_id;
        self.events.extend(changes.added);
        changes.removed.into_iter().for_each(|id| self.cancel(id));
    }
2 Likes

Thanks, i think i'll be able to take it from here. Good you mentioned that we generally cant iterate a collection and change at the same time, so maybe some functional pattern will help with that. Also thanks for clarifying that collections need to know their element sizes at compile time and thats the reason for using a smart pointer. Last clarifying question:

Before i had the following code that threw an error, something along the lines of unsafe object trait.

trait Event {
    // executes an event for a simulator
    fn execute(&self, simulator: &mut impl Simulator);
}

That is changed to:

trait Event {
    // executes an event for a simulator
    fn execute(&self, simulator: &mut dyn Simulator);
}

Was it because the my trait object had a generic type that is not allowed for the trait to be object safe according to this?

1 Like

Right. The version with impl Simulator will make the compiler codegen a separate instance of the method for each Simulator type it's called with, which means it can't construct the vtable used in trait objects. By changing it to dyn Simulator, the compiler only needs to make one implementation, which will use Simulator's vtable to find the right implementation.

This has the downside that Simulator also needs to be object-safe. There are some tricks to get around that if it becomes a problem, but they're not exactly straightforward.

2 Likes

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.