uazu / stakker

A lightweight low-level single-threaded actor runtime
https://uazu.github.io/stakker/
Apache License 2.0
167 stars 9 forks source link

Call and refer to actors that implement a trait #1

Closed anacrolix closed 4 years ago

anacrolix commented 4 years ago

Hi Jim,

I've been trying out several actor model implementations, include Stakker, and wasn't able to figure out how to refer to an actor that implements a trait.

https://github.com/anacrolix/eratosthenes/blob/8bdb8c9ed5bd9e924bd483e2f480dd87f9fc7359/rust/stakker/src/main.rs#L104

On that line, I want to initialize a Link, with a Printer, instead of another Link, but I get this error:

$ cargo check
    Checking eratosthenes-stakker v0.1.0 (/Users/anacrolix/src/eratosthenes/rust/stakker)
error[E0308]: mismatched types
   --> src/main.rs:104:16
    |
104 |     let tail = actor!(system, Link::init_tail(printer), ret_nop!());
    |                ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ expected struct `Link`, found struct `Printer`
    |
    = note: expected struct `stakker::actor::ActorOwn<Link>`
               found struct `stakker::actor::ActorOwn<Printer>`
    = note: this error originates in a macro (in Nightly builds, run with -Z macro-backtrace for more info)

I expect it has to do with Link::next, which is of type ActorOwn<Link>. Actually I'm happy with a reference to an actor that merely implements Next, but I'm not sure how I'd go about expressing that. (I don't think ActorOwn<Next>) will work.

To experiment with it, checkout that repo, and run cargo check in rust/stakker.

uazu commented 4 years ago

I'm trying to figure out what you're trying to do. Do you want both Printer and Link to implement Next, and then have an ActorOwn<Next> which could refer to either? It just doesn't work that way. The type has to be statically known. You can't have a Vec<Next> either. Really it looks like you're translating code from another language, and the way you're approaching it in Rust isn't the most natural way at all.

The current actor code is designed around static types. Where we need dynamic glue (e.g. callbacks) it is solved in Stakker using Ret or Fwd.

However, I can imagine situations where someone might want to have an actor own one of a class of child actors without knowing the exact type. So this is a dynamic type, rather than a statically-known type. Rust uses the dyn keyword for this kind of case, and usually it mean boxing things and creating a fat pointer which attaches a vtable to the pointer, so that the code knows how to do all the different operations.

Rust does magic around Box and Rc, converting impl to dyn, called coercion. This is a complicated corner of Rust. There is an unstable feature of Rust called CoerceUnsized which might help, but I'm not sure whether it can be applied to how I have actors working. So this is all pretty complicated deep Rust hacking to do impl to dyn.

However, this can probably be solved with some glue, getting things into the right place for Rust to do its coercion magic on a type it knows how to coerce. So below is an example that does what you want. However I still don't think that this is the right way to approach the problem in Rust. There are much quicker ways of implementing the algorithm without using dynamic types, for example using an enum containing all the different options for example.

use stakker::*;
use std::time::Instant;

// External interface of all Animals
trait Animal {
    fn sound(&self);
}

// A particular animal, wraps any actor that implments AnimalActor
struct AnAnimal<T: AnimalActor + 'static>(ActorOwn<T>);
impl<T: AnimalActor + 'static> Animal for AnAnimal<T> {
    fn sound(&self) {
        call!([self.0], sound());
    }
}

// Internal interface of animal actors
trait AnimalActor: Sized {
    fn sound(&self, cx: CX![]);
}

struct Cat;
impl Cat {
    fn init(_: CX![]) -> Option<Self> {
        Some(Self)
    }
}
impl AnimalActor for Cat {
    fn sound(&self, _: CX![]) {
        println!("Miaow");
    }
}

struct Dog;
impl Dog {
    fn init(_: CX![]) -> Option<Self> {
        Some(Self)
    }
}
impl AnimalActor for Dog {
    fn sound(&self, _: CX![]) {
        println!("Woof");
    }
}

