Skip to content

Commit

Permalink
Support await for promises to complete (jeroen#134)
Browse files Browse the repository at this point in the history
  • Loading branch information
jeroen authored Dec 21, 2021
1 parent 5f972a0 commit 8166f92
Show file tree
Hide file tree
Showing 14 changed files with 141 additions and 19 deletions.
4 changes: 2 additions & 2 deletions DESCRIPTION
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
Package: V8
Type: Package
Title: Embedded JavaScript and WebAssembly Engine for R
Version: 3.6.0.9000
Version: 4.0.0
Authors@R: person("Jeroen", "Ooms", role = c("aut", "cre"), email = "jeroen@berkeley.edu",
comment = c(ORCID = "0000-0002-4035-0289"))
Description: An R interface to V8: Google's open source JavaScript and WebAssembly
Expand All @@ -27,7 +27,7 @@ Suggests:
testthat,
knitr,
rmarkdown
RoxygenNote: 7.1.1
RoxygenNote: 7.1.2
Roxygen: list(load = "installed", markdown = TRUE)
Language: en-US
Encoding: UTF-8
1 change: 1 addition & 0 deletions NAMESPACE
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export(engine_info)
export(new_context)
export(v8)
export(wasm)
export(wasm_features)
importFrom(Rcpp,sourceCpp)
importFrom(curl,curl)
importFrom(jsonlite,fromJSON)
Expand Down
6 changes: 6 additions & 0 deletions NEWS
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
4.0.0
- If a call to ct$eval(),ct$get(), or ct$call() returns a JavaScript promise,
you can set await = TRUE to wait for the promise to be resolved. It will
then return the result of the promise, or an error in case the promise is
rejected.

3.6.1
- Updated static libv8 for Mac and Linux to v8-v8-9.6.180.12
- Windows ucrt: update libv8 to 9.1.269.38
Expand Down
4 changes: 2 additions & 2 deletions R/RcppExports.R
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ version <- function() {
.Call('_V8_version', PACKAGE = 'V8')
}

context_eval <- function(src, ctx, serialize = FALSE) {
.Call('_V8_context_eval', PACKAGE = 'V8', src, ctx, serialize)
context_eval <- function(src, ctx, serialize = FALSE, await = FALSE) {
.Call('_V8_context_eval', PACKAGE = 'V8', src, ctx, serialize, await)
}

write_array_buffer <- function(key, data, ctx) {
Expand Down
20 changes: 12 additions & 8 deletions R/V8.R
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@
#' literal JavaScript arguments that should not be converted to JSON, wrap them in
#' `JS()`, see examples.
#'
#' If a call to `ct$eval()`,`ct$get()`, or `ct$call()` returns a JavaScript promise,
#' you can set `await = TRUE` to wait for the promise to be resolved. It will then
#' return the result of the promise, or an error in case the promise is rejected.
#'
#' The `ct$validate` function is used to test
#' if a piece of code is valid JavaScript syntax within the context, and always
#' returns TRUE or FALSE.
Expand Down Expand Up @@ -135,20 +139,20 @@ v8 <- function(global = "global", console = TRUE, typed_arrays = TRUE) {
private <- environment();

# Low level evaluate
evaluate_js <- function(src, serialize = FALSE){
get_str_output(context_eval(join(src), private$context, serialize))
evaluate_js <- function(src, serialize = FALSE, await = FALSE){
get_str_output(context_eval(join(src), private$context, serialize, await))
}

# Public methods
this <- local({
eval <- function(src, serialize = FALSE){
eval <- function(src, serialize = FALSE, await = FALSE){
# serialize=TRUE does not unserialize: user has to parse json/raw
evaluate_js(src, serialize = serialize)
evaluate_js(src, serialize = serialize, await = await)
}
validate <- function(src){
context_validate(join(src), private$context)
}
call <- function(fun, ..., auto_unbox = TRUE){
call <- function(fun, ..., auto_unbox = TRUE, await = FALSE){
stopifnot(is.character(fun))
stopifnot(this$validate(c("fun=", fun)));
jsargs <- list(...);
Expand All @@ -167,7 +171,7 @@ v8 <- function(global = "global", console = TRUE, typed_arrays = TRUE) {
}, character(1));
jsargs <- paste(jsargs, collapse=",")
src <- paste0("(", fun ,")(", jsargs, ");")
get_json_output(evaluate_js(src, serialize = TRUE))
get_json_output(evaluate_js(src, serialize = TRUE, await = await))
}
source <- function(file){
if(is.character(file) && length(file) == 1 && grepl("^https?://", file)){
Expand All @@ -177,9 +181,9 @@ v8 <- function(global = "global", console = TRUE, typed_arrays = TRUE) {
# Always assume UTF8, even on Windows.
evaluate_js(readLines(file, encoding = "UTF-8", warn = FALSE))
}
get <- function(name, ...){
get <- function(name, ..., await = FALSE){
stopifnot(is.character(name))
get_json_output(evaluate_js(name, serialize = TRUE), ...)
get_json_output(evaluate_js(name, serialize = TRUE, await = await), ...)
}
assign <- function(name, value, auto_unbox = TRUE, ...){
stopifnot(is.character(name))
Expand Down
24 changes: 24 additions & 0 deletions R/wasm.R
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,12 @@
#' exported functions. This will probably be moved into it's own package
#' once WebAssembly matures.
#'
#' The `wasm_features()` function uses the [wasm-feature-detect](https://github.com/GoogleChromeLabs/wasm-feature-detect)
#' JavaScript library to test which WASM capabilities are supported in the
#' current version of libv8.
#'
#' @export
#' @rdname wasm
#' @param data either raw vector or file path with the binary wasm program
#' @examples # Load example wasm program
#' if(engine_info()$version > 6){
Expand All @@ -30,3 +35,22 @@ wasm <- function(data){
exports = exports
)
}

#' @export
#' @rdname wasm
#' @examples wasm_features()
wasm_features <- function(){
ctx <- v8()
ctx$source(system.file('js/wasm-feature-detect.js', package = 'V8'))
wrapper <- "async function test_wasm_features() {
let out = {};
keys = Object.keys(wasmFeatureDetect);
for (var i = 0; i < keys.length; i++) {
var key = keys[i];
out[key] = await wasmFeatureDetect[key]()
}
return out;
}"
ctx$eval(wrapper)
ctx$call('test_wasm_features', await = TRUE)
}
1 change: 0 additions & 1 deletion V8.Rproj
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,3 @@ StripTrailingWhitespace: Yes

BuildType: Package
PackageInstallArgs: --no-multiarch --with-keep.source
PackageRoxygenize: rd,namespace
1 change: 1 addition & 0 deletions inst/js/wasm-feature-detect.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions man/V8.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 9 additions & 0 deletions man/wasm.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 5 additions & 4 deletions src/RcppExports.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -22,15 +22,16 @@ BEGIN_RCPP
END_RCPP
}
// context_eval
Rcpp::RObject context_eval(Rcpp::String src, Rcpp::XPtr< v8::Persistent<v8::Context> > ctx, bool serialize);
RcppExport SEXP _V8_context_eval(SEXP srcSEXP, SEXP ctxSEXP, SEXP serializeSEXP) {
Rcpp::RObject context_eval(Rcpp::String src, Rcpp::XPtr< v8::Persistent<v8::Context> > ctx, bool serialize, bool await);
RcppExport SEXP _V8_context_eval(SEXP srcSEXP, SEXP ctxSEXP, SEXP serializeSEXP, SEXP awaitSEXP) {
BEGIN_RCPP
Rcpp::RObject rcpp_result_gen;
Rcpp::RNGScope rcpp_rngScope_gen;
Rcpp::traits::input_parameter< Rcpp::String >::type src(srcSEXP);
Rcpp::traits::input_parameter< Rcpp::XPtr< v8::Persistent<v8::Context> > >::type ctx(ctxSEXP);
Rcpp::traits::input_parameter< bool >::type serialize(serializeSEXP);
rcpp_result_gen = Rcpp::wrap(context_eval(src, ctx, serialize));
Rcpp::traits::input_parameter< bool >::type await(awaitSEXP);
rcpp_result_gen = Rcpp::wrap(context_eval(src, ctx, serialize, await));
return rcpp_result_gen;
END_RCPP
}
Expand Down Expand Up @@ -95,7 +96,7 @@ END_RCPP

static const R_CallMethodDef CallEntries[] = {
{"_V8_version", (DL_FUNC) &_V8_version, 0},
{"_V8_context_eval", (DL_FUNC) &_V8_context_eval, 3},
{"_V8_context_eval", (DL_FUNC) &_V8_context_eval, 4},
{"_V8_write_array_buffer", (DL_FUNC) &_V8_write_array_buffer, 3},
{"_V8_context_validate", (DL_FUNC) &_V8_context_validate, 2},
{"_V8_context_null", (DL_FUNC) &_V8_context_null, 1},
Expand Down
46 changes: 45 additions & 1 deletion src/bindings.cpp
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
#include <libplatform/libplatform.h>
#include "V8_types.h"

#if (V8_MAJOR_VERSION * 100 + V8_MINOR_VERSION) < 803
#define PerformMicrotaskCheckpoint RunMicrotasks
#endif

/* __has_feature is a clang-ism, while __SANITIZE_ADDRESS__ is a gcc-ism */
#if defined(__clang__) && !defined(__SANITIZE_ADDRESS__)
#if defined(__has_feature) && __has_feature(address_sanitizer)
Expand All @@ -27,6 +31,7 @@ void ctx_finalizer( v8::Persistent<v8::Context>* context ){
}

static v8::Isolate* isolate = NULL;
static v8::Platform* platformptr = NULL;

// Extracts a C string from a V8 Utf8Value.
static const char* ToCString(const v8::String::Utf8Value& value) {
Expand Down Expand Up @@ -58,6 +63,7 @@ void start_v8_isolate(void *dll){
#if (V8_MAJOR_VERSION * 100 + V8_MINOR_VERSION) >= 704
std::unique_ptr<v8::Platform> platform = v8::platform::NewDefaultPlatform();
v8::V8::InitializePlatform(platform.get());
platformptr = platform.get();
platform.release(); //UBSAN complains if platform is destroyed when out of scope
#else
v8::V8::InitializePlatform(v8::platform::CreateDefaultPlatform());
Expand Down Expand Up @@ -94,6 +100,19 @@ static v8::Local<v8::Script> compile_source(std::string src, v8::Local<v8::Conte
return safe_to_local(script);
}

static void pump_promises(){
v8::platform::PumpMessageLoop(platformptr, isolate, v8::platform::MessageLoopBehavior::kDoNotWait);
isolate->PerformMicrotaskCheckpoint();
Rcpp::checkUserInterrupt();
}

/* Try to resolve pending promises */
static void ConsolePump(const v8::FunctionCallbackInfo<v8::Value>& args) {
pump_promises();
args.GetReturnValue().Set(v8::Undefined(args.GetIsolate()));
}


/* console.log */
static void ConsoleLog(const v8::FunctionCallbackInfo<v8::Value>& args) {
for (int i=0; i < args.Length(); i++) {
Expand Down Expand Up @@ -200,7 +219,7 @@ static Rcpp::RObject convert_object(v8::Local<v8::Value> value){
}

// [[Rcpp::export]]
Rcpp::RObject context_eval(Rcpp::String src, Rcpp::XPtr< v8::Persistent<v8::Context> > ctx, bool serialize = false){
Rcpp::RObject context_eval(Rcpp::String src, Rcpp::XPtr< v8::Persistent<v8::Context> > ctx, bool serialize = false, bool await = false){
// Test if context still exists
if(!ctx)
throw std::runtime_error("v8::Context has been disposed.");
Expand Down Expand Up @@ -233,6 +252,30 @@ Rcpp::RObject context_eval(Rcpp::String src, Rcpp::XPtr< v8::Persistent<v8::Cont
throw std::runtime_error(ToCString(exception));
}

/* PumpMessageLoop is needed to load wasm from the background threads
After this we still need to call PerformMicrotaskCheckpoint to resolve outstanding promises
This may be better, but HasPendingBackgroundTasks() requires v8 8.3, see also
https://docs.google.com/document/d/18vaABH1mR35PQr8XPHZySuQYgSjJbWFyAW63LW2m8-w
*/

// while (v8::platform::PumpMessageLoop(platformptr, isolate, isolate->HasPendingBackgroundTasks() ?
// v8::platform::MessageLoopBehavior::kWaitForWork : v8::platform::MessageLoopBehavior::kDoNotWait)){
// }


// See https://groups.google.com/g/v8-users/c/r8nn6m6Lsj4/m/WrjLpk1PBAAJ
if (await && result->IsPromise()) {
v8::Local<v8::Promise> promise = result.As<v8::Promise>();
while (promise->State() == v8::Promise::kPending)
pump_promises();
if (promise->State() == v8::Promise::kRejected) {
v8::String::Utf8Value rejectmsg(isolate, promise->Result());
throw std::runtime_error(ToCString(rejectmsg));
} else {
result = promise->Result();
}
}

// Serialize to JSON or Raw
if(serialize == true)
return convert_object(result);
Expand Down Expand Up @@ -308,6 +351,7 @@ v8::Local<v8::Object> console_template(){
console->Set(ToJSString("log"), v8::FunctionTemplate::New(isolate, ConsoleLog));
console->Set(ToJSString("warn"), v8::FunctionTemplate::New(isolate, ConsoleWarn));
console->Set(ToJSString("error"), v8::FunctionTemplate::New(isolate, ConsoleError));
console->Set(ToJSString("pump"), v8::FunctionTemplate::New(isolate, ConsolePump));

// R callback interface
v8::Local<v8::ObjectTemplate> console_r = v8::ObjectTemplate::New(isolate);
Expand Down
2 changes: 1 addition & 1 deletion src/legacy/V8.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,7 @@ static Rcpp::RObject convert_object(v8::Local<v8::Value> value){
}

// [[Rcpp::export]]
Rcpp::RObject context_eval(Rcpp::String src, Rcpp::XPtr< v8::Persistent<v8::Context> > ctx, bool serialize = false){
Rcpp::RObject context_eval(Rcpp::String src, Rcpp::XPtr< v8::Persistent<v8::Context> > ctx, bool serialize = false, bool await = false){
// Test if context still exists
if(!ctx)
throw std::runtime_error("Context has been disposed.");
Expand Down
29 changes: 29 additions & 0 deletions vignettes/v8_intro.Rmd
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,35 @@ ct$call("_.filter", mtcars, JS("function(x){return x.mpg < 15}"))

It looks a bit like `.Call` but then for JavaScript instead of C.

## Promises

If a call to `ct$eval()`, `ct$get()`, or `ct$call()` returns a JavaScript promise, you can set `await = TRUE` to wait for the promise to be resolved. It will then return the result of the promise, or an error in case the promise is rejected.

```{r error=TRUE}
js = 'function test_number(x){
var promise = new Promise(function(resolve, reject) {
if(x == 42)
resolve(true)
else
reject("This is wrong")
})
return promise;
}'
# Call will just show a promise
ctx <- V8::v8()
ctx$eval(js)
# A promise does not return anything in itself:
ctx$call("test_number", 42)
# Resolve the promise to the result
ctx$call("test_number", 42, await = TRUE)
# A rejected promise will throw an error
ctx$call("test_number", 41, await = TRUE)
```

## Interactive JavaScript Console

A fun way to learn JavaScript or debug a session is by entering the interactive console:
Expand Down

0 comments on commit 8166f92

Please sign in to comment.