erksch
erksch•3mo ago

RustyV8: Example of using PromiseResolver / async?

Is there an example using PromiseResolver? Because it uses a HandleScope, I'm unsure about how to reference it at a later point when async work has been done. Could you provide an example of implementing a async simple sleep function, ideally by pushing an async task to the main Tokio runtime? Problem:
borrowed data escapes outside of closure
`scope` escapes the closure body here
borrowed data escapes outside of closure
`scope` escapes the closure body here
From JS: await doRustAsync(5000); Calling RS:
let export_do_rust_async = |scope: &mut v8::HandleScope,
_args: v8::FunctionCallbackArguments,
mut rv: v8::ReturnValue| {
let millis = args.get(0).to_number(scope).unwrap().value() as u64;

let resolver = v8::PromiseResolver::new(scope).unwrap();
let promise = resolver.get_promise(scope);
rv.set(promise.into());

tokio::spawn(async move {
// Do async work
tokio::time::sleep(Duration::from_millis(millis)).await;
promise.resolve()
});
};
let export_do_rust_async = |scope: &mut v8::HandleScope,
_args: v8::FunctionCallbackArguments,
mut rv: v8::ReturnValue| {
let millis = args.get(0).to_number(scope).unwrap().value() as u64;

let resolver = v8::PromiseResolver::new(scope).unwrap();
let promise = resolver.get_promise(scope);
rv.set(promise.into());

tokio::spawn(async move {
// Do async work
tokio::time::sleep(Duration::from_millis(millis)).await;
promise.resolve()
});
};
This Rust code obviously doesn't work, since: 1. the tokio runtime handle isn't available to the thread. 2. the promise/resolver uses scope, which is borrowed.
9 Replies
bartlomieju
bartlomieju•3mo ago
That's gonna fairly complex. I'l try to give an example tomorrow
erksch
erkschOP•3mo ago
@bartlomieju I managed to get it all to work, but it was quite a fight 🫠
I did manage to implement bidirectional async without MaskFutureAsSend, so that's something 🥂 Can I get back to you if I face another blocking issue? I realize this is a Deno support forum and not really for RustyV8 🙂
bartlomieju
bartlomieju•3mo ago
Sure. In general you probably want to use deno_core for easier async integration
erksch
erkschOP•3mo ago
@bartlomieju When I call a JS function that returns a promise, polling if the promise has finished is expensive. So I implemented a callback on .then, but it needs both the promise and the oneshot sender, so I'm embedding it as data:
let promise = js_test_async.call(scope, global.into(), &[]).unwrap().cast::<v8::Promise>();
let data = PromiseThenCallbackData {
sender: x.1,
promise,
};
let data_box: *mut T = Box::leak(Box::new(typed_data));
let cvoid = data_box as *mut c_void;
let data_external = v8::External::new(scope, cvoid);
let f = v8::FunctionBuilder::<v8::Function>::new(promise_then_callback)
.data(data_external.into())
.build(scope)
.unwrap();
promise.then(scope, f);
let promise = js_test_async.call(scope, global.into(), &[]).unwrap().cast::<v8::Promise>();
let data = PromiseThenCallbackData {
sender: x.1,
promise,
};
let data_box: *mut T = Box::leak(Box::new(typed_data));
let cvoid = data_box as *mut c_void;
let data_external = v8::External::new(scope, cvoid);
let f = v8::FunctionBuilder::<v8::Function>::new(promise_then_callback)
.data(data_external.into())
.build(scope)
.unwrap();
promise.then(scope, f);
Unfortunately this either causes a 1) use-after-free, if I create a short-lived scope (callback outlives it) 2) memory leak I tried to use CallbackScope but that isnt accepted as a scope arg. What's the right approach? Basically I want all of the above to get dropped only when the callback has finished. The callback looks like this:
fn promise_then_callback(
scope: &mut HandleScope<'_>,
args: v8::FunctionCallbackArguments<'_>,
_: v8::ReturnValue<'_>,
)
{
let cvoid = args.data().cast::<v8::External>().value();
let data = helper::cvoid_to_type::<PromiseThenCallbackData>(cvoid);
let data = unsafe { Box::from_raw(data) };

match data.promise.state() {
PromiseState::Pending => {
exit(1); //TODO
}
PromiseState::Fulfilled => {
let result = data.promise.result(scope);
let number: f64 = v8::Local::<v8::Number>::try_from(result)
.ok()
.unwrap()
.value();
data.sender.send(Ok(Box::new(number))).unwrap();
}
PromiseState::Rejected => {
exit(1); //TODO
}
}
fn promise_then_callback(
scope: &mut HandleScope<'_>,
args: v8::FunctionCallbackArguments<'_>,
_: v8::ReturnValue<'_>,
)
{
let cvoid = args.data().cast::<v8::External>().value();
let data = helper::cvoid_to_type::<PromiseThenCallbackData>(cvoid);
let data = unsafe { Box::from_raw(data) };

match data.promise.state() {
PromiseState::Pending => {
exit(1); //TODO
}
PromiseState::Fulfilled => {
let result = data.promise.result(scope);
let number: f64 = v8::Local::<v8::Number>::try_from(result)
.ok()
.unwrap()
.value();
data.sender.send(Ok(Box::new(number))).unwrap();
}
PromiseState::Rejected => {
exit(1); //TODO
}
}
bartlomieju
bartlomieju•3mo ago
You'd need to create a v8::Global out of that promise first to store it in external Other than that it should pretty much work correctly
I tried to use CallbackScope but that isnt accepted as a scope arg. What's the right approach?
Where did you use CallbackScope?
erksch
erkschOP•3mo ago
@bartlomieju Oh, very nice, that makes sense. Thanks a lot!
let data = PromiseThenCallbackData {
sender: received.1,
promise: v8::Global::new(scope, promise),
};
let data = PromiseThenCallbackData {
sender: received.1,
promise: v8::Global::new(scope, promise),
};
I was thinking that the result of the js call would be saved to the scope, which would be released before the callback is executed, so that I would need to run it in a special callbackscope that is then destroyed with the callback, but apparently this works.
bartlomieju
bartlomieju•3mo ago
Nah, the HandleScope is getting dropped at the end of the lexical scope, so you can't really hold onto the values after that You can always leak something, but as you saw that leads to a problems If you need to use v8::Local<...> later you always need to create a v8::Global<...>
erksch
erkschOP•3mo ago
Oh sorry, yes of course, the promise is the result 🙂 but the External and the Function is created on a scope that is destroyed
bartlomieju
bartlomieju•3mo ago
But you "assign it" to the promise and V8 figures out that they need to be kept alive since they are now associated with that promise