fn main() {
    let mut stakker = Stakker::new(Instant::now());
    let s = &mut stakker;

    let animal1 = AnAnimal(actor!(s, Dog::init(), ret_nop!()));
    let animal2 = AnAnimal(actor!(s, Cat::init(), ret_nop!()));

    let mut list: Vec<Box<dyn Animal>> = Vec::new();
    list.push(Box::new(animal1)); // <- dyn coercion occurs here
    list.push(Box::new(animal2)); // <- dyn coercion occurs here

    for a in list {
        a.sound();
    }
    s.run(Instant::now(), false);
}
uazu commented 4 years ago

Do any other Rust actor systems allow dynamic references to actors by trait? I might have another go at finding a better solution at the weekend.

anacrolix commented 4 years ago

Thanks for the detailed response. I'll get back to you next week.

uazu commented 4 years ago

After some investigation, there is a fundamental problem. If I do Rc<RefCell<Option<T>>>, which gives very roughly the core behaviour we want from an actor (although with runtime checks so less safe and less efficient), Rust rejects this if T is dyn Trait, because of the enum Option, because you can't have an Option<dyn Trait>. Option is just an enum, and digging deeper, it turns out that this is true for all enums. We need the Option because we need to be able to "stop" an actor whilst people still have references, i.e. drop the actor's Self value. So it is not possible to create an Actor<dyn Trait> in safe Rust.

However, I think it can be done (safely) in unsafe Rust. It's a pretty big change, though. There are two workarounds:

Anyway, this will take a while to decide. In any case it means I can't implement this with the "no-unsafe" feature, which is not so good. So maybe I won't put it in. But still making Actor<dyn Trait> work would be nice if it can be done safely. So I'm in two minds still.

For now, there are some techniques you can use to work around the problem:

The second option means having to decide who owns the actor. You could keep all ownership at the top level, or else if you wish to pass ownership to the actor that will do the calling, you need a wrapper that owns the actor without knowing which actor it is exactly. So this is a variation on the "external dyn" code above:

struct AnonActorOwn(Box<dyn AnonActorOwnTrait>);
trait AnonActorOwnTrait {}
struct AnonActorOwnInstance<T: 'static>(ActorOwn<T>);
impl<T: 'static> AnonActorOwnTrait for AnonActorOwnInstance<T> {}

...

// AnonActorOwn hides the type of the contained actor, i.e. makes
// it anonymous
let cat = actor!(s, Cat::init(), ret_nop!());
let dog = actor!(s, Dog::init(), ret_nop!());
let mut owner_list: Vec<AnonActorOwn> = Vec::new();
owner_list.push(AnonActorOwn(Box::new(AnonActorOwnInstance(cat))));
owner_list.push(AnonActorOwn(Box::new(AnonActorOwnInstance(dog))));

Maybe I could add AnonActorOwn to Stakker, to support this kind of case.

So in summary, I think yes it is valid in general to want Actor<dyn Trait> in Stakker. It seems possible to do, but rather involved. I will check if there's any reason why it shouldn't be done. For now, there are some possible workarounds.

Thanks for the question! It was a good one. I will write it up in a FAQ I'm preparing. Reply here if you have any more info on the subject. When you think this is done, we can close the issue.

uazu commented 4 years ago

Thanks to various people in users.rust-lang.org, things are clearer now. Stable enums can't support dyn Trait because it messes up their layout optimisations. So there is absolutely no way to do Actor<dyn Trait> in safe Rust. I could do it safely in unsafe Rust, just that it would make the Actor<dyn Trait> feature unavailable with "no-unsafe".

However there is another workaround that would let you write trait-based actors, that works out quite neatly. There's an example in the discussion comments.

So I'm going to create issues for two new features: One for the macro to help create these trait-based actors, and another for ActorOwnAnon, which I think could be useful in other cases. I will get these added when I can. But for now you can use the example code from the discussion.

uazu commented 4 years ago

Okay, I've implemented those two features now in version 0.1.2: actor_of_trait! and ActorOwnAnon, so that resolves the issue.

anacrolix commented 4 years ago

Thanks!