Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Consider std::future::Future <-> Promise integration #23

Open
theduke opened this issue Aug 12, 2019 · 2 comments
Open

Consider std::future::Future <-> Promise integration #23

theduke opened this issue Aug 12, 2019 · 2 comments
Labels
enhancement New feature or request

Comments

@theduke
Copy link
Owner

theduke commented Aug 12, 2019

No description provided.

@theduke theduke added the enhancement New feature or request label Aug 12, 2019
This was referenced Aug 12, 2019
@only-cliches
Copy link

only-cliches commented May 25, 2020

I got this partly working today, don't quite have it in a clean PR form but I figured I'd at least share my progress.

I'm also pretty new to Rust so it's possible (even probable) that there are better solutions to the problems I encountered, which is partially why I'm sharing my progress here.

So there are a few limitations to make this work:

  1. The JS Context must be in a global thread local. This makes the javascript runtime accessible to tokio::task::spawn_local calls.
  2. The tokio runtime MUST be setup as single threaded using a LocalSet.

This is the first piece of the puzzle and allows you to build callback based asynchronous javascript functions. setTimeout is now doable:

thread_local!(pub static CONTEXT: Context = Context::new().unwrap());

// in main..
CONTEXT.with(|context | {
        context.add_callback("print", |val: String| {
            println!("{}", val);
            return "";
        }).unwrap();

        context.eval("let timerCbs = []; const setTimeout = (cb, timeout) => {
            let len = timerCbs.length;
            timerCbs.push(cb);
            async_timers(len, timeout);
            return len;
        };").unwrap();
        
        context.add_callback("async_timers", |index: i32, timeout: i32| {
            let time = timeout as u64;
            let idx = index;
            tokio::task::spawn_local(async move {
                tokio::time::delay_for(Duration::from_millis(time)).await;
                CONTEXT.with(|ctx| {
                    ctx.eval(format!("timerCbs[{}]();", idx).as_str()).unwrap();
                });
            });
            index
        }).unwrap();
})

With the code above, you can do this and it works:

// MUST be ran in tokio::task::LocalSet
CONTEXT.with(|context | {
        context.eval("setTimeout(() => {
            print('hello, world!');
        }, 2000)").unwrap();
});

The nice thing here is you are tying into the tokio runtime completely, only two calls into the javascript runtime are needed.

The second piece is setting up an async runtime in the quickjs library. This will let you tie .await in rust to await in javascript.

First, the ContextWrapper needs some new properties to store Future waker functions.

pub struct ContextWrapper {
    runtime: *mut q::JSRuntime,
    context: *mut q::JSContext,
    /// Stores callback closures and quickjs data pointers.
    /// This array is write-only and only exists to ensure the lifetime of
    /// the closure.
    // A Mutex is used over a RefCell because it needs to be unwind-safe.
    callbacks: Mutex<Vec<(Box<WrappedCallback>, Box<q::JSValue>)>>,
    pub wakers: Arc<Mutex<HashMap<u64, Waker>>>,
    pub wakerCt: Arc<Mutex<u64>>
}

Now a new data type can be created that will execute async javascript using callbacks.

/// Exectute async javascript
pub struct JsAsync<X> {
    context: &'static LocalKey<Context>,
    code: String,
    p: Option<PhantomData<X>>,
    setup: bool,
    index: u64
}

impl<X> JsAsync<X> where X: TryFrom<JsValue>,
X::Error: Into<ValueError> {
    /// fire off async javascript
    pub fn exec(context: &'static LocalKey<Context>, code: String) -> JsAsync<X> {
        JsAsync {
            context: context,
            code: code,
            p: None,
            setup: false,
            index: 0
        }
    }
}

impl<X> Unpin for JsAsync<X> where X: TryFrom<JsValue>,
X::Error: Into<ValueError>  {

}

