Closed flavius closed 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.
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.
Is this issue resolved?
@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.
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);
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.
@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.
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
.
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?
OK closing this issue for now. Please open a new issue if you still have problems!
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.