From 8dbdc638f0d15e3c6eb7cfbcf08a73e2e0266c14 Mon Sep 17 00:00:00 2001 From: Alex Alikiotis Date: Sat, 27 Apr 2024 20:28:00 +0300 Subject: [PATCH] Adding `queueMicrotask()` to global scope (#218) * Adding queueMicrotask() to the global scope * Updating the README.md * Bumping version to v0.7 * Catching exceptions from microtask callbacks * Fixing multiple queueMicrotask calls throwing exceptions --- Cargo.lock | 2 +- Cargo.toml | 2 +- README.md | 1 + package.json | 2 +- src/bindings.rs | 61 +++++++++++++++++++++++++++++++++++++---------- src/exceptions.rs | 8 ++----- src/js/main.js | 21 ++++++++++++++++ 7 files changed, 76 insertions(+), 21 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 329b425..0fbf9cc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -651,7 +651,7 @@ checksum = "9ea835d29036a4087793836fa931b08837ad5e957da9e23886b29586fb9b6650" [[package]] name = "dune" -version = "0.6.0" +version = "0.7.0" dependencies = [ "anyhow", "assert_fs", diff --git a/Cargo.toml b/Cargo.toml index b2ae87f..c713488 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "dune" -version = "0.6.0" +version = "0.7.0" authors = ["Alex Alikiotis "] edition = "2021" license = "MIT" diff --git a/README.md b/README.md index ed78db9..dc7e7b7 100644 --- a/README.md +++ b/README.md @@ -105,6 +105,7 @@ For more examples look at the examples directory. - [x] `structuredClone`: Creates a deep clone of a given value. - [x] `AbortController` / `AbortSignal`: Allows you to communicate with a request and abort it. - [x] `fetch`: A wrapper around `http.request` (not fully compatible with WHATWG fetch). +- [x] `queueMicrotask`: Queues a microtask to invoke a callback. ### Module Metadata diff --git a/package.json b/package.json index 739966b..5b8c092 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "dune", - "version": "0.6.0", + "version": "0.7.0", "description": "A hobby runtime for JavaScript and TypeScript 🚀", "homepage": "https://github.com/aalykiot/dune#readme", "keywords": [], diff --git a/src/bindings.rs b/src/bindings.rs index 70ffca9..fc0ce8f 100644 --- a/src/bindings.rs +++ b/src/bindings.rs @@ -1,5 +1,6 @@ use crate::dns; use crate::errors::extract_error_code; +use crate::errors::report_and_exit; use crate::errors::IoError; use crate::exceptions; use crate::file; @@ -8,6 +9,8 @@ use crate::net; use crate::perf_hooks; use crate::process; use crate::promise; +use crate::runtime::check_exceptions; +use crate::runtime::JsRuntime; use crate::signals; use crate::stdio; use crate::timers; @@ -48,24 +51,58 @@ pub fn create_new_context<'s>(scope: &mut v8::HandleScope<'s, ()>) -> v8::Local< let global = context.global(scope); let scope = &mut v8::ContextScope::new(scope, context); - // Simple print function bound to Rust's println! macro (synchronous call). - set_function_to( - scope, - global, - "print", - |scope: &mut v8::HandleScope, - args: v8::FunctionCallbackArguments, - mut _rv: v8::ReturnValue| { - let value = args.get(0).to_rust_string_lossy(scope); - println!("{value}"); - }, - ); + set_function_to(scope, global, "print", global_print); + set_function_to(scope, global, "reportError", global_report_error); + set_function_to(scope, global, "$$queueMicro", global_queue_micro); // Expose low-level functions to JavaScript. process::initialize(scope, global); scope.escape(context) } +// Simple print function bound to Rust's println! macro. +fn global_print( + scope: &mut v8::HandleScope, + args: v8::FunctionCallbackArguments, + _: v8::ReturnValue, +) { + let value = args.get(0).to_rust_string_lossy(scope); + println!("{value}"); +} + +// This method may be used to report errors to global event handlers. +// https://html.spec.whatwg.org/multipage/webappapis.html#report-the-exception +fn global_report_error( + scope: &mut v8::HandleScope, + args: v8::FunctionCallbackArguments, + _: v8::ReturnValue, +) { + let exception = v8::Global::new(scope, args.get(0)); + let state_rc = JsRuntime::state(scope); + let mut state = state_rc.borrow_mut(); + + state.exceptions.capture_exception(exception); + drop(state); + + if let Some(error) = check_exceptions(scope) { + report_and_exit(error); + } +} + +// This method queues a microtask to invoke callback. +fn global_queue_micro( + scope: &mut v8::HandleScope, + args: v8::FunctionCallbackArguments, + _: v8::ReturnValue, +) { + let callback = v8::Local::::try_from(args.get(0)).unwrap(); + let state_rc = JsRuntime::state(scope); + let state = state_rc.borrow(); + let ctx = state.context.open(scope); + + ctx.get_microtask_queue().enqueue_microtask(scope, callback); +} + /// Adds a property with the given name and value, into the given object. pub fn set_property_to( scope: &mut v8::HandleScope<'_>, diff --git a/src/exceptions.rs b/src/exceptions.rs index a38a58c..c3faa4f 100644 --- a/src/exceptions.rs +++ b/src/exceptions.rs @@ -98,7 +98,7 @@ pub fn initialize(scope: &mut v8::HandleScope) -> v8::Global { fn set_uncaught_exception_callback( scope: &mut v8::HandleScope, args: v8::FunctionCallbackArguments, - mut rv: v8::ReturnValue, + _: v8::ReturnValue, ) { // Note: Passing `null` from JavaScript essentially will unset the defined callback. let callback = match v8::Local::::try_from(args.get(0)) { @@ -110,15 +110,13 @@ fn set_uncaught_exception_callback( let mut state = state_rc.borrow_mut(); state.exceptions.set_uncaught_exception_callback(callback); - - rv.set(v8::Boolean::new(scope, true).into()); } /// Setting the `unhandled_rejection_callback` from JavaScript. fn set_unhandled_rejection_callback( scope: &mut v8::HandleScope, args: v8::FunctionCallbackArguments, - mut rv: v8::ReturnValue, + _: v8::ReturnValue, ) { // Note: Passing `null` from JavaScript essentially will unset the defined callback. let callback = match v8::Local::::try_from(args.get(0)) { @@ -130,6 +128,4 @@ fn set_unhandled_rejection_callback( let mut state = state_rc.borrow_mut(); state.exceptions.set_unhandled_rejection_callback(callback); - - rv.set(v8::Boolean::new(scope, true).into()); } diff --git a/src/js/main.js b/src/js/main.js index 68c50f6..9d41732 100644 --- a/src/js/main.js +++ b/src/js/main.js @@ -15,6 +15,26 @@ function makeGlobal(name, value) { globalThis[name] = value; } +const { $$queueMicro, reportError } = globalThis; + +// Note: We wrap `queueMicrotask` and manually emit the exception because +// v8 doesn't provide any mechanism to handle callback exceptions during +// the microtask_checkpoint phase. +function queueMicrotask(callback) { + // Check if the callback argument is a valid type. + if (typeof callback !== 'function') { + throw new TypeError(`The "callback" argument must be of type function.`); + } + + $$queueMicro(() => { + try { + callback(); + } catch (err) { + reportError(err); + } + }); +} + const console = new Console(); const consoleFromV8 = globalThis['console']; @@ -23,6 +43,7 @@ wrapConsole(console, consoleFromV8); /* Initialize global environment for user script */ makeGlobal('process', process); +makeGlobal('queueMicrotask', queueMicrotask); makeGlobal('console', console); makeGlobal('prompt', prompt);