impl<X> Future for JsAsync<X> where X: TryFrom<JsValue>,
X::Error: Into<ValueError> {
    type Output = std::result::Result<X, ExecutionError>;
    fn poll(self: std::pin::Pin<&mut Self>, task_ctx: &mut std::task::Context<'_>) -> std::task::Poll<<Self>::Output> { 
        
        let this = std::pin::Pin::into_inner(self);

        if this.setup {
            this.context.with(|ctx| {
                {
                    let mut wakers = ctx.wrapper.wakers.lock().unwrap();
                    wakers.remove(&this.index);
                }
                let js = format!("this.__async_values[{}][1];", this.index);
                std::task::Poll::Ready(ctx.eval_as::<X>(js.as_str()))
            })
        } else {
            this.setup = true;
            this.context.with(|ctx| {
                let mut idx;
                {
                    let mut ct = *ctx.wrapper.wakerCt.lock().unwrap();
                    idx = ct;
                    ct += 1;
                }
                this.index = idx;
                {
                    let mut wakers = ctx.wrapper.wakers.lock().unwrap();
                    wakers.insert(idx, task_ctx.waker().clone());
                }
                let jsExec = format!("(async function (complete, error) {{
                    {}
                }})(this.__async_callback({}, false), this.__async_callback({}, true));", this.code, idx, idx);
                ctx.eval(jsExec.as_str()).unwrap();
            });
            std::task::Poll::Pending
        }
    }
}

Also the ContextWrapper needs a new method that should be called ONCE.

    pub fn setup_async(&self) -> Result<(), ExecutionError> {

        let wakers = Arc::clone(&self.wakers);

        self.add_callback("rs_async_callback", move |index: i32| {
            let waker = wakers.lock().unwrap();
            match waker.get(&(index as u64)) {
                Some(x) => {
                    x.clone().wake();
                },
                None => {

                }
            }
            0i32
        })?;

        self.eval("
            this.__async_values = [];
            this.__async_callback = (idx, error) => {
                return (result) => {
                    this.__async_values[idx] = [error, result];
                    rs_async_callback(idx);
                }
            }
        ")?;

        Ok(())
    }

With the code above, we can now run async javascript with complete or error as callbacks:

// will return string 'hello' after waiting 1 second
let str_result = JsAsync::<String>::exec(&CONTEXT, "setTimeout(() => {
    complete('hello');
}, 1000)".to_string()).await;

Setting up a simple fetch example

First need some glue code...

CONTEXT.with(|context | {
        context.eval("const fetch = (url) => {
            return new Promise((res, rej) => {
                let ln = __fetch_cbs.length;
                __fetch_async(ln, url);
                __fetch_cbs.push([res, rej]);
            });
        };
        const __fetch_cbs = [];
        ").unwrap();
        
        context.add_callback("__fetch_async", |index: i32, url: String| {
            tokio::task::spawn_local(async move {
                // Await the response...
                let body = reqwest::get(url.as_str()).await.unwrap().text().await.unwrap();

                CONTEXT.with(|ctx| {
                    ctx.eval(format!("__fetch_cbs[{}][0]({:?});", index, body).as_str()).unwrap();
                    ctx.step(); // resolve promise
                });
            });
            0i32
        }).unwrap();
});

And viola:

// str_result will contain HTML of google.com
let str_result = JsAsync::<String>::exec(&CONTEXT, "
    complete(await fetch('https://google.com'));
".to_string()).await;

There's obviously lots of cleaning up that needs to happen (and probably a few optimizations) before this can be safely released, but it's a start.

@only-cliches
Copy link

only-cliches commented May 25, 2020

A quick additional note, I tried for quite a while to make this happen:

let str_result = JsAsync::<String>::exec(&CONTEXT, "
    return await fetch('https://google.com');
".to_string()).await;

But I don't think it's possible without constantly triggering the javascript runtime in an event loop (something I'm trying to avoid).

The problem is no matter how you shake it there MUST be a callback of some kind at the end of the javascript function call to wake up the top level Future.

So I think the callback based method is going to be the fastest and easiest way to do things, even if it doesn't feel super modern.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

No branches or pull requests

2 participants