rhaiscript / rhai

Rhai - An embedded scripting language for Rust.
https://crates.io/crates/rhai
Apache License 2.0
3.73k stars 175 forks source link

Using trait objects with rhai #384

Closed flavius closed 3 years ago

flavius commented 3 years ago

From my experiments, it's relatively easy to put dyn traits into the engine.

But how to get them out?

Consider this code:

https://gist.github.com/rust-play/90a0b04c9f6960e7770397b4029187be#file-playground-rs-L78 line 78, get_value requires Clone, but if I add that bound on line 7 then I cannot use Box.

schungx commented 3 years ago

Yes, you can. It is a little trickier than you think.

Your main issue is how to create a trait object that is guaranteed to implement Clone, because you cannot do SomeTrait: Clone.

use std::any::{Any, TypeId};
trait SomeTrait: Any {
    fn as_any(&self) -> &dyn Any;
}
impl Clone for Box<dyn SomeTrait> {
    fn clone(&self) -> Self {
        // Do type inspection and clone
        let any_type = self.as_any();

        if any_type.type_id() == TypeId::of::<SomeType>() {
            let actual = any_type.downcast_ref::<SomeType>().unwrap();
            return Box::new(actual.clone()) as Box<dyn SomeTrait>;
        } else if any_type.type_id() == TypeId::of::<SomeOtherType>() {
            let actual = any_type.downcast_ref::<SomeOtherType>().unwrap();
            return Box::new(actual.clone()) as Box<dyn SomeTrait>;
        } else {
            unreachable!("This is not any type we know!!!");
        }
    }
}

This solution uses the Any trait to do runtime type inspection. The down side is that you must know all the types in advance, or you simply cannot clone it. You cannot clone a trait object generically.

schungx commented 3 years ago

Or, as discussed on Discord, use Rc<dyn SomeTrait> and it will automatically be Clone. This may be the solution you need, unless you cannot share that object for some reason.

schungx commented 3 years ago

Is this issue resolved?

flavius commented 3 years ago

@schungx Thanks. Not really, no. I've tried with Rc but I didn't manage to get it to compile. I also tried with Any (it's not a problem, I'm fine in this case because I know all types), but because of clone, I think that the tasks are then saved in a clone of the storage, and so the ".debug" command does not output anything, see https://gist.github.com/rust-play/90a0b04c9f6960e7770397b4029187be#file-playground-rs-L89

I think that this deserves a page in the documentation and/or a working example: how to get data in and out over your own types.

schungx commented 3 years ago

If it doesn't compile, maybe you have a syntax error. Try the following example:

    trait TestTrait {
        fn greet(&self) -> INT;
    }

    #[derive(Debug, Clone)]
    struct ABC(INT);

    impl TestTrait for ABC {
        fn greet(&self) -> INT {
            self.0
        }
    }

    type MySharedTestTrait = Rc<dyn TestTrait>;

    let mut engine = Engine::new();

    engine
        .register_type_with_name::<MySharedTestTrait>("MySharedTestTrait")
        .register_fn("new_ts", || Rc::new(ABC(42)) as MySharedTestTrait)
        .register_fn("greet", |x: MySharedTestTrait| x.greet());

    assert_eq!(
        engine.eval::<String>("type_of(new_ts())")?,
        "MySharedTestTrait"
    );
    assert_eq!(engine.eval::<INT>("let x = new_ts(); greet(x)")?, 42);
schungx commented 3 years ago

Getting data in/out of custom types are as simple as registering an API for that type. The tricky thing for your case is that it is a trait object instead of a concrete type, but essentially to Rhai it is the same, as shown in the code above.

flavius commented 3 years ago

@schungx not sure what I'm missing, this gives at runtime "ErrorFunctionNotFound":

use std::io::{self, BufRead};
use rhai::RegisterFn;
use serde::{Deserialize, Serialize};
use std::rc::Rc;
use std::sync::Arc;
use std::cell::RefCell;
use std::any::{Any, TypeId};

trait StorageSpecification {
    fn tasks_iter(&self) -> Box<dyn Iterator<Item=&Task> + '_>;
    fn save_task(&mut self, task: Task) -> bool;
}

#[derive(Clone, Debug, Serialize, Deserialize)]
struct InMemoryStorage {
    tasks: Vec<Task>,
}

impl InMemoryStorage {
    fn new() -> Self {
        Self {
            tasks: Vec::new(),
        }
    }
}

impl StorageSpecification for InMemoryStorage {
    fn tasks_iter(&self) -> Box<dyn Iterator<Item=&Task> + '_> {
        Box::new(self.tasks.iter())
    }
    fn save_task(&mut self, task: Task) -> bool {
        self.tasks.push(task);
        true
    }
}

type Storage = Arc<dyn StorageSpecification + Send + Sync>;

#[derive(Clone, Debug, Serialize, Deserialize)]
struct Task {
    description: String,
}
impl Task {
    fn new(description: String) -> Self {
        Self {
            description,
        }
    }
}

struct Engine<'a> {
    real_engine: rhai::Engine,
    scope: rhai::Scope<'a>,
}

impl<'a> Engine<'a> {
    fn new() -> Self {
        let mut real_engine = rhai::Engine::new();
        let mut scope = rhai::Scope::new();

        real_engine.register_type_with_name::<Storage>("Storage")
            .register_fn("save_task", |mut x: Storage, t: Task| Arc::<dyn StorageSpecification + Send + Sync>::get_mut(&mut x).unwrap().save_task(t));

        real_engine.register_type::<Task>()
            .register_fn("new_task", Task::new);

        let database = InMemoryStorage::new();
        scope.push("storage", Arc::new(database));

        Self {
            real_engine,
            scope,
        }
    }

    fn eval(&mut self, line: String) -> Result<rhai::Dynamic, Box<rhai::EvalAltResult>> {
        self.real_engine.eval_with_scope::<rhai::Dynamic>(&mut self.scope, line.as_str())
    }

    fn storage(&self) -> Storage {
        self.scope.get_value::<Storage>("storage").unwrap()
    }
}

fn main() {
    let mut stdin = io::stdin();
    let mut engine = Engine::new();
    engine.eval("let t = new_task(\"a\")".to_string());
    for line in stdin.lock().lines() {
        match line {
            Ok(line) => {
                if line == ".debug" {
                    for task in engine.storage().tasks_iter() {
                        println!("task {:?}", task);
                    }
                } else {
                    match engine.eval(line) {
                        Ok(val) => println!("=> {}", val),
                        Err(e) => println!("error: {:?}", e),
                    };
                }
            },
            Err(e) => {
                println!("Error: {}", e);
                break;
            }
        }
    }
}

For testing, the line is: storage.save_task(t)

Also the line .debug is supposed to list all tasks.

schungx commented 3 years ago

You have to learn to communicate more clearly. For example, it'd help to copy the exact error message, including which function is not found.

Also which script call is giving you the error.

And finally, if you inspect the error, you'll find the problem right away: Your storage variable holds an Arc<InMemoryStorage>, but save_task expects Arc<dyn StorageSpecification + Send + Sync>. One is a concrete type, the other is a trait object. They are completely different. Rhai doesn't do automatic type conversion between the two, and neither does Rust.

The key to resolve your problem is to store Arc<dyn StorageSpecification + Send + Sync> as value of storage.

schungx commented 3 years ago

Hi @flavius I see that you've succeeded in getting it working and would like to submit a write-up example for inclusion in the Book. May I ask how it is progressing now?

schungx commented 3 years ago

OK closing this issue for now. Please open a new issue if you still have problems!