rhai: Cannot reach a function that defined in non-entry module from Rust
Hello!
I’m trying to call a Rhai closure from Rust. I have two Rhai script files. The entry.rhai is the only file evaluated by Rust.
entry.rhai
import "utils/fps-counter" as FPSCounter;
utils/fps-counter.rhai
mk::Event::Update::listen(|event| {
print("called");
});
The problem is, Rhai runtime is unable to reach the registered closure.
Function not found: anon$5ea4915a2a21b372 (mk::script::api::event::lifecycles::Update)
But it works well if I move the function call of mk::Event::Update::listen to entry.rhai.
entry.rhai
mk::Event::Update::listen(|event| {
print("called");
});
called
called
called
...
I’m calling that closure like below:
struct CompiledModule {
ast: AST,
scope: Scope<'static>,
}
pub struct ScriptManager {
engine: Engine,
module: Option<CompiledModule>,
}
impl ScriptManager {
pub fn compile(&mut self, script: impl AsRef<str>) -> Result<()> {
let scope = Scope::new();
let ast = self
.engine
.compile_into_self_contained(&scope, script)
.with_context(|| "failed to compile the entry script")?;
self.module = Some(CompiledModule { ast, scope });
Ok(())
}
pub fn call<R>(&self, f: &FnPtr, args: impl FuncArgs) -> Result<R>
where
R: Clone + Send + Sync + 'static,
{
let module = if let Some(module) = &self.module {
module
} else {
return Err(anyhow!("no entry script compiled"));
};
f.call(&self.engine, &module.ast, args)
.map_err(|err| {
println!("err: {}", err);
err
})
.with_context(|| "failed to call the function")
}
}
Any idea on this problem? What should I check?
Thank you as always!
About this issue
- Original URL
- State: closed
- Created 2 years ago
- Comments: 28
You’re correct. Some systems simply store the the function itself with the closure, so it can, sort of, “always find” it whenever you need to call it. However, this doesn’t resolve you from having to make available other functions that this closure may call, which essentially is the same problem.
It is much easier to do this without multiple nested layers of modules that import each other, each with proper encapsulation. Encapsulated modules mean that you’re not supposed to affect what’s inside a module from the outside. However, by storing a closure to call later on, if it depends on something within a deeply-nested module, then there are basically only a few options:
Of course, if you are not loading dynamic modules (e.g. you’re using a system where the modules are pre-loaded into the engine), then it obviously is not a problem.
The problem always surfaces when: 1) you have a closure, 2) that depends on some function, 3) within a dynamically-loaded module that’s deeply-nested, 4) and modules are generally encapsulated. Take away one of them and you don’t have a problem.
Ok, I successfully handled this issue. I can call closures that are not defined in root module now! But, I think, it is very very counterintuitive, and very tricky to implement - I have written module caching logics and many other things to overcome this issue. Moreover, it’s very inefficient because this solution introduces extra overhead. And I have to allocate some dynamic length of vectors every time to call a Rhai function/closure.
Can it be better somehow? If not, it’s ok for now, but I will have to consider to use other languages 😦
Anyway, thank you 😃
I have tried it and successfully recreated the
NativeCallContextsimply by storing all the fields:In this case, you don’t even need to keep the AST around, as all your functions/closures reside in modules. All you need is the path to the correct module for each closure. But of course you’d need it for the resolver anyway.
Seems like closures are somehow not kept in loaded modules. That’s a good catch. I’ll fix it.
And I think it’s very common to register a script function/closure to the host side, and call them back later. The Rhai should support those features, as the Rhai is script language, in my opinion.
Well, at least
Luadoesn’t have is problem. Normally, there’s not state to management, to call closures. Closures are always callable, because that closure holds captures in their hand(closure = function + captures).But I don’t know the Rhai’s implementation well. Maybe the Rhai need context to call closures, to restore/reconstruct an environment to emulate capturing where the closures are defined.
Anyway, As I said, it’s good for now. I hope there’s no further problems… thank you as always 😃
It really is, because you’re really forcing Rhai to do something that it wasn’t designed to do – which is to suspend an execution state and resume it later on (probably on another thread).
My feeling is that any language you embed is going to give you the exact same problem because eventually you’re going to need to do essentially the same thing – which is to save the execution state (up to that point) as a struct… then restore that state (as much as possible) when you’re ready to resume.
Some languages may give you a pre-built way to store the execution state and you might save some keystrokes, but essentially, in the background, it’ll all be done in the same way.
For example, if you do it in JavaScript, and you have multiple layers of modules (like you do), and you want to save a closure and call it later on in some other context (without those layers of modules), then you’re going to find yourself in exactly the same situation. The simple solution, of course, is how JavaScript started off with – forget about moduling and put everything in a global shared namespace (called
window).Ok, let me try. I’ll report here when it’s done.
BTW, I think you’d want the first module in the list, not the last. Anyhow, check the
idof the module to see which one is the right path. I think it should be the first one.Actually I have edited my message just 5 minutes before you replied. So here is my idea of a solution:
You call
NativeCallContext:;iter_namespaces()and get the last namespace (which should be the calling environment). In fact, with this, you can recreate the entire chain of modules.The last module obviously should contain your closure.
You check
Module::id()which should contain the path of that module.You resolve that module. Currently you have to do it via a new resolver, but I’ll add a method to
Enginethat lets you just resolve modules from it.Once you get that module, you recreate the list:
&[ast.as_ref(), module]which you can then use to create your newNativeCalContext. After that, Bob’s your Uncle.