rhaiscript / rhai

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

How do I checkpoint and resume Rhai evaluation? #769

Open vi opened 10 months ago

vi commented 10 months ago

I want something like this

use rhai::{Engine, EvalAltResult, Position};

struct MyContext {
    e: Engine,
}

impl MyContext {
    fn new() -> MyContext {
        let mut e = Engine::new();
        fn suspend() -> Result<i32, Box<EvalAltResult>> {
            let resumed = true; // ?
            if resumed {
                Ok(3 /* assuming getting from some channel */)
            } else {
                // How to I save execution context to be able to return twice?
                Err(Box::new(EvalAltResult::ErrorTerminated(().into(), Position::NONE)))
            }
        }

        e.register_fn("suspend", suspend);
        MyContext { e }
    }
    fn start(&self) {
        let _ = self.e.eval::<()>(r#"
            print(1);
            print(suspend());
            print(4);
        "#);
    }
    fn resume(&self, _x: i32) {
        // (Assuming sending _x to the channel)

        // ?
    }
}

fn main()
{
    let state = MyContext::new();
    state.start();
    // Rhai evaluation stack frames should not be absent. It should be in inert, "frozen" state.
    // N suspended evaluations should not occupy N operation system threads.
    println!("2");
    state.resume(3);
}

It should print 1\n2\n3\n4\n.

Is there some example (e.g. using internals feature) to attain this?

Alternatively, is there something like "call with current continuation" in Rhai, to avoid each suspension point becoming a closure in Rhai code (i.e. callback hell)?

schungx commented 10 months ago

What you want is called async, which Rhai isn't.

Async means being able to suspend operation in the middle, store the context (whatever), and then resume later on.

You can easily do that by running the Engine in a separate thread though. Due to the popularity of this, I have just coded up an example, which you'll find here: https://github.com/schungx/rhai/blob/master/examples/pause_and_resume.rs

vi commented 10 months ago

The example does not seem to prevent many paused executions from hogging as many OS threads. Pausing from on_progress does not seem to be significantly different from pausing from suspend.


Callbacks / closures seem to work as a makeshift suspension points. But they lead to ugly Rhai source code.

What can be done with a patterns like this:

func1(arg1, |ret1| {
   func2(arg2, ret1, |ret2| {
      func3(...)
   })
})

, so it would look like

ret1 <- func1(arg1);
ret2 <- func2(arg2, ret1);
func3(...)

Can Rhai's "custom syntax" be used for that? Or maybe better to just pre-process source code and just turn A <- B();s into B(|A| { /* rest of the file/block */ })?

schungx commented 10 months ago

Well, that's the pitfalls of using raw threads.

Async is invented just to solve your problem. The compiler saves up you calling stack, allowing you to resume later on. What you described is actually "coroutines" which forms the basis of async in modern languages.

Unfortunately Rhai is not intended for async use. If you need it, the you can look into an async scripting engine.

IMHO, it is a very bad idea to do async in scripts (look at the mess called JavaScript). If you intend scripts to be written by non-expert programmers (otherwise why use scripts?) then you don't really want users to hassle with the problem of async.

Rhai is not intended for this usage scenario. You are better advised to split your API into sections with the async barriers in between. If you want to script an entire application in Rhai with an async API, just like JavaScript... then maybe using JavaScript is best for your use case.

In your example, you can separate your API into different functions:

fn on_init() {
    print(1);
}

fn on_suspend() {
    return 42;
}

fn on_resume(x) {
    print(x);
}

fn on_stop() {
    print(4);
}
schungx commented 10 months ago

Callbacks / closures seem to work as a makeshift suspension points. But they lead to ugly Rhai source code.

What you described is, again, async.

This is called the CPS (Continuation-Passing Style) of an async call, before the popular async/await keywords were invented.

JavaScript has massive amounts of CPS code which led to callback-hell.