-
Notifications
You must be signed in to change notification settings - Fork 47
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
Comments
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:
This is the first piece of the puzzle and allows you to build callback based asynchronous javascript functions. 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 First, the 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 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 // 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 exampleFirst 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. |
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 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. |
No description provided.
The text was updated successfully, but these errors were encountered: