diff --git a/.cargo/config.toml b/.cargo/config.toml index 40490189..79d08f1d 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -2,7 +2,11 @@ runner = 'wasmtime run --dir=.::/cwd --dir=. --dir=/tmp -Sinherit-env=y,inherit-network=y,http=y,tcp=y,udp=y -C native-unwind-info=n' [target.wasm32-wasip2] +# Keep in sync with the RUSTFLAGS overrides in scripts/build-runtime.sh and scripts/build-dylib.sh. +rustflags = ["-C", "target-feature=+simd128"] runner = 'wasmtime run --dir=.::/cwd --dir=. --dir=/tmp -Sinherit-env=y,inherit-network=y,http=y,tcp=y,udp=y -C native-unwind-info=n' [target.wasm32-wasip3] +# Keep in sync with the RUSTFLAGS overrides in scripts/build-runtime.sh and scripts/build-dylib.sh. +rustflags = ["-C", "target-feature=+simd128"] runner = 'wasmtime run --dir=.::/cwd --dir=. --dir=/tmp -Sinherit-env=y,inherit-network=y,http=y,tcp=y,udp=y -C native-unwind-info=n' diff --git a/Cargo.lock b/Cargo.lock index 5c1d9f05..708631d8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -572,7 +572,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -1436,10 +1436,13 @@ dependencies = [ name = "js" version = "0.3.0" dependencies = [ + "encoding_rs", "indexmap", "mozjs", "mozjs_sys", "num-traits", + "rustc-hash", + "smallvec", "starling-macro", ] @@ -1613,7 +1616,7 @@ dependencies = [ [[package]] name = "mozjs" version = "0.26.1" -source = "git+https://github.com/tschneidereit/mozjs.git?branch=main#9ebf0195f42b3fb80586784fa8ee56bab9e7a23a" +source = "git+https://github.com/tschneidereit/mozjs.git?branch=main#65f8c50cdac8bf14339e88ca0ea1123c9eec6069" dependencies = [ "bindgen", "cc", @@ -1627,7 +1630,7 @@ dependencies = [ [[package]] name = "mozjs_collator_glue" version = "153.0.0" -source = "git+https://github.com/tschneidereit/mozjs.git?branch=main#9ebf0195f42b3fb80586784fa8ee56bab9e7a23a" +source = "git+https://github.com/tschneidereit/mozjs.git?branch=main#65f8c50cdac8bf14339e88ca0ea1123c9eec6069" dependencies = [ "arraystring", "arrayvec", @@ -1643,7 +1646,7 @@ dependencies = [ [[package]] name = "mozjs_icu_collator" version = "153.0.0" -source = "git+https://github.com/tschneidereit/mozjs.git?branch=main#9ebf0195f42b3fb80586784fa8ee56bab9e7a23a" +source = "git+https://github.com/tschneidereit/mozjs.git?branch=main#65f8c50cdac8bf14339e88ca0ea1123c9eec6069" dependencies = [ "displaydoc", "icu_locale_core", @@ -1662,7 +1665,7 @@ dependencies = [ [[package]] name = "mozjs_icu_collections" version = "153.0.0" -source = "git+https://github.com/tschneidereit/mozjs.git?branch=main#9ebf0195f42b3fb80586784fa8ee56bab9e7a23a" +source = "git+https://github.com/tschneidereit/mozjs.git?branch=main#65f8c50cdac8bf14339e88ca0ea1123c9eec6069" dependencies = [ "displaydoc", "icu_casemap", @@ -1676,7 +1679,7 @@ dependencies = [ [[package]] name = "mozjs_icu_normalizer" version = "153.0.0" -source = "git+https://github.com/tschneidereit/mozjs.git?branch=main#9ebf0195f42b3fb80586784fa8ee56bab9e7a23a" +source = "git+https://github.com/tschneidereit/mozjs.git?branch=main#65f8c50cdac8bf14339e88ca0ea1123c9eec6069" dependencies = [ "displaydoc", "icu_properties", @@ -1692,16 +1695,18 @@ dependencies = [ [[package]] name = "mozjs_icu_provider_glue" version = "153.0.0" -source = "git+https://github.com/tschneidereit/mozjs.git?branch=main#9ebf0195f42b3fb80586784fa8ee56bab9e7a23a" +source = "git+https://github.com/tschneidereit/mozjs.git?branch=main#65f8c50cdac8bf14339e88ca0ea1123c9eec6069" dependencies = [ + "icu_locale", "icu_provider", + "icu_provider_adapters", "icu_provider_blob", ] [[package]] name = "mozjs_locale_glue" version = "153.0.0" -source = "git+https://github.com/tschneidereit/mozjs.git?branch=main#9ebf0195f42b3fb80586784fa8ee56bab9e7a23a" +source = "git+https://github.com/tschneidereit/mozjs.git?branch=main#65f8c50cdac8bf14339e88ca0ea1123c9eec6069" dependencies = [ "cbindgen", "icu_locale", @@ -1711,7 +1716,7 @@ dependencies = [ [[package]] name = "mozjs_normalizer_glue" version = "153.0.0" -source = "git+https://github.com/tschneidereit/mozjs.git?branch=main#9ebf0195f42b3fb80586784fa8ee56bab9e7a23a" +source = "git+https://github.com/tschneidereit/mozjs.git?branch=main#65f8c50cdac8bf14339e88ca0ea1123c9eec6069" dependencies = [ "cbindgen", "encoding_rs", @@ -1724,7 +1729,7 @@ dependencies = [ [[package]] name = "mozjs_properties_glue" version = "153.0.0" -source = "git+https://github.com/tschneidereit/mozjs.git?branch=main#9ebf0195f42b3fb80586784fa8ee56bab9e7a23a" +source = "git+https://github.com/tschneidereit/mozjs.git?branch=main#65f8c50cdac8bf14339e88ca0ea1123c9eec6069" dependencies = [ "cbindgen", "icu_casemap", @@ -1736,7 +1741,7 @@ dependencies = [ [[package]] name = "mozjs_sys" version = "153.0.0-2" -source = "git+https://github.com/tschneidereit/mozjs.git?branch=main#9ebf0195f42b3fb80586784fa8ee56bab9e7a23a" +source = "git+https://github.com/tschneidereit/mozjs.git?branch=main#65f8c50cdac8bf14339e88ca0ea1123c9eec6069" dependencies = [ "bindgen", "cc", @@ -1759,7 +1764,7 @@ dependencies = [ [[package]] name = "mozjs_unicode_bidi_ffi" version = "153.0.0" -source = "git+https://github.com/tschneidereit/mozjs.git?branch=main#9ebf0195f42b3fb80586784fa8ee56bab9e7a23a" +source = "git+https://github.com/tschneidereit/mozjs.git?branch=main#65f8c50cdac8bf14339e88ca0ea1123c9eec6069" dependencies = [ "cbindgen", "icu_properties", @@ -1771,7 +1776,7 @@ dependencies = [ [[package]] name = "mozjs_utf16_iter" version = "153.0.0" -source = "git+https://github.com/tschneidereit/mozjs.git?branch=main#9ebf0195f42b3fb80586784fa8ee56bab9e7a23a" +source = "git+https://github.com/tschneidereit/mozjs.git?branch=main#65f8c50cdac8bf14339e88ca0ea1123c9eec6069" dependencies = [ "mozjs_icu_collections", ] @@ -1779,7 +1784,7 @@ dependencies = [ [[package]] name = "mozjs_utf8_iter" version = "153.0.0" -source = "git+https://github.com/tschneidereit/mozjs.git?branch=main#9ebf0195f42b3fb80586784fa8ee56bab9e7a23a" +source = "git+https://github.com/tschneidereit/mozjs.git?branch=main#65f8c50cdac8bf14339e88ca0ea1123c9eec6069" dependencies = [ "mozjs_icu_collections", ] @@ -1956,6 +1961,7 @@ dependencies = [ "futures-channel", "futures-lite", "http", + "libc", "reqwest", "url", "wasip3", @@ -2058,7 +2064,7 @@ dependencies = [ "once_cell", "socket2", "tracing", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -2237,7 +2243,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -2294,7 +2300,7 @@ dependencies = [ "security-framework", "security-framework-sys", "webpki-root-certs", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -2632,7 +2638,7 @@ dependencies = [ "getrandom 0.4.3", "once_cell", "rustix", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -3049,6 +3055,7 @@ dependencies = [ "js", "libstarling", "platform", + "smallvec", "tokio", "url", "web-globals", @@ -3067,6 +3074,7 @@ dependencies = [ "js", "libstarling", "platform", + "smallvec", "url", ] @@ -3077,6 +3085,7 @@ dependencies = [ "core-runtime", "js", "libstarling", + "smallvec", "web-globals", ] @@ -3126,7 +3135,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index a43c7a7a..6a9e3f04 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -39,6 +39,7 @@ oxc_resolver = "11" platform = { path = "crates/platform" } reqwest = { version = "0.13", default-features = false, features = ["rustls", "stream"] } serve-test-support = { path = "crates/serve-test-support" } +smallvec = "1.16" starling-macro = { path = "crates/starling-macro" } tokio = { version = "1", features = ["rt", "time", "net", "io-util"] } url = "2.5.8" diff --git a/README.md b/README.md index 98c18fd7..f62c353c 100644 --- a/README.md +++ b/README.md @@ -104,6 +104,7 @@ Options: --legacy-script Run as a classic script instead of an ES module -v, --verbose Enable verbose logging -d, --debug Enable script debugging via socket + --async-stacks Show async parent frames in Error.stack (always on under a debugger) --wpt-mode Enable WPT (Web Platform Tests) mode --init-location Override the location URL for initialization --strip-path-prefix Strip this prefix from script paths @@ -140,6 +141,10 @@ is `undefined` at the top level. **Legacy script mode** (`--legacy-script`) — sloppy mode, no `import`/`export`, `this` is the global object. +**Exit status** — `0` when the script and all the work it scheduled complete, `1` +when the script throws or an initializer, the event loop or the runtime fails (the +error is printed to stderr), and `2` for invalid command-line arguments. + --- ## Extending with Custom Builtins diff --git a/crates/builtins/cpp-builtins/build.rs b/crates/builtins/cpp-builtins/build.rs index e0f7283f..50aa00cd 100644 --- a/crates/builtins/cpp-builtins/build.rs +++ b/crates/builtins/cpp-builtins/build.rs @@ -113,6 +113,13 @@ fn main() { .flag("-mthread-model") .flag("single") .flag("-m32"); + + // `cc` does not pass the target features rustc is given on to clang, so the C++ code is + // compiled with the wasm features enabled for the Rust code here. + let features = env::var("CARGO_CFG_TARGET_FEATURE").unwrap_or_default(); + if features.split(',').any(|enabled| enabled == "simd128") { + build.flag("-msimd128"); + } } // Single unity file instead of individual sources. diff --git a/crates/builtins/cpp-builtins/src/lib.rs b/crates/builtins/cpp-builtins/src/lib.rs index 76a2bedd..a8838d1e 100644 --- a/crates/builtins/cpp-builtins/src/lib.rs +++ b/crates/builtins/cpp-builtins/src/lib.rs @@ -22,7 +22,8 @@ mod tests { fn eval(code: &str) -> bool { libstarling::register_builtins(); let rt = - libstarling::runtime::Runtime::init(&libstarling::config::RuntimeConfig::default()); + libstarling::runtime::Runtime::init(&libstarling::config::RuntimeConfig::default()) + .expect("runtime init"); let scope = rt.default_global(); let rval = js::compile::evaluate_with_filename(&scope, code, "test.js", 1) .expect("eval failed"); diff --git a/crates/builtins/fetch-event/src/extendable_event.rs b/crates/builtins/fetch-event/src/extendable_event.rs index d89e619d..ccd795ff 100644 --- a/crates/builtins/fetch-event/src/extendable_event.rs +++ b/crates/builtins/fetch-event/src/extendable_event.rs @@ -5,8 +5,8 @@ use core_runtime::event_loop::{with_active_event_loop, InterestHandle}; use core_runtime::{jsclass, jsmethods}; use core_runtime::{webidl_dictionary, webidl_interface, webidl_methods}; -use js::conversion::FromJSVal; use js::error::{ExnThrown, ThrowException}; +use js::function::cast_payload; use js::function::CallbackArgs; use js::gc::handle::Heap; use js::gc::scope::Scope; @@ -174,7 +174,7 @@ fn lifetime_promise_settled_cb( _args: CallbackArgs, payload: HandleValue, ) -> Result { - let holder = LifetimePromisePayload::from_jsval(scope, payload, ()).unwrap(); + let holder = cast_payload::(scope, payload); holder .data() .event diff --git a/crates/builtins/fetch-event/src/fetch_event.rs b/crates/builtins/fetch-event/src/fetch_event.rs index e96c7551..32e65c0d 100644 --- a/crates/builtins/fetch-event/src/fetch_event.rs +++ b/crates/builtins/fetch-event/src/fetch_event.rs @@ -8,6 +8,7 @@ use core_runtime::{webidl_dictionary, webidl_interface, webidl_methods}; use js::class::Ref; use js::conversion::FromJSVal; use js::error::{ExnThrown, ThrowException}; +use js::function::cast_payload; use js::function::CallbackArgs; use js::gc::handle::Heap; use js::gc::scope::Scope; @@ -17,7 +18,7 @@ use js::{value, Function, Promise, PromiseOf}; use std::ops::Deref; use web_fetch::request::{Request, RequestImpl}; use web_fetch::response::{Response, ResponseImpl}; -use web_globals::dom_exception::DOMExceptionError; +use web_globals::dom_exception::{DOMException, DOMExceptionError}; use web_globals::events::algorithms::ScriptStackState; use web_globals::events::EventTarget; @@ -283,7 +284,7 @@ fn on_respond_fulfilled( args: CallbackArgs<'_>, payload: HandleValue<'_>, ) -> Result { - let event = FetchEvent::from_jsval(scope, payload, ()).unwrap(); + let event = cast_payload::(scope, payload); // Step 10.1: If _response_ is not a `Response` object, then set the `respond-with error // flag`. let response = @@ -292,22 +293,23 @@ fn on_respond_fulfilled( // Step 10.2.2.1: If _response_ is `unusable`, then set the `respond-with error flag` and abort // these steps. let unusable = response.is_body_unusable(scope); - // Step 10.2.1: Let _potentialResponse_ be a copy of _response_’s associated response, except - // for its body. - // Only copying the headers, since everything else is effectively unchangeable from content. - let headers = (!unusable).then(|| response.headers_list(scope)); + if !unusable { + // Steps 10.2.2 to 10.2.5 are implemented at the platform layer instead of here, except + // for the effect step 10.2.2's transform has on the response handed in, which is to take + // its body, and step 10.2.5.1's getting a reader for the body's stream, which + // `reserve_body_for_sending` acquires now so author code can no longer read from it. + response.reserve_body_for_sending(scope)?; + } { let mut data = event.data_mut(); if unusable { data.respond_with_error = true; } else { - // Steps 10.2.2 to 10.2.5 are implemented at the platform layer instead of here, except - // for the effect step 10.2.2's transform has on the response handed in, which is to - // take its body. - // `mark_body_used` has the same content-visible effect as step 10.2.5.1's getting a - // reader for the body: it locks the stream, so no other readers can be acquired. - response.mark_body_used(); - data.potential_response_headers = headers; + // Step 10.2.1: Let _potentialResponse_ be a copy of _response_’s associated response, + // except for its body. + // Note: We only copy the headers, since everything else is effectively unchangeable + // from content. + data.potential_response_headers = Some(response.headers_list(scope)); // Step 10.2.6: Set _event_’s `potential response` to _potentialResponse_. data.potential_response.set(response); } @@ -331,7 +333,7 @@ fn on_respond_rejected( args: CallbackArgs<'_>, payload: HandleValue<'_>, ) -> Result { - let event = FetchEvent::from_jsval(scope, payload, ()).unwrap(); + let event = cast_payload::(scope, payload); { let mut data = event.data_mut(); // Step 9.1: Set _event_’s `respond-with error flag`. @@ -358,7 +360,7 @@ fn describe_rejection(scope: &Scope<'_>, reason: HandleValue<'_>) -> String { match String::from_jsval(scope, reason, ()) { Ok(text) => text, Err(_) => { - let _ = js::exception::take_pending_or_undefined(scope); + js::exception::clear(scope); "a value that could not be converted to a string".to_string() } } @@ -398,8 +400,11 @@ impl<'s> FetchEvent<'s> { } else { "the fetch event was not responded to" }; - let _ = DOMExceptionError::new("NetworkError", reason).throw(scope); - handled.reject_with_pending(scope).and_then(|()| { + match DOMException::new(scope, reason.into(), "NetworkError".into()) { + Ok(exception) => handled.reject(scope, exception), + Err(_) => handled.reject_with_pending(scope), + } + .and_then(|()| { // Most handlers never look at `handled`, and a rejection nobody subscribed to is // reported as an unhandled rejection, which would put a spurious warning on the // stderr of every failed request. diff --git a/crates/builtins/fetch-event/tests/dispatch.rs b/crates/builtins/fetch-event/tests/dispatch.rs index 79d71991..845aa542 100644 --- a/crates/builtins/fetch-event/tests/dispatch.rs +++ b/crates/builtins/fetch-event/tests/dispatch.rs @@ -9,7 +9,6 @@ #![cfg(not(target_arch = "wasm32"))] use core_runtime::event_loop::{run_to_completion, with_event_loop, EventLoop}; -use core_runtime::report_pending_exception; use core_runtime::runtime::{clear_global_initializers, Runtime}; use fetch_event::fetch_event::FetchEvent; use js::conversion::FromJSVal; @@ -22,12 +21,10 @@ use web_globals::events::algorithms::ScriptStackState; fn eval<'s>(scope: &'s Scope, src: &str) -> HandleValue<'s> { match js::compile::evaluate_with_filename(scope, src, "", 1) { Ok(v) => v, - Err(_) => { - unsafe { - report_pending_exception(scope); - } - panic!("eval error for input: {}", src) - } + Err(_) => panic!( + "eval error for input: {src}: {}", + js::error::ExnThrown::capture(scope) + ), } } @@ -41,7 +38,7 @@ fn eval<'s>(scope: &'s Scope, src: &str) -> HandleValue<'s> { fn test_runtime() -> std::rc::Rc { clear_global_initializers(); libstarling::register_builtins(); - Runtime::init(&core_runtime::config::RuntimeConfig::default()) + Runtime::init(&core_runtime::config::RuntimeConfig::default()).expect("runtime init") } /// Register the builtins on `scope`'s global, plus the `mark` helper these tests respond with. @@ -64,7 +61,7 @@ fn install_globals(scope: &Scope) { fn response_marker(scope: &Scope, event: &FetchEvent) -> String { event .potential_response(scope) - .map(|response| response.status_text()) + .map(|response| response.status_text().to_string()) .unwrap_or_default() } @@ -1019,3 +1016,25 @@ fn responding_marks_the_body_used_at_settle_time() { String::from_jsval(&scope, eval(&scope, "String(globalThis.kept.bodyUsed)"), ()).unwrap(); assert_eq!(used, "true", "no byte has been transmitted yet"); } + +/// Once `respondWith`'s promise settles, a `.body` stream the handler already +/// materialized is locked for sending, so author code can no longer read it. +#[test] +fn respond_with_locks_a_materialized_body_stream() { + let (_, probe) = dispatch( + "(event) => { + const response = new Response(new ReadableStream({ + start(c) { c.enqueue(new Uint8Array([1])); c.close(); }, + })); + response.body; + event.respondWith(response); + event.waitUntil(new Promise(resolve => setTimeout(() => { + try { response.body.getReader(); globalThis.__probe = 'readable'; } + catch (e) { globalThis.__probe = `${e.constructor.name},${response.bodyUsed}`; } + resolve(); + }, 0))); + }", + "new Request('http://example.com/')", + ); + assert_eq!(probe, "TypeError,true"); +} diff --git a/crates/builtins/web-fetch/Cargo.toml b/crates/builtins/web-fetch/Cargo.toml index 99996aa3..ef275fd6 100644 --- a/crates/builtins/web-fetch/Cargo.toml +++ b/crates/builtins/web-fetch/Cargo.toml @@ -16,6 +16,7 @@ core-runtime.workspace = true data-url.workspace = true js.workspace = true platform.workspace = true +smallvec.workspace = true url.workspace = true web-globals.workspace = true web-streams.workspace = true diff --git a/crates/builtins/web-fetch/src/abort.rs b/crates/builtins/web-fetch/src/abort.rs index c1ea8473..3c3af882 100644 --- a/crates/builtins/web-fetch/src/abort.rs +++ b/crates/builtins/web-fetch/src/abort.rs @@ -12,8 +12,8 @@ use crate::request::Request; use crate::response::Response; use core_runtime::jsclass; use core_runtime::jsmethods; -use js::conversion::FromJSVal; use js::error::ExnThrown; +use js::function::cast_payload; use js::function::CallbackArgs; use js::gc::handle::{Heap, OptionHeapExt}; use js::gc::scope::Scope; @@ -109,7 +109,11 @@ pub(crate) fn abort_fetch( // that now owns it. `on_abort` does that before calling this algorithm. if let Some(stream) = HostBackedBodyOwner::body_stream(request, scope) { if stream.is_readable() { - stream.cancel_internal(scope, error); + // Rejects if the stream's `cancel()` throws. Nothing observes this promise, so it is + // marked handled rather than reported as an unhandled rejection. + let _ = stream + .cancel_internal(scope, error) + .set_any_is_handled(scope); } } @@ -164,8 +168,9 @@ fn on_settled( _args: CallbackArgs<'_>, payload: HandleValue<'_>, ) -> Result { - let state = AbortFetchState::from_jsval_throwing(scope, payload, ())?; - if let Some(response) = state.data().response.get(scope) { + let state = cast_payload::(scope, payload); + let response = state.data().response.get(scope); + if let Some(response) = response { if response_body_is_abortable(scope, &response) { // Still abortable: `Response::consume` detaches once the body has been read. response.set_abort_state(&state); @@ -184,7 +189,7 @@ fn on_abort( args: CallbackArgs<'_>, payload: HandleValue<'_>, ) -> Result { - let state = AbortFetchState::from_jsval_throwing(scope, payload, ())?; + let state = cast_payload::(scope, payload); let promise = state.data().promise.get(scope); let reason = args.get(0); diff --git a/crates/builtins/web-fetch/src/algorithms.rs b/crates/builtins/web-fetch/src/algorithms.rs index 7736827c..5b3d8dc6 100644 --- a/crates/builtins/web-fetch/src/algorithms.rs +++ b/crates/builtins/web-fetch/src/algorithms.rs @@ -10,10 +10,11 @@ use crate::incoming_body::HostBackedBodyOwner; use crate::response::{is_null_body_status, Response, ResponseInit, ResponseRecord}; use js::conversion::ToJSVal; use js::error::{throw_type_error, ExnThrown, RangeError}; -use js::gc::handle::{Heap, OptionHeapExt}; +use js::gc::handle::Heap; use js::gc::scope::Scope; use js::prelude::HandleValue; use js::{ArrayBuffer, Promise, Uint8Array}; +use smallvec::SmallVec; use web_streams::readable::readable_stream::ReadableStream; /// The source of a [`Body`]'s bytes. @@ -104,7 +105,7 @@ pub(crate) fn extract_body<'r>( // `application/x-www-form-urlencoded` serializer` with _object_’s `list`. Set _type_ // to `application/x-www-form-urlencoded;charset=UTF-8`. BodyInitOrBytes::BodyInit(BodyInit::URLSearchParams(params)) => { - let serialized = params.to_string(scope)?; + let serialized = params.to_string(); source = BodySource::Bytes(bytes::Bytes::from(serialized.into_bytes())); content_type = Some(Cow::Borrowed( "application/x-www-form-urlencoded;charset=UTF-8", @@ -169,7 +170,7 @@ pub(crate) fn contains(list: &HeaderList, name: &str) -> bool { /// /// To get a header name name from a header list list, run these steps. They return null or a header value. -pub(crate) fn get_header_name<'l>(list: &'l HeaderList, name: &str) -> Option> { +pub(crate) fn get_header_for_name<'l>(list: &'l HeaderList, name: &str) -> Option> { debug_assert!(is_header_name(name)); // Step 1: If _list_ `does not contain` _name_, then return null. @@ -178,9 +179,16 @@ pub(crate) fn get_header_name<'l>(list: &'l HeaderList, name: &str) -> Option = name.bytes().map(|b| b.to_ascii_lowercase()).collect(); let mut values = list .iter() - .filter(|(n, _)| n.eq_ignore_ascii_case(name)) + .filter(|(n, _)| { + n.len() == lowercase.len() + && n.bytes() + .zip(&lowercase) + .all(|(a, &b)| a.to_ascii_lowercase() == b) + }) .map(|(_, v)| v.as_str()); let first = values.next()?; let Some(second) = values.next() else { @@ -300,7 +308,7 @@ pub(crate) fn sort_and_combine_a_header_list(list: &HeaderList) -> HeaderList { // Step 3.2.1: Let _value_ be the result of `getting` _name_ from _list_. // Step 3.2.2: `Assert`: _value_ is non-null. // Step 3.2.3: `Append` (_name_, _value_) to _headers_. - let value = get_header_name(list, &name) + let value = get_header_for_name(list, &name) .expect("name is in list") .into_owned(); headers.push((name, value)); @@ -344,7 +352,8 @@ fn is_safelisted_content_type_essence(essence: &str) -> bool { /// To determine whether a header (name, value) is a CORS-safelisted request-header, run these steps: pub(crate) fn is_cors_safelisted_request_header(name: &str, value: &str) -> bool { // Step 1: If _value_’s `length` is greater than 128, then return false. - if value.len() > 128 { + // (A byte sequence is held as a string of code points ≤ U+00FF, one per byte.) + if value.chars().count() > 128 { return false; } @@ -531,7 +540,7 @@ pub(crate) fn clone_a_body<'r>( let (out1, out2) = stream.tee(scope, true)?; // Step 2: Set _body_’s `stream` to _out1_. - body_owner.replace_body_stream_after_tee(scope, out1); + body_owner.replace_body_stream_after_tee(out1); // Step 3: Return a `body` whose `stream` is _out2_ and other members are copied from _body_. // _out2_ is returned alongside the cloned record. The source bytes now live in the teed @@ -633,22 +642,6 @@ pub(crate) fn is_method(method: &str) -> bool { !method.is_empty() && method.bytes().all(is_http_token_byte) } -/// Whether `value` is a valid `ReferrerPolicy` WebIDL enum value (including the empty string). -/// -pub(crate) fn is_valid_referrer_policy(value: &str) -> bool { - matches!( - value, - "" | "no-referrer" - | "no-referrer-when-downgrade" - | "same-origin" - | "origin" - | "strict-origin" - | "origin-when-cross-origin" - | "strict-origin-when-cross-origin" - | "unsafe-url" - ) -} - /// /// A CORS-safelisted method is a byte-case-insensitive match for `GET`, `HEAD`, or `POST`. pub(crate) fn is_cors_safelisted_method(method: &str) -> bool { @@ -706,7 +699,7 @@ pub(crate) fn is_no_cors_safelisted_request_header_after_append( return false; } // `CORS-safelisted request-header` Step 1: the combined value's length. - if existing.len() + 2 + value.len() > 128 { + if existing.chars().count() + 2 + value.chars().count() > 128 { return false; } // ``accept``: a `CORS-unsafe request-header byte` in the combined value is one in either part. @@ -813,7 +806,7 @@ pub(crate) fn append_to_headers( { let safelisted = { let data = headers.data(); - let existing = get_header_name(&data.header_list, &name); + let existing = get_header_for_name(&data.header_list, &name); is_no_cors_safelisted_request_header_after_append(&name, existing.as_deref(), &value) }; if !safelisted { @@ -951,7 +944,7 @@ pub(crate) fn refill_headers_from_own_list(headers: &Headers<'_>) { Guard::RequestNoCors => { let safelisted = { let data = headers.data(); - let existing = get_header_name(&data.header_list, &name); + let existing = get_header_for_name(&data.header_list, &name); is_no_cors_safelisted_request_header_after_append( &name, existing.as_deref(), @@ -1017,7 +1010,11 @@ pub(crate) enum ConsumeType { /// UTF-8 decode: strip a leading UTF-8 BOM, then decode (malformed sequences become U+FFFD). fn utf8_decode(bytes: &[u8]) -> Cow<'_, str> { let bytes = bytes.strip_prefix(b"\xEF\xBB\xBF").unwrap_or(bytes); - String::from_utf8_lossy(bytes) + // Validating first is much faster than `from_utf8_lossy` for the common, valid case. + match std::str::from_utf8(bytes) { + Ok(text) => Cow::Borrowed(text), + Err(_) => String::from_utf8_lossy(bytes), + } } /// Run `convertBytesToJSValue` for the given `ConsumeType`. @@ -1195,7 +1192,7 @@ pub(crate) fn initialize_a_response<'r>( // Step 5: If _init_["``headers``"] `exists`, then `fill` _response_’s `headers` with // _init_["``headers``"]. if let Some(init_headers) = init_headers { - let headers = response.data().headers.get(scope).unwrap(); + let headers = response.data().headers.get(scope); fill_headers(scope, &headers, init_headers)?; } // Step 6: If _body_ is non-null, then: @@ -1210,9 +1207,9 @@ pub(crate) fn initialize_a_response<'r>( )); } // Step 6.2: Set _response_’s `body` to _body_’s `body`. - response.data_mut().body = Some(body_record); + response.data_mut().body.record = Some(body_record); if let Some(stream) = stream { - response.data_mut().body_stream = Some(Heap::from(stream)); + response.data_mut().body.stream = Some(Heap::from(stream)); } // Step 6.3: If _body_’s `type` is non-null and _response_’s `header list` `does not // contain` ``Content-Type``, then `append` (``Content-Type``, _body_’s `type`) to diff --git a/crates/builtins/web-fetch/src/body_mixin.rs b/crates/builtins/web-fetch/src/body_mixin.rs index 007b5b0a..742faeac 100644 --- a/crates/builtins/web-fetch/src/body_mixin.rs +++ b/crates/builtins/web-fetch/src/body_mixin.rs @@ -4,17 +4,69 @@ //! //! The `Body` interface mixin, which both `Request` and `Response` include. -use crate::algorithms::{self, BodySource, ConsumeType}; -use crate::incoming_body::{consume_host_body, materialize_host_body, HostBackedBodyOwner}; +use crate::algorithms::{self, Body, BodySource, ConsumeType}; +use crate::incoming_body::{ + consume_host_body, materialize_host_body, HostBackedBodyOwner, HostBodySourceImpl, +}; use core_runtime::webidl_union; use js::error::{throw_type_error, ExnThrown}; +use js::gc::handle::Heap; use js::gc::scope::Scope; use js::prelude::HandleValue; use js::{ArrayBuffer, ArrayBufferView, Promise}; +use platform::http::IncomingBody; use std::ffi::CStr; use web_streams::readable::readable_stream::ReadableStream; +use web_streams::readable::readable_stream::ReadableStreamImpl; use web_url::url_search_params::URLSearchParams; +/// The body state of a `Request` or `Response` object. +#[js::must_root] +#[derive(core_runtime::Traceable, Default)] +pub(crate) struct BodyStorage { + /// The body record, or `None` for a null body. + #[no_trace] + pub(crate) record: Option, + /// The body's `ReadableStream`: a user-provided stream, or one materialized lazily from the + /// body's byte source or host body on `.body` access. + pub(crate) stream: Option>, + /// The body as received from the host, until the first of `consume`/`.body` takes it. + #[no_trace] + pub(crate) host: Option, + /// The `.body` stream's native host source, kept so an abort can cancel an in-flight host + /// read. Set when `.body` materializes a host-backed stream. + pub(crate) source: Option>, + /// The record's byte source, attributed to the owning object for the GC. Kept in sync by + /// [`sync_accounting`](Self::sync_accounting). + #[no_trace] + pub(crate) accounted: js::gc::AssociatedMemory, +} + +impl BodyStorage { + /// A body with `record` and, if given, the stream it already has. + pub(crate) fn new(record: Option, stream: Option>) -> Self { + Self { + record, + stream: stream.map(Heap::from), + ..Default::default() + } + } + + /// Attribute the record's byte source to `object`, the object owning this storage, for the + /// GC, replacing the previous amount. Called wherever the byte source is set, taken, or + /// dropped. + pub(crate) fn sync_accounting(&mut self, object: js::Object<'_>) { + let bytes = match &self.record { + Some(Body { + source: BodySource::Bytes(bytes), + .. + }) => bytes.len(), + _ => 0, + }; + self.accounted.set(object, bytes); + } +} + #[webidl_union] pub enum BodyInit<'a> { Stream(ReadableStream<'a>), @@ -39,11 +91,22 @@ pub(crate) trait BodyMixin: HostBackedBodyOwner + Sized { const TEXT_STREAM_UNSUPPORTED: &'static CStr; /// Store the `.body` stream materialized from the body's byte source. - fn set_body_stream(&self, stream: ReadableStream<'_>); + fn set_body_stream(&self, stream: ReadableStream<'_>) { + self.with_body(|body| { + debug_assert!(body.stream.is_none()); + body.stream = Some(Heap::from(stream)); + }); + } /// Mark the body's byte source as read — the `disturbed` of the stream it /// would have materialized into. - fn set_source_disturbed(&self); + fn set_source_disturbed(&self) { + self.with_body(|body| { + if let Some(record) = body.record.as_mut() { + record.source_disturbed = true; + } + }); + } /// Run once the body has been consumed. `Response` overrides it to detach /// its `fetch` abort algorithm, which can no longer act on a read body. diff --git a/crates/builtins/web-fetch/src/body_proxy.rs b/crates/builtins/web-fetch/src/body_proxy.rs index 0556981d..9e1482ce 100644 --- a/crates/builtins/web-fetch/src/body_proxy.rs +++ b/crates/builtins/web-fetch/src/body_proxy.rs @@ -7,20 +7,21 @@ use core_runtime::jsclass; use core_runtime::jsmethods; -use js::conversion::FromJSVal; use js::error::ExnThrown; +use js::function::cast_payload; use js::function::CallbackArgs; use js::gc::handle::Heap; use js::gc::scope::Scope; use js::native::Value; use js::prelude::{HandleValue, OptionHeapExt}; -use js::{Function, Object, Promise}; +use js::{Object, Promise}; use web_streams::readable::default_reader::DefaultReaderImpl; use web_streams::readable::native_read::{ acquire_native_reader, native_reader_cancel, native_reader_read, NativeReadSteps, }; use web_streams::readable::readable_stream::{ReadableStream, ReadableStreamImpl}; use web_streams::readable::{DefaultReader, ReadableStreamDefaultController}; +use web_streams::AlgorithmArg; /// State for one body proxy: the internal reader on the original stream, the /// proxy stream's controller (captured at pull time), and the in-flight pull's @@ -50,12 +51,8 @@ impl BodyProxySource { } fn controller<'r>(&self, scope: &'r Scope) -> ReadableStreamDefaultController<'r> { - let controller = self - .stream(scope) - .controller(scope) - .expect("source is fully initialized"); - controller - .cast::() + self.stream(scope) + .default_controller(scope) .expect("Body proxies always have a default controller") } } @@ -68,16 +65,13 @@ pub(crate) fn proxy_body_stream<'r>( ) -> Result, ExnThrown> { let reader = acquire_native_reader(scope, source)?; let state = BodyProxySource::new(scope, reader)?; - let pull = Function::new_callback(scope, c"", 1, proxy_pull, state)?; - let pull_value = scope.root_value(pull.as_value()); + let pull = AlgorithmArg::native(scope, proxy_pull, state)?; // Cancelling the proxy must cancel the stream it is draining, otherwise the original stays // locked to the internal reader forever, and the source isn't stopped & dropped. - let cancel = Function::new_callback(scope, c"", 1, proxy_cancel, state)?; - let cancel_value = scope.root_value(cancel.as_value()); + let cancel = AlgorithmArg::native(scope, proxy_cancel, state)?; // No native-source marker: the proxy must deliver through its own queue, so // the incoming→outgoing shortcut does not apply to it. - let stream = - ReadableStream::new_native(scope, HandleValue::undefined(), pull_value, cancel_value)?; + let stream = ReadableStream::new_native(scope, HandleValue::undefined(), pull, cancel)?; state.data_mut().stream.set(stream); Ok(stream) } @@ -88,8 +82,7 @@ fn proxy_cancel( args: CallbackArgs<'_>, payload: HandleValue<'_>, ) -> Result { - let state = - BodyProxySource::from_jsval(scope, payload, ()).expect("payload is a BodyProxySource"); + let state = cast_payload::(scope, payload); let reader = state.data().reader.get(scope); let cancelled = native_reader_cancel(scope, reader, args.get(0)); // Any pull still in flight will never be delivered now; settle it so the stream machinery @@ -114,8 +107,7 @@ fn proxy_pull( args: CallbackArgs<'_>, payload: HandleValue<'_>, ) -> Result { - let state = - BodyProxySource::from_jsval(scope, payload, ()).expect("payload is a BodyProxySource"); + let state = cast_payload::(scope, payload); let promise = Promise::new_pending(scope)?; state.data_mut().current_pull = Some(Heap::from(promise)); let reader = state.data().reader.get(scope); @@ -138,7 +130,7 @@ fn settle_pull(scope: &Scope<'_>, state: &BodyProxySource<'_>) { // See `proxy_close_step`: a native callback must not return with an exception // pending, and there is no caller here to propagate one to. // TODO: use promise rejection reporting once available. - let _ = js::exception::take_pending(scope); + js::exception::clear(scope); } } } @@ -156,7 +148,7 @@ fn proxy_chunk_step( // body — can no longer be enqueued to; the chunk has nowhere to go. See // `proxy_close_step` for why the pending exception must be cleared too. if controller.enqueue(scope, chunk).is_err() { - let _ = js::exception::take_pending(scope); + js::exception::clear(scope); } settle_pull(scope, &state); Ok(()) @@ -176,7 +168,7 @@ fn proxy_close_step(scope: &Scope<'_>, payload: Object<'_>) -> Result<(), ExnThr if controller.close(scope).is_err() { // Clear the exception the failed close left pending: a native callback must not // return with one set, and there is no caller here to propagate it to. - let _ = js::exception::take_pending(scope); + js::exception::clear(scope); } settle_pull(scope, &state); Ok(()) @@ -194,7 +186,7 @@ fn proxy_error_step( // Already-closed/cancelled proxy: erroring it is a no-op that fails. Ignore it for the // same reason as the close step — these are a read request's `error steps`. if controller.error(scope, Some(error)).is_err() { - let _ = js::exception::take_pending(scope); + js::exception::clear(scope); } settle_pull(scope, &state); Ok(()) diff --git a/crates/builtins/web-fetch/src/byte_string.rs b/crates/builtins/web-fetch/src/byte_string.rs index 139a5dab..9f158527 100644 --- a/crates/builtins/web-fetch/src/byte_string.rs +++ b/crates/builtins/web-fetch/src/byte_string.rs @@ -18,7 +18,7 @@ use js::prelude::HandleValue; /// A WebIDL `ByteString`. The inner `String`'s `char`s are guaranteed to all be /// ≤ U+00FF, so each maps one-to-one to a byte. #[derive(Debug, Clone, Default, PartialEq, Eq, Hash)] -pub struct ByteString(pub String); +pub struct ByteString(String); impl ByteString { /// Borrow the contents as a string slice. diff --git a/crates/builtins/web-fetch/src/globals.rs b/crates/builtins/web-fetch/src/globals.rs index 90260f33..cf599b18 100644 --- a/crates/builtins/web-fetch/src/globals.rs +++ b/crates/builtins/web-fetch/src/globals.rs @@ -6,7 +6,6 @@ use core_runtime::jsglobals; #[jsglobals] pub mod globals { - use crate::abort::AbortFetchState; use crate::request::{Request, RequestInfo, RequestInit}; use crate::response::response_from_platform; use js::conversion::ToJSVal; @@ -14,9 +13,8 @@ pub mod globals { use js::error::ExnThrown; use js::gc::handle::RootedHeap; use js::gc::scope::Scope; - use js::prelude::HandleValue; use js::promise::{PromiseFuture, PromiseOutcome}; - use js::{Function, Object, Promise}; + use js::Promise; /// /// diff --git a/crates/builtins/web-fetch/src/headers.rs b/crates/builtins/web-fetch/src/headers.rs index d8938418..ded734e8 100644 --- a/crates/builtins/web-fetch/src/headers.rs +++ b/crates/builtins/web-fetch/src/headers.rs @@ -6,13 +6,13 @@ use std::cell::OnceCell; use std::ops::{Deref, DerefMut}; use core_runtime::{webidl_interface, webidl_methods, webidl_union}; -use js::class::{get_iterator_prototype, get_prototype_for}; use js::conversion::{Record, ToJSVal}; use js::error::{throw_type_error, ExnThrown}; use js::gc::handle::Heap; use js::gc::scope::Scope; +use js::iteration::{create_iter_result, pair_iterator_result, IterationKind}; use js::prelude::HandleValue; -use js::{Function, Object}; +use js::{Callable, JSString, Object}; use crate::algorithms; use crate::algorithms::sort_and_combine_a_header_list; @@ -192,7 +192,11 @@ impl Headers { /// #[method] - pub fn get(&self, scope: &Scope<'_>, name: ByteString) -> Result, ExnThrown> { + pub fn get<'r>( + &self, + scope: &'r Scope<'_>, + name: ByteString, + ) -> Result>, ExnThrown> { let name = name.as_str(); // Step 1: If _name_ is not a `header name`, then `throw` a `TypeError`. if !algorithms::is_header_name(name) { @@ -200,24 +204,24 @@ impl Headers { } // Step 2: Return the result of `getting` _name_ from `this`’s `header list`. let data = self.data(); - Ok(algorithms::get_header_name(&data.header_list, name).map(std::borrow::Cow::into_owned)) + algorithms::get_header_for_name(&data.header_list, name) + .map(|value| JSString::from_str(scope, &value)) + .transpose() } /// #[method] - pub fn get_set_cookie(&self, scope: &Scope<'_>) -> Result, ExnThrown> { - let _ = scope; + pub fn get_set_cookie(&self) -> Vec { // Step 1: If `this`’s `header list` `does not contain` `Set-Cookie`, then return « ». // Step 2: Return the `values` of all `headers` in `this`’s `header list` whose `name` is a // `byte-case-insensitive` match for `Set-Cookie`, in order. // An empty header list yields an empty sequence, subsuming step 1. - Ok(self - .data() + self.data() .header_list .iter() .filter(|(name, _)| name.eq_ignore_ascii_case("set-cookie")) .map(|(_, value)| value.clone()) - .collect()) + .collect() } /// @@ -264,85 +268,53 @@ impl Headers { Ok(()) } - /// — `entries()` yields `[name, value]` - /// pairs over the result of `sort and combine` of this's header list. + /// : `entries`. The value pairs to iterate over + /// are the result of `sort and combine` of this's header list. #[method] fn entries<'r>(&self, scope: &'r Scope<'_>) -> Result, ExnThrown> { - self.create_iterator(scope, IteratorKind::Entries) + // Return a newly created `default iterator object` for _definition_, with _jsValue_ as + // its `target`, "`key+value`" as its `kind`, and `index` set to 0. + HeadersIterator::new(scope, *self, IterationKind::KeyValue) } - /// — `keys()` yields header names. + /// : `keys`. #[method] fn keys<'r>(&self, scope: &'r Scope<'_>) -> Result, ExnThrown> { - self.create_iterator(scope, IteratorKind::Keys) + // Return a newly created `default iterator object` for _definition_, with _jsValue_ as + // its `target`, "`key`" as its `kind`, and `index` set to 0. + HeadersIterator::new(scope, *self, IterationKind::Key) } - /// — `values()` yields header values. + /// : `values`. #[method] fn values<'r>(&self, scope: &'r Scope<'_>) -> Result, ExnThrown> { - self.create_iterator(scope, IteratorKind::Values) + // Return a newly created `default iterator object` for _definition_, with _jsValue_ as + // its `target`, "`value`" as its `kind`, and `index` set to 0. + HeadersIterator::new(scope, *self, IterationKind::Value) } - /// — invoke `callback` once per (value, name) - /// over the sorted-and-combined header list. + /// : `forEach`. #[method(name = "forEach")] fn for_each( &self, scope: &Scope<'_>, - callback: HandleValue, + callback: Callable<'_>, this_arg: Option, ) -> Result<(), ExnThrown> { - // WebIDL converts the argument to a callback function, which requires it to be callable — - // a non-callable object is a TypeError, not something to be called and fail later. - let is_callable = - Object::from_value(scope, callback.get()).is_ok_and(|object| object.is_callable()); - if !is_callable { - return Err(throw_type_error( - scope, - c"forEach callback must be a function", - )); - } - // WebIDL's `forEach` re-reads "the list of value pairs to iterate over" on every turn, so - // a callback that appends to or deletes from these headers changes what the rest of the - // iteration sees. Recompute rather than iterating a snapshot taken up front. - let mut index = 0; - loop { - // The callback may mutate the list, which drops the cached result these references - // point into, so none of them may outlive this block. - let (value_js, name_js) = { - let data = self.data(); - let combined = data.header_list.sorted_and_combined(); - let Some((name, value)) = combined.get(index) else { - break; - }; - ( - value.as_str().to_jsval_throwing(scope)?, - name.as_str().to_jsval_throwing(scope)?, - ) + let this_arg = this_arg.unwrap_or(HandleValue::undefined()); + js::iteration::for_each_pair(scope, *self, callback, this_arg, |i| { + let data = self.data(); + let Some((name, value)) = data.header_list.sorted_and_combined().get(i) else { + return Ok(None); }; - let self_js = scope.root_value(self.as_value()); - Function::call( - scope, - this_arg.unwrap_or(HandleValue::undefined()), - callback, - &[value_js, name_js, self_js], - )?; - index += 1; - } - Ok(()) + let name = name.as_str().to_jsval_throwing(scope)?; + let value = value.as_str().to_jsval_throwing(scope)?; + Ok(Some((name, value))) + }) } } impl Headers<'_> { - /// Create a `HeadersIterator` over this object's headers for the given kind. - fn create_iterator<'r>( - &self, - scope: &'r Scope<'_>, - kind: IteratorKind, - ) -> Result, ExnThrown> { - HeadersIterator::new(scope, *self, kind) - } - /// Define `Symbol.iterator` on `Headers.prototype` (an alias of `entries`). fn install_symbol_iterator(scope: &Scope<'_>) { js::class::add_symbol_alias::( @@ -353,33 +325,20 @@ impl Headers<'_> { } } -/// Which kind of values the iterator produces. -#[derive(Clone, Copy, Default)] -pub enum IteratorKind { - /// Yields `[name, value]` pairs. - #[default] - Entries, - /// Yields names only. - Keys, - /// Yields values only. - Values, -} - -/// +/// #[webidl_interface(hidden, name = "Headers Iterator")] pub struct HeadersIterator { /// The `Headers` object being iterated. pub(crate) headers: Heap, /// Current position in the sorted-and-combined list. pub(crate) index: usize, - /// What kind of values to yield. #[no_trace] - pub(crate) kind: IteratorKind, + pub(crate) kind: IterationKind, } #[webidl_methods] impl HeadersIterator { - fn new(headers: Headers, kind: IteratorKind) -> Self { + fn new(headers: Headers, kind: IterationKind) -> Self { Self { headers: Heap::from(headers), index: 0, @@ -390,52 +349,26 @@ impl HeadersIterator { /// #[method] fn next<'a>(&self, scope: &'a Scope<'a>) -> Result, ExnThrown> { - let result = Object::new(scope, None)?; + // Steps 1-5: Implemented in `#[method]`. + // Step 6: Let _index_ be _object_'s `index`. let index = self.data().index; + // Step 7: Let _kind_ be _object_'s `kind`. let kind = self.data().kind; + // Step 8: Let _values_ be _object_'s `target`'s `value pairs to iterate over`. let headers = self.data().headers.get(scope); let headers_data = headers.data(); - let combined = headers_data.header_list.sorted_and_combined(); - - if let Some((name, value)) = combined.get(index) { - let js_value = match kind { - IteratorKind::Entries => { - let arr = js::Array::new(scope, 2)?; - let name_val = name.as_str().to_jsval_throwing(scope)?; - let val_val = value.as_str().to_jsval_throwing(scope)?; - arr.set_element(scope, 0, name_val)?; - arr.set_element(scope, 1, val_val)?; - scope.root_value(arr.as_value()) - } - IteratorKind::Keys => name.as_str().to_jsval_throwing(scope)?, - IteratorKind::Values => value.as_str().to_jsval_throwing(scope)?, - }; - self.data_mut().index = index + 1; - result.set_property(scope, c"value", js_value)?; - result.set_property(scope, c"done", false)?; - } else { - result.set_property(scope, c"value", js::value::undefined())?; - result.set_property(scope, c"done", true)?; - } - - Ok(result) - } - - /// Chain the `HeadersIterator` prototype under `%IteratorPrototype%` to satisfy - /// the interface's `iterable<>` declaration. - fn install_symbol_iterator(scope: &Scope<'_>) { - let proto = unsafe { - Object::from_raw( - scope, - get_prototype_for::(scope) - .expect("HeadersIterator class not registered"), - ) - .expect("HeadersIterator prototype is null") + let values = headers_data.header_list.sorted_and_combined(); + // Step 9: Let _len_ be the length of _values_. + // Step 10: If _index_ is greater than or equal to _len_, then return + // CreateIteratorResultObject(undefined, true). + // Step 11: Let _pair_ be the entry in _values_ at index _index_. + let Some((name, value)) = values.get(index) else { + return create_iter_result(scope, HandleValue::undefined(), true); }; - - if let Ok(iterator_proto) = get_iterator_prototype(scope) { - let _ = proto.set_prototype(scope, iterator_proto.handle()); - } + // Step 12: Set _object_'s index to _index_ + 1. + self.data_mut().index = index + 1; + // Step 13: Return the `iterator result` for _pair_ and _kind_. + pair_iterator_result(scope, name.as_str(), value.as_str(), kind) } } @@ -443,5 +376,6 @@ pub(crate) fn add_to_global(scope: &Scope, global: Object) { Headers::add_to_global(scope, global); Headers::install_symbol_iterator(scope); HeadersIterator::add_to_global(scope, global); - HeadersIterator::install_symbol_iterator(scope); + js::class::inherit_from_iterator_prototype::(scope) + .expect("setting a class prototype's prototype can only fail due to OOM"); } diff --git a/crates/builtins/web-fetch/src/incoming_body.rs b/crates/builtins/web-fetch/src/incoming_body.rs index 32f26e72..8abdb8e9 100644 --- a/crates/builtins/web-fetch/src/incoming_body.rs +++ b/crates/builtins/web-fetch/src/incoming_body.rs @@ -5,11 +5,13 @@ //! Used as the underlying source for a `body` [`ReadableStream`], or directly in //! [`consume_host_body`] for `text`/`json`/`arrayBuffer`/`bytes`. -use crate::algorithms::{convert_owned_bytes_to_js_value, ConsumeType}; +use crate::algorithms::{convert_owned_bytes_to_js_value, Body, BodySource, ConsumeType}; +use crate::body_mixin::BodyStorage; use core_runtime::jsclass; use core_runtime::jsmethods; use js::conversion::FromJSVal; use js::error::{ExnThrown, TypeError}; +use js::function::cast_payload; use js::function::CallbackArgs; use js::gc::handle::{Heap, OptionHeapExt, RootedHeap}; use js::gc::scope::Scope; @@ -19,8 +21,10 @@ use js::promise::{PromiseFuture, PromiseOutcome}; use js::{value, Function, Promise}; use platform::http::IncomingBody; use web_streams::readable::default_controller::ReadableStreamDefaultControllerImpl; +use web_streams::readable::default_reader::DefaultReader; use web_streams::readable::readable_stream::{ReadableStream, ReadableStreamImpl}; use web_streams::readable::ReadableStreamDefaultController; +use web_streams::AlgorithmArg; /// Back a new external `ArrayBuffer` with `bytes`, zero-copy when the chunk is /// uniquely owned. A shared chunk (another `Bytes` clone alive, e.g. a body @@ -125,7 +129,8 @@ impl HostBodySource<'_> { /// Abort the body: cancel the in-flight chunk read (if any) — which drops the /// host body and closes the connection — and drop any idle host body. pub(crate) fn abort(&self, scope: &Scope<'_>) { - if let Some(pull) = self.data_mut().current_pull.take_rooted(scope) { + let pull = self.data_mut().current_pull.take_rooted(scope); + if let Some(pull) = pull { js::promise::cancel_pending_future(pull); } // A deferred pull has no host read to cancel, but its promise must still be @@ -166,43 +171,93 @@ enum CloseStream { /// A `Request` or `Response` carrying a body still unread on the host. /// -/// Both keep the same quartet of fields — the body record, the unread host body, -/// the `.body` stream it materializes into, and that stream's native source — so -/// the operations over them ([`materialize_host_body`], [`clone_body_onto`], -/// [`crate::outgoing_body::consume_outgoing_body`]) are written here once rather than -/// once per interface. The -/// accessors are deliberately fine-grained rather than one `&mut`-fields -/// accessor, because a caller must not hold a borrow of the object's data across -/// [`host_body_stream`]: allocating the stream can trigger a GC, whose trace of -/// this object reads the same data. +/// Both keep a [`BodyStorage`], so the operations over it ([`materialize_host_body`], +/// [`clone_body_onto`], [`crate::outgoing_body::consume_outgoing_body`]) are written here once +/// rather than once per interface. The provided methods each take the storage borrow only for +/// their own duration, because a caller must not hold a borrow of the object's data across +/// [`host_body_stream`]: allocating the stream can trigger a GC, whose trace of this object +/// reads the same data. pub(crate) trait HostBackedBodyOwner { + /// Run `f` on this object's body storage. `f` must not allocate GC things. + fn with_body(&self, f: impl FnOnce(&mut BodyStorage) -> R) -> R; + + /// This object. + fn object(&self) -> js::Object<'_>; + + /// Attribute the body's byte source to this object for the GC, replacing the previous + /// amount. Called wherever the byte source is set, taken, or dropped. + fn sync_body_accounting(&self) { + let object = self.object(); + self.with_body(|body| body.sync_accounting(object)); + } + /// Take the host body, but only while it is still unread — once a `.body` /// stream exists it owns the body, and the caller has nothing to do. - fn take_unread_host_body(&self) -> Option; + fn take_unread_host_body(&self) -> Option { + self.with_body(|body| body.stream.is_none().then(|| body.host.take())?) + } /// Store the stream the host body materialized into, and its native source. - fn set_host_body_stream(&self, stream: ReadableStream<'_>, source: HostBodySource<'_>); + fn set_host_body_stream(&self, stream: ReadableStream<'_>, source: HostBodySource<'_>) { + self.with_body(|body| { + debug_assert!(body.stream.is_none()); + body.stream = Some(Heap::from(stream)); + body.source = Some(Heap::from(source)); + }); + } /// The body record, if this object has a body. A copy: a [`BodySource::Bytes`] it carries is /// refcounted, so holding the copy holds the buffer. - /// - /// [`BodySource::Bytes`]: crate::algorithms::BodySource::Bytes - fn body_record(&self) -> Option; + fn body_record(&self) -> Option { + self.with_body(|body| body.record.clone()) + } /// Take the body's in-memory bytes, leaving it marked as read. The emptied source stays a byte /// source, so a `.body` asked for later still materializes the consumed stream it would have. - fn take_byte_source(&self) -> Option; + fn take_byte_source(&self) -> Option { + let taken = self.with_body(|body| { + let record = body.record.as_mut()?; + let BodySource::Bytes(bytes) = &mut record.source else { + return None; + }; + record.source_disturbed = true; + Some(std::mem::take(bytes)) + })?; + self.sync_body_accounting(); + Some(taken) + } /// The `.body` stream, once materialized. - fn body_stream<'r>(&self, scope: &'r Scope<'_>) -> Option>; + fn body_stream<'r>(&self, scope: &'r Scope<'_>) -> Option> { + self.with_body(|body| body.stream.get(scope)) + } + + /// Take the reader acquired on the `.body` stream when the body was committed to being sent, + /// if any. Sending reads the stream through it. + fn take_send_reader<'r>(&self, _scope: &'r Scope<'_>) -> Option> { + None + } /// Take the host body whether or not a stream exists (the outgoing path, /// which hands it straight to the transport). - fn take_host_body(&self) -> Option; + fn take_host_body(&self) -> Option { + self.with_body(|body| body.host.take()) + } /// Replace the `.body` stream, and drop the byte and host sources it now /// supersedes — used after teeing, when the bytes live in the tee branches. - fn replace_body_stream_after_tee(&self, scope: &Scope<'_>, stream: ReadableStream<'_>); + fn replace_body_stream_after_tee(&self, stream: ReadableStream<'_>) { + self.with_body(|body| { + debug_assert!(body.stream.is_some()); + body.stream = Some(Heap::from(stream)); + if let Some(record) = body.record.as_mut() { + record.source = BodySource::Null; + } + body.host = None; + body.source = None; + }); + self.sync_body_accounting(); + } } /// `clone a body` for a host-backed owner: materialize the stream if the body is @@ -250,14 +305,11 @@ pub(crate) fn host_body_stream<'r>( host_body: IncomingBody, ) -> Result<(ReadableStream<'r>, HostBodySource<'r>), ExnThrown> { let state = HostBodySource::new(scope, host_body)?; - // TODO: check if `pull` and `cancel` could be per-global singletons and read `state` from their args. - let pull = Function::new_callback(scope, c"", 1, host_pull, state)?; - let cancel = Function::new_callback(scope, c"", 1, host_cancel, state)?; - let pull_value = scope.root_value(pull.as_value()); - let cancel_value = scope.root_value(cancel.as_value()); + let pull = AlgorithmArg::native(scope, host_pull, state)?; + let cancel = AlgorithmArg::native(scope, host_cancel, state)?; // Record the source so the stream can be recognized as host-backed via // `ReadableStream::native_source`. - let stream = ReadableStream::new_native(scope, state, pull_value, cancel_value)?; + let stream = ReadableStream::new_native(scope, state, pull, cancel)?; // Backlink the stream, so a pull can ask what it is feeding. state.data_mut().stream = Some(Heap::from(stream)); Ok((stream, state)) @@ -276,7 +328,7 @@ fn host_pull( args: CallbackArgs<'_>, payload: HandleValue<'_>, ) -> Result { - let state = HostBodySource::from_jsval_throwing(scope, payload, ())?; + let state = cast_payload::(scope, payload); let promise = Promise::new_pending(scope)?; let controller = ReadableStreamDefaultController::from_jsval_throwing(scope, args.get(0), ())?; if let Some(demand) = state.deferred_demand(scope) { @@ -296,7 +348,7 @@ fn host_pull_resume( _args: CallbackArgs<'_>, payload: HandleValue<'_>, ) -> Result { - let state = HostBodySource::from_jsval_throwing(scope, payload, ())?; + let state = cast_payload::(scope, payload); let controller = state.data_mut().deferred_controller.take_rooted(scope); let pull = state.data_mut().deferred_pull.take_rooted(scope); // Already settled: the body was claimed for direct forwarding, cancelled, or aborted while @@ -387,7 +439,7 @@ fn deliver_chunk( // Read failure (host body dropped): error the stream. Err(error) => { let _ = TypeError(format!("Failed to read response body: {error}")).throw(scope); - let reason = js::exception::take_pending(scope).map_err(|_| ExnThrown)?; + let reason = js::exception::take_pending(scope).ok_or(ExnThrown)?; controller.error(scope, Some(reason))?; } } @@ -400,7 +452,7 @@ fn host_cancel( _args: CallbackArgs<'_>, payload: HandleValue<'_>, ) -> Result { - let state = HostBodySource::from_jsval_throwing(scope, payload, ())?; + let state = cast_payload::(scope, payload); state.data_mut().host_body = None; // The stream is being torn down, so a deferred pull's demand will never arrive. state.settle_deferred_pull(scope, CloseStream::No); diff --git a/crates/builtins/web-fetch/src/outgoing_body.rs b/crates/builtins/web-fetch/src/outgoing_body.rs index 09ac5ced..f08664c8 100644 --- a/crates/builtins/web-fetch/src/outgoing_body.rs +++ b/crates/builtins/web-fetch/src/outgoing_body.rs @@ -26,7 +26,7 @@ use js::prelude::HandleValue; use js::promise::{PromiseFuture, PromiseOutcome}; use js::{Object, Promise, Uint8Array}; use platform::http::{BodySender, OutgoingBody}; -use web_streams::readable::default_reader::DefaultReaderImpl; +use web_streams::readable::default_reader::{DefaultReader, DefaultReaderImpl}; use web_streams::readable::native_read::{ acquire_native_reader, native_reader_read, NativeReadSteps, }; @@ -127,18 +127,41 @@ fn outgoing_body_inner( return OutgoingBody::Host(host_body); } match object.body_stream(scope) { - Some(stream) => outgoing_body_from_stream(scope, stream, start_reading), + Some(stream) => { + let reader = object.take_send_reader(scope); + outgoing_body_from_stream(scope, stream, reader, start_reading) + } None => OutgoingBody::Bytes(bytes::Bytes::new()), } } -/// State for the body pump: the reader being drained and the channel's write -/// end. The sender is dropped (closing the body) when the stream ends or errors. +/// State for the body pump: the reader being drained, the channel's write end, and the bytes read +/// but not yet sent. The sender is taken out while a send waits for channel capacity, and dropped +/// (closing the body) when the stream ends or errors. #[jsclass(hidden)] pub struct OutgoingBodyPump { reader: Heap, #[no_trace] sender: Option, + /// Bytes of the chunks read since the last send. + #[no_trace] + batch: Vec, + /// How the stream ended, once its close or error steps ran, or a chunk was not a + /// `Uint8Array`. + #[no_trace] + end: Option, + /// Whether a read request is waiting for a chunk. + #[no_trace] + reading: bool, + /// Whether [`run`] is on the stack. + #[no_trace] + running: bool, +} + +/// How a pumped stream ended. +enum StreamEnd { + Closed, + Errored(String), } #[jsmethods] @@ -149,6 +172,10 @@ impl OutgoingBodyPump { OutgoingBodyPumpImpl { reader: Heap::default(), sender: None, + batch: Vec::new(), + end: None, + reading: false, + running: false, } } } @@ -157,7 +184,8 @@ impl OutgoingBodyPump { /// /// A host body that has never been read is handed straight through; anything /// else is pumped through a `DefaultReader`, or cancelled unsent where -/// `start_reading` is false. +/// `start_reading` is false. The pump reads through `reader` when the caller +/// already locked the stream with one, and acquires its own otherwise. /// /// By this point the request/response is committed to being sent, so a failure /// to start reading cannot be thrown to the caller. It becomes a body that @@ -165,6 +193,7 @@ impl OutgoingBodyPump { pub(crate) fn outgoing_body_from_stream( scope: &Scope<'_>, stream: ReadableStream<'_>, + reader: Option>, start_reading: bool, ) -> OutgoingBody { // The shortcut refuses once the body has actually been read from: a chunk may sit in a @@ -196,11 +225,11 @@ pub(crate) fn outgoing_body_from_stream( let _ = promise.set_any_is_handled(scope); OutgoingBody::Consumed } - None => pump_body_from_stream(scope, stream).unwrap_or_else(|_| { + None => pump_body_from_stream(scope, stream, reader).unwrap_or_else(|_| { // The pump could not be started, so the exception it left pending has no caller to // propagate to. Clear it rather than leaving it to surface at an unrelated point, // and let the failing body carry the failure instead. - let _ = js::exception::take_pending(scope); + js::exception::clear(scope); platform::http::failed_body("the request body stream could not be read".to_string()) }), } @@ -211,42 +240,127 @@ pub(crate) fn outgoing_body_from_stream( fn pump_body_from_stream( scope: &Scope<'_>, stream: ReadableStream<'_>, + reader: Option>, ) -> Result { let (sender, body) = platform::http::body_channel(); - let reader = acquire_native_reader(scope, &stream)?; + let reader = match reader { + Some(reader) => reader, + None => acquire_native_reader(scope, &stream)?, + }; let state = create_instance_with::(scope, |_| OutgoingBodyPumpImpl { reader: Heap::from(reader), sender: Some(sender), + batch: Vec::new(), + end: None, + reading: false, + running: false, })?; - read_next(scope, &state)?; + run(scope, &state); Ok(body) } -/// The pump's native read-request steps. Each chunk pauses the loop until the -/// channel accepted it (`spawn_send` resumes via [`read_next`]), so the pump -/// never outruns the peer and queued chunks cannot recurse. +/// The most bytes the pump reads into one batch before sending it. +const BATCH_LIMIT: usize = 64 * 1024; + +/// The pump's native read-request steps. Each records what it received in the pump's state and +/// then calls [`run`], which does nothing when the step ran from inside it. const PUMP_STEPS: NativeReadSteps = NativeReadSteps { chunk: pump_chunk_step, close: pump_close_step, error: pump_error_step, }; +/// Advance the pump. Reads chunks into the batch for as long as reads complete synchronously and +/// the batch is below [`BATCH_LIMIT`], then sends the batch once a read has to wait, the batch is +/// full, or the stream has ended. A batch the channel accepts right away is followed by more +/// reads. Otherwise the pump waits until the channel accepts it (`spawn_send` resumes it), so it +/// never outruns the peer. At most one read or send is outstanding at a time. +fn run(scope: &Scope<'_>, state: &OutgoingBodyPump<'_>) { + if state.data().running { + return; + } + state.data_mut().running = true; + loop { + while can_read(state) { + state.data_mut().reading = true; + if read_next(scope, state).is_err() { + js::exception::report_and_clear(scope, "reading a request or response body"); + state.data_mut().reading = false; + fail_body(state, "the body stream could not be read"); + } + if state.data().reading { + break; + } + } + if !send_batch(scope, state) { + break; + } + } + state.data_mut().running = false; +} + +/// Whether the pump may issue another read: the channel is not waiting for a send, the stream has +/// not ended, no read is outstanding, and the batch has room. +fn can_read(state: &OutgoingBodyPump<'_>) -> bool { + let data = state.data(); + data.sender.is_some() && data.end.is_none() && !data.reading && data.batch.len() < BATCH_LIMIT +} + +/// Send the batch, or end the body, if there is reason to. Returns whether [`run`] should continue +/// reading: true if the channel accepted the batch right away. +fn send_batch(scope: &Scope<'_>, state: &OutgoingBodyPump<'_>) -> bool { + let mut data = state.data_mut(); + if data.sender.is_none() { + // A send is waiting for capacity, or the body is done. + return false; + } + let ready = data.reading || data.end.is_some() || data.batch.len() >= BATCH_LIMIT; + if !data.batch.is_empty() && ready { + let batch = std::mem::take(&mut data.batch); + let sent = data + .sender + .as_mut() + .expect("checked above") + .try_send_chunk(batch); + drop(data); + return match sent { + Ok(()) => true, + Err(batch) => { + spawn_send(scope, state, Send::Chunk(batch)); + false + } + }; + } + if data.batch.is_empty() { + match data.end.take() { + // End of body: dropping the sender closes the channel. + Some(StreamEnd::Closed) => data.sender = None, + Some(StreamEnd::Errored(message)) => { + drop(data); + spawn_send(scope, state, Send::Error(message)); + } + None => {} + } + } + false +} + /// Issue the next internal read, delivering to the pump's steps. fn read_next(scope: &Scope<'_>, state: &OutgoingBodyPump<'_>) -> Result<(), ExnThrown> { let reader = state.data().reader.get(scope); native_reader_read(scope, reader, PUMP_STEPS, state.as_object()) } -/// A pending send to the body channel: a data chunk (after which the next chunk -/// is read) or a terminal error (after which the pump stops). +/// A pending send to the body channel: a batch of bytes (after which the pump reads on) or a +/// terminal error (after which the pump stops). enum Send { Chunk(Vec), Error(String), } -/// Chunk steps: send a `Uint8Array` chunk to the channel; anything else aborts -/// the body (the spec's transmit-body chunk steps treat a non-`Uint8Array` -/// chunk as a fatal error). +/// Chunk steps: append a `Uint8Array` chunk's bytes to the batch. Anything else ends the body with +/// an error, since the spec's transmit-body chunk steps treat a non-`Uint8Array` chunk as a fatal +/// error. fn pump_chunk_step( scope: &Scope<'_>, payload: Object<'_>, @@ -255,34 +369,47 @@ fn pump_chunk_step( let state = payload .cast::() .expect("payload is an OutgoingBodyPump"); - let chunk_array = Uint8Array::from_jsval(scope, chunk, ()).ok(); - match chunk_array { - // SAFETY: the slice is copied immediately; nothing here can GC or - // detach the chunk's buffer. - Some(array) => { - let bytes = unsafe { array.as_slice() }.to_vec(); - spawn_send(scope, &state, Send::Chunk(bytes)); + let mut data = state.data_mut(); + data.reading = false; + match Uint8Array::from_jsval(scope, chunk, ()) { + // The spec's "a copy of chunk": the chunk's bytes are copied once, into the batch. + Ok(array) => { + // SAFETY: the view's bytes are only read by the copy, which cannot run JS or trigger a + // GC. + let bytes = unsafe { array.as_array_buffer_view().bytes() }; + // Appending to a batch reserves room up to the limit, so the batch is copied once + // rather than through every doubling of its capacity. + if !data.batch.is_empty() && data.batch.capacity() - data.batch.len() < bytes.len() { + let wanted = BATCH_LIMIT.max(data.batch.len() + bytes.len()) - data.batch.len(); + data.batch.reserve_exact(wanted); + } + data.batch.extend_from_slice(bytes); + } + Err(_) => { + data.end = Some(StreamEnd::Errored( + "Body stream chunks must be of type Uint8Array".to_string(), + )) } - None => spawn_send( - scope, - &state, - Send::Error("Body stream chunks must be of type Uint8Array".to_string()), - ), } + drop(data); + run(scope, &state); Ok(()) } -/// Close steps: end of body — drop the sender, closing the channel. +/// Close steps: end of body, after the batch is sent. fn pump_close_step(scope: &Scope<'_>, payload: Object<'_>) -> Result<(), ExnThrown> { let state = payload .cast::() .expect("payload is an OutgoingBodyPump"); - let _ = scope; - state.data_mut().sender = None; + let mut data = state.data_mut(); + data.reading = false; + data.end = Some(StreamEnd::Closed); + drop(data); + run(scope, &state); Ok(()) } -/// Error steps: abort the body with the stream's error. +/// Error steps: abort the body with the stream's error, after the batch is sent. fn pump_error_step( scope: &Scope<'_>, payload: Object<'_>, @@ -291,24 +418,36 @@ fn pump_error_step( let state = payload .cast::() .expect("payload is an OutgoingBodyPump"); - spawn_send( - scope, - &state, - Send::Error("body stream errored".to_string()), - ); + let mut data = state.data_mut(); + data.reading = false; + data.end = Some(StreamEnd::Errored("body stream errored".to_string())); + drop(data); + run(scope, &state); Ok(()) } +/// End the pump's body with an error, so the transport fails the send instead of treating the +/// body as complete. +fn fail_body(state: &OutgoingBodyPump<'_>, message: &str) { + if let Some(sender) = state.data_mut().sender.take() { + sender.fail(message.to_string()); + } +} + /// Send `item` to the body channel (taking the sender out of the pump), awaiting -/// channel capacity for backpressure. For a chunk, the next `read()` is issued -/// only once the chunk is accepted (so the pump never outruns the peer); for an -/// error, the pump stops and the sender is dropped (closing the channel after the -/// queued error). +/// channel capacity for backpressure. After a batch is accepted, the pump runs again. +/// After an error, the pump stops and the sender is dropped (closing the channel after +/// the queued error). fn spawn_send(scope: &Scope<'_>, state: &OutgoingBodyPump<'_>, item: Send) { - let Some(sender) = state.data_mut().sender.take() else { + if state.data().sender.is_none() { return; - }; + } let Ok(promise) = Promise::new_pending(scope) else { + js::exception::report_and_clear(scope, "sending a request or response body"); + fail_body(state, "the body could not be sent"); + return; + }; + let Some(sender) = state.data_mut().sender.take() else { return; }; let _ = promise.set_any_is_handled(scope); @@ -331,10 +470,8 @@ fn spawn_send(scope: &Scope<'_>, state: &OutgoingBodyPump<'_>, item: Send) { if is_chunk { let state = state.get(scope); if accepted { - // For an accepted chunk, restore the sender and read the next - // one. state.data_mut().sender = Some(sender); - let _ = read_next(scope, &state); + run(scope, &state); } else { // A refused chunk means the receiver is gone: the peer stopped reading the // body, typically because the client disconnected. Cancel the stream, and diff --git a/crates/builtins/web-fetch/src/request.rs b/crates/builtins/web-fetch/src/request.rs index 6c1fb749..75397e7a 100644 --- a/crates/builtins/web-fetch/src/request.rs +++ b/crates/builtins/web-fetch/src/request.rs @@ -8,19 +8,21 @@ use super::{ headers::{Guard, HeaderList, Headers, HeadersImpl, HeadersInit}, }; use crate::body_mixin::BodyInit; -use crate::incoming_body::{HostBackedBodyOwner, HostBodySource}; +use crate::body_mixin::BodyStorage; +use crate::incoming_body::HostBackedBodyOwner; use core_runtime::config; use core_runtime::{webidl_dictionary, webidl_interface, webidl_methods, webidl_union}; use js::class::create_instance_with; +use js::class::Ref; use js::error::{throw_type_error, ExnThrown}; use js::gc::handle::{Heap, OptionHeapExt}; use js::gc::scope::Scope; use js::prelude::HandleValue; -use js::{Object, Promise}; +use js::Promise; use url::Url; use web_globals::signals::algorithms::create_dependent_abort_signal; use web_globals::signals::{AbortSignal, AbortSignalImpl}; -use web_streams::readable::readable_stream::{ReadableStream, ReadableStreamImpl}; +use web_streams::readable::readable_stream::ReadableStream; js::webidl_enum! { pub enum RequestDestination { @@ -49,6 +51,21 @@ js::webidl_enum! { } } +js::webidl_enum! { + /// + pub enum ReferrerPolicy { + Empty => "", + NoReferrer => "no-referrer", + NoReferrerWhenDowngrade => "no-referrer-when-downgrade", + SameOrigin => "same-origin", + Origin => "origin", + StrictOrigin => "strict-origin", + OriginWhenCrossOrigin => "origin-when-cross-origin", + StrictOriginWhenCrossOrigin => "strict-origin-when-cross-origin", + UnsafeUrl => "unsafe-url", + } +} + js::webidl_enum! { pub enum RequestMode { Navigate => "navigate", @@ -129,7 +146,7 @@ pub struct RequestRecord { /// pub referrer: Referrer, /// - pub referrer_policy: String, + pub referrer_policy: ReferrerPolicy, /// pub mode: RequestMode, /// @@ -162,7 +179,7 @@ impl Default for RequestRecord { url_list: Vec::new(), destination: RequestDestination::Empty, referrer: Referrer::Client, - referrer_policy: String::new(), + referrer_policy: ReferrerPolicy::Empty, mode: RequestMode::NoCors, credentials_mode: RequestCredentials::SameOrigin, cache_mode: RequestCache::Default, @@ -220,29 +237,13 @@ pub struct Request { /// Note: The header list lives in `headers` and the body in `body`/`body_stream`. #[no_trace] request: RequestRecord, - /// - #[no_trace] - body: Option, - /// The body's `ReadableStream`: a user-provided stream, or one materialized lazily from the - /// body's byte source or host body on `.body` access. - body_stream: Option>, - /// The incoming request's body, set only for a request built by [`Request::from_incoming`]). - /// Consumed by the first of `consume`/`.body`. - #[no_trace] - host_body: Option, - /// The `body`'s byte source, attributed to this object for the GC. Kept in sync by - /// `sync_body_accounting`. - #[no_trace] - accounted: js::gc::AssociatedMemory, - /// The `.body` stream's native host source. Set when `.body` materializes a host-backed - /// stream. - body_source: Option>, + /// , with its stream and host body. + /// The host body is set only for a request built by [`Request::from_incoming`]. + body: BodyStorage, /// - /// initially null. - headers: Option>, + headers: Heap, /// - /// initially null. - signal: Option>, + signal: Heap, } /// @@ -259,7 +260,7 @@ impl Request { #[destructor] fn release_body_accounting(&mut self, object: *mut js::native::JSObject) { // SAFETY: `object` is the object being finalized, whose private data this is. - unsafe { self.accounted.release(object) }; + unsafe { self.body.accounted.release(object) }; } /// @@ -314,14 +315,15 @@ impl Request { // Step 6.2: Set _request_ to _input_’s `request`. request = input_req.data().request.clone(); // Step 6.3: Set _signal_ to _input_’s `signal`. - signal = input_req.data().signal.get(scope); + signal = Some(input_req.data().signal.get(scope)); init_header_list = input_req .data() .headers .get(scope) - .map(|headers| headers.data().header_list.clone()) - .unwrap_or_default(); - input_body = input_req.data().body.clone(); + .data() + .header_list + .clone(); + input_body = input_req.data().body.record.clone(); input_request = Some(input_req); fallback_mode = None; } @@ -339,7 +341,7 @@ impl Request { // the request-restrictions switch.) if config::enforce_fetch_restrictions() { if let Some(window) = init.as_ref().and_then(|i| i.window.as_ref()) { - if !window.get().is_null() && !window.get().is_undefined() { + if !window.is_null_or_undefined() { return Err(throw_type_error(scope, c"Request window must be null")); } } @@ -369,7 +371,7 @@ impl Request { // Step 13.5: Set _request_’s `referrer` to "`client`". request.referrer = Referrer::Client; // Step 13.6: Set _request_’s `referrer policy` to the empty string. - request.referrer_policy = String::new(); + request.referrer_policy = ReferrerPolicy::Empty; // Step 13.7: Set _request_’s `URL` to _request_’s `current URL`. // Step 13.8: Set _request_’s `URL list` to « _request_’s `URL` ». let current = request.current_url().clone(); @@ -406,14 +408,8 @@ impl Request { } // Step 15: If _init_["`referrerPolicy`"] `exists`, then set _request_’s `referrer // policy` to it. - if config::enforce_fetch_restrictions() { - if let Some(policy) = init.referrer_policy.take() { - // ReferrerPolicy is a WebIDL enum, so an invalid value is a TypeError. - if !algorithms::is_valid_referrer_policy(&policy) { - return Err(throw_type_error(scope, c"Invalid referrerPolicy value")); - } - request.referrer_policy = policy; - } + if let Some(policy) = init.referrer_policy { + request.referrer_policy = policy; } } // Step 16: Let _mode_ be _init_["`mode`"] if it `exists`, and _fallbackMode_ otherwise. @@ -483,24 +479,8 @@ impl Request { request.method = algorithms::normalize_a_method(method); } // Step 26: If _init_["`signal`"] `exists`, then set _signal_ to it. - if let Some(signal_init) = &init.signal { - signal = match signal_init { - None => None, - Some(value) => { - let obj = Object::from_value(scope, value.get()) - .ok() - .and_then(|o| o.cast::().ok()); - match obj { - Some(s) => Some(s), - None => { - return Err(throw_type_error( - scope, - c"Request signal must be an AbortSignal", - )); - } - } - } - }; + if let Some(signal_init) = init.signal { + signal = signal_init; } // Step 27: If _init_["`priority`"] `exists`, then: // Step 27.1: If _request_’s `internal priority` is not null, then update _request_’s @@ -520,7 +500,7 @@ impl Request { Some(signal) => create_dependent_abort_signal(scope, std::slice::from_ref(signal))?, None => create_dependent_abort_signal(scope, &[])?, }; - self.data_mut().signal = Some(Heap::from(dependent)); + self.data_mut().signal.set(dependent); // Step 31: Set `this`’s `headers` to a `new` `Headers` object with `this`’s `relevant // realm`, whose `header list` is _request_’s `header list` and `guard` is "`request`". let headers = Headers::from_list(scope, init_header_list, Guard::Request)?; @@ -565,7 +545,7 @@ impl Request { } } } - self.data_mut().headers = Some(Heap::from(headers)); + self.data_mut().headers.set(headers); // Step 34: Let _inputBody_ be _input_’s `request`’s `body` if _input_ is a `Request` // object; otherwise null. // (Captured as `input_body` in step 6.) @@ -604,7 +584,7 @@ impl Request { // contain` `Content-Type`, then `append` (`Content-Type`, _type_) to `this`’s // `headers`. if let Some(content_type) = content_type { - let headers = self.data().headers.get(scope).unwrap(); + let headers = self.data().headers.get(scope); if !algorithms::contains(&headers.data().header_list, "Content-Type") { // The simple `push` here is equivalent to `append`ing, since the checks // that operation does are guaranteed to succeed if we got here. @@ -661,14 +641,11 @@ impl Request { } let input_stream = input_request .as_ref() - .and_then(|req| req.data().body_stream.get(scope)); + .and_then(|req| req.data().body.stream.get(scope)); // Step 41.1: If _inputBody_ is `unusable`, then `throw` a `TypeError`. - let unusable = input_body + let unusable = input_request .as_ref() - .is_some_and(|body| body.source_disturbed) - || input_stream - .as_ref() - .is_some_and(|stream| stream.is_disturbed() || stream.is_locked()); + .is_some_and(|req| req.is_unusable(scope)); if unusable { return Err(throw_type_error(scope, c"input Request body is unusable")); } @@ -685,10 +662,10 @@ impl Request { (None, None) }; // Step 42: Set `this`’s `request`’s `body` to _finalBody_. - self.data_mut().body = final_body; + self.data_mut().body.record = final_body; self.sync_body_accounting(); if let Some(stream) = final_stream { - self.data_mut().body_stream.set(stream); + self.data_mut().body.stream.set(stream); } // When the input is a Request with a body, constructing a new Request from it consumes // (disturbs) the input's body. @@ -698,7 +675,7 @@ impl Request { // bytes, so the input's own handle on them holds the buffer for a body it can no // longer read. let _ = input_request.take_byte_source(); - if let Some(body) = input_request.data_mut().body.as_mut() { + if let Some(body) = input_request.data_mut().body.record.as_mut() { body.source_disturbed = true; } } @@ -737,38 +714,34 @@ impl Request { let headers = Headers::from_list(scope, headers, Guard::Immutable)?; Ok(Self { request: record, - body: body_record, - body_stream: None, - host_body: body, - accounted: Default::default(), - body_source: None, - headers: Some(Heap::from(headers)), - signal: Some(Heap::from(signal)), + body: BodyStorage { + host: body, + ..BodyStorage::new(body_record, None) + }, + headers: Heap::from(headers), + signal: Heap::from(signal), }) } /// #[getter] - fn method(&self) -> String { + fn method(&self) -> Ref<'_, str> { // Step 1: Return `this`’s `request`’s `method`. - self.data().request.method.clone() + Ref::map(self.data(), |data| data.request.method.as_str()) } /// #[getter] - fn url(&self) -> String { + fn url(&self) -> Ref<'_, str> { // Step 1: Return `this`’s `request`’s `URL`, `serialized`. - self.data().request.current_url().to_string() + Ref::map(self.data(), |data| data.request.current_url().as_str()) } /// #[getter] fn headers<'r>(&self, scope: &'r Scope<'_>) -> Headers<'r> { // Step 1: Return `this`’s `headers`. - self.data() - .headers - .get(scope) - .expect("headers are set during construction") + self.data().headers.get(scope) } /// @@ -794,10 +767,9 @@ impl Request { /// #[getter] - fn referrer_policy(&self) -> String { - // WebIDL: ReferrerPolicy + fn referrer_policy(&self) -> ReferrerPolicy { // Step 1: Return `this`’s `request`’s `referrer policy`. - self.data().request.referrer_policy.clone() + self.data().request.referrer_policy } /// @@ -863,10 +835,7 @@ impl Request { pub fn signal<'r>(&self, scope: &'r Scope<'_>) -> AbortSignal<'r> { // WebIDL: AbortSignal // Step 1: Return `this`’s `signal`. - self.data() - .signal - .get(scope) - .expect("signal is set during construction") + self.data().signal.get(scope) } /// @@ -908,7 +877,7 @@ impl Request { let cloned_record = self.data().request.clone(); let (cloned_body, cloned_stream) = crate::incoming_body::clone_body_onto(scope, self)?; // Step 3: `Assert`: `this`’s `signal` is non-null. - let this_signal = self.data().signal.get(scope).expect("signal is non-null"); + let this_signal = self.data().signal.get(scope); // Step 4: Let _clonedSignal_ be the result of `creating a dependent abort signal` from « // `this`’s `signal` », using `AbortSignal` and `this`’s `relevant realm`. let cloned_signal = @@ -920,7 +889,7 @@ impl Request { // `Headers` object with _realm_, whose `headers list` is _request_’s `headers list` // and `guard` is _guard_. let (header_list, guard) = { - let headers = self.data().headers.get(scope).expect("headers set"); + let headers = self.data().headers.get(scope); let header_list = headers.data().header_list.clone(); let guard = headers.data().guard; (header_list, guard) @@ -931,15 +900,11 @@ impl Request { // _requestObject_’s `signal` to _signal_. let cloned_object = create_instance_with::(scope, |_| RequestImpl { request: cloned_record, - body: cloned_body, - body_stream: cloned_stream.map(Heap::from), // A cloned request reads through its teed stream; the host body stays with the // source (whose stream is the other tee branch). - host_body: None, - accounted: Default::default(), - body_source: None, - headers: Some(Heap::from(cloned_headers)), - signal: Some(Heap::from(cloned_signal)), + body: BodyStorage::new(cloned_body, cloned_stream), + headers: Heap::from(cloned_headers), + signal: Heap::from(cloned_signal), })?; cloned_object.sync_body_accounting(); // [inlined create a Request object] Step 5: Return _requestObject_. @@ -1003,26 +968,20 @@ impl Request<'_> { let mut has_accept = false; let mut has_content_length = false; - let mut headers: Vec<(String, String)> = self + let request_headers = self.data().headers.get(scope); + let mut headers: Vec<(String, String)> = request_headers .data() - .headers - .get(scope) - .map(|headers| { - headers - .data() - .header_list - .iter() - .map(|(name, value)| { - if name.eq_ignore_ascii_case("accept") { - has_accept = true; - } else if name.eq_ignore_ascii_case("content-length") { - has_content_length = true; - } - (name.clone(), value.clone()) - }) - .collect() + .header_list + .iter() + .map(|(name, value)| { + if name.eq_ignore_ascii_case("accept") { + has_accept = true; + } else if name.eq_ignore_ascii_case("content-length") { + has_content_length = true; + } + (name.clone(), value.clone()) }) - .unwrap_or_default(); + .collect(); // [inlined fetch](https://fetch.spec.whatwg.org/#concept-fetch) Step 12: If _request_'s // `header list` `does not contain` `Accept`, then: // [inlined fetch] Step 12.1: Let _value_ be `*/*`. @@ -1037,7 +996,7 @@ impl Request<'_> { // gets none, so it is sent chunked. // // Read before the body is taken, which empties a byte source of its bytes. - let content_length = match self.data().body.as_ref() { + let content_length = match self.data().body.record.as_ref() { None => (method == "POST" || method == "PUT").then_some(0), Some(body) => match &body.source { BodySource::Bytes(bytes) => Some(bytes.len() as u64), @@ -1083,90 +1042,15 @@ impl BodyMixin for Request<'_> { const UNUSABLE_MESSAGE: &'static std::ffi::CStr = c"Request body is unusable"; const TEXT_STREAM_UNSUPPORTED: &'static std::ffi::CStr = c"Request.textStream() is not yet supported"; - - fn set_body_stream(&self, stream: ReadableStream<'_>) { - debug_assert!(self.data().body_stream.is_none()); - self.data_mut().body_stream = Some(Heap::from(stream)); - } - - fn set_source_disturbed(&self) { - if let Some(body) = self.data_mut().body.as_mut() { - body.source_disturbed = true; - } - } } impl HostBackedBodyOwner for Request<'_> { - fn take_unread_host_body(&self) -> Option { - let mut data = self.data_mut(); - data.body_stream.is_none().then(|| data.host_body.take())? - } - - fn set_host_body_stream(&self, stream: ReadableStream<'_>, source: HostBodySource<'_>) { - debug_assert!(self.data().body_stream.is_none()); - let mut data = self.data_mut(); - data.body_stream = Some(Heap::from(stream)); - data.body_source = Some(Heap::from(source)); - } - - fn body_record(&self) -> Option { - self.data().body.clone() - } - - fn take_byte_source(&self) -> Option { - let taken = { - let mut data = self.data_mut(); - let body = data.body.as_mut()?; - let BodySource::Bytes(bytes) = &mut body.source else { - return None; - }; - let bytes = std::mem::take(bytes); - body.source_disturbed = true; - bytes - }; - self.sync_body_accounting(); - Some(taken) - } - - fn body_stream<'r>(&self, scope: &'r Scope<'_>) -> Option> { - self.data().body_stream.get(scope) - } - - fn take_host_body(&self) -> Option { - self.data_mut().host_body.take() - } - - fn replace_body_stream_after_tee(&self, _scope: &Scope<'_>, stream: ReadableStream<'_>) { - debug_assert!(self.data().body_stream.is_some()); - { - let mut data = self.data_mut(); - data.body_stream = Some(Heap::from(stream)); - // The source's bytes now live in the teed stream, so read via that branch, not the - // byte/host fast paths: drop the byte source and the now-stale host source. - if let Some(body) = data.body.as_mut() { - body.source = BodySource::Null; - } - data.host_body = None; - data.body_source = None; - } - self.sync_body_accounting(); + fn with_body(&self, f: impl FnOnce(&mut BodyStorage) -> R) -> R { + f(&mut self.data_mut().body) } -} -impl Request<'_> { - /// Attribute the `body`'s byte source to this object for the GC, replacing the previous - /// amount. Called wherever the byte source is set, taken, or dropped. - pub(crate) fn sync_body_accounting(&self) { - let mut data = self.data_mut(); - let bytes = match &data.body { - Some(Body { - source: BodySource::Bytes(bytes), - .. - }) => bytes.len(), - _ => 0, - }; - // SAFETY: `self` is a rooted handle to the object owning this private data. - unsafe { data.accounted.set(self.as_raw(), bytes) }; + fn object(&self) -> js::Object<'_> { + self.as_object() } } @@ -1177,14 +1061,14 @@ pub struct RequestInit<'a> { pub headers: Option, pub body: Option>, pub referrer: Option, - pub referrer_policy: Option, // WebIDL: ReferrerPolicy + pub referrer_policy: Option, pub mode: Option, pub credentials: Option, pub cache: Option, pub redirect: Option, pub integrity: Option, pub keepalive: Option, - pub signal: Option>>, // WebIDL: AbortSignal + pub signal: Option>>, pub duplex: Option, pub priority: Option, pub window: Option>, // WebIDL: any diff --git a/crates/builtins/web-fetch/src/response.rs b/crates/builtins/web-fetch/src/response.rs index 4fda94f6..c4d3dfa8 100644 --- a/crates/builtins/web-fetch/src/response.rs +++ b/crates/builtins/web-fetch/src/response.rs @@ -3,18 +3,21 @@ //! use super::algorithms::{self, Body, BodySource}; -use super::body_mixin::{BodyInit, BodyMixin}; +use super::body_mixin::{BodyInit, BodyMixin, BodyStorage}; use super::headers::{Guard, HeaderList, Headers, HeadersImpl, HeadersInit}; +use super::incoming_body::HostBackedBodyOwner; use super::request::RequestRedirect; use core_runtime::webidl_methods; use core_runtime::{webidl_dictionary, webidl_interface}; +use js::class::Ref; use js::error::{throw_type_error, ExnThrown, RangeError}; use js::gc::handle::{Heap, OptionHeapExt}; use js::gc::scope::Scope; use js::prelude::HandleValue; use js::Promise; use url::Url; -use web_streams::readable::readable_stream::ReadableStreamImpl; +use web_streams::readable::default_reader::{DefaultReader, DefaultReaderImpl}; +use web_streams::readable::native_read::acquire_native_reader; use web_streams::readable::ReadableStream; js::webidl_enum! { @@ -102,28 +105,19 @@ pub struct Response { /// #[no_trace] pub(crate) response: ResponseRecord, - /// - #[no_trace] - pub(crate) body: Option, - /// The body's `ReadableStream`, stored separately for easier GC-rooting. - pub(crate) body_stream: Option>, - /// The network response body for incoming responses. - #[no_trace] - pub(crate) host_body: Option, - /// The `body`'s byte source, attributed to this object for the GC. Kept in sync by - /// `sync_body_accounting`. - #[no_trace] - pub(crate) accounted: js::gc::AssociatedMemory, - /// The `.body` stream's native byte source, kept so an abort can cancel an in-flight host - /// read. Set when `.body` materializes a host-backed stream. - pub(crate) body_source: Option>, + /// , with its stream and host body. + /// The host body is set only for an incoming response. + pub(crate) body: BodyStorage, /// - pub(crate) headers: Option>, + pub(crate) headers: Heap, /// The `fetch` abort algorithm's state, for a response delivered by `fetch` whose body was /// still abortable when the fetch settled. Set so consuming the body can detach the /// algorithm from the `AbortSignal`, which would otherwise keep it, and this response, /// alive for the life of the signal. pub(crate) abort_state: Option>, + /// The reader [`Response::reserve_body_for_sending`] acquired on `body_stream`, which the + /// send path reads the body through. + pub(crate) send_reader: Option>, } #[webidl_methods] @@ -133,7 +127,7 @@ impl Response { #[destructor] fn release_body_accounting(&mut self, object: *mut js::native::JSObject) { // SAFETY: `object` is the object being finalized, whose private data this is. - unsafe { self.accounted.release(object) }; + unsafe { self.body.accounted.release(object) }; } /// @@ -153,7 +147,7 @@ impl Response { header_list: HeaderList::new().into(), guard: Guard::Response, })?; - self.data_mut().headers = Some(Heap::from(headers)); + self.data_mut().headers.set(headers); // Step 3: Let _bodyWithType_ be null. // Step 4: If _body_ is non-null, then set _bodyWithType_ to the result of `extracting` // _body_. @@ -177,13 +171,10 @@ impl Response { ) -> Self { Self { response: record, - body, - body_stream: body_stream.map(Heap::from), - host_body: None, - accounted: Default::default(), - body_source: None, - headers: Some(Heap::from(headers)), + body: BodyStorage::new(body, body_stream), + headers: Heap::from(headers), abort_state: None, + send_reader: None, } } @@ -196,17 +187,13 @@ impl Response { /// #[getter(name = "url")] - pub fn url_string(&self) -> String { + pub fn url_string(&self) -> Ref<'_, str> { // Step 1: Return the empty string if `this`’s `response`’s `URL` is null; otherwise // `this`’s `response`’s `URL`, `serialized` with `_exclude fragment_` set to true. - match self.data().response.url() { - None => String::new(), - Some(url) => { - let mut url = url.clone(); - url.set_fragment(None); - url.to_string() - } - } + Ref::map(self.data(), |data| match data.response.url() { + None => "", + Some(url) => &url[..url::Position::AfterQuery], + }) } /// Returns a reference to the [`Url`] of this response. @@ -239,19 +226,16 @@ impl Response { /// #[getter] - pub fn status_text(&self) -> String { + pub fn status_text(&self) -> Ref<'_, str> { // Step 1: Return `this`’s `response`’s `status message`. - self.data().response.status_message.clone() + Ref::map(self.data(), |data| data.response.status_message.as_str()) } /// #[getter] pub fn headers<'r>(&self, scope: &'r Scope<'_>) -> Headers<'r> { // Step 1: Return `this`’s `headers`. - self.data() - .headers - .get(scope) - .expect("headers are set during construction") + self.data().headers.get(scope) } /// @@ -400,7 +384,7 @@ impl Response { let value = parsed.to_string(); // Step 7: `Append` (`Location`, _value_) to _responseObject_’s `response`’s `header // list`. - let headers = response.data().headers.get(scope).unwrap(); + let headers = response.data().headers.get(scope); algorithms::append_a_header( &mut headers.data_mut().header_list, "Location".to_string(), @@ -466,32 +450,17 @@ impl Response<'_> { pub(crate) fn clear_body(&self) { { let mut data = self.data_mut(); - data.body = None; - data.body_stream = None; - data.host_body = None; - data.body_source = None; + data.body.record = None; + data.body.stream = None; + data.body.host = None; + data.body.source = None; } self.sync_body_accounting(); } - /// Attribute the `body`'s byte source to this object for the GC, replacing the previous - /// amount. Called wherever the byte source is set, taken, or dropped. - pub(crate) fn sync_body_accounting(&self) { - let mut data = self.data_mut(); - let bytes = match &data.body { - Some(Body { - source: BodySource::Bytes(bytes), - .. - }) => bytes.len(), - _ => 0, - }; - // SAFETY: `self` is a rooted handle to the object owning this private data. - unsafe { data.accounted.set(self.as_raw(), bytes) }; - } - /// Whether a host body is still sitting unread on this response. pub(crate) fn has_unread_host_body(&self) -> bool { - self.data().host_body.is_some() + self.data().body.host.is_some() } /// Remember the `fetch` abort algorithm's state, so consuming the body can detach it. @@ -502,7 +471,8 @@ impl Response<'_> { /// The body has been consumed, so an abort can no longer affect it: detach the `fetch` abort /// algorithm from its signal. fn detach_abort_state(&self, scope: &Scope<'_>) { - if let Some(state) = self.data_mut().abort_state.take_rooted(scope) { + let state = self.data_mut().abort_state.take_rooted(scope); + if let Some(state) = state { state.detach(scope); } } @@ -511,7 +481,8 @@ impl Response<'_> { /// would still have an observable effect. pub(crate) fn body_stream_is_unfinished(&self, scope: &Scope<'_>) -> bool { self.data() - .body_stream + .body + .stream .get(scope) .is_some_and(|stream| stream.is_readable()) } @@ -521,13 +492,15 @@ impl Response<'_> { /// host body, which closes the connection. Called by the `fetch` abort algorithm. pub(crate) fn abort_body(&self, scope: &Scope<'_>, reason: HandleValue<'_>) { // Drop an unread host body (the `.body`/consume path never ran): closes the connection. - self.data_mut().host_body = None; + self.data_mut().body.host = None; // Stop an in-flight host read driving the `.body` stream. - if let Some(source) = self.data().body_source.get(scope) { + let source = self.data().body.source.get(scope); + if let Some(source) = source { source.abort(scope); } // Error the `.body` stream so pending and future reads reject. - if let Some(stream) = self.data().body_stream.get(scope) { + let stream = self.data().body.stream.get(scope); + if let Some(stream) = stream { stream.error(scope, reason); } } @@ -685,7 +658,7 @@ pub(crate) fn response_from_platform<'r>( None, )?; if has_body { - response_object.data_mut().host_body = Some(response.body); + response_object.data_mut().body.host = Some(response.body); } Ok(response_object) } @@ -695,16 +668,6 @@ impl BodyMixin for Response<'_> { const TEXT_STREAM_UNSUPPORTED: &'static std::ffi::CStr = c"Response.textStream() is not yet supported"; - fn set_body_stream(&self, stream: web_streams::readable::readable_stream::ReadableStream<'_>) { - self.data_mut().body_stream = Some(Heap::from(stream)); - } - - fn set_source_disturbed(&self) { - if let Some(body) = self.data_mut().body.as_mut() { - body.source_disturbed = true; - } - } - /// The body is being read to completion, so an abort can no longer error it: /// drop the `fetch` abort algorithm that was keeping this response alive. fn on_body_consumed(&self, scope: &Scope<'_>) { @@ -712,70 +675,17 @@ impl BodyMixin for Response<'_> { } } -impl crate::incoming_body::HostBackedBodyOwner for Response<'_> { - fn take_unread_host_body(&self) -> Option { - let mut data = self.data_mut(); - data.body_stream.is_none().then(|| data.host_body.take())? - } - - fn set_host_body_stream( - &self, - stream: web_streams::readable::readable_stream::ReadableStream<'_>, - source: crate::incoming_body::HostBodySource<'_>, - ) { - let mut data = self.data_mut(); - data.body_stream = Some(Heap::from(stream)); - data.body_source = Some(Heap::from(source)); - } - - fn body_record(&self) -> Option { - self.data().body.clone() - } - - fn take_byte_source(&self) -> Option { - let taken = { - let mut data = self.data_mut(); - let body = data.body.as_mut()?; - let BodySource::Bytes(bytes) = &mut body.source else { - return None; - }; - let bytes = std::mem::take(bytes); - body.source_disturbed = true; - bytes - }; - self.sync_body_accounting(); - Some(taken) +impl HostBackedBodyOwner for Response<'_> { + fn with_body(&self, f: impl FnOnce(&mut BodyStorage) -> R) -> R { + f(&mut self.data_mut().body) } - fn body_stream<'r>( - &self, - scope: &'r Scope<'_>, - ) -> Option> { - self.data().body_stream.get(scope) + fn object(&self) -> js::Object<'_> { + self.as_object() } - fn take_host_body(&self) -> Option { - self.data_mut().host_body.take() - } - - fn replace_body_stream_after_tee( - &self, - scope: &Scope<'_>, - stream: web_streams::readable::readable_stream::ReadableStream<'_>, - ) { - let _ = scope; - { - let mut data = self.data_mut(); - data.body_stream = Some(Heap::from(stream)); - // The source's bytes now live in the teed stream, so read via that branch, not the - // byte/host fast paths: drop the byte source and the now-stale host source. - if let Some(body) = data.body.as_mut() { - body.source = BodySource::Null; - } - data.host_body = None; - data.body_source = None; - } - self.sync_body_accounting(); + fn take_send_reader<'r>(&self, scope: &'r Scope<'_>) -> Option> { + self.data_mut().send_reader.take_rooted(scope) } } @@ -811,8 +721,18 @@ impl Response<'_> { BodyMixin::is_unusable(self, scope) } - /// Mark the body as read, so `bodyUsed` reports true. - pub fn mark_body_used(&self) { + /// Commit the body to being sent: mark it as read, so `bodyUsed` reports true, and lock a + /// materialized `.body` stream with the reader [`Response::take_send_body`] later reads it + /// through, so author code can no longer read from it. + pub fn reserve_body_for_sending(&self, scope: &Scope<'_>) -> Result<(), ExnThrown> { BodyMixin::set_source_disturbed(self); + let stream = self.data().body.stream.get(scope); + if let Some(stream) = stream { + if !stream.is_locked() { + let reader = acquire_native_reader(scope, &stream)?; + self.data_mut().send_reader.set(reader); + } + } + Ok(()) } } diff --git a/crates/builtins/web-fetch/tests/body_memory.rs b/crates/builtins/web-fetch/tests/body_memory.rs index 02c9ae18..1a721691 100644 --- a/crates/builtins/web-fetch/tests/body_memory.rs +++ b/crates/builtins/web-fetch/tests/body_memory.rs @@ -30,7 +30,7 @@ fn evaluate(scope: &js::gc::scope::Scope<'_>, code: &str) { fn collections_while_building(constructor: &str) -> u32 { clear_global_initializers(); libstarling::register_builtins(); - let rt = Runtime::init(&RuntimeConfig::default()); + let rt = Runtime::init(&RuntimeConfig::default()).expect("runtime init"); let scope = rt.default_global(); evaluate(&scope, "globalThis.body = 'x'.repeat(1 << 20);"); let before = gc_count(&scope); @@ -62,7 +62,7 @@ fn consumed_and_cloned_bodies_keep_the_accounting_balanced() { // so this test's value is in running under that feature. clear_global_initializers(); libstarling::register_builtins(); - let rt = Runtime::init(&RuntimeConfig::default()); + let rt = Runtime::init(&RuntimeConfig::default()).expect("runtime init"); let scope = rt.default_global(); evaluate( &scope, diff --git a/crates/builtins/web-fetch/tests/config.rs b/crates/builtins/web-fetch/tests/config.rs index 01b0e365..e9c40b24 100644 --- a/crates/builtins/web-fetch/tests/config.rs +++ b/crates/builtins/web-fetch/tests/config.rs @@ -17,24 +17,16 @@ use core_runtime::config::RuntimeConfig; use core_runtime::runtime::{clear_global_initializers, Runtime}; -use js::conversion::FromJSVal; +use core_runtime::test_util::eval_and_read_out; /// Evaluate `code` in a runtime initialized from `config`, returning /// `String(globalThis.__out)`. fn run(config: &RuntimeConfig, code: &str) -> String { clear_global_initializers(); libstarling::register_builtins(); - let rt = Runtime::init(config); + let rt = Runtime::init(config).expect("runtime init"); let scope = rt.default_global(); - if js::compile::evaluate_with_filename(&scope, code, "test.js", 1).is_err() { - panic!( - "evaluation threw: {:?}", - js::error::ExnThrown::capture(&scope) - ); - } - let out = js::compile::evaluate_with_filename(&scope, "String(globalThis.__out)", "out.js", 1) - .expect("reading __out threw"); - String::from_jsval(&scope, out, ()).unwrap() + eval_and_read_out(&scope, code) } const PERMISSIVE_PROBE: &str = r#" @@ -91,18 +83,32 @@ fn explicit_flag_overrides_either_default() { fn setter_overrides_after_init() { clear_global_initializers(); libstarling::register_builtins(); - let rt = Runtime::init(&RuntimeConfig::default()); + let rt = Runtime::init(&RuntimeConfig::default()).expect("runtime init"); let scope = rt.default_global(); core_runtime::config::set_enforce_fetch_restrictions(true); assert!(core_runtime::config::enforce_fetch_restrictions()); - let out = js::compile::evaluate_with_filename(&scope, ENFORCED_PROBE, "test.js", 1); - assert!(out.is_ok()); - let out = js::compile::evaluate_with_filename(&scope, "String(globalThis.__out)", "out.js", 1) - .expect("reading __out threw"); assert_eq!( - String::from_jsval(&scope, out, ()).unwrap(), + eval_and_read_out(&scope, ENFORCED_PROBE), "host=null,connectThrew=true" ); // Restore for other tests on this thread. core_runtime::config::set_enforce_fetch_restrictions(false); } + +/// `referrerPolicy` is stored and validated whether or not fetch restrictions +/// are enforced. +#[test] +fn referrer_policy_is_kept_without_restrictions() { + let config = RuntimeConfig::from_arg_string("--enforce-fetch-restrictions=false").unwrap(); + let out = run( + &config, + r#" + const r = new Request("http://example.com/", { referrerPolicy: "origin" }); + let threw = false; + try { new Request("http://example.com/", { referrerPolicy: "bogus" }); } + catch (e) { threw = e instanceof TypeError; } + globalThis.__out = `${r.referrerPolicy},${threw}`; + "#, + ); + assert_eq!(out, "origin,true"); +} diff --git a/crates/builtins/web-fetch/tests/fetch_network.rs b/crates/builtins/web-fetch/tests/fetch_network.rs index 257fdfb9..8f5b8692 100644 --- a/crates/builtins/web-fetch/tests/fetch_network.rs +++ b/crates/builtins/web-fetch/tests/fetch_network.rs @@ -245,7 +245,7 @@ fn start_same_origin_redirect_auth_server() -> String { fn run_and_get_out(code: &str) -> String { clear_global_initializers(); libstarling::register_builtins(); - let rt = Runtime::init(&core_runtime::config::RuntimeConfig::default()); + let rt = Runtime::init(&core_runtime::config::RuntimeConfig::default()).expect("runtime init"); let scope = rt.default_global(); let rawcx = unsafe { scope.cx_mut().raw_cx() }; let el = EventLoop::new(); @@ -785,7 +785,7 @@ fn a_reused_abort_signal_does_not_accumulate_fetch_abort_algorithms() { let url = start_repeating_server(3, "body"); clear_global_initializers(); libstarling::register_builtins(); - let rt = Runtime::init(&core_runtime::config::RuntimeConfig::default()); + let rt = Runtime::init(&core_runtime::config::RuntimeConfig::default()).expect("runtime init"); let scope = rt.default_global(); let rawcx = unsafe { scope.cx_mut().raw_cx() }; let el = EventLoop::new(); diff --git a/crates/builtins/web-globals/Cargo.toml b/crates/builtins/web-globals/Cargo.toml index ac69bc70..da8ec285 100644 --- a/crates/builtins/web-globals/Cargo.toml +++ b/crates/builtins/web-globals/Cargo.toml @@ -13,6 +13,7 @@ data-url.workspace = true encoding_rs.workspace = true js.workspace = true platform.workspace = true +smallvec.workspace = true url.workspace = true [dev-dependencies] diff --git a/crates/builtins/web-globals/src/base64.rs b/crates/builtins/web-globals/src/base64.rs index 5144c743..50cbf220 100644 --- a/crates/builtins/web-globals/src/base64.rs +++ b/crates/builtins/web-globals/src/base64.rs @@ -18,6 +18,12 @@ pub mod base64_globals { /// Throws `DOMException` with name `"InvalidCharacterError"` if the string /// contains any code point above U+00FF. pub fn btoa(data: String) -> Result { + // : The `btoa(data)` method must throw an + // "``InvalidCharacterError``" ``DOMException`` if _data_ contains any character whose code + // point is greater than U+00FF. Otherwise, the user agent must convert _data_ to a byte + // sequence whose _n_th byte is the eight-bit representation of the _n_th code point of + // _data_, and then must apply `forgiving-base64 encode` to that byte sequence and return + // the result. super::btoa(&data).map_err(|msg| DOMExceptionError::new("InvalidCharacterError", msg)) } @@ -27,6 +33,11 @@ pub mod base64_globals { /// decodes it. Throws `DOMException` with name `"InvalidCharacterError"` if /// the input is not valid base64. pub fn atob(data: String) -> Result { + // + // Step 1: Let _decodedData_ be the result of running `forgiving-base64 decode` on _data_. + // Step 2: If _decodedData_ is failure, then throw an "``InvalidCharacterError``" + // ``DOMException``. + // Step 3: Return _decodedData_. super::atob(&data).map_err(|msg| DOMExceptionError::new("InvalidCharacterError", msg)) } } diff --git a/crates/builtins/web-globals/src/console.rs b/crates/builtins/web-globals/src/console.rs index 68bddb33..1b57310b 100644 --- a/crates/builtins/web-globals/src/console.rs +++ b/crates/builtins/web-globals/src/console.rs @@ -25,10 +25,9 @@ fn format_args(scope: &Scope<'_>, args: &CallArgs) -> String { // Fast path for strings; slow path calls JS ToString. let js_str = if val.is_string() { NonNull::new(val.to_string()) - .map(|p| js::JSString::from_handle(scope.root_string(p))) + .map(|p| js::JSString::from_non_null(scope, p)) } else { - let handle = scope.root_value(val); - js::JSString::from_value(scope, handle).ok() + js::JSString::from_value(scope, val).ok() }; match js_str { Some(s) => match s.to_utf8(scope) { diff --git a/crates/builtins/web-globals/src/dom_exception.rs b/crates/builtins/web-globals/src/dom_exception.rs index 565524cc..b3a44d1f 100644 --- a/crates/builtins/web-globals/src/dom_exception.rs +++ b/crates/builtins/web-globals/src/dom_exception.rs @@ -6,12 +6,13 @@ //! including the constructor, prototype accessors, legacy error code constants, //! and the `Symbol.toStringTag` property. //! -//! Uses `#[webidl_interface]` and `#[jsmethods]` proc macros for declarative +//! Uses `#[webidl_interface]` and `#[webidl_methods]` proc macros for declarative //! class registration. //! //! [`DOMException`]: https://webidl.spec.whatwg.org/#idl-DOMException use core_runtime::{webidl_interface, webidl_methods}; +use js::class::Ref; use js::error::{ExnThrown, ThrowException}; use js::gc::scope::Scope; use js::native::ExceptionStackBehavior; @@ -117,27 +118,39 @@ impl DOMException { /// /// `constructor(optional DOMString message = "", optional DOMString name = "Error")` #[constructor] - fn new(message: Option, name: Option) -> Self { + fn new_js(message: Option, name: Option) -> Self { Self { - message: message.unwrap_or_default(), + // Step 1: Set `this`'s `name` to _name_. name: name.unwrap_or_else(|| "Error".to_string()), + // Step 2: Set `this`'s `message` to _message_. + message: message.unwrap_or_default(), } } + /// Creates a new `DOMException`. + /// + /// In difference to the JS-facing constructor `new_js`, both arguments + /// to `new` are required, with no defaults.` + pub fn new(message: String, name: String) -> Self { + Self { name, message } + } + // ----------------------------------------------------------------------- // Getters // ----------------------------------------------------------------------- /// https://webidl.spec.whatwg.org/#dom-domexception-name #[getter] - fn name(&self) -> String { - self.data().name.clone() + fn name(&self) -> Ref<'_, str> { + // Return `this`'s `name`. + Ref::map(self.data(), |data| data.name.as_str()) } /// https://webidl.spec.whatwg.org/#dom-domexception-message #[getter] - fn message(&self) -> String { - self.data().message.clone() + fn message(&self) -> Ref<'_, str> { + // Return `this`'s `message`. + Ref::map(self.data(), |data| data.message.as_str()) } /// https://webidl.spec.whatwg.org/#dom-domexception-code @@ -157,11 +170,9 @@ impl DOMException { /// Throw a DOMException with the given name and message. /// /// This creates a new DOMException object and sets it as the pending exception. -/// Returns `false` to indicate an exception has been thrown (for use in -/// JSNative return values). pub fn throw_dom_exception(scope: &Scope<'_>, name: &str, message: &str) -> ExnThrown { // Get the DOMException constructor from the global. - let exception = match DOMException::new(scope, Some(message.into()), Some(name.into())) { + let exception = match DOMException::new(scope, message.into(), name.into()) { Ok(exc) => exc, Err(_) => return ExnThrown, }; @@ -257,11 +268,11 @@ mod dom_exception_integration { #[test] fn rust_new() { runtime::register_global_initializer(super::DOMException::add_to_global); - let rt = Runtime::init(&RuntimeConfig::default()); + let rt = Runtime::init(&RuntimeConfig::default()).expect("runtime init"); let scope = rt.default_global(); - let e = super::DOMException::new(&scope, None, None).unwrap(); - assert_eq!(e.name(), "Error"); - assert_eq!(e.message(), ""); + let e = super::DOMException::new(&scope, String::new(), "Error".into()).unwrap(); + assert_eq!(&*e.name(), "Error"); + assert_eq!(&*e.message(), ""); assert_eq!(e.code(), 0); } @@ -429,6 +440,21 @@ mod throw_exception_integration { ) } + /// Capturing the stack for a new `DOMException` is best-effort: a throwing + /// `Error.prototype.stack` accessor leaves the construction intact and no + /// exception pending. + #[test] + fn construction_survives_a_throwing_stack_accessor() { + assert_eq!( + eval( + "Object.defineProperty(Error.prototype, 'stack', { get() { throw 1; }, configurable: true }); + const e = new DOMException('m', 'NotFoundError'); + e.name + ',' + e.message" + ), + "NotFoundError,m" + ); + } + #[test] fn type_error_thrown() { assert_eq!( diff --git a/crates/builtins/web-globals/src/encoding/text_decoder.rs b/crates/builtins/web-globals/src/encoding/text_decoder.rs index 20fa2ca5..12d40100 100644 --- a/crates/builtins/web-globals/src/encoding/text_decoder.rs +++ b/crates/builtins/web-globals/src/encoding/text_decoder.rs @@ -5,26 +5,25 @@ use core_runtime::{webidl_dictionary, webidl_interface, webidl_methods, webidl_union}; use js::error::{ExnThrown, RangeError}; use js::gc::scope::Scope; -use js::{ArrayBuffer, ArrayBufferView}; +use js::{ArrayBuffer, ArrayBufferView, JSString}; /// #[webidl_interface] pub struct TextDecoder { - /// The encoding label, lowercased (e.g. "utf-8", "windows-1252"). - encoding_name: String, - /// The encoding_rs encoding object (always `Some` after construction). + /// + /// (always `Some` after construction). #[no_trace] encoding: Option<&'static encoding_rs::Encoding>, - /// + /// , which also holds the I/O queue + /// and BOM seen state. `None` until the first `decode()` call. + #[no_trace] + decoder: Option, + /// : whether it is "fatal". fatal: bool, /// ignore_bom: bool, /// do_not_flush: bool, - /// The streaming decoder state, stored as an Option so we can replace it - /// when resetting. - #[no_trace] - decoder: Option, } #[webidl_methods] @@ -39,7 +38,8 @@ impl TextDecoder<'_> { let label = label.as_deref().unwrap_or("utf-8"); let options = options.unwrap_or_default(); - // Steps 1-2: Look up the encoding, rejecting failure and "replacement". + // Step 1: Let _encoding_ be the result of `getting an encoding` from _label_. + // Step 2: If _encoding_ is failure or `replacement`, then `throw` a ``RangeError``. let encoding = encoding_rs::Encoding::for_label_no_replacement(label.as_bytes()) .ok_or_else(|| { RangeError(format!( @@ -48,12 +48,13 @@ impl TextDecoder<'_> { })?; let mut data = self.data_mut(); - data.encoding_name = encoding.name().to_ascii_lowercase(); + // Step 3: Set `this`'s `encoding` to _encoding_. data.encoding = Some(encoding); + // Step 4: If _options_["``fatal``"] is true, then set `this`'s `error mode` to + // "`fatal`". data.fatal = options.fatal; + // Step 5: Set `this`'s `ignore BOM` to _options_["``ignoreBOM``"]. data.ignore_bom = options.ignore_bom; - data.do_not_flush = false; - data.decoder = Some(new_decoder(encoding, options.ignore_bom)); Ok(()) } @@ -61,50 +62,51 @@ impl TextDecoder<'_> { /// #[getter] fn encoding(&self) -> String { - // Step 1: Return this's encoding's name, ASCII lowercased. - self.data().encoding_name.clone() + // Step 1: Return `this`'s `encoding`'s `name`, `ASCII lowercased`. + let encoding = self.data().encoding.expect("set by the constructor"); + encoding.name().to_ascii_lowercase() } /// #[getter(name = "fatal")] fn get_fatal(&self) -> bool { - // Step 1: Return true if this's error mode is "fatal"; otherwise false. + // Step 1: Return true if `this`'s `error mode` is "`fatal`"; otherwise false. self.data().fatal } /// #[getter(name = "ignoreBOM")] fn ignore_bom(&self) -> bool { - // Step 1: Return this's ignore BOM. + // Step 1: Return `this`'s `ignore BOM`. self.data().ignore_bom } /// #[method] - fn decode( + fn decode<'r>( &self, - scope: &Scope<'_>, + scope: &'r Scope<'_>, input: Option>, options: Option, - ) -> Result { - let stream = options.is_some_and(|o| o.stream); - let last = !stream; - + ) -> Result, ExnThrown> { let mut data = self.data_mut(); - let encoding = data - .encoding - .expect("encoding must be set after construction"); - let fatal = data.fatal; - let ignore_bom = data.ignore_bom; - - // Step 1: If do-not-flush is false, replace the decoder so the new - // decode starts fresh; otherwise carry over state from the prior call. - if !data.do_not_flush { - data.decoder = Some(new_decoder(encoding, ignore_bom)); + let encoding = data.encoding.expect("set by the constructor"); + // Step 1: If `this`'s `do not flush` is false, then set `this`'s `decoder` to a new + // instance of `this`'s `encoding`'s `decoder`, `this`'s `I/O queue` to the `I/O + // queue` of bytes « `end-of-queue` », and `this`'s `BOM seen` to false. + // (The `encoding_rs` decoder holds the I/O queue and BOM seen state.) + let fresh = !data.do_not_flush; + if fresh { + data.decoder = Some(new_decoder(encoding, data.ignore_bom)); } - data.do_not_flush = stream; - let decoder = data.decoder.as_mut().expect("decoder must be initialized"); + // Step 2: Set `this`'s `do not flush` to _options_["``stream``"]. + data.do_not_flush = options.is_some_and(|o| o.stream); + let last = !data.do_not_flush; + let fatal = data.fatal; + let decoder = data.decoder.as_mut().expect("set by step 1"); + // Step 3: If _input_ is given, then `push` a `copy of` _input_ to `this`'s `I/O queue`. + // (We decode directly from _input_'s bytes. Nothing retains them after this call.) // SAFETY: `input` is rooted via the Uint8Array handle and the slice // is only borrowed for the duration of the decode call below, which // is pure CPU work in `encoding_rs` and cannot trigger GC or detach @@ -115,17 +117,51 @@ impl TextDecoder<'_> { None => &[], }; - if fatal { - let max_len = decoder - .max_utf8_buffer_length_without_replacement(bytes.len()) - .unwrap_or(bytes.len().saturating_mul(4)); - let mut output = String::with_capacity(max_len); + // windows-1252 decodes every byte outside 0x80..=0x9F to the code point of the same + // value, and its decoder keeps no state between calls, so such input is its own + // Latin-1 output. + if encoding == encoding_rs::WINDOWS_1252 && !bytes.iter().any(|b| (0x80..=0x9F).contains(b)) + { + return JSString::from_latin1(scope, bytes); + } + // A fresh decoder that decodes its whole input at once turns ASCII into the same + // characters in every ASCII-compatible encoding. + if fresh + && last + && encoding.is_ascii_compatible() + && encoding_rs::Encoding::ascii_valid_up_to(bytes) == bytes.len() + { + return JSString::from_latin1(scope, bytes); + } - let (result, _read) = - decoder.decode_to_string_without_replacement(bytes, &mut output, last); + // Step 4: Let _output_ be the `I/O queue` of scalar values « `end-of-queue` ». + // Step 5: While true: + // Step 5.1: Let _item_ be the result of `reading` from `this`'s `I/O queue`. + // Step 5.2: If _item_ is `end-of-queue` and `this`'s `do not flush` is true, then return + // the result of running `serialize I/O queue` with `this` and _output_. + // Step 5.3: Otherwise: + // Step 5.3.1: Let _result_ be the result of `processing an item` with _item_, `this`'s + // `decoder`, `this`'s `I/O queue`, _output_, and `this`'s `error mode`. + // Step 5.3.2: If _result_ is `finished`, then return the result of running `serialize + // I/O queue` with `this` and _output_. + // Step 5.3.3: Otherwise, if _result_ is `error`, `throw` a ``TypeError``. + // + // (One `encoding_rs` call processes every item. `last` is true when `end-of-queue` is + // processed rather than returned at step 5.2. The decoder's BOM removal performs + // `serialize I/O queue`'s BOM handling. We decode to UTF-16, which SpiderMonkey copies + // as is, or narrows to Latin-1 if every code unit fits.) + let max_len = decoder + .max_utf16_buffer_length(bytes.len()) + .unwrap_or(bytes.len().saturating_add(1)); + let mut output = vec![0u16; max_len]; + if fatal { + let (result, _read, written) = + decoder.decode_to_utf16_without_replacement(bytes, &mut output, last); match result { - encoding_rs::DecoderResult::InputEmpty => Ok(output), + encoding_rs::DecoderResult::InputEmpty => { + JSString::from_utf16(scope, &output[..written]) + } encoding_rs::DecoderResult::Malformed(_, _) => Err(js::error::throw_type_error( scope, c"The encoded data was not valid.", @@ -136,14 +172,10 @@ impl TextDecoder<'_> { } } else { // Malformed sequences become U+FFFD. - let max_len = decoder - .max_utf8_buffer_length(bytes.len()) - .unwrap_or(bytes.len().saturating_mul(4).saturating_add(3)); - let mut output = String::with_capacity(max_len); - - let (_result, _read, _had_errors) = decoder.decode_to_string(bytes, &mut output, last); + let (_result, _read, written, _had_errors) = + decoder.decode_to_utf16(bytes, &mut output, last); - Ok(output) + JSString::from_utf16(scope, &output[..written]) } } } diff --git a/crates/builtins/web-globals/src/encoding/text_encoder.rs b/crates/builtins/web-globals/src/encoding/text_encoder.rs index 7db7cd14..271e4fb0 100644 --- a/crates/builtins/web-globals/src/encoding/text_encoder.rs +++ b/crates/builtins/web-globals/src/encoding/text_encoder.rs @@ -6,7 +6,8 @@ use core_runtime::{webidl_dictionary, webidl_interface, webidl_methods}; use js::conversion::{ConversionError, ToJSVal}; use js::error::ExnThrown; use js::gc::scope::Scope; -use js::Uint8Array; +use js::string::StrChars; +use js::{JSString, Uint8Array}; /// /// @@ -48,33 +49,52 @@ impl TextEncoder<'_> { #[method] fn encode_into( &self, - source: String, + scope: &Scope<'_>, + source: JSString<'_>, destination: Uint8Array, ) -> Result { - // SAFETY: `destination` is rooted; the slice is only borrowed for the - // `copy_from_slice` below, which is pure CPU work and cannot trigger GC - // or detach the underlying buffer. + // Step 1: Let _read_ be 0. + // Step 2: Let _written_ be 0. + // Step 3: Let _encoder_ be an instance of the `UTF-8 encoder`. + // Step 4: Let _unused_ be the `I/O queue` of scalar values « `end-of-queue` ». + // Step 5: `Convert` _source_ to an `I/O queue` of scalar values. + // Step 6: While true: + // Step 6.1: Let _item_ be the result of `reading` from _source_. + // Step 6.2: Let _result_ be the result of running _encoder_'s `handler` on _unused_ and + // _item_. + // Step 6.3: If _result_ is `finished`, then `break`. + // Step 6.4: Otherwise: + // Step 6.4.1: If _destination_'s `byte length` − _written_ is greater than or equal to + // the number of bytes in _result_: + // Step 6.4.1.1: If _item_ is greater than U+FFFF, then increment _read_ by 2. + // Step 6.4.1.2: Otherwise, increment _read_ by 1. + // Step 6.4.1.3: `Write` the bytes in _result_ into _destination_, with _startingOffset_ + // set to _written_. + // Step 6.4.1.4: Increment _written_ by the number of bytes in _result_. + // Step 6.4.2: Otherwise, `break`. + // + // (`encoding_rs`' partial converters run this loop over _source_'s characters: they + // encode whole scalar values while they fit, replace unpaired surrogates with U+FFFD as + // step 5's conversion does, and return _read_ in code units and _written_ in bytes.) + // + // SAFETY: the destination slice is only borrowed for the conversion, which cannot run JS + // or trigger a GC. let dest_buf = unsafe { destination.as_mut_slice() }; - let dest_len = dest_buf.len(); - - // The source has already been converted to UTF-8, so encoding boils - // down to finding the longest prefix that fits in the destination on - // a char boundary, then a single slice copy. `read` counts UTF-16 code - // units (1 for BMP scalars, 2 for supplementary). - let mut written = 0usize; - let mut read = 0u64; - for (idx, ch) in source.char_indices() { - let next = idx + ch.len_utf8(); - if next > dest_len { - break; - } - written = next; - read += ch.len_utf16() as u64; - } - dest_buf[..written].copy_from_slice(&source.as_bytes()[..written]); + // SAFETY: same as above. + let (read, written) = unsafe { + source.with_chars(scope, |chars| match chars { + StrChars::Latin1(chars) => { + encoding_rs::mem::convert_latin1_to_utf8_partial(chars, dest_buf) + } + StrChars::TwoByte(chars) => { + encoding_rs::mem::convert_utf16_to_utf8_partial(chars, dest_buf) + } + }) + }?; + // Step 7: Return «[ "``read``" → _read_, "``written``" → _written_ ]». Ok(TextEncoderEncodeIntoResult { - read, + read: read as u64, written: written as u64, }) } @@ -91,8 +111,15 @@ impl<'s> ToJSVal<'s> for TextEncoderEncodeIntoResult { #[inline] fn to_jsval_raw(&self, scope: &'s Scope<'s>) -> Result { let obj = js::Object::new_plain(scope)?; - obj.set_property(scope, c"read", self.read)?; - obj.set_property(scope, c"written", self.written)?; + let attrs = js::class_spec::JSPROP_ENUMERATE as std::ffi::c_uint; + let read = js::class::get_or_init_property_id(scope, c"read") + .map_err(|_| ConversionError::ExnPending)?; + obj.define_value_by_id(scope, read, self.read, attrs) + .map_err(|_| ConversionError::ExnPending)?; + let written = js::class::get_or_init_property_id(scope, c"written") + .map_err(|_| ConversionError::ExnPending)?; + obj.define_value_by_id(scope, written, self.written, attrs) + .map_err(|_| ConversionError::ExnPending)?; Ok(obj.as_value()) } } diff --git a/crates/builtins/web-globals/src/events/algorithms.rs b/crates/builtins/web-globals/src/events/algorithms.rs index 76e6374c..f92a8ae3 100644 --- a/crates/builtins/web-globals/src/events/algorithms.rs +++ b/crates/builtins/web-globals/src/events/algorithms.rs @@ -13,6 +13,7 @@ use js::gc::handle::Heap; use js::gc::scope::Scope; use js::prelude::HandleValue; use js::{Function, Object}; +use smallvec::SmallVec; use super::event::Event; use super::event_target::{EventListener, EventTarget, EventTargetImpl}; @@ -51,29 +52,59 @@ fn next_listener_id() -> u64 { /// (Note: while the spec presents this as a WebIDL spec concept, it's entirely /// specific to event handling, so this file is indeed the right place for it.) /// +/// `options` is converted as the WebIDL union `(EventListenerOptions or boolean)`: +/// `undefined`, `null` and objects become the dictionary, and any other value is +/// converted with `ToBoolean`. +/// /// To flatten options, run these steps: -pub(crate) fn flatten_options(scope: &Scope<'_>, options: Option>) -> bool { +pub(crate) fn flatten_options( + scope: &Scope<'_>, + options: Option>, +) -> Result { + match options_dictionary(scope, options) { + // Step 1: If _options_ is a boolean, then return _options_. + OptionsArgument::Boolean(capture) => Ok(capture), + // Step 2: Return _options_["``capture``"]. + OptionsArgument::Dictionary(obj) => read_bool_option(scope, obj.as_ref(), c"capture"), + } +} + +/// An `(EventListenerOptions or boolean)` argument, converted as the WebIDL union. +enum OptionsArgument<'s> { + Boolean(bool), + /// The dictionary, read from the object, or `None` for `undefined` and `null`. + Dictionary(Option>), +} + +fn options_dictionary<'s>( + scope: &'s Scope<'_>, + options: Option>, +) -> OptionsArgument<'s> { match options { - None => false, - Some(val) => { - // Step 1: If _options_ is a boolean, then return _options_. - if val.is_boolean() { - val.to_boolean() - } else if val.is_object() { - // Step 2: Return _options_["``capture``"]. - let obj = js::Object::from_value(scope, *val).expect("options should be an object"); - if let Ok(capture_val) = obj.get_property(scope, c"capture") { - bool::from_jsval(scope, capture_val, ()).unwrap() - } else { - false - } - } else { - false - } - } + None => OptionsArgument::Dictionary(None), + Some(val) if val.is_null_or_undefined() => OptionsArgument::Dictionary(None), + Some(val) => match Object::from_value(scope, *val) { + Ok(obj) => OptionsArgument::Dictionary(Some(obj)), + Err(_) => OptionsArgument::Boolean( + bool::from_jsval(scope, val, ()).expect("ToBoolean cannot fail"), + ), + }, } } +/// Read a `boolean` member of an options dictionary, defaulting to false. +fn read_bool_option( + scope: &Scope<'_>, + dictionary: Option<&Object<'_>>, + name: &std::ffi::CStr, +) -> Result { + let Some(dictionary) = dictionary else { + return Ok(false); + }; + let value = dictionary.get_property(scope, name)?; + Ok(bool::from_jsval(scope, value, ()).expect("ToBoolean cannot fail")) +} + /// /// /// To flatten more options, run these steps. @@ -85,70 +116,51 @@ pub(crate) fn flatten_more_options<'s>( scope: &'s Scope<'_>, options: Option>, ) -> Result, ExnThrown> { - match options { - None => Ok(ListenerOptions { - capture: false, - passive: None, - once: false, - signal: None, - }), - Some(val) => { - if val.is_boolean() { - // Step 1: Let _capture_ be the result of `flattening` _options_. - Ok(ListenerOptions { - capture: val.to_boolean(), - passive: None, - once: false, - signal: None, - }) - } else if val.is_object() { - let obj = Object::from_value(scope, *val).unwrap(); - - // Step 1: Let _capture_ be the result of `flattening` _options_. - let capture = obj - .get_property(scope, c"capture") - .map(|v| bool::from_jsval(scope, v, ()).unwrap()) - .unwrap_or(false); - - // Step 2: Let _once_ be false. - // Step 4: If _options_ is a `dictionary`: Set _once_ to _options_["``once``"]. - let once = obj - .get_property(scope, c"once") - .map(|v| bool::from_jsval(scope, v, ()).unwrap()) - .unwrap_or(false); - - // Step 3: Let _passive_ and _signal_ be null. - // Step 4 (cont.): If _options_["``passive``"] `exists`, then set _passive_ to - // _options_["``passive``"]. - let passive = obj.get_property(scope, c"passive").ok().and_then(|v| { - if v.is_undefined() { - None - } else { - Some(bool::from_jsval(scope, v, ()).unwrap()) - } - }); - - // Step 4 (cont.): If _options_["``signal``"] `exists`, then set _signal_ to - // _options_["``signal``"]. - let signal = read_signal_option(scope, &obj)?; - - // Step 5: Return _capture_, _passive_, _once_, and _signal_. - Ok(ListenerOptions { - capture, - passive, - once, - signal, - }) - } else { - Ok(ListenerOptions { - capture: false, - passive: None, - once: false, - signal: None, - }) - } + let dictionary = match options_dictionary(scope, options) { + // Step 1: Let _capture_ be the result of `flattening` _options_. + OptionsArgument::Boolean(capture) => { + // Step 2: Let _once_ be false. + // Step 3: Let _passive_ and _signal_ be null. + // Step 5: Return _capture_, _passive_, _once_, and _signal_. + return Ok(ListenerOptions { + capture, + passive: None, + once: false, + signal: None, + }); } - } + OptionsArgument::Dictionary(dictionary) => dictionary, + }; + // Step 1: Let _capture_ be the result of `flattening` _options_. + let capture = read_bool_option(scope, dictionary.as_ref(), c"capture")?; + // Step 2: Let _once_ be false. + // Step 3: Let _passive_ and _signal_ be null. + // Step 4: If _options_ is a `dictionary`, then: + // Step 4.1: Set _once_ to _options_["``once``"]. + let once = read_bool_option(scope, dictionary.as_ref(), c"once")?; + // Step 4.2: If _options_["``passive``"] `exists`, then set _passive_ to + // _options_["``passive``"]. + let passive = match &dictionary { + Some(obj) => { + let v = obj.get_property(scope, c"passive")?; + (!v.is_undefined()) + .then(|| bool::from_jsval(scope, v, ()).expect("ToBoolean cannot fail")) + } + None => None, + }; + // Step 4.3: If _options_["``signal``"] `exists`, then set _signal_ to + // _options_["``signal``"]. + let signal = match &dictionary { + Some(obj) => read_signal_option(scope, obj)?, + None => None, + }; + // Step 5: Return _capture_, _passive_, _once_, and _signal_. + Ok(ListenerOptions { + capture, + passive, + once, + signal, + }) } /// Read the `signal` member of an `addEventListener` options dictionary. @@ -183,7 +195,7 @@ fn read_signal_option<'s>( pub(crate) fn add_an_event_listener( target: &EventTarget<'_>, event_type: String, - callback: Function<'_>, + callback: Object<'_>, capture: bool, passive: Option, once: bool, @@ -213,11 +225,7 @@ pub(crate) fn add_an_event_listener( .event_listener_list .iter() .find(|l| { - !l.removed - && l.event_type == event_type - && l.capture == capture - // TODO: implement Eq for Heap and Stack and remove the use of raw pointers. - && l.callback.eq_stack(&callback) + l.event_type == event_type && l.capture == capture && l.callback.eq_stack(&callback) }) .map(|l| l.id); let listener_id = match existing_id { @@ -231,7 +239,6 @@ pub(crate) fn add_an_event_listener( capture, passive, once, - removed: false, }); id } @@ -251,7 +258,7 @@ pub(crate) fn add_an_event_listener( pub(crate) fn remove_an_event_listener( target: &mut EventTargetImpl, event_type: &str, - callback: &Function<'_>, + callback: &Object<'_>, capture: bool, ) { // Step 1: (ServiceWorkerGlobalScope warning — not applicable.) @@ -259,6 +266,8 @@ pub(crate) fn remove_an_event_listener( // Step 2: Set _listener_'s `removed` to true and `remove` _listener_ from _eventTarget_'s // `event listener list`. Identity is compared via the stored Heap's // live pointer (see `add_an_event_listener`). + // Listeners have no `removed` flag: dispatch re-locates each listener in the live + // list by id, so it skips any listener that is no longer in the list. target.event_listener_list.retain(|l| { !(l.event_type == event_type && l.capture == capture && l.callback.eq_stack(callback)) }); @@ -360,7 +369,7 @@ fn invoke_listeners( return; } - // Snapshot the listener list to avoid issues with listeners added or + // Snapshot the listener list for a specific type to avoid issues with listeners added or // removed during dispatch. We capture only listener `id`s and metadata; the // live list is re-consulted by `id` each iteration, so callbacks are always // fetched fresh and rooted (no stored GC pointers, no rooting gap). @@ -368,43 +377,41 @@ fn invoke_listeners( id: u64, passive: Option, once: bool, - event_type: String, } - let snapshots: Vec = target - .data() - .event_listener_list - .iter() - .filter(|l| !l.removed) - .map(|l| ListenerSnapshot { - id: l.id, - passive: l.passive, - once: l.once, - event_type: l.event_type.clone(), - }) - .collect(); - - let event_type = event.data().event_type.clone(); + let snapshots: SmallVec<[ListenerSnapshot; 8]> = { + let event_data = event.data(); + target + .data() + .event_listener_list + .iter() + .filter(|l| l.event_type == event_data.event_type) + .map(|l| ListenerSnapshot { + id: l.id, + passive: l.passive, + once: l.once, + }) + .collect() + }; for snap in &snapshots { - if snap.event_type != event_type { - continue; - } - // Re-locate the listener in the live list; it may have been removed by // a previous callback. let Some(pos) = target .data() .event_listener_list .iter() - .position(|l| l.id == snap.id && !l.removed) + .position(|l| l.id == snap.id) else { continue; }; + // Root the callback before a `once` removal drops the listener's `Heap`. + let callback: Object<'_> = target.data().event_listener_list[pos].callback.get(scope); + // If listener's `once` is true, remove it. if snap.once { - target.data_mut().event_listener_list[pos].removed = true; + target.data_mut().event_listener_list.remove(pos); } // Set in-passive-listener flag if passive. @@ -412,21 +419,11 @@ fn invoke_listeners( event.set_in_passive_listener(true); } - // Call the listener. We root the callback from the Heap at call time. - // The `target.data()` borrow is released before the call so a listener - // that mutates the list (e.g. by aborting a signal that removes - // listeners) does not hit a `RefCell` double borrow. - { - let callback: Function<'_> = { - let listener = &target.data().event_listener_list[pos]; - listener.callback.get(scope) - }; - - if Function::call(scope, target, callback, &[event]).is_err() { - // Per the spec, exceptions from event listeners are "reported" - // but do not stop event dispatch. - js::exception::report_and_clear(scope, "event listener"); - } + // Call the listener. + if call_event_listener(scope, target, callback, event).is_err() { + // Per the spec, exceptions from event listeners are "reported" + // but do not stop event dispatch. + js::exception::report_and_clear(scope, "event listener"); } script_stack_state.clean_up_after_running_script(scope); @@ -439,6 +436,32 @@ fn invoke_listeners( } } +/// , for the +/// `handleEvent` operation of the `EventListener` callback interface: a callable +/// listener is called with the event's current target as `this`, and any other +/// object has its `handleEvent` method called with the listener as `this`. +fn call_event_listener( + scope: &Scope<'_>, + target: &EventTarget<'_>, + listener: Object<'_>, + event: &Event<'_>, +) -> Result<(), ExnThrown> { + if listener.is_callable() { + Function::call(scope, target, listener, &[event])?; + return Ok(()); + } + let handle_event_id = js::class::get_or_init_property_id(scope, c"handleEvent")?; + let handle_event = listener.get_property_by_id(scope, handle_event_id)?; + if !Object::from_value(scope, handle_event).is_ok_and(|f| f.is_callable()) { + return Err(throw_type_error( + scope, + c"event listener's handleEvent is not callable", + )); + } + Function::call(scope, listener, handle_event, &[event])?; + Ok(()) +} + /// /// /// To fire an event named e at target, optionally using an eventConstructor, diff --git a/crates/builtins/web-globals/src/events/event.rs b/crates/builtins/web-globals/src/events/event.rs index 788548b2..7345a222 100644 --- a/crates/builtins/web-globals/src/events/event.rs +++ b/crates/builtins/web-globals/src/events/event.rs @@ -6,6 +6,7 @@ use bitflags::bitflags; use core_runtime::{webidl_dictionary, webidl_interface, webidl_methods}; +use js::class::Ref; use js::gc::handle::{Heap, OptionHeapExt}; use js::gc::scope::Scope; @@ -65,7 +66,7 @@ impl Event { Self { event_type, target: None, - time_stamp: performance::now(), + time_stamp: performance::coarse_now(), flags, } } @@ -79,8 +80,8 @@ impl Event { /// #[getter(name = "type")] - pub fn get_type(&self) -> String { - self.data().event_type.clone() + pub fn get_type(&self) -> Ref<'_, str> { + Ref::map(self.data(), |data| data.event_type.as_str()) } /// diff --git a/crates/builtins/web-globals/src/events/event_target.rs b/crates/builtins/web-globals/src/events/event_target.rs index 492c914d..f12197a7 100644 --- a/crates/builtins/web-globals/src/events/event_target.rs +++ b/crates/builtins/web-globals/src/events/event_target.rs @@ -8,11 +8,10 @@ use core_runtime::webidl_interface; use core_runtime::webidl_methods; -use js::error::ExnThrown; +use js::error::{throw_type_error, ExnThrown}; use js::gc::handle::Heap; use js::gc::scope::Scope; use js::prelude::HandleValue; -use js::Function; use js::Object; use super::algorithms; @@ -24,27 +23,39 @@ use crate::events::event::EventFlags; /// /// #[js::must_root] +#[derive(core_runtime::Traceable)] pub(crate) struct EventListener { /// Identity used to re-locate this listener in the live list during /// dispatch (the snapshot stores ids, not pointers — see `algorithms.rs`). + #[no_trace] pub(crate) id: u64, + #[no_trace] pub(crate) event_type: String, - /// The GC-traced callback function. Listener identity is compared by - /// reading this Heap's live pointer ([`Heap::as_ptr`]), so it stays - /// correct across a compacting GC. - pub(crate) callback: Heap, + /// The `EventListener` callback object: a callable, or an object with a + /// `handleEvent` method. Listener identity is compared by reading this + /// Heap's live pointer ([`Heap::as_ptr`]), so it stays correct across a + /// compacting GC. + pub(crate) callback: Heap, + #[no_trace] pub(crate) capture: bool, + #[no_trace] pub(crate) passive: Option, + #[no_trace] pub(crate) once: bool, - pub(crate) removed: bool, } -// Safety: the only GC pointer is the `callback` Heap; all other fields are -// primitive. -unsafe impl js::heap::Trace for EventListener { - unsafe fn trace(&self, trc: *mut js::native::JSTracer) { - self.callback.trace(trc); +/// Convert an `EventListener?` argument: `None` for `null` and `undefined`, the +/// object for any object, and a TypeError for anything else. +fn event_listener_argument<'s>( + scope: &'s Scope<'_>, + callback: HandleValue<'_>, +) -> Result>, ExnThrown> { + if callback.is_null_or_undefined() { + return Ok(None); } + Object::from_value(scope, *callback) + .map(Some) + .map_err(|_| throw_type_error(scope, c"event listener must be an object")) } /// @@ -79,13 +90,7 @@ impl EventTarget { // must happen even when `callback` is null. let options = algorithms::flatten_more_options(scope, options)?; - if callback.is_null() || callback.is_undefined() { - return Ok(()); - } - let Ok(obj) = Object::from_value(scope, *callback) else { - return Ok(()); - }; - let Ok(callback) = obj.cast::>() else { + let Some(callback) = event_listener_argument(scope, callback)? else { return Ok(()); }; @@ -112,24 +117,19 @@ impl EventTarget { event_type: String, callback: HandleValue<'_>, options: Option>, - ) { + ) -> Result<(), ExnThrown> { // Step 1: Let _capture_ be the result of `flattening` _options_. - let capture = algorithms::flatten_options(scope, options); + let capture = algorithms::flatten_options(scope, options)?; - if callback.is_null() || callback.is_undefined() { - return; - } - let Ok(obj) = Object::from_value(scope, *callback) else { - return; - }; - let Ok(callback) = obj.cast::>() else { - return; + let Some(callback) = event_listener_argument(scope, callback)? else { + return Ok(()); }; // Step 2: If `this`'s `event listener list` `contains` an `event listener` whose `type` // is _type_, `callback` is _callback_, and `capture` is _capture_, then `remove an // event listener` with `this` and that `event listener`. algorithms::remove_an_event_listener(&mut self.data_mut(), &event_type, &callback, capture); + Ok(()) } /// @@ -174,12 +174,44 @@ impl EventTarget { let _ = algorithms::dispatch(scope, &event, self, script_stack_state); } - /// Whether any listener for `event_type` would be invoked by a dispatch. Listeners already - /// removed are still in the list until dispatch prunes them, and do not count. + /// Whether any listener for `event_type` would be invoked by a dispatch. pub fn has_listener_for(&self, event_type: &str) -> bool { self.data() .event_listener_list .iter() - .any(|listener| !listener.removed && listener.event_type == event_type) + .any(|listener| listener.event_type == event_type) + } +} + +// Nothing platform-specific in these tests, so skip them on wasm32. +#[cfg(all(test, not(target_arch = "wasm32")))] +mod tests { + use super::EventTarget; + use core_runtime::config::RuntimeConfig; + use core_runtime::runtime::{self, Runtime}; + use js::conversion::FromJSVal; + + /// A `once` listener leaves the event listener list when it runs. JS cannot observe the + /// list's length, so this reads it directly. + #[test] + fn dispatch_removes_once_listeners_from_the_list() { + runtime::register_global_initializer(crate::add_to_global); + let rt = Runtime::init(&RuntimeConfig::default()).expect("runtime init"); + let scope = rt.default_global(); + let value = js::compile::evaluate_with_filename( + &scope, + "const t = new EventTarget(); + t.addEventListener('x', () => {}); + for (let i = 0; i < 3; i++) { + t.addEventListener('x', () => {}, { once: true }); + t.dispatchEvent(new Event('x')); + } + t", + "test.js", + 1, + ) + .expect("script should evaluate"); + let target = EventTarget::from_jsval(&scope, value, ()).expect("an EventTarget"); + assert_eq!(target.data().event_listener_list.len(), 1); } } diff --git a/crates/builtins/web-globals/src/navigator.rs b/crates/builtins/web-globals/src/navigator.rs index ee285126..07eb5a58 100644 --- a/crates/builtins/web-globals/src/navigator.rs +++ b/crates/builtins/web-globals/src/navigator.rs @@ -15,7 +15,7 @@ impl Navigator { /// #[getter] fn user_agent(&self) -> String { - String::from("StarlingMonkey)") + String::from("StarlingMonkey") } } diff --git a/crates/builtins/web-globals/src/performance.rs b/crates/builtins/web-globals/src/performance.rs index 873fdcc5..1bd9e5e4 100644 --- a/crates/builtins/web-globals/src/performance.rs +++ b/crates/builtins/web-globals/src/performance.rs @@ -89,6 +89,13 @@ pub fn now() -> f64 { elapsed_ms(&time_origin()) } +/// [`now`] from [`platform::clock::coarse_monotonic_ns`], for timestamps that don't need the +/// precise clock, such as `Event.timeStamp`. +pub fn coarse_now() -> f64 { + let origin = time_origin(); + platform::clock::coarse_monotonic_ns().saturating_sub(origin.monotonic_ns) as f64 / 1_000_000. +} + #[webidl_methods] impl Performance { /// diff --git a/crates/builtins/web-globals/src/signals/abort_signal.rs b/crates/builtins/web-globals/src/signals/abort_signal.rs index 8c7e8fb9..5b6fb6c9 100644 --- a/crates/builtins/web-globals/src/signals/abort_signal.rs +++ b/crates/builtins/web-globals/src/signals/abort_signal.rs @@ -6,14 +6,16 @@ //! should be aborted. It extends EventTarget so it can fire "abort" events. use core_runtime::{webidl_interface, webidl_methods}; +use js::conversion::FromJSVal; use js::error::ExnThrown; use js::gc::handle::{Heap, OptionHeapExt}; use js::gc::scope::Scope; -use js::native::{CallArgs, ExceptionStackBehavior, Value}; +use js::native::{ExceptionStackBehavior, Value}; use js::prelude::HandleValue; -use js::{value, Function, Object}; +use js::Callable; use super::algorithms; +use crate::dom_exception::DOMException; use crate::events::algorithms as event_algorithms; use crate::events::event_target::{EventTarget, EventTargetImpl}; @@ -78,7 +80,7 @@ pub struct AbortSignal { /// The `onabort` event handler attribute. /// /// - pub(crate) onabort_handler: Option>, + pub(crate) onabort_handler: Option>, } #[webidl_methods] @@ -115,8 +117,11 @@ impl AbortSignal { // Step 1: Throw this's abort reason, if this is aborted. if !self.data().abort_reason.is_undefined() { let reason = self.data().abort_reason.get(scope); - js::exception::set_pending(scope, reason, ExceptionStackBehavior::DoNotCapture); - return Err(ExnThrown); + return Err(js::exception::set_pending( + scope, + reason, + ExceptionStackBehavior::DoNotCapture, + )); } Ok(()) } @@ -125,17 +130,8 @@ impl AbortSignal { /// /// IDL event handler attribute: `attribute EventHandler onabort;` #[getter] - fn onabort(&self, scope: &Scope<'_>, args: &CallArgs) -> Result<(), ExnThrown> { - match &self.data().onabort_handler { - Some(heap) => { - let func = heap.get(scope); - args.rval().set(func.as_value()); - } - None => { - args.rval().set(value::null()); - } - } - Ok(()) + fn onabort<'r>(&self, scope: &'r Scope<'_>) -> Option> { + self.data().onabort_handler.get(scope) } /// @@ -154,31 +150,21 @@ impl AbortSignal { } // If the new value is a function, store it and add as listener. - if val.is_null() || val.is_undefined() { - self.data_mut().onabort_handler = None; - } else if let Ok(obj) = Object::from_value(scope, *val) { - if let Ok(func) = obj.cast::>() { - // View this signal as the EventTarget it extends to register the - // listener (the onabort handler never carries its own signal). - let target = Object::from_value(scope, self.as_value()) - .expect("AbortSignal is an object") - .cast::>() - .expect("AbortSignal is an EventTarget"); + match Callable::from_jsval(scope, val, ()) { + Ok(func) => { + // The onabort handler is registered without an abort signal. event_algorithms::add_an_event_listener( - &target, + self, "abort".to_string(), - func, + *func, false, None, false, None, ); - self.data_mut().onabort_handler = Some(Heap::from(func)); - } else { - self.data_mut().onabort_handler = None; + self.data_mut().onabort_handler.set(func); } - } else { - self.data_mut().onabort_handler = None; + Err(_) => self.data_mut().onabort_handler = None, } } @@ -196,8 +182,12 @@ impl AbortSignal { match reason { Some(r) => signal.data_mut().abort_reason.set(r.get()), None => { - let err_val = algorithms::create_abort_error(scope)?; - signal.data_mut().abort_reason.set(err_val.get()); + let err_val = DOMException::new( + scope, + "signal is aborted without reason".into(), + "AbortError".into(), + )?; + signal.data_mut().abort_reason.set(err_val.as_value()); } } @@ -234,47 +224,10 @@ impl AbortSignal { // TODO: use a WebIDL sequence for this, after making that usable outside of unions. signals: HandleValue<'_>, ) -> Result, ExnThrown> { - let signals = read_signal_sequence(scope, signals)?; + let signals = Vec::::from_jsval_throwing(scope, signals, ())?; // Step 1: Return the result of creating a dependent abort signal from signals using // AbortSignal and the current realm. algorithms::create_dependent_abort_signal(scope, &signals) } } - -/// Convert a `sequence` argument into a `Vec` of signals. -/// -/// WebIDL sequences are defined in terms of the iterator protocol; this only -/// handles array-like inputs (index access via the `length` property), which -/// covers every enabled caller. A non-`AbortSignal` element throws a -/// `TypeError`. -fn read_signal_sequence<'r>( - scope: &'r Scope<'_>, - value: HandleValue<'_>, -) -> Result>, ExnThrown> { - let obj = Object::from_value(scope, *value) - .map_err(|_| js::error::throw_type_error(scope, c"AbortSignal.any expects a sequence"))?; - - let len_val = obj.get_property(scope, c"length")?; - let length = if len_val.is_int32() { - len_val.to_int32().max(0) as u32 - } else if len_val.is_double() { - len_val.to_double().max(0.0) as u32 - } else { - 0 - }; - - let mut signals = Vec::new(); - for i in 0..length { - let elem = obj.get_element(scope, i)?; - let elem_obj = Object::from_value(scope, *elem).map_err(|_| { - js::error::throw_type_error(scope, c"AbortSignal.any: element is not an AbortSignal") - })?; - let signal = elem_obj.cast::>().map_err(|_| { - js::error::throw_type_error(scope, c"AbortSignal.any: element is not an AbortSignal") - })?; - signals.push(signal); - } - - Ok(signals) -} diff --git a/crates/builtins/web-globals/src/signals/algorithms.rs b/crates/builtins/web-globals/src/signals/algorithms.rs index 9374f487..0168aa74 100644 --- a/crates/builtins/web-globals/src/signals/algorithms.rs +++ b/crates/builtins/web-globals/src/signals/algorithms.rs @@ -21,26 +21,6 @@ use crate::events; use crate::events::algorithms::ScriptStackState; use crate::events::event_target::EventTarget; -/// Create a new "AbortError" DOMException as a JS value without throwing it. -pub(crate) fn create_abort_error<'s>(scope: &'s Scope<'_>) -> Result, ExnThrown> { - let exception = DOMException::new( - scope, - Some("signal is aborted without reason".into()), - Some("AbortError".into()), - )?; - exception.to_jsval_throwing(scope) -} - -/// Create a new "TimeoutError" DOMException as a JS value without throwing it. -fn create_timeout_error<'s>(scope: &'s Scope<'_>) -> Result, ExnThrown> { - let exception = DOMException::new( - scope, - Some("signal timed out".into()), - Some("TimeoutError".into()), - )?; - exception.to_jsval_throwing(scope) -} - /// /// /// To create a dependent abort signal, given a list `signals`, returns a new @@ -117,10 +97,10 @@ pub fn create_dependent_abort_signal<'r>( /// /// /// To signal abort, given an AbortSignal object signal and an optional reason. -pub(crate) fn signal_abort( - scope: &Scope<'_>, +pub(crate) fn signal_abort<'s>( + scope: &'s Scope<'_>, signal: &AbortSignal<'_>, - reason: HandleValue<'_>, + reason: impl ToJSVal<'s>, script_stack_state: ScriptStackState, ) -> Result<(), ExnThrown> { // Step 1: If _signal_ is `aborted`, then return. @@ -130,11 +110,16 @@ pub(crate) fn signal_abort( // Step 2: Set _signal_'s `abort reason` to _reason_ if it is given; otherwise to a new // "``AbortError``" ``DOMException``. + let reason = reason.to_jsval_throwing(scope)?; if !reason.is_undefined() { signal.data_mut().abort_reason.set(reason.get()); } else { - let err_val = create_abort_error(scope)?; - signal.data_mut().abort_reason.set(err_val.get()); + let err_val = DOMException::new( + scope, + "signal is aborted without reason".into(), + "AbortError".into(), + )?; + signal.data_mut().abort_reason.set(err_val.as_value()); } // Step 3: Let _dependentSignalsToAbort_ be a new `list`. @@ -212,10 +197,7 @@ fn run_the_abort_steps( // callback is reported and must not keep the remaining // algorithms, the `abort` event (step 3), or dependent // signals' abort steps (signal abort step 6) from running. - let callback_val = scope.root_value(callback.as_value()); - if js::Function::call(scope, HandleValue::undefined(), callback_val, &[reason]) - .is_err() - { + if Function::call(scope, HandleValue::undefined(), callback, &[reason]).is_err() { js::exception::report_and_clear(scope, "abort algorithm"); } script_stack_state.clean_up_after_running_script(scope); @@ -332,7 +314,7 @@ impl Task for AbortTimeoutTask { let signal: AbortSignal<'_> = self.signal.take(scope); // `Queue a global task` [...] to `signal abort` given _signal_ and a new // "``TimeoutError``" ``DOMException``. - let reason = create_timeout_error(scope)?; + let reason = DOMException::new(scope, "signal timed out".into(), "TimeoutError".into())?; signal_abort(scope, &signal, reason, ScriptStackState::Empty) } diff --git a/crates/builtins/web-globals/src/structured_clone.rs b/crates/builtins/web-globals/src/structured_clone.rs index 6ea2044f..2ff801e3 100644 --- a/crates/builtins/web-globals/src/structured_clone.rs +++ b/crates/builtins/web-globals/src/structured_clone.rs @@ -9,58 +9,43 @@ //! //! [structured cloning API]: https://html.spec.whatwg.org/multipage/structured-data.html#dom-structuredclone +/// +#[core_runtime::webidl_dictionary] +pub struct StructuredSerializeOptions<'a> { + #[webidl(default = Vec::new())] + pub transfer: Vec>, +} + /// #[core_runtime::jsglobals] pub mod structured_clone_globals { + use super::StructuredSerializeOptions; + use js::conversion::ToJSVal; use js::error::ExnThrown; use js::gc::scope::Scope; use js::prelude::HandleValue; /// - /// - /// The `structuredClone(value, options)` method steps are: - /// - /// 1. Let _serialized_ be ? StructuredSerializeWithTransfer(_value_, - /// _options_["`transfer`"]). - /// 2. Let _deserializeRecord_ be ? StructuredDeserializeWithTransfer( - /// _serialized_, `this`'s relevant realm). - /// 3. Return _deserializeRecord_.[[Deserialized]]. pub fn structured_clone<'r>( scope: &'r Scope<'_>, value: HandleValue<'_>, - options: Option>, + options: Option>, ) -> Result, ExnThrown> { - // Extract the transfer list from options, if provided. - let transfer = if let Some(opts) = options { - if opts.is_object() { - let opts_obj = js::Object::from_value(scope, *opts).expect("is_object() was true"); - let transfer_val = opts_obj.get_property(scope, c"transfer")?; - if transfer_val.is_undefined() { - None - } else { - Some(transfer_val) - } - } else if opts.is_null_or_undefined() { - None - } else { - return Err(js::error::throw_type_error( - scope, - c"structuredClone: options must be an object", - )); - } - } else { - None - }; - - match transfer { - // Steps 1-3 (no transfer): use the simple clone path. - None => unsafe { + let transfer = options.map(|o| o.transfer).unwrap_or_default(); + // Step 1: Let _serialized_ be ? StructuredSerializeWithTransfer(_value_, + // _options_["``transfer``"]). + // Step 2: Let _deserializeRecord_ be ? StructuredDeserializeWithTransfer(_serialized_, + // `this`'s `relevant realm`). + // Step 3: Return _deserializeRecord_.[[Deserialized]]. + // (SpiderMonkey's structured clone performs all three steps.) + if transfer.is_empty() { + // SAFETY: null callbacks select SpiderMonkey's defaults, and the closure is unused. + unsafe { js::structured_clone::clone(scope, value, std::ptr::null(), std::ptr::null_mut()) - }, - // Steps 1-3 (with transfer): serialize with transfer, then deserialize. - Some(transfer_val) => { - js::structured_clone::clone_with_transfer(scope, value, transfer_val) } + } else { + let transfer = js::Array::with_contents(scope, &transfer)?.to_jsval_throwing(scope)?; + js::structured_clone::clone_with_transfer(scope, value, transfer) } } } diff --git a/crates/builtins/web-globals/tests/dom_events.rs b/crates/builtins/web-globals/tests/dom_events.rs index 99c9672a..8617d7a7 100644 --- a/crates/builtins/web-globals/tests/dom_events.rs +++ b/crates/builtins/web-globals/tests/dom_events.rs @@ -305,7 +305,7 @@ fn listener_identity_survives_compacting_gc() { use js::gc::{self, GCOptions, GCReason}; setup(); - let rt = Runtime::init(&RuntimeConfig::default()); + let rt = Runtime::init(&RuntimeConfig::default()).expect("runtime init"); let scope = rt.default_global(); // Register one listener. State lives on `globalThis` so it survives across @@ -438,7 +438,7 @@ fn global_listener_survives_compacting_gc() { use js::gc::{self, GCOptions, GCReason}; setup(); - let rt = Runtime::init(&RuntimeConfig::default()); + let rt = Runtime::init(&RuntimeConfig::default()).expect("runtime init"); let scope = rt.default_global(); // The closure's only strong reference is the global's listener list. @@ -471,3 +471,69 @@ fn global_listener_survives_compacting_gc() { assert_eq!(String::from_jsval(&scope, val, ()).unwrap(), "1"); } + +/// `addEventListener` accepts `EventListener` objects with a `handleEvent` +/// method, calling it with the object as `this`. +#[test] +fn add_event_listener_accepts_handle_event_objects() { + assert_eq!( + eval( + "const t = new EventTarget(); + const listener = { seen: '', handleEvent(e) { this.seen = e.type; } }; + t.addEventListener('ping', listener); + t.dispatchEvent(new Event('ping')); + listener.seen" + ), + "ping" + ); +} + +/// A non-object callback is a TypeError, while `null` adds nothing. +#[test] +fn add_event_listener_rejects_non_object_callbacks() { + assert_eq!( + eval( + "const t = new EventTarget(); + t.addEventListener('ping', null); + let threw = false; + try { t.addEventListener('ping', 42); } catch (e) { threw = e instanceof TypeError; } + String(threw)" + ), + "true" + ); +} + +/// A non-boolean, non-object `options` value is converted with `ToBoolean`, so +/// `1` registers a capturing listener that `removeEventListener(.., true)` removes. +#[test] +fn listener_options_convert_primitives_with_to_boolean() { + assert_eq!( + eval( + "const t = new EventTarget(); + let count = 0; + const f = () => count++; + t.addEventListener('ping', f, 1); + t.removeEventListener('ping', f, true); + t.dispatchEvent(new Event('ping')); + String(count)" + ), + "0" + ); +} + +/// An exception thrown while reading an options member propagates instead of +/// being left pending. +#[test] +fn throwing_options_getter_propagates() { + assert_eq!( + eval( + "const t = new EventTarget(); + let msg = ''; + try { + t.addEventListener('ping', () => {}, { get capture() { throw new Error('boom'); } }); + } catch (e) { msg = e.message; } + msg" + ), + "boom" + ); +} diff --git a/crates/builtins/web-globals/tests/dom_signals.rs b/crates/builtins/web-globals/tests/dom_signals.rs index 1435b086..371fae14 100644 --- a/crates/builtins/web-globals/tests/dom_signals.rs +++ b/crates/builtins/web-globals/tests/dom_signals.rs @@ -41,9 +41,8 @@ fn setup() { (), ) .expect("create __addAbortAlgorithm"); - let helper_val = scope.root_value(helper.as_value()); global - .set_property(scope, c"__addAbortAlgorithm", helper_val) + .set_property(scope, c"__addAbortAlgorithm", helper) .expect("install __addAbortAlgorithm"); }); } diff --git a/crates/builtins/web-globals/tests/encoding.rs b/crates/builtins/web-globals/tests/encoding.rs index 057b7c1c..74ba59d8 100644 --- a/crates/builtins/web-globals/tests/encoding.rs +++ b/crates/builtins/web-globals/tests/encoding.rs @@ -357,3 +357,22 @@ fn text_decoder_to_string_tag() { "[object TextDecoder]" ); } + +/// windows-1252 input without bytes in 0x80..=0x9F decodes to the same code points, and input +/// with them maps those bytes through the encoding's table, including when a streaming decode +/// alternates between the two. +#[test] +fn text_decoder_windows_1252_maps_every_byte() { + assert_eq!( + eval( + "const d = new TextDecoder('latin1'); + const codes = s => Array.from(s, c => c.charCodeAt(0)).join(','); + [d.decode(new Uint8Array([0x41, 0xe9, 0xff, 0xa0])), + d.decode(new Uint8Array([0x41, 0x80, 0x9f, 0xe9])), + d.decode(new Uint8Array([0xe9]), { stream: true }) + + d.decode(new Uint8Array([0x80]), { stream: true }) + d.decode(), + d.decode(new Uint8Array([]))].map(codes).join(' | ')" + ), + "65,233,255,160 | 65,8364,376,233 | 233,8364 | " + ); +} diff --git a/crates/builtins/web-streams/Cargo.toml b/crates/builtins/web-streams/Cargo.toml index efa1465c..678630cf 100644 --- a/crates/builtins/web-streams/Cargo.toml +++ b/crates/builtins/web-streams/Cargo.toml @@ -11,6 +11,7 @@ edition.workspace = true test = false [dependencies] +smallvec = "1.16" core-runtime.workspace = true js.workspace = true web-globals.workspace = true diff --git a/crates/builtins/web-streams/src/algorithms.rs b/crates/builtins/web-streams/src/algorithms.rs index 61c810f4..09e38af9 100644 --- a/crates/builtins/web-streams/src/algorithms.rs +++ b/crates/builtins/web-streams/src/algorithms.rs @@ -6,9 +6,8 @@ use js::{ error::ExnThrown, gc::{handle::Heap, scope::Scope}, heap::RootedTraceableBox, - native::Value, - prelude::{CallbackArgs, HandleValue}, - value, Array, Function, Object, Promise, + prelude::{HandleValue, ToJSVal}, + Array, Object, Promise, }; use crate::queuing::{QueueWithSizes, QueuingStrategy, ValueWithSize}; @@ -25,8 +24,8 @@ pub fn make_type_error<'r>(scope: &'r Scope<'_>, message: &std::ffi::CStr) -> Ha /// Pack two values into a JS object for a two-state reaction payload. pub(crate) fn pair_payload<'r>( scope: &'r Scope<'_>, - a: HandleValue<'_>, - b: HandleValue<'_>, + a: impl ToJSVal<'r>, + b: impl ToJSVal<'r>, ) -> Result, ExnThrown> { let attrs = js::class_spec::JSPROP_ENUMERATE as std::ffi::c_uint; let obj = Object::new_plain(scope)?; @@ -42,11 +41,7 @@ pub(crate) fn composite_reason<'r>( a: HandleValue<'_>, b: HandleValue<'_>, ) -> Result, ExnThrown> { - let attrs = js::class_spec::JSPROP_ENUMERATE as std::ffi::c_uint; - let arr = Array::new(scope, 2)?; - arr.define_element(scope, 0, a, attrs)?; - arr.define_element(scope, 1, b, attrs)?; - Ok(scope.root_value(arr.as_value())) + Ok(scope.root_value(Array::with_contents(scope, &[a, b])?.as_value())) } /// Unpack a two-value reaction payload. @@ -61,20 +56,6 @@ pub(crate) fn pair_parts<'r>( ) } -/// Recover a rooted stream/reader/controller newtype `T` from a reaction payload -/// value. Reaction payloads are wired internally by this crate, so the value is -/// always a JS object of the expected class — a mismatch is a wiring bug, hence -/// the panics. -pub(crate) fn cast_payload<'r, T>(scope: &'r Scope<'_>, payload: HandleValue<'_>) -> T -where - T: js::builtins::CastTarget<'r, Output = T>, -{ - Object::from_value(scope, *payload) - .expect("reaction payload is an object") - .cast::() - .expect("reaction payload has the expected class") -} - /// /// ExtractHighWaterMark(strategy, defaultHWM) performs the following steps: pub(crate) fn extract_high_water_mark( @@ -102,54 +83,23 @@ pub(crate) fn extract_high_water_mark( /// /// ExtractSizeAlgorithm(strategy) performs the following steps: /// -/// In this implementation a controller's size algorithm is stored as a JS -/// callable. A missing `size` member — "an algorithm that returns 1" — becomes -/// a native callback returning 1 ([`default_size_algorithm`]); otherwise the -/// algorithm is the `size` callback itself, later `invoked` with argument list -/// « chunk ». Either way the stored value is callable while the stream is in -/// use (it is only reset to `undefined` by `…ClearAlgorithms`). -pub(crate) fn extract_size_algorithm( - scope: &Scope<'_>, +/// A controller stores its size algorithm as a JS value: the `size` callback +/// itself, later `invoked` with argument list « chunk », or `undefined` for "an +/// algorithm that returns 1", which the controllers apply without a call. +pub(crate) fn extract_size_algorithm<'r>( + scope: &'r Scope<'_>, strategy: &Option>, -) -> Result { +) -> Result, ExnThrown> { // Step 1: If _strategy_["``size``"] does not `exist`, return an algorithm that returns 1. // Step 2: Return an algorithm that performs the following steps, taking a _chunk_ argument: // Return the result of `invoking` _strategy_["``size``"] with argument list « _chunk_ // ». - // `size` is a `QueuingStrategySize` callback type: held as an `Object`, so its callability — - // checked by the WebIDL conversion of callback function types — is enforced here. Both branches - // return a callable value; the controllers store and invoke it uniformly, so `[[strategySizeAlgorithm]]` - // is never undefined while the stream is in use. match strategy.as_ref().and_then(|s| s.size.as_ref()) { - Some(size_fn) if size_fn.is_callable() => Ok(size_fn.as_value()), - Some(_) => Err(js::error::throw_type_error( - scope, - c"queuing strategy size must be a function", - )), - // The "returns 1" default size algorithm is a per-global singleton (it is - // an internal slot, never author-observable). - None => js::class::get_or_init_shared_function( - scope, - default_size_algorithm as *const () as usize, - |scope| { - let undef = HandleValue::undefined(); - Function::new_callback(scope, c"size", 1, default_size_algorithm, undef) - }, - ) - .map(|f| f.as_value()), + Some(size_fn) => Ok(scope.root_value(size_fn.as_value())), + None => Ok(HandleValue::undefined()), } } -/// The default `QueuingStrategySize` algorithm — `ExtractSizeAlgorithm` step 1's -/// "an algorithm that returns 1". Used when a strategy omits `size`. -fn default_size_algorithm( - _scope: &Scope<'_>, - _args: CallbackArgs<'_>, - _payload: HandleValue<'_>, -) -> Result { - Ok(value::from_f64(1.0)) -} - /// /// DequeueValue(container) performs the following steps: pub(crate) fn dequeue_value<'r>( @@ -170,7 +120,7 @@ pub(crate) fn dequeue_value<'r>( let value_with_size = RootedTraceableBox::new(container.queue_mut().pop_front().unwrap()); // Step 5: Set _container_.[[queueTotalSize]] to _container_.[[queueTotalSize]] − // _valueWithSize_’s `size`. - let mut total = container.queue_total_size() - value_with_size.size; + let mut total = container.queue_total_size() - value_with_size.size(); // Step 6: If _container_.[[queueTotalSize]] < 0, set _container_.[[queueTotalSize]] to 0. (This // can occur due to rounding errors.) if total < 0.0 { @@ -178,7 +128,11 @@ pub(crate) fn dequeue_value<'r>( } container.set_queue_total_size(total); // Step 7: Return _valueWithSize_’s `value`. - value_with_size.value.get(scope) + // (The close sentinel's value is never read, so it dequeues as undefined.) + match &*value_with_size { + ValueWithSize::Chunk { value, .. } => value.get(scope), + ValueWithSize::CloseSentinel => HandleValue::undefined(), + } } /// @@ -207,10 +161,9 @@ pub(crate) fn enqueue_value_with_size( } // Step 4: `Append` a new `value-with-size` with `value` _value_ and `size` _size_ to // _container_.[[queue]]. - container.queue_mut().push_back(ValueWithSize { + container.queue_mut().push_back(ValueWithSize::Chunk { value: Heap::from(value.get()), size, - is_close_sentinel: false, }); // Step 5: Set _container_.[[queueTotalSize]] to _container_.[[queueTotalSize]] + _size_. let total = container.queue_total_size() + size; diff --git a/crates/builtins/web-streams/src/lib.rs b/crates/builtins/web-streams/src/lib.rs index c54c840e..93cf275a 100644 --- a/crates/builtins/web-streams/src/lib.rs +++ b/crates/builtins/web-streams/src/lib.rs @@ -7,6 +7,8 @@ pub(crate) mod support; pub mod transform; pub mod writable; +pub use support::AlgorithmArg; + use js::gc::scope::Scope; use js::Object; use readable::readable_stream::ReadableStreamImpl; diff --git a/crates/builtins/web-streams/src/queuing/byte_length_queuing_strategy.rs b/crates/builtins/web-streams/src/queuing/byte_length_queuing_strategy.rs index 447e838a..7a67db54 100644 --- a/crates/builtins/web-streams/src/queuing/byte_length_queuing_strategy.rs +++ b/crates/builtins/web-streams/src/queuing/byte_length_queuing_strategy.rs @@ -63,8 +63,7 @@ impl ByteLengthQueuingStrategy { scope, byte_length_queuing_strategy_size as *const () as usize, |scope| { - let undef = HandleValue::undefined(); - Function::new_callback(scope, c"size", 1, byte_length_queuing_strategy_size, undef) + Function::new_callback(scope, c"size", 1, byte_length_queuing_strategy_size, ()) }, ) } diff --git a/crates/builtins/web-streams/src/queuing/count_queuing_strategy.rs b/crates/builtins/web-streams/src/queuing/count_queuing_strategy.rs index 2f093460..4f9f30cc 100644 --- a/crates/builtins/web-streams/src/queuing/count_queuing_strategy.rs +++ b/crates/builtins/web-streams/src/queuing/count_queuing_strategy.rs @@ -56,10 +56,7 @@ impl CountQueuingStrategy { js::class::get_or_init_shared_function( scope, count_queuing_strategy_size as *const () as usize, - |scope| { - let undef = HandleValue::undefined(); - Function::new_callback(scope, c"size", 0, count_queuing_strategy_size, undef) - }, + |scope| Function::new_callback(scope, c"size", 0, count_queuing_strategy_size, ()), ) } } diff --git a/crates/builtins/web-streams/src/queuing/mod.rs b/crates/builtins/web-streams/src/queuing/mod.rs index fcc85b73..705f6369 100644 --- a/crates/builtins/web-streams/src/queuing/mod.rs +++ b/crates/builtins/web-streams/src/queuing/mod.rs @@ -10,7 +10,7 @@ use core_runtime::webidl_dictionary; pub use byte_length_queuing_strategy::ByteLengthQueuingStrategy; pub use count_queuing_strategy::CountQueuingStrategy; -use js::Object; +use js::Callable; pub use queue_with_sizes::{QueueWithSizes, ValueWithSize}; /// @@ -27,4 +27,4 @@ pub struct QueuingStrategyInit { } /// WebIDL callback `QueuingStrategySize`: (chunk: HandleValue<'_>) -> f64 -pub type QueuingStrategySize<'s> = Object<'s>; +pub type QueuingStrategySize<'s> = Callable<'s>; diff --git a/crates/builtins/web-streams/src/queuing/queue_with_sizes.rs b/crates/builtins/web-streams/src/queuing/queue_with_sizes.rs index 6460e79e..2ef7c926 100644 --- a/crates/builtins/web-streams/src/queuing/queue_with_sizes.rs +++ b/crates/builtins/web-streams/src/queuing/queue_with_sizes.rs @@ -22,22 +22,31 @@ use js::native::Value; /// /// #[js::must_root] -#[derive(Traceable, Default)] -pub struct ValueWithSize { - /// The enqueued chunk. - pub value: Heap, - /// The chunk's size, as computed by the stream's size algorithm. Already - /// `ToNumber`-coerced (the size algorithm returns an `unrestricted double`), - /// so it is a plain `f64` rather than a JS value. - #[no_trace] - pub size: f64, - /// Whether this entry is the writable stream's `close sentinel` (enqueued by - /// `WritableStreamDefaultControllerClose` with size 0). The readable - /// controllers never set this. +#[derive(Traceable)] +pub enum ValueWithSize { + /// An enqueued chunk. + Chunk { + value: Heap, + /// The chunk's size, as computed by the stream's size algorithm. + #[no_trace] + size: f64, + }, + /// The writable stream's `close sentinel`, enqueued by + /// `WritableStreamDefaultControllerClose` with size 0. Never enqueued by the readable + /// controllers. /// /// - #[no_trace] - pub is_close_sentinel: bool, + CloseSentinel, +} + +impl ValueWithSize { + /// The entry's `size`. + pub fn size(&self) -> f64 { + match self { + Self::Chunk { size, .. } => *size, + Self::CloseSentinel => 0.0, + } + } } /// A container with `[[queue]]` and `[[queueTotalSize]]` internal slots, as diff --git a/crates/builtins/web-streams/src/readable/algorithms.rs b/crates/builtins/web-streams/src/readable/algorithms.rs index d93126ed..93d68a9a 100644 --- a/crates/builtins/web-streams/src/readable/algorithms.rs +++ b/crates/builtins/web-streams/src/readable/algorithms.rs @@ -6,10 +6,11 @@ use core_runtime::{jsclass, jsmethods}; use js::conversion::FromJSVal; use js::error::ExnThrown; use js::exception::take_pending_or_undefined; +use js::function::cast_payload; use js::gc::handle::{Heap, OptionHeapExt}; use js::gc::scope::Scope; use js::heap::RootedTraceableBox; -use js::native::{Handle, Value}; +use js::native::Value; use js::prelude::{CallbackArgs, HandleObject, HandleValue, ToJSVal}; use js::{value, Function, Object, Promise}; use web_globals::signals::{AbortSignal, AbortSignalImpl}; @@ -17,19 +18,24 @@ use web_globals::signals::{AbortSignal, AbortSignalImpl}; use super::byob_reader::BYOBReader; use super::byob_request::ReadableStreamBYOBRequest; use super::byte_stream_controller::{ - ByteQueueEntry, PullIntoDescriptor, ReadableByteStreamController, ReaderType, + ByteQueueEntry, PullIntoDescriptor, ReadableByteStreamController, + ReadableByteStreamControllerImpl, ReaderType, +}; +use super::default_controller::{ + ReadableStreamDefaultController, ReadableStreamDefaultControllerImpl, }; -use super::default_controller::ReadableStreamDefaultController; use super::default_reader::DefaultReader; use super::read_request::{ReadIntoRequest, ReadRequest}; use super::readable_stream::{ReadableStream, ReadableStreamImpl, ReadableStreamState}; use super::underlying_source::UnderlyingSource; +use super::unions::{HeapReadableStreamController, HeapReadableStreamReader, ReadableStreamReader}; use crate::algorithms::{ composite_reason, dequeue_value, enqueue_value_with_size, is_non_negative_number, make_type_error, pair_parts, pair_payload, reset_queue, resolved_undefined_promise, }; use crate::readable::default_reader::DefaultReaderImpl; use crate::support; +use crate::support::{Algorithm, AlgorithmArg}; use crate::writable::algorithms::{ acquire_writable_stream_default_writer, is_writable_stream_locked, writable_stream_abort, writable_stream_close_queued_or_in_flight, @@ -43,18 +49,28 @@ use crate::writable::writable_stream::{WritableStream, WritableStreamImpl, Writa use web_globals::events::algorithms::ScriptStackState; // --------------------------------------------------------------------------- -// Private accessors bridging the polymorphic `[[controller]]`/`[[reader]]` -// object slots to the concrete default-stream newtypes. +// Private accessors for the stream's `[[reader]]` slot. // --------------------------------------------------------------------------- +/// The stream's `[[reader]]`, or `None` if the stream is unlocked. +fn stream_reader<'r>( + scope: &'r Scope<'_>, + stream: &ReadableStream<'_>, +) -> Option> { + let reader = stream.data().reader.as_ref()?.get(scope); + Some(reader) +} + /// The stream's `[[reader]]` as a default reader, or `None` if unlocked or /// locked to a BYOB reader. fn stream_default_reader<'r>( scope: &'r Scope<'_>, stream: &ReadableStream<'_>, ) -> Option> { - let obj: Object<'r> = stream.data().reader.get(scope)?; - obj.cast::().ok() + match stream_reader(scope, stream)? { + ReadableStreamReader::Default(reader) => Some(reader), + ReadableStreamReader::Byob(_) => None, + } } /// The stream's `[[reader]]` as a BYOB reader, or `None` if unlocked or locked @@ -63,8 +79,10 @@ fn stream_byob_reader<'r>( scope: &'r Scope<'_>, stream: &ReadableStream<'_>, ) -> Option> { - let obj: Object<'r> = stream.data().reader.get(scope)?; - obj.cast::().ok() + match stream_reader(scope, stream)? { + ReadableStreamReader::Byob(reader) => Some(reader), + ReadableStreamReader::Default(_) => None, + } } /// The `[[closedPromise]]` of the stream's reader, whichever reader type it is @@ -73,20 +91,9 @@ fn stream_reader_closed_promise<'r>( scope: &'r Scope<'_>, stream: &ReadableStream<'_>, ) -> Promise<'r> { - let reader = stream.reader(scope).expect("a locked stream has a reader"); - reader_closed_promise_for(scope, &reader) -} - -/// The `[[closedPromise]]` of a specific reader object (default or BYOB). -fn reader_closed_promise_for<'r>(scope: &'r Scope<'_>, reader_obj: &Object<'_>) -> Promise<'r> { - if let Ok(reader) = reader_obj.cast::() { - reader.generic_closed_promise(scope) - } else { - reader_obj - .cast::() - .expect("a default or BYOB reader") - .generic_closed_promise(scope) - } + stream_reader(scope, stream) + .expect("a locked stream has a reader") + .generic_closed_promise(scope) } /// The `ReadableStreamGenericReader` mixin (WHATWG Streams §4.8), shared by the @@ -104,8 +111,8 @@ pub(crate) trait GenericReader { fn generic_closed_promise<'r>(&self, scope: &'r Scope<'_>) -> Promise<'r>; /// Set `[[closedPromise]]`. fn set_generic_closed_promise(&self, promise: Promise<'_>); - /// The reader's own JS value (for wiring `stream.[[reader]]`). - fn as_reader_value(&self) -> Value; + /// The reader as a `ReadableStreamReader` union value. + fn as_generic(&self) -> ReadableStreamReader<'_>; } impl GenericReader for DefaultReader<'_> { @@ -113,7 +120,7 @@ impl GenericReader for DefaultReader<'_> { self.data().stream.get(scope) } fn set_generic_stream(&self, stream: &ReadableStream<'_>) { - self.data_mut().stream = Some(Heap::from(*stream)); + self.data_mut().stream.set(*stream); } fn clear_generic_stream(&self) { self.data_mut().stream = None; @@ -124,8 +131,8 @@ impl GenericReader for DefaultReader<'_> { fn set_generic_closed_promise(&self, promise: Promise<'_>) { self.data_mut().closed_promise.set(promise); } - fn as_reader_value(&self) -> Value { - self.as_value() + fn as_generic(&self) -> ReadableStreamReader<'_> { + ReadableStreamReader::Default(*self) } } @@ -134,7 +141,7 @@ impl GenericReader for BYOBReader<'_> { self.data().stream.get(scope) } fn set_generic_stream(&self, stream: &ReadableStream<'_>) { - self.data_mut().stream = Some(Heap::from(*stream)); + self.data_mut().stream.set(*stream); } fn clear_generic_stream(&self) { self.data_mut().stream = None; @@ -145,8 +152,8 @@ impl GenericReader for BYOBReader<'_> { fn set_generic_closed_promise(&self, promise: Promise<'_>) { self.data_mut().closed_promise.set(promise); } - fn as_reader_value(&self) -> Value { - self.as_value() + fn as_generic(&self) -> ReadableStreamReader<'_> { + ReadableStreamReader::Byob(*self) } } @@ -180,8 +187,7 @@ fn error_controller_with_pending( scope, value, js::native::ExceptionStackBehavior::DoNotCapture, - ); - ExnThrown + ) } /// A native fulfillment reaction that ignores its argument and returns @@ -205,7 +211,7 @@ fn start_promise_fulfilled( _args: CallbackArgs<'_>, payload: HandleValue<'_>, ) -> Result { - let controller = ReadableStreamDefaultController::from_jsval_throwing(scope, payload, ())?; + let controller = cast_payload::(scope, payload); // Set _controller_.[[started]] to true. controller.data_mut().started = true; debug_assert!(!controller.data().pulling); @@ -221,7 +227,7 @@ fn start_promise_rejected( args: CallbackArgs<'_>, payload: HandleValue<'_>, ) -> Result { - let controller = ReadableStreamDefaultController::from_jsval_throwing(scope, payload, ())?; + let controller = cast_payload::(scope, payload); // Perform ! DefaultControllerError(controller, r). readable_stream_default_controller_error(scope, &controller, args.get(0)); Ok(value::undefined()) @@ -233,7 +239,7 @@ fn pull_promise_fulfilled( _args: CallbackArgs<'_>, payload: HandleValue<'_>, ) -> Result { - let controller = ReadableStreamDefaultController::from_jsval_throwing(scope, payload, ())?; + let controller = cast_payload::(scope, payload); // Set _controller_.[[pulling]] to false. controller.data_mut().pulling = false; // If _controller_.[[pullAgain]] is true, set it to false and call pull-if-needed again. @@ -250,7 +256,7 @@ fn pull_promise_rejected( args: CallbackArgs<'_>, payload: HandleValue<'_>, ) -> Result { - let controller = ReadableStreamDefaultController::from_jsval_throwing(scope, payload, ())?; + let controller = cast_payload::(scope, payload); // Perform ! DefaultControllerError(controller, e). readable_stream_default_controller_error(scope, &controller, args.get(0)); Ok(value::undefined()) @@ -265,7 +271,7 @@ fn byte_start_promise_fulfilled( _args: CallbackArgs<'_>, payload: HandleValue<'_>, ) -> Result { - let controller = ReadableByteStreamController::from_jsval_throwing(scope, payload, ())?; + let controller = cast_payload::(scope, payload); // Set _controller_.[[started]] to true. controller.data_mut().started = true; debug_assert!(!controller.data().pulling); @@ -281,7 +287,7 @@ fn byte_start_promise_rejected( args: CallbackArgs<'_>, payload: HandleValue<'_>, ) -> Result { - let controller = ReadableByteStreamController::from_jsval_throwing(scope, payload, ())?; + let controller = cast_payload::(scope, payload); // Perform ! ByteStreamControllerError(controller, r). readable_byte_stream_controller_error(scope, &controller, args.get(0)); Ok(value::undefined()) @@ -293,7 +299,7 @@ fn byte_pull_promise_fulfilled( _args: CallbackArgs<'_>, payload: HandleValue<'_>, ) -> Result { - let controller = ReadableByteStreamController::from_jsval_throwing(scope, payload, ())?; + let controller = cast_payload::(scope, payload); // Set _controller_.[[pulling]] to false. controller.data_mut().pulling = false; // If _controller_.[[pullAgain]] is true, set it to false and call pull-if-needed again. @@ -310,7 +316,7 @@ fn byte_pull_promise_rejected( args: CallbackArgs<'_>, payload: HandleValue<'_>, ) -> Result { - let controller = ReadableByteStreamController::from_jsval_throwing(scope, payload, ())?; + let controller = cast_payload::(scope, payload); // Perform ! ByteStreamControllerError(controller, e). readable_byte_stream_controller_error(scope, &controller, args.get(0)); Ok(value::undefined()) @@ -344,9 +350,9 @@ fn tee_pull(scope: &Scope<'_>, state: TeeState<'_>) { fn tee_pull_native( scope: &Scope<'_>, _args: CallbackArgs<'_>, - state: HandleValue<'_>, + payload: HandleValue<'_>, ) -> Result { - let state = TeeState::from_jsval(scope, state, ()).unwrap(); + let state = cast_payload::(scope, payload); tee_pull(scope, state); // Return `a promise resolved with` undefined. // Fully internal, so the per-global reused instance serves. @@ -383,7 +389,7 @@ fn tee_cancel( let stream = state.data().stream.get(scope); let cancel_result = readable_stream_cancel(scope, &stream, composite); cancel_promise - .resolve(scope, scope.root_value(cancel_result.as_value())) + .resolve(scope, cancel_result) .expect("resolve cancel"); } // Return _cancelPromise_. @@ -396,7 +402,7 @@ fn tee_cancel1_native( args: CallbackArgs<'_>, payload: HandleValue<'_>, ) -> Result { - let state = TeeState::from_jsval(scope, payload, ()).unwrap(); + let state = cast_payload::(scope, payload); Ok(tee_cancel(scope, state, args.get(0), true)) } @@ -406,7 +412,7 @@ fn tee_cancel2_native( args: CallbackArgs<'_>, payload: HandleValue<'_>, ) -> Result { - let state = TeeState::from_jsval(scope, payload, ()).unwrap(); + let state = cast_payload::(scope, payload); Ok(tee_cancel(scope, state, args.get(0), false)) } @@ -416,24 +422,12 @@ fn tee_closed_rejected( args: CallbackArgs<'_>, payload: HandleValue<'_>, ) -> Result { - let state = TeeState::from_jsval(scope, payload, ()).unwrap(); + let state = cast_payload::(scope, payload); let r = args.get(0); - let branch1 = state.data().branch1.get(scope); - let branch2 = state.data().branch2.get(scope); - readable_stream_default_controller_error( - scope, - &branch1 - .default_controller(scope) - .expect("branch1 controller"), - r, - ); - readable_stream_default_controller_error( - scope, - &branch2 - .default_controller(scope) - .expect("branch2 controller"), - r, - ); + let branch1_controller = state.data().branch1_controller.get(scope); + let branch2_controller = state.data().branch2_controller.get(scope); + readable_stream_default_controller_error(scope, &branch1_controller, r); + readable_stream_default_controller_error(scope, &branch2_controller, r); if !state.data().canceled1 || !state.data().canceled2 { state .data() @@ -454,9 +448,9 @@ const TEE_ENQUEUE_INFALLIBLE: &str = fn tee_chunk_microtask( scope: &Scope<'_>, _args: CallbackArgs<'_>, - state: HandleValue<'_>, + payload: HandleValue<'_>, ) -> Result { - let state = TeeState::from_jsval(scope, state, ()).unwrap(); + let state = cast_payload::(scope, payload); let chunk = state.data().pending_chunk.get(scope); state.data().pending_chunk.set(value::undefined()); // Set _readAgain_ to false. @@ -480,24 +474,12 @@ fn tee_chunk_microtask( let clone_err = take_pending_or_undefined(scope); // Perform ! `ReadableStreamDefaultControllerError`(branch1.`[[controller]]`, // _cloneResult_.`[[Value]]`). - let branch1 = state.data().branch1.get(scope); - readable_stream_default_controller_error( - scope, - &branch1 - .default_controller(scope) - .expect("branch1 controller"), - clone_err, - ); + let branch1_controller = state.data().branch1_controller.get(scope); + readable_stream_default_controller_error(scope, &branch1_controller, clone_err); // Perform ! `ReadableStreamDefaultControllerError`(branch2.`[[controller]]`, // _cloneResult_.`[[Value]]`). - let branch2 = state.data().branch2.get(scope); - readable_stream_default_controller_error( - scope, - &branch2 - .default_controller(scope) - .expect("branch2 controller"), - clone_err, - ); + let branch2_controller = state.data().branch2_controller.get(scope); + readable_stream_default_controller_error(scope, &branch2_controller, clone_err); // `Resolve` _cancelPromise_ with ! `ReadableStreamCancel`(_stream_, // _cloneResult_.`[[Value]]`). let stream = state.data().stream.get(scope); @@ -515,27 +497,15 @@ fn tee_chunk_microtask( } // If _canceled1_ is false, perform ! `ReadableStreamDefaultControllerEnqueue`(branch1, chunk1). if !state.data().canceled1 { - let branch1 = state.data().branch1.get(scope); - readable_stream_default_controller_enqueue( - scope, - &branch1 - .default_controller(scope) - .expect("branch1 controller"), - chunk, - ) - .expect(TEE_ENQUEUE_INFALLIBLE); + let branch1_controller = state.data().branch1_controller.get(scope); + readable_stream_default_controller_enqueue(scope, &branch1_controller, chunk) + .expect(TEE_ENQUEUE_INFALLIBLE); } // If _canceled2_ is false, perform ! `ReadableStreamDefaultControllerEnqueue`(branch2, chunk2). if !state.data().canceled2 { - let branch2 = state.data().branch2.get(scope); - readable_stream_default_controller_enqueue( - scope, - &branch2 - .default_controller(scope) - .expect("branch2 controller"), - chunk2, - ) - .expect(TEE_ENQUEUE_INFALLIBLE); + let branch2_controller = state.data().branch2_controller.get(scope); + readable_stream_default_controller_enqueue(scope, &branch2_controller, chunk2) + .expect(TEE_ENQUEUE_INFALLIBLE); } // Set _reading_ to false. If _readAgain_ is true, perform _pullAlgorithm_. state.data_mut().reading = false; @@ -567,23 +537,13 @@ pub(crate) fn tee_read_request_close_steps( state.data_mut().reading = false; // If _canceled1_ is false, close branch1. if !state.data().canceled1 { - let branch1 = state.data().branch1.get(scope); - readable_stream_default_controller_close( - scope, - &branch1 - .default_controller(scope) - .expect("branch1 controller"), - ); + let branch1_controller = state.data().branch1_controller.get(scope); + readable_stream_default_controller_close(scope, &branch1_controller); } // If _canceled2_ is false, close branch2. if !state.data().canceled2 { - let branch2 = state.data().branch2.get(scope); - readable_stream_default_controller_close( - scope, - &branch2 - .default_controller(scope) - .expect("branch2 controller"), - ); + let branch2_controller = state.data().branch2_controller.get(scope); + readable_stream_default_controller_close(scope, &branch2_controller); } // If _canceled1_ is false or _canceled2_ is false, resolve _cancelPromise_ with undefined. if !state.data().canceled1 || !state.data().canceled2 { @@ -655,8 +615,7 @@ pub(crate) fn pull_steps( .take() .unwrap() .root(scope) - .chunk_steps(scope, chunk) - .expect("read request chunk steps"); + .chunk_steps(scope, chunk); } else { // Step 3: Otherwise, Perform ! `ReadableStreamAddReadRequest`(_stream_, _readRequest_). // Perform ! `DefaultControllerCallPullIfNeeded`(`this`). @@ -738,8 +697,7 @@ pub(crate) fn byte_pull_steps( .take() .unwrap() .root(scope) - .error_steps(scope, error) - .expect("read request error steps"); + .error_steps(scope, error); return; } }; @@ -747,7 +705,7 @@ pub(crate) fn byte_pull_steps( .data_mut() .pending_pull_intos .push_back(PullIntoDescriptor { - buffer: Heap::from(*buffer), + buffer: Heap::from(buffer), buffer_byte_length: size, byte_offset: 0, byte_length: size, @@ -813,11 +771,11 @@ pub(crate) fn acquire_readable_stream_default_reader<'r>( /// CreateReadableStream(startAlgorithm, pullAlgorithm, cancelAlgorithm[, highWaterMark, [, sizeAlgorithm]]) performs the following steps: pub(crate) fn create_readable_stream<'r>( scope: &'r Scope<'_>, - start_algorithm: HandleValue<'_>, - pull_algorithm: HandleValue<'_>, - cancel_algorithm: HandleValue<'_>, + start_algorithm: AlgorithmArg<'r>, + pull_algorithm: AlgorithmArg<'r>, + cancel_algorithm: AlgorithmArg<'r>, high_water_mark: f64, - size_algorithm: HandleValue<'_>, + size_algorithm: impl ToJSVal<'r>, ) -> Result, ExnThrown> { // Step 1: If _highWaterMark_ was not passed, set it to 1. (Passed by the caller.) // Step 2: If _sizeAlgorithm_ was not passed, set it to an algorithm that returns 1. (Passed.) @@ -830,7 +788,7 @@ pub(crate) fn create_readable_stream<'r>( // Step 5: Perform ! `InitializeReadableStream`(_stream_). initialize_readable_stream(&stream); // Step 6: Let _controller_ be a `new` ``ReadableStreamDefaultController``. - let controller = ReadableStreamDefaultController::new(scope)?; + let controller = ReadableStreamDefaultController::new(scope, stream)?; // Step 7: Perform ? `SetUpDefaultController`(_stream_, _controller_, // _startAlgorithm_, _pullAlgorithm_, _cancelAlgorithm_, _highWaterMark_, // _sizeAlgorithm_). @@ -845,7 +803,7 @@ pub(crate) fn create_readable_stream<'r>( cancel_algorithm, receiver, high_water_mark, - size_algorithm, + size_algorithm.to_jsval_throwing(scope)?, )?; // Step 8: Return _stream_. Ok(stream) @@ -855,9 +813,9 @@ pub(crate) fn create_readable_stream<'r>( /// CreateReadableByteStream(startAlgorithm, pullAlgorithm, cancelAlgorithm) performs the following steps: pub(crate) fn create_readable_byte_stream<'r>( scope: &'r Scope<'_>, - start_algorithm: HandleValue<'_>, - pull_algorithm: HandleValue<'_>, - cancel_algorithm: HandleValue<'_>, + start_algorithm: AlgorithmArg<'r>, + pull_algorithm: AlgorithmArg<'r>, + cancel_algorithm: AlgorithmArg<'r>, ) -> Result, ExnThrown> { // Step 1: Let _stream_ be a `new` ``ReadableStream``. let stream = js::class::create_instance_with::(scope, |_| { @@ -866,7 +824,7 @@ pub(crate) fn create_readable_byte_stream<'r>( // Step 2: Perform ! `InitializeReadableStream`(_stream_). initialize_readable_stream(&stream); // Step 3: Let _controller_ be a `new` ``ReadableByteStreamController``. - let controller = ReadableByteStreamController::new(scope)?; + let controller = ReadableByteStreamController::new(scope, stream)?; // Step 4: Perform ? `SetUpByteStreamController`(_stream_, _controller_, // _startAlgorithm_, _pullAlgorithm_, _cancelAlgorithm_, 0, undefined). // The algorithms are native (no JS receiver), so `algorithm_receiver` is undefined. @@ -942,13 +900,13 @@ pub(crate) fn readable_stream_from_iterable<'r>( // (Step 3's start algorithm is the default undefined; steps 4 and 5's pull/cancel // algorithms are the native `from_pull_native` / `from_cancel_native`, carrying the // iterator state as their payload.) - let pull = Function::new_callback(scope, c"", 1, from_pull_native, state)?; - let cancel = Function::new_callback(scope, c"", 1, from_cancel_native, state)?; + let pull = AlgorithmArg::native(scope, from_pull_native, state)?; + let cancel = AlgorithmArg::native(scope, from_cancel_native, state)?; let stream = create_readable_stream( scope, - HandleValue::undefined(), - scope.root_value(pull.as_value()), - scope.root_value(cancel.as_value()), + AlgorithmArg::None, + pull, + cancel, 0.0, HandleValue::undefined(), )?; @@ -985,7 +943,7 @@ fn from_pull_native( args: CallbackArgs<'_>, payload: HandleValue<'_>, ) -> Result { - let state = FromIterableState::from_jsval(scope, payload, ()).unwrap(); + let state = cast_payload::(scope, payload); let controller = args.get(0); let record: js::iteration::AsyncIteratorRecord<'_> = state.data().record.get(scope); // Let _nextResult_ be `IteratorNext`(_iteratorRecord_). If abrupt, return a @@ -993,29 +951,24 @@ fn from_pull_native( let next_result = match record.call_next(scope) { Ok(r) => r, Err(_) => { - return Ok(Promise::new_rejected_with_pending_error(scope) - .map_err(|_| ExnThrown)? - .as_value()); + return Ok(Promise::new_rejected_with_pending_error(scope)?.as_value()); } }; // Let _nextPromise_ be a promise resolved with _nextResult_. - let next_promise = Promise::call_original_resolve(scope, next_result).map_err(|_| ExnThrown)?; + let next_promise = Promise::call_original_resolve(scope, next_result)?; // React to _nextPromise_ with the fulfillment steps (`from_pull_fulfilled`). // The callback's payload — the stream's controller — is the same for every // pull, so it is created once and reused for every subsequent iteration. if state.data().pull_fulfilled_fn.is_none() { let cb = Function::new_callback(scope, c"", 1, from_pull_fulfilled, controller)?; - state.data_mut().pull_fulfilled_fn = Some(Heap::from(cb)); + state.data_mut().pull_fulfilled_fn.set(cb); } let cb = state .data() .pull_fulfilled_fn .get(scope) .expect("created above"); - let p = next_promise - .then(scope, Some(*cb), None) - .map_err(|_| ExnThrown)?; - Ok(p.as_value()) + Ok(next_promise.then(scope, Some(*cb), None)?.as_value()) } /// The pull algorithm's fulfillment steps (payload = the controller, arg 0 = the @@ -1026,10 +979,7 @@ fn from_pull_fulfilled( payload: HandleValue<'_>, ) -> Result { let iter_result = args.get(0); - let controller = Object::from_value(scope, *payload) - .map_err(|_| ExnThrown)? - .cast::() - .map_err(|_| ExnThrown)?; + let controller = cast_payload::(scope, payload); // If _iterResult_ is not an Object, throw a TypeError. let result_obj = js::iteration::iter_result_object(scope, iter_result)?; // Let _done_ be ? `IteratorComplete`(_iterResult_). @@ -1052,7 +1002,7 @@ fn from_cancel_native( args: CallbackArgs<'_>, payload: HandleValue<'_>, ) -> Result { - let state = FromIterableState::from_jsval(scope, payload, ()).unwrap(); + let state = cast_payload::(scope, payload); let record: js::iteration::AsyncIteratorRecord<'_> = state.data().record.get(scope); let p = record.return_promise(scope, args.get(0))?; Ok(p.as_value()) @@ -1091,7 +1041,8 @@ pub(crate) fn readable_stream_pipe_to<'r>( // Step 8: If _source_.`[[controller]]` `implements` ``ReadableByteStreamController``, let // _reader_ be either ! `AcquireBYOBReader`(_source_) or ! // `AcquireDefaultReader`(_source_), at the user agent’s discretion. - // TODO: implement this, now that byte streams are implemented. + // (We use the default reader for byte streams too: the pipe loop reads chunks and + // never supplies a view.) // Step 9: Otherwise, let _reader_ be ! `AcquireDefaultReader`(_source_). let reader = acquire_readable_stream_default_reader(scope, source)?; @@ -1111,7 +1062,8 @@ pub(crate) fn readable_stream_pipe_to<'r>( // one allows native sources to detect that a `pull` is triggered by a TransformStream, // and defer acting on it until there's actual demand. if let Some(host_source) = source.native_source(scope) { - if let Some(transform) = dest.data().identity_transform.get(scope) { + let transform = dest.data().identity_transform.get(scope); + if let Some(transform) = transform { // Only when this pipe will close the destination behind it. Handing the native source // on says "the whole of what comes out of this transform is that one body", and a sink // that takes it up bypasses the transform entirely — so anything written into the @@ -1126,7 +1078,7 @@ pub(crate) fn readable_stream_pipe_to<'r>( .get(scope) .set_native_source(&host_source); } - source.data_mut().piped_to_identity_transform = Some(Heap::from(transform)); + source.data_mut().piped_to_identity_transform.set(transform); } } @@ -1150,7 +1102,7 @@ pub(crate) fn readable_stream_pipe_to<'r>( // Step 14.2: If _signal_ is `aborted`, perform _abortAlgorithm_ and return _promise_. if let Some(signal) = signal { if signal.aborted() { - return Ok(state.data_mut().promise.get(scope)); + return Ok(state.data().promise.get(scope)); } } @@ -1237,10 +1189,10 @@ pub(crate) fn readable_stream_pipe_to<'r>( // every reaction receives the state object as its payload. // --------------------------------------------------------------------------- -// The deferred shutdown action recorded in the pipe state's `action_kind` field. -#[derive(Default)] +/// The action a pipe shutdown performs once pending writes finish. +#[derive(Clone, Copy)] enum PipeAction { - #[default] + /// `Shutdown` without an action. None, AbortDest, CancelSource, @@ -1248,12 +1200,22 @@ enum PipeAction { AbortAlgorithm, } -// `PipeAction` holds no GC pointers, so tracing it is a no-op. The `Traceable` -// derive rejects enums, so `Trace` is implemented by hand here (`PipeState`'s -// derive calls `action_kind.trace()`). -unsafe impl js::heap::Trace for PipeAction { - #[inline] - unsafe fn trace(&self, _trc: *mut js::native::JSTracer) {} +/// A pipe shutdown in progress: the spec's _shuttingDown_ is true exactly while +/// the pipe state holds one. +#[js::must_root] +#[derive(core_runtime::Traceable)] +struct PipeShutdown { + #[no_trace] + action: PipeAction, + /// The error passed to `action`. Unused for `PipeAction::None` and + /// `PipeAction::CloseWriter`. + action_error: Heap, + /// The error the pipe finalizes with, if any. + original_error: Option>, + /// Waiting for `pending_writes` to reach zero. The settle reaction that + /// drains the count proceeds with the shutdown. + #[no_trace] + waiting_for_writes: bool, } /// The pipe's shutdown actions never run on an empty stack: they are reached either from a promise @@ -1292,7 +1254,6 @@ fn pipe_setup_propagation_and_start( let reader: DefaultReader<'_> = state.data().reader.get(scope); let writer: WritableStreamDefaultWriter<'_> = state.data().writer.get(scope); let prevent_cancel = state.data().prevent_cancel; - let payload = scope.root_value(state.as_value()); let reader_closed = reader.data().closed_promise.get(scope); let writer_closed = writer.data().closed_promise.get(scope); @@ -1307,7 +1268,7 @@ fn pipe_setup_propagation_and_start( scope, &reader_closed, None, - Some((pipe_fwd_error_rejected, payload)), + Some((pipe_fwd_error_rejected, state)), )?; } @@ -1321,7 +1282,7 @@ fn pipe_setup_propagation_and_start( scope, &writer_closed, None, - Some((pipe_bwd_error_rejected, payload)), + Some((pipe_bwd_error_rejected, state)), )?; } @@ -1333,7 +1294,7 @@ fn pipe_setup_propagation_and_start( support::react( scope, &reader_closed, - Some((pipe_fwd_close_fulfilled, payload)), + Some((pipe_fwd_close_fulfilled, state)), None, )?; } @@ -1369,7 +1330,7 @@ fn pipe_setup_propagation_and_start( /// has cleared), read a chunk and write it. Stops once `shuttingDown` is set. fn pipe_step(scope: &Scope<'_>, state: PipeState<'_>) { // Shutdown must stop activity: do not initiate further reads. - if state.data().shutting_down { + if state.data().shutdown.is_some() { return; } @@ -1390,10 +1351,10 @@ fn pipe_ready_fulfilled( _args: CallbackArgs<'_>, payload: HandleValue<'_>, ) -> Result { - let state = PipeState::from_jsval(scope, payload, ()).unwrap(); + let state = cast_payload::(scope, payload); // Re-check before reading: shutdown may have begun (and released the reader) // while we waited for the writer. - if state.data().shutting_down { + if state.data().shutdown.is_some() { return Ok(value::undefined()); } @@ -1450,9 +1411,9 @@ pub(crate) fn pipe_read_request_chunk_steps( fn pipe_deferred_write( scope: &Scope<'_>, _args: CallbackArgs<'_>, - state: HandleValue<'_>, + payload: HandleValue<'_>, ) -> Result { - let state = PipeState::from_jsval(scope, state, ()).unwrap(); + let state = cast_payload::(scope, payload); let chunk = state.data().pending_chunk.get(scope); state.data().pending_chunk.set(value::undefined()); let writer = pipe_writer(scope, state); @@ -1465,7 +1426,7 @@ fn pipe_deferred_write( // which happens only after this write runs, so the writer is still attached here. if writer_stream(scope, &writer).is_none() { // Nothing consults the count after finalize, but keep it exact. - debug_assert!(state.data_mut().pending_writes > 0); + debug_assert!(state.data().pending_writes > 0); state.data_mut().pending_writes -= 1; return Ok(value::undefined()); } @@ -1491,13 +1452,12 @@ fn pipe_deferred_write( fn pipe_write_settled( scope: &Scope<'_>, _args: CallbackArgs<'_>, - state: HandleValue<'_>, + payload: HandleValue<'_>, ) -> Result { - let state = PipeState::from_jsval(scope, state, ()).unwrap(); - debug_assert!(state.data_mut().pending_writes > 0); + let state = cast_payload::(scope, payload); + debug_assert!(state.data().pending_writes > 0); state.data_mut().pending_writes -= 1; - if state.data().pending_writes == 0 && state.data().shutdown_waiting { - state.data_mut().shutdown_waiting = false; + if state.data().pending_writes == 0 && pipe_stop_waiting_for_writes(state) { pipe_shutdown_do_proceed(scope, state); } Ok(value::undefined()) @@ -1524,7 +1484,7 @@ fn pipe_fwd_error_rejected( args: CallbackArgs<'_>, payload: HandleValue<'_>, ) -> Result { - let state = PipeState::from_jsval(scope, payload, ()).unwrap(); + let state = cast_payload::(scope, payload); pipe_fwd_error(scope, state, args.get(0)); Ok(value::undefined()) } @@ -1548,7 +1508,7 @@ fn pipe_bwd_error_rejected( args: CallbackArgs<'_>, payload: HandleValue<'_>, ) -> Result { - let state = PipeState::from_jsval(scope, payload, ()).unwrap(); + let state = cast_payload::(scope, payload); pipe_bwd_error(scope, state, args.get(0)); Ok(value::undefined()) } @@ -1567,54 +1527,71 @@ fn pipe_fwd_close_fulfilled( _args: CallbackArgs<'_>, payload: HandleValue<'_>, ) -> Result { - let state = PipeState::from_jsval(scope, payload, ()).unwrap(); + let state = cast_payload::(scope, payload); pipe_fwd_close(scope, state); Ok(value::undefined()) } // --- Shutdown / finalize ---------------------------------------------------- -/// Begin a shutdown that, after pending writes finish, performs `action` (given -/// by `kind` + `action_error`) and finalizes with `original_error` (if any). +/// Begin a shutdown that, after pending writes finish, performs `action` with +/// `action_error` and finalizes with `original_error` (if any). fn pipe_begin_shutdown_with_action( scope: &Scope<'_>, state: PipeState<'_>, - kind: PipeAction, + action: PipeAction, action_error: HandleValue<'_>, original_error: Option>, ) { - if state.data().shutting_down { + if state.data().shutdown.is_some() { return; } - state.data_mut().shutting_down = true; - state.data_mut().action_kind = kind; - state.data_mut().action_error.set(*action_error); - match original_error { - Some(e) => { - state.data_mut().has_original = true; - state.data_mut().original_error.set(*e); - } - None => state.data_mut().has_original = false, - } + state.data_mut().shutdown = Some(PipeShutdown { + action, + action_error: Heap::from(action_error.get()), + original_error: original_error.map(|e| Heap::from(e.get())), + waiting_for_writes: false, + }); pipe_shutdown_wait_then_proceed(scope, state); } /// Begin a shutdown that, after pending writes finish, finalizes directly (with /// `error` if one was given). fn pipe_shutdown(scope: &Scope<'_>, state: PipeState<'_>, error: Option>) { - if state.data().shutting_down { - return; - } - state.data_mut().shutting_down = true; - // No action: `action_kind` stays `PipeAction::None` (its default). - match error { - Some(e) => { - state.data_mut().has_original = true; - state.data_mut().original_error.set(*e); - } - None => state.data_mut().has_original = false, + let undefined = HandleValue::undefined(); + pipe_begin_shutdown_with_action(scope, state, PipeAction::None, undefined, error); +} + +/// The shutdown's action and the error passed to it. Panics if the pipe is not +/// shutting down. +fn pipe_action<'r>(scope: &'r Scope<'_>, state: PipeState<'_>) -> (PipeAction, HandleValue<'r>) { + let data = state.data(); + let shutdown = data.shutdown.as_ref().expect("the pipe is shutting down"); + (shutdown.action, shutdown.action_error.get(scope)) +} + +/// Set the shutdown's `waiting_for_writes` flag. Panics if the pipe is not +/// shutting down. +fn pipe_wait_for_writes(state: PipeState<'_>) { + let mut data = state.data_mut(); + let shutdown = data.shutdown.as_mut().expect("the pipe is shutting down"); + shutdown.waiting_for_writes = true; +} + +/// Clear the shutdown's `waiting_for_writes` flag, returning whether it was set. +fn pipe_stop_waiting_for_writes(state: PipeState<'_>) -> bool { + let mut data = state.data_mut(); + match data.shutdown.as_mut() { + Some(shutdown) => std::mem::replace(&mut shutdown.waiting_for_writes, false), + None => false, } - pipe_shutdown_wait_then_proceed(scope, state); +} + +/// The error the shutdown finalizes with, if any. +fn pipe_original_error<'r>(scope: &'r Scope<'_>, state: PipeState<'_>) -> Option> { + let data = state.data(); + let shutdown = data.shutdown.as_ref().expect("the pipe is shutting down"); + shutdown.original_error.as_ref().map(|e| e.get(scope)) } /// If the destination can still accept writes, wait for the in-flight writes to @@ -1633,14 +1610,12 @@ fn pipe_shutdown_wait_then_proceed(scope: &Scope<'_>, state: PipeState<'_>) { && !writable_stream_close_queued_or_in_flight(&dest) { if state.data().pending_writes > 0 { - state.data_mut().shutdown_waiting = true; + pipe_wait_for_writes(state); } else { - let Ok(cb) = + let result = Function::new_callback(scope, c"", 0, pipe_shutdown_proceed_deferred, state) - else { - return; - }; - let _ = js::jobs::queue_microtask(scope, &cb); + .and_then(|cb| js::jobs::queue_microtask(scope, &cb)); + support::report_failure(scope, result, "pipeTo"); } } else { pipe_shutdown_do_proceed(scope, state); @@ -1655,9 +1630,9 @@ fn pipe_shutdown_proceed_deferred( _args: CallbackArgs<'_>, payload: HandleValue<'_>, ) -> Result { - let state = PipeState::from_jsval(scope, payload, ()).unwrap(); + let state = cast_payload::(scope, payload); if state.data().pending_writes > 0 { - state.data_mut().shutdown_waiting = true; + pipe_wait_for_writes(state); } else { pipe_shutdown_do_proceed(scope, state); } @@ -1666,57 +1641,57 @@ fn pipe_shutdown_proceed_deferred( /// Perform the recorded shutdown action (if any) and then finalize. fn pipe_shutdown_do_proceed(scope: &Scope<'_>, state: PipeState<'_>) { - if matches!(state.data().action_kind, PipeAction::None) { - let error = if state.data().has_original { - Some(state.data().original_error.get(scope)) - } else { - None - }; + if matches!(pipe_action(scope, state).0, PipeAction::None) { + let error = pipe_original_error(scope, state); pipe_finalize(scope, state, error); return; } let action_promise = pipe_run_action(scope, state); - let payload = scope.root_value(state.as_value()); - let _ = support::react( + let result = support::react( scope, &action_promise, - Some((pipe_action_fulfilled, payload)), - Some((pipe_action_rejected, payload)), + Some((pipe_action_fulfilled, state)), + Some((pipe_action_rejected, state)), ); + support::report_failure(scope, result, "pipeTo"); } fn pipe_run_action<'r>(scope: &'r Scope<'_>, state: PipeState<'_>) -> Promise<'r> { - match state.data().action_kind { + // The actions run author code (abort listeners, sink `abort()`), which can re-enter the + // pipe's read request and borrow `state` mutably, so no borrow is held across them. + let (action, error) = pipe_action(scope, state); + match action { PipeAction::AbortDest => { let dest = pipe_dest(scope, state); - let e: Handle<'_, Value> = state.data().action_error.get(scope); - writable_stream_abort(scope, &dest, e, PIPE_SCRIPT_STACK_STATE) + writable_stream_abort(scope, &dest, error, PIPE_SCRIPT_STACK_STATE) } PipeAction::CancelSource => { let source = pipe_source(scope, state); - let e: Handle<'_, Value> = state.data().action_error.get(scope); - readable_stream_cancel(scope, &source, e) + readable_stream_cancel(scope, &source, error) } PipeAction::CloseWriter => { let writer = pipe_writer(scope, state); writable_stream_default_writer_close_with_error_propagation(scope, &writer) } - PipeAction::AbortAlgorithm => pipe_build_abort_actions_promise(scope, state), + PipeAction::AbortAlgorithm => pipe_build_abort_actions_promise(scope, state, error), PipeAction::None => unreachable!(), } } /// The abort algorithm's action: get a promise to wait for all of (conditionally) /// aborting the destination and cancelling the source, using the abort reason. -fn pipe_build_abort_actions_promise<'r>(scope: &'r Scope<'_>, state: PipeState<'_>) -> Promise<'r> { - let error = state.data().action_error.get(scope); +fn pipe_build_abort_actions_promise<'r>( + scope: &'r Scope<'_>, + state: PipeState<'_>, + error: HandleValue<'r>, +) -> Promise<'r> { let mut promises: Vec> = Vec::new(); if !state.data().prevent_abort { let dest = pipe_dest(scope, state); let p = if dest.data().state == WritableStreamState::Writable { writable_stream_abort(scope, &dest, error, PIPE_SCRIPT_STACK_STATE) } else { - resolved_undefined_promise(scope) + Promise::shared_resolved_undefined(scope).expect("shared resolved promise") }; promises.push(p); } @@ -1725,7 +1700,7 @@ fn pipe_build_abort_actions_promise<'r>(scope: &'r Scope<'_>, state: PipeState<' let p = if source.data().state == ReadableStreamState::Readable { readable_stream_cancel(scope, &source, error) } else { - resolved_undefined_promise(scope) + Promise::shared_resolved_undefined(scope).expect("shared resolved promise") }; promises.push(p); } @@ -1738,12 +1713,8 @@ fn pipe_action_fulfilled( _args: CallbackArgs<'_>, payload: HandleValue<'_>, ) -> Result { - let state = PipeState::from_jsval(scope, payload, ()).unwrap(); - let error = if state.data().has_original { - Some(state.data().original_error.get(scope)) - } else { - None - }; + let state = cast_payload::(scope, payload); + let error = pipe_original_error(scope, state); pipe_finalize(scope, state, error); Ok(value::undefined()) } @@ -1753,7 +1724,7 @@ fn pipe_action_rejected( args: CallbackArgs<'_>, payload: HandleValue<'_>, ) -> Result { - let state = PipeState::from_jsval(scope, payload, ()).unwrap(); + let state = cast_payload::(scope, payload); pipe_finalize(scope, state, Some(args.get(0))); Ok(value::undefined()) } @@ -1764,25 +1735,25 @@ fn pipe_finalize(scope: &Scope<'_>, state: PipeState<'_>, error: Option { - let _ = promise.reject(scope, e); - } - None => { - let _ = promise.resolve(scope, HandleValue::undefined()); - } - } + let result = match error { + Some(e) => promise.reject(scope, e), + None => promise.resolve(scope, HandleValue::undefined()), + }; + support::report_failure(scope, result, "pipeTo"); } /// The abort algorithm registered on the pipe's signal: shut down with an action @@ -1798,7 +1769,7 @@ fn pipe_abort_algorithm( _args: CallbackArgs<'_>, payload: HandleValue<'_>, ) -> Result { - let state = PipeState::from_jsval(scope, payload, ()).unwrap(); + let state = cast_payload::(scope, payload); pipe_run_abort_algorithm(scope, state); Ok(value::undefined()) } @@ -1815,8 +1786,8 @@ pub(crate) struct TeeState { canceled2: bool, reason1: Heap, reason2: Heap, - branch1: Heap, - branch2: Heap, + branch1_controller: Heap, + branch2_controller: Heap, /// The chunk delivered by the current read, parked until the chunk-steps /// microtask consumes it. pending_chunk: Heap, @@ -1836,7 +1807,6 @@ impl TeeState<'_> { clone_for_branch2: bool, ) -> Result<(), ExnThrown> { self.data_mut().clone_for_branch2 = clone_for_branch2; - let state_value = scope.root_value(self.as_value()); // Step 4: Let _reading_ be false. // Step 5: Let _readAgain_ be false. @@ -1856,46 +1826,37 @@ impl TeeState<'_> { self.data_mut().reader.set(reader); // The chunk-steps microtask callback, reused for every chunk. - let microtask = Function::new_callback(scope, c"", 0, tee_chunk_microtask, state_value)?; + let microtask = Function::new_callback(scope, c"", 0, tee_chunk_microtask, self)?; self.data_mut().chunk_microtask_fn.set(microtask); // Step 13: Let _pullAlgorithm_ be the following steps: (steps implemented in `tee_pull_native` and its callees) - let pull = Function::new_callback(scope, c"", 0, tee_pull_native, state_value)?; - let pull = scope.root_value(pull.as_value()); + let pull = AlgorithmArg::native(scope, tee_pull_native, *self)?; // Step 14: Let _cancel1Algorithm_ be the following steps: (steps implemented in `tee_cancel1_native` and its callees) - let cancel1 = Function::new_callback(scope, c"", 1, tee_cancel1_native, state_value)?; + let cancel1 = AlgorithmArg::native(scope, tee_cancel1_native, *self)?; // Step 15: Let _cancel2Algorithm_ be the following steps: (steps implemented in `tee_cancel2_native` and its callees) - let cancel2 = Function::new_callback(scope, c"", 1, tee_cancel2_native, state_value)?; + let cancel2 = AlgorithmArg::native(scope, tee_cancel2_native, *self)?; // Step 16: Let _startAlgorithm_ be an algorithm that returns undefined. - let start = HandleValue::undefined(); + let start = AlgorithmArg::None; let hwm = HandleValue::undefined(); // Step 17: Set _branch1_ to ! `CreateReadableStream`(_startAlgorithm_, _pullAlgorithm_, // _cancel1Algorithm_). - let branch1 = create_readable_stream( - scope, - start, - pull, - scope.root_value(cancel1.as_value()), - 1.0, - hwm, - )?; - self.data_mut().branch1.set(branch1); + let branch1 = create_readable_stream(scope, start, pull, cancel1, 1.0, hwm)?; + let controller = branch1 + .default_controller(scope) + .expect("a default controller"); + self.data_mut().branch1_controller.set(controller); // Step 18: Set _branch2_ to ! `CreateReadableStream`(_startAlgorithm_, _pullAlgorithm_, // _cancel2Algorithm_). - let branch2 = create_readable_stream( - scope, - start, - pull, - scope.root_value(cancel2.as_value()), - 1.0, - hwm, - )?; - self.data_mut().branch2.set(branch2); + let branch2 = create_readable_stream(scope, start, pull, cancel2, 1.0, hwm)?; + let controller = branch2 + .default_controller(scope) + .expect("a default controller"); + self.data_mut().branch2_controller.set(controller); Ok(()) } @@ -1910,15 +1871,8 @@ pub(crate) struct PipeState { prevent_close: bool, prevent_abort: bool, prevent_cancel: bool, - has_original: bool, - shutting_down: bool, - /// A shutdown has begun and is waiting for `pending_writes` to reach zero; - /// the settle reaction that drains the count proceeds with the shutdown. - shutdown_waiting: bool, + shutdown: Option, signal: Option>, - action_kind: PipeAction, - action_error: Heap, - original_error: Heap, /// The chunk read by the current loop turn, parked until the deferred write /// consumes it. Single-occupancy: the next read is only issued after /// `pipe_deferred_write` has taken the value out (see @@ -1964,20 +1918,17 @@ impl PipeState<'_> { self.data_mut().prevent_cancel = prevent_cancel; self.data_mut().signal = signal.map(Heap::from); - let payload = scope.root_value(self.as_value()); - let deferred = Function::new_callback(scope, c"", 0, pipe_deferred_write, payload)?; + let deferred = Function::new_callback(scope, c"", 0, pipe_deferred_write, self)?; self.data_mut().deferred_write_fn.set(deferred); - let settled = Function::new_callback(scope, c"", 1, pipe_write_settled, payload)?; + let settled = Function::new_callback(scope, c"", 1, pipe_write_settled, self)?; self.data_mut().write_settled_fn.set(settled); - let ready_ok = Function::new_callback(scope, c"", 1, pipe_ready_fulfilled, payload)?; + let ready_ok = Function::new_callback(scope, c"", 1, pipe_ready_fulfilled, self)?; self.data_mut().ready_fulfilled_fn.set(ready_ok); // The ready-rejection swallower uses no per-pipe state; share one per global. let ready_err = js::class::get_or_init_shared_function( scope, pipe_ready_rejected as *const () as usize, - |scope| { - Function::new_callback(scope, c"", 1, pipe_ready_rejected, HandleValue::undefined()) - }, + |scope| Function::new_callback(scope, c"", 1, pipe_ready_rejected, ()), )?; self.data_mut().ready_rejected_fn.set(ready_err); self.data_mut().promise.set(Promise::new_pending(scope)?); @@ -1994,8 +1945,8 @@ impl PipeState<'_> { // If _signal_ is `aborted`, perform _abortAlgorithm_ and return _promise_. `Add` // _abortAlgorithm_ to _signal_. if let Some(signal) = signal { - let abort_fn = Function::new_callback(scope, c"", 0, pipe_abort_algorithm, payload)?; - self.data_mut().abort_algorithm = Some(Heap::from(abort_fn)); + let abort_fn = Function::new_callback(scope, c"", 0, pipe_abort_algorithm, self)?; + self.data_mut().abort_algorithm.set(abort_fn); if signal.aborted() { pipe_run_abort_algorithm(scope, *self); return Ok(()); @@ -2021,21 +1972,20 @@ pub(crate) fn readable_stream_default_tee<'r>( // Steps 4-18 implemented in `TeeState::new` and the native functions for the algorithms it sets up. let state = TeeState::new(scope, *stream, reader, clone_for_branch2)?; - let state_value = scope.root_value(state.as_value()); // Step 19: `Upon rejection` of _reader_.`[[closedPromise]]` with reason _r_, Perform ! // `DefaultControllerError`(_branch1_.`[[controller]]`, _r_). Perform ! // `DefaultControllerError`(_branch2_.`[[controller]]`, _r_). If // _canceled1_ is false or _canceled2_ is false, `resolve` _cancelPromise_ with // undefined. - let on_rejected = Function::new_callback(scope, c"", 1, tee_closed_rejected, state_value)?; + let on_rejected = Function::new_callback(scope, c"", 1, tee_closed_rejected, state)?; reader .closed(scope) .add_reactions(scope, None, Some(*on_rejected))?; // Step 20: Return « _branch1_, _branch2_ ». - let branch1 = state.data().branch1.get(scope); - let branch2 = state.data().branch2.get(scope); + let branch1 = state.data().branch1_controller.get(scope).stream(scope); + let branch2 = state.data().branch2_controller.get(scope).stream(scope); Ok((branch1, branch2)) } @@ -2186,8 +2136,8 @@ pub(crate) fn readable_byte_stream_tee<'r>( // (Steps 3-24 implemented in `ByteTeeState::new` and the `byte_tee_*` helpers.) let state = ByteTeeState::new(scope, *stream)?; // Step 25: Return « _branch1_, _branch2_ ». - let branch1 = state.data().branch1.get(scope); - let branch2 = state.data().branch2.get(scope); + let branch1 = state.data().branch1_controller.get(scope).stream(scope); + let branch2 = state.data().branch2_controller.get(scope).stream(scope); Ok((branch1, branch2)) } @@ -2198,7 +2148,7 @@ pub(crate) fn readable_byte_stream_tee<'r>( #[jsclass(hidden)] pub(crate) struct ByteTeeState { stream: Heap, - reader: Heap, + reader: HeapReadableStreamReader, reading: bool, read_again_for_branch1: bool, read_again_for_branch2: bool, @@ -2206,8 +2156,8 @@ pub(crate) struct ByteTeeState { canceled2: bool, reason1: Heap, reason2: Heap, - branch1: Heap, - branch2: Heap, + branch1_controller: Heap, + branch2_controller: Heap, cancel_promise: Heap, /// The chunk (and, for a BYOB read, its target branch) delivered by the /// current read, parked until the chunk-steps microtask consumes it. @@ -2224,9 +2174,9 @@ impl ByteTeeState<'_> { fn new(&self, scope: &Scope<'_>, stream: ReadableStream<'_>) -> Result<(), ExnThrown> { self.data_mut().stream.set(stream); // Step 3: _reader_ = `AcquireDefaultReader`(_stream_). - let reader = acquire_readable_stream_default_reader(scope, &stream)?; - let reader_obj = Object::from_value(scope, reader.as_value()).map_err(|_| ExnThrown)?; - self.data_mut().reader.set(reader_obj); + let reader = + ReadableStreamReader::Default(acquire_readable_stream_default_reader(scope, &stream)?); + self.data_mut().reader = HeapReadableStreamReader::from(reader); // Steps 4-12: _reading_, _readAgainForBranchN_, _canceledN_ are false; // _reasonN_ and _branchN_ are undefined (the field defaults; branches set below). // Step 13: _cancelPromise_ = `a new promise`. @@ -2240,29 +2190,21 @@ impl ByteTeeState<'_> { let byob_mt = Function::new_callback(scope, c"", 0, byte_tee_byob_microtask, self)?; self.data_mut().byob_microtask_fn.set(byob_mt); - let undef = HandleValue::undefined(); + let start = AlgorithmArg::None; // Step 22: _branch1_ = `CreateReadableByteStream`(start, _pull1Algorithm_, _cancel1Algorithm_). - let pull1 = Function::new_callback(scope, c"", 1, byte_tee_pull1, self)?; - let cancel1 = Function::new_callback(scope, c"", 1, byte_tee_cancel1, self)?; - let branch1 = create_readable_byte_stream( - scope, - undef, - scope.root_value(pull1.as_value()), - scope.root_value(cancel1.as_value()), - )?; - self.data_mut().branch1.set(branch1); + let pull1 = AlgorithmArg::native(scope, byte_tee_pull1, *self)?; + let cancel1 = AlgorithmArg::native(scope, byte_tee_cancel1, *self)?; + let branch1 = create_readable_byte_stream(scope, start, pull1, cancel1)?; + let controller = branch1.byte_controller(scope).expect("a byte controller"); + self.data_mut().branch1_controller.set(controller); // Step 23: _branch2_ = `CreateReadableByteStream`(start, _pull2Algorithm_, _cancel2Algorithm_). - let pull2 = Function::new_callback(scope, c"", 1, byte_tee_pull2, self)?; - let cancel2 = Function::new_callback(scope, c"", 1, byte_tee_cancel2, self)?; - let branch2 = create_readable_byte_stream( - scope, - undef, - scope.root_value(pull2.as_value()), - scope.root_value(cancel2.as_value()), - )?; - self.data_mut().branch2.set(branch2); + let pull2 = AlgorithmArg::native(scope, byte_tee_pull2, *self)?; + let cancel2 = AlgorithmArg::native(scope, byte_tee_cancel2, *self)?; + let branch2 = create_readable_byte_stream(scope, start, pull2, cancel2)?; + let controller = branch2.byte_controller(scope).expect("a byte controller"); + self.data_mut().branch2_controller.set(controller); // Step 24: Perform _forwardReaderError_, given _reader_. - byte_tee_forward_reader_error(scope, *self, &reader_obj)?; + byte_tee_forward_reader_error(scope, *self, reader)?; Ok(()) } } @@ -2278,33 +2220,23 @@ fn byte_tee_stream<'r>(scope: &'r Scope<'_>, state: ByteTeeState<'_>) -> Readabl state.data().stream.get(scope) } -fn byte_tee_reader_obj<'r>(scope: &'r Scope<'_>, state: ByteTeeState<'_>) -> Object<'r> { +fn byte_tee_reader<'r>(scope: &'r Scope<'_>, state: ByteTeeState<'_>) -> ReadableStreamReader<'r> { state.data().reader.get(scope) } /// Branch 1 or branch 2 of the byte tee. -fn byte_tee_branch<'r>( +fn byte_tee_branch_controller<'r>( scope: &'r Scope<'_>, state: ByteTeeState<'_>, for_branch2: bool, -) -> ReadableStream<'r> { +) -> ReadableByteStreamController<'r> { if for_branch2 { - state.data().branch2.get(scope) + state.data().branch2_controller.get(scope) } else { - state.data().branch1.get(scope) + state.data().branch1_controller.get(scope) } } -fn byte_tee_branch_controller<'r>( - scope: &'r Scope<'_>, - state: ByteTeeState<'_>, - for_branch2: bool, -) -> ReadableByteStreamController<'r> { - byte_tee_branch(scope, state, for_branch2) - .byte_controller(scope) - .expect("branch has a byte controller") -} - fn byte_tee_cancel_promise<'r>(scope: &'r Scope<'_>, state: ByteTeeState<'_>) -> Promise<'r> { state.data().cancel_promise.get(scope) } @@ -2316,10 +2248,7 @@ pub(crate) fn byte_tee_set_not_reading(_scope: &Scope<'_>, state: ByteTeeState<' /// A chunk value as an `ArrayBufferView` (byte-tee chunks are always views). fn byte_tee_view<'r>(scope: &'r Scope<'_>, v: HandleValue<'_>) -> js::ArrayBufferView<'r> { - Object::from_value(scope, *v) - .ok() - .and_then(js::ArrayBufferView::from_object) - .expect("byte-tee chunk is an ArrayBufferView") + js::ArrayBufferView::from_jsval(scope, v, ()).expect("byte-tee chunk is an ArrayBufferView") } /// `CloneAsUint8Array`(_O_): clone the view's region into a fresh `Uint8Array`. @@ -2327,10 +2256,7 @@ fn byte_tee_clone_as_uint8array<'r>( scope: &'r Scope<'_>, chunk: HandleValue<'_>, ) -> Result, ExnThrown> { - let view = Object::from_value(scope, *chunk) - .ok() - .and_then(js::ArrayBufferView::from_object) - .ok_or(ExnThrown)?; + let view = js::ArrayBufferView::from_jsval_throwing(scope, chunk, ())?; let length = view.byte_length(); let byte_offset = view.byte_offset(); let buffer = view.viewed_buffer(scope)?; @@ -2344,14 +2270,10 @@ fn byte_tee_clone_as_uint8array<'r>( fn byte_tee_forward_reader_error( scope: &Scope<'_>, state: ByteTeeState<'_>, - this_reader: &Object<'_>, + this_reader: ReadableStreamReader<'_>, ) -> Result<(), ExnThrown> { - let closed = reader_closed_promise_for(scope, this_reader); - let payload = pair_payload( - scope, - scope.root_value(state.as_value()), - scope.root_value(this_reader.as_value()), - )?; + let closed = this_reader.generic_closed_promise(scope); + let payload = pair_payload(scope, state, this_reader.as_object())?; support::react( scope, &closed, @@ -2366,11 +2288,12 @@ fn byte_tee_forward_rejected( payload: HandleValue<'_>, ) -> Result { let (state_v, this_reader_v) = pair_parts(scope, payload); - let state = ByteTeeState::from_jsval(scope, state_v, ()).unwrap(); + let state = cast_payload::(scope, state_v); let r = args.get(0); // If _thisReader_ is not _reader_ (the current reader), return. - let this_reader = Object::from_value(scope, *this_reader_v).map_err(|_| ExnThrown)?; - let current = byte_tee_reader_obj(scope, state); + let this_reader = + Object::from_value(scope, *this_reader_v).expect("payload reader is an object"); + let current = byte_tee_reader(scope, state).as_object(); if this_reader.as_raw() != current.as_raw() { return Ok(value::undefined()); } @@ -2387,20 +2310,28 @@ fn byte_tee_forward_rejected( /// Step 15 `pullWithDefaultReader`: switch to a default reader if needed, then /// issue a default read driving both branches. fn byte_tee_pull_with_default_reader(scope: &Scope<'_>, state: ByteTeeState<'_>) { - let reader_obj = byte_tee_reader_obj(scope, state); - if let Ok(byob) = reader_obj.cast::() { - debug_assert!(byob.data().read_into_requests.is_empty()); - let _ = readable_stream_byob_reader_release(scope, &byob); - let stream = byte_tee_stream(scope, state); - let new_reader = - acquire_readable_stream_default_reader(scope, &stream).expect("acquire default reader"); - let new_obj = Object::from_value(scope, new_reader.as_value()).expect("reader object"); - state.data_mut().reader.set(new_obj); - let _ = byte_tee_forward_reader_error(scope, state, &new_obj); - } - let reader = byte_tee_reader_obj(scope, state) - .cast::() - .expect("default reader"); + let reader = match byte_tee_reader(scope, state) { + ReadableStreamReader::Default(reader) => reader, + ReadableStreamReader::Byob(byob) => { + debug_assert!(byob.data().read_into_requests.is_empty()); + support::report_failure( + scope, + readable_stream_byob_reader_release(scope, &byob), + "ReadableByteStreamTee", + ); + let stream = byte_tee_stream(scope, state); + let new_reader = acquire_readable_stream_default_reader(scope, &stream) + .expect("acquire default reader"); + let new_generic = new_reader.as_generic(); + state.data_mut().reader = HeapReadableStreamReader::from(new_generic); + support::report_failure( + scope, + byte_tee_forward_reader_error(scope, state, new_generic), + "ReadableByteStreamTee", + ); + new_reader + } + }; readable_stream_default_reader_read( scope, reader, @@ -2415,30 +2346,34 @@ fn byte_tee_pull_with_default_reader(scope: &Scope<'_>, state: ByteTeeState<'_>) fn byte_tee_pull_with_byob_reader( scope: &Scope<'_>, state: ByteTeeState<'_>, - view: HandleValue<'_>, + view: Option>, for_branch2: bool, ) { - let reader_obj = byte_tee_reader_obj(scope, state); - if let Ok(default) = reader_obj.cast::() { - debug_assert!(default.data().read_requests.is_empty()); - let _ = readable_stream_default_reader_release(scope, &default); - let stream = byte_tee_stream(scope, state); - let new_reader = - acquire_readable_stream_byob_reader(scope, &stream).expect("acquire BYOB reader"); - let new_obj = Object::from_value(scope, new_reader.as_value()).expect("reader object"); - state.data_mut().reader.set(new_obj); - let _ = byte_tee_forward_reader_error(scope, state, &new_obj); - } - let view = match Object::from_value(scope, *view) - .ok() - .and_then(js::ArrayBufferView::from_object) - { - Some(v) => v, - None => return, + let reader = match byte_tee_reader(scope, state) { + ReadableStreamReader::Byob(reader) => reader, + ReadableStreamReader::Default(default) => { + debug_assert!(default.data().read_requests.is_empty()); + support::report_failure( + scope, + readable_stream_default_reader_release(scope, &default), + "ReadableByteStreamTee", + ); + let stream = byte_tee_stream(scope, state); + let new_reader = + acquire_readable_stream_byob_reader(scope, &stream).expect("acquire BYOB reader"); + let new_generic = new_reader.as_generic(); + state.data_mut().reader = HeapReadableStreamReader::from(new_generic); + support::report_failure( + scope, + byte_tee_forward_reader_error(scope, state, new_generic), + "ReadableByteStreamTee", + ); + new_reader + } + }; + let Some(view) = view else { + return; }; - let reader = byte_tee_reader_obj(scope, state) - .cast::() - .expect("BYOB reader"); readable_stream_byob_reader_read( scope, &reader, @@ -2477,7 +2412,7 @@ fn byte_tee_pull1( _args: CallbackArgs<'_>, payload: HandleValue<'_>, ) -> Result { - let state = ByteTeeState::from_jsval(scope, payload, ()).unwrap(); + let state = cast_payload::(scope, payload); byte_tee_pull(scope, state, false); // The returned resolved promise is internal (the branch controller only // attaches its pull reactions to it): the per-global reused instance serves. @@ -2489,7 +2424,7 @@ fn byte_tee_pull2( _args: CallbackArgs<'_>, payload: HandleValue<'_>, ) -> Result { - let state = ByteTeeState::from_jsval(scope, payload, ()).unwrap(); + let state = cast_payload::(scope, payload); byte_tee_pull(scope, state, true); // As in `byte_tee_pull1`: the per-global reused instance serves. Ok(Promise::shared_resolved_undefined(scope)?.as_value()) @@ -2533,7 +2468,7 @@ fn byte_tee_cancel1( args: CallbackArgs<'_>, payload: HandleValue<'_>, ) -> Result { - let state = ByteTeeState::from_jsval(scope, payload, ()).unwrap(); + let state = cast_payload::(scope, payload); Ok(byte_tee_cancel(scope, state, args.get(0), false)) } @@ -2542,7 +2477,7 @@ fn byte_tee_cancel2( args: CallbackArgs<'_>, payload: HandleValue<'_>, ) -> Result { - let state = ByteTeeState::from_jsval(scope, payload, ()).unwrap(); + let state = cast_payload::(scope, payload); Ok(byte_tee_cancel(scope, state, args.get(0), true)) } @@ -2595,9 +2530,9 @@ fn byte_tee_error_both_branches_and_cancel( fn byte_tee_default_microtask( scope: &Scope<'_>, _args: CallbackArgs<'_>, - state: HandleValue<'_>, + payload: HandleValue<'_>, ) -> Result { - let state = ByteTeeState::from_jsval(scope, state, ()).unwrap(); + let state = cast_payload::(scope, payload); let chunk = state.data().pending_chunk.get(scope); state.data().pending_chunk.set(value::undefined()); state.data_mut().read_again_for_branch1 = false; @@ -2707,7 +2642,7 @@ fn byte_tee_byob_microtask( _args: CallbackArgs<'_>, payload: HandleValue<'_>, ) -> Result { - let state = ByteTeeState::from_jsval(scope, payload, ()).unwrap(); + let state = cast_payload::(scope, payload); let chunk = state.data().pending_chunk.get(scope); state.data().pending_chunk.set(value::undefined()); let for_branch2 = state.data().pending_for_branch2; @@ -2903,8 +2838,7 @@ pub(crate) fn readable_stream_cancel<'r>( match state { // Step 2: If _stream_.`[[state]]` is "`closed`", return `a promise resolved with` undefined. ReadableStreamState::Closed => { - return Promise::new_resolved_with_value(scope, HandleValue::undefined()) - .expect("resolved promise"); + return resolved_undefined_promise(scope); } // Step 3: If _stream_.`[[state]]` is "`errored`", return `a promise rejected with` // _stream_.`[[storedError]]`. @@ -2931,10 +2865,7 @@ pub(crate) fn readable_stream_cancel<'r>( std::mem::take(&mut data.read_into_requests) }, |read_into_request| { - read_into_request - .root(scope) - .close_steps(scope, undef) - .expect("read-into request close steps"); + read_into_request.root(scope).close_steps(scope, undef); }, ); } @@ -2950,9 +2881,12 @@ pub(crate) fn readable_stream_cancel<'r>( }; // Step 8: Return the result of `reacting` to _sourceCancelPromise_ with a fulfillment step that // returns undefined. - let on_fulfilled = - Function::new_callback(scope, c"", 1, return_undefined, HandleValue::undefined()) - .expect("create reaction"); + let on_fulfilled = js::class::get_or_init_shared_function( + scope, + return_undefined as *const () as usize, + |scope| Function::new_callback(scope, c"", 1, return_undefined, ()), + ) + .expect("create reaction"); source_cancel_promise .then(scope, Some(*on_fulfilled), None) .expect("then") @@ -3006,10 +2940,7 @@ pub(crate) fn readable_stream_close(scope: &Scope<'_>, stream: &ReadableStream<' std::mem::take(&mut data.read_requests) }, |read_request| { - read_request - .root(scope) - .close_steps(scope) - .expect("read request close steps"); + read_request.root(scope).close_steps(scope); }, ); } @@ -3081,14 +3012,10 @@ pub(crate) fn readable_stream_fulfill_read_into_request( .root(scope); if done { // Step 6: If _done_ is true, perform _readIntoRequest_’s `close steps`, given _chunk_. - read_into_request - .close_steps(scope, chunk) - .expect("read-into request close steps"); + read_into_request.close_steps(scope, chunk); } else { // Step 7: Otherwise, perform _readIntoRequest_’s `chunk steps`, given _chunk_. - read_into_request - .chunk_steps(scope, chunk) - .expect("read-into request chunk steps"); + read_into_request.chunk_steps(scope, chunk); } } @@ -3121,14 +3048,10 @@ pub(crate) fn readable_stream_fulfill_read_request( .root(scope); if done { // Step 6: If _done_ is true, perform _readRequest_’s `close steps`. - read_request - .close_steps(scope) - .expect("read request close steps"); + read_request.close_steps(scope); } else { // Step 7: Otherwise, perform _readRequest_’s `chunk steps`, given _chunk_. - read_request - .chunk_steps(scope, chunk) - .expect("read request chunk steps"); + read_request.chunk_steps(scope, chunk); } } @@ -3215,8 +3138,7 @@ pub(crate) fn readable_stream_reader_generic_initialize( // Step 1: Set _reader_.`[[stream]]` to _stream_. reader.set_generic_stream(stream); // Step 2: Set _stream_.`[[reader]]` to _reader_. - let reader_obj = Object::from_value(scope, reader.as_reader_value()).map_err(|_| ExnThrown)?; - stream.data_mut().reader = Some(Heap::from(reader_obj)); + stream.data_mut().reader = Some(HeapReadableStreamReader::from(reader.as_generic())); let state = stream.data().state; let closed_promise = match state { // Step 3: If _stream_.`[[state]]` is "`readable`", Set _reader_.`[[closedPromise]]` to `a @@ -3305,10 +3227,7 @@ pub(crate) fn readable_stream_byob_reader_error_read_into_requests( std::mem::take(&mut data.read_into_requests) }, |read_into_request| { - read_into_request - .root(scope) - .error_steps(scope, e) - .expect("read-into request error steps"); + read_into_request.root(scope).error_steps(scope, e); }, ); } @@ -3355,8 +3274,7 @@ pub(crate) fn readable_stream_byob_reader_read( .take() .unwrap() .root(scope) - .error_steps(scope, stored_error) - .expect("read-into request error steps"); + .error_steps(scope, stored_error); } else { let controller = stream .byte_controller(scope) @@ -3398,10 +3316,7 @@ pub(crate) fn readable_stream_default_reader_error_read_requests( std::mem::take(&mut data.read_requests) }, |read_request| { - read_request - .root(scope) - .error_steps(scope, e) - .expect("read request error steps"); + read_request.root(scope).error_steps(scope, e); }, ); } @@ -3437,12 +3352,7 @@ pub(crate) fn readable_stream_default_reader_read( match state { // Step 4: If _stream_.`[[state]]` is "`closed`", perform _readRequest_’s `close steps`. ReadableStreamState::Closed => { - read_request - .take() - .unwrap() - .root(scope) - .close_steps(scope) - .expect("read request close steps"); + read_request.take().unwrap().root(scope).close_steps(scope); } // Step 5: Otherwise, if _stream_.`[[state]]` is "`errored`", perform _readRequest_’s `error // steps` given _stream_.`[[storedError]]`. @@ -3452,8 +3362,7 @@ pub(crate) fn readable_stream_default_reader_read( .take() .unwrap() .root(scope) - .error_steps(scope, stored_error) - .expect("read request error steps"); + .error_steps(scope, stored_error); } // Step 6: Otherwise, Assert: _stream_.`[[state]]` is "`readable`". Perform ! // _stream_.`[[controller]]`.`[[PullSteps]]`(_readRequest_). @@ -3563,12 +3472,8 @@ pub(crate) fn readable_stream_default_controller_call_pull_if_needed( // source's `pull(controller)`). let pull_algorithm = controller.data().pull_algorithm.get(scope); let receiver = controller.data().algorithm_receiver.get(scope); - let pull_promise = support::invoke_promise_algorithm( - scope, - pull_algorithm, - receiver, - &[scope.root_value(controller.as_value())], - ); + let pull_promise = + support::invoke_promise_algorithm(scope, pull_algorithm, receiver, &[controller]); // Step 7: `Upon fulfillment` of _pullPromise_, Set _controller_.`[[pulling]]` to false. If // _controller_.`[[pullAgain]]` is true, Set _controller_.`[[pullAgain]]` to false. // Perform ! `DefaultControllerCallPullIfNeeded`(_controller_). @@ -3576,13 +3481,12 @@ pub(crate) fn readable_stream_default_controller_call_pull_if_needed( // `DefaultControllerError`(_controller_, _e_). // (Steps 7 and 8 are implemented by `pull_promise_fulfilled` / `pull_promise_rejected`.) if controller.data().pull_fulfilled_fn.is_none() { - let payload = scope.root_value(controller.as_value()); - let fulfilled = Function::new_callback(scope, c"", 1, pull_promise_fulfilled, payload) + let fulfilled = Function::new_callback(scope, c"", 1, pull_promise_fulfilled, controller) .expect("create pull reaction"); - controller.data_mut().pull_fulfilled_fn = Some(Heap::from(fulfilled)); - let rejected = Function::new_callback(scope, c"", 1, pull_promise_rejected, payload) + controller.data_mut().pull_fulfilled_fn.set(fulfilled); + let rejected = Function::new_callback(scope, c"", 1, pull_promise_rejected, controller) .expect("create pull reaction"); - controller.data_mut().pull_rejected_fn = Some(Heap::from(rejected)); + controller.data_mut().pull_rejected_fn.set(rejected); } let fulfilled = controller .data() @@ -3636,12 +3540,9 @@ pub(crate) fn readable_stream_default_controller_clear_algorithms( controller: &ReadableStreamDefaultController<'_>, ) { // Step 1: Set _controller_.`[[pullAlgorithm]]` to undefined. - controller.data_mut().pull_algorithm.set(value::undefined()); + controller.data_mut().pull_algorithm = Algorithm::None; // Step 2: Set _controller_.`[[cancelAlgorithm]]` to undefined. - controller - .data_mut() - .cancel_algorithm - .set(value::undefined()); + controller.data_mut().cancel_algorithm = Algorithm::None; // Step 3: Set _controller_.`[[strategySizeAlgorithm]]` to undefined. controller .data_mut() @@ -3707,9 +3608,15 @@ pub(crate) fn readable_stream_default_controller_enqueue( // Absent size algorithm: the constant-1 algorithm. Ok(1.0) } else { - support::invoke_algorithm(scope, size_algorithm, undef, &[chunk]).and_then(|v| { + support::invoke_algorithm( + scope, + support::AlgorithmArg::Js(size_algorithm), + undef, + &[chunk], + ) + .and_then(|v| { use js::conversion::FromJSVal; - f64::from_jsval(scope, v, ()).map_err(|_| ExnThrown) + f64::from_jsval_throwing(scope, v, ()) }) }; let chunk_size = match chunk_size { @@ -3804,9 +3711,9 @@ pub(crate) fn set_up_readable_stream_default_controller( scope: &Scope<'_>, stream: &ReadableStream<'_>, controller: &ReadableStreamDefaultController<'_>, - start_algorithm: HandleValue<'_>, - pull_algorithm: HandleValue<'_>, - cancel_algorithm: HandleValue<'_>, + start_algorithm: AlgorithmArg<'_>, + pull_algorithm: AlgorithmArg<'_>, + cancel_algorithm: AlgorithmArg<'_>, algorithm_receiver: HandleValue<'_>, high_water_mark: f64, size_algorithm: HandleValue<'_>, @@ -3814,7 +3721,8 @@ pub(crate) fn set_up_readable_stream_default_controller( // Step 1: Assert: _stream_.`[[controller]]` is undefined. debug_assert!(stream.data().controller.is_none()); // Step 2: Set _controller_.`[[stream]]` to _stream_. - controller.data_mut().stream = Some(Heap::from(*stream)); + // (Already done by `ReadableStreamDefaultController::new`.) + debug_assert!(stream.eq_heap(&controller.data().stream)); // Step 3: Perform ! `ResetQueue`(_controller_). reset_queue(&mut *controller.data_mut()); // Step 4: Set _controller_.`[[started]]`, _controller_.`[[closeRequested]]`, @@ -3834,15 +3742,9 @@ pub(crate) fn set_up_readable_stream_default_controller( .set(size_algorithm.get()); controller.data_mut().strategy_hwm = high_water_mark; // Step 6: Set _controller_.`[[pullAlgorithm]]` to _pullAlgorithm_. - controller - .data_mut() - .pull_algorithm - .set(pull_algorithm.get()); + controller.data_mut().pull_algorithm = Algorithm::from(pull_algorithm); // Step 7: Set _controller_.`[[cancelAlgorithm]]` to _cancelAlgorithm_. - controller - .data_mut() - .cancel_algorithm - .set(cancel_algorithm.get()); + controller.data_mut().cancel_algorithm = Algorithm::from(cancel_algorithm); // (The algorithms close over `algorithm_receiver` — the underlying source — as their // `this` value; see the `algorithm_receiver` field.) controller @@ -3850,16 +3752,11 @@ pub(crate) fn set_up_readable_stream_default_controller( .algorithm_receiver .set(algorithm_receiver.get()); // Step 8: Set _stream_.`[[controller]]` to _controller_. - let controller_obj = Object::from_value(scope, controller.as_value()).map_err(|_| ExnThrown)?; - stream.data_mut().controller = Some(Heap::from(controller_obj)); + stream.data_mut().controller = Some(HeapReadableStreamController::from(*controller)); // Step 9: Let _startResult_ be the result of performing _startAlgorithm_. (This might throw an // exception.) - let start_result = support::invoke_algorithm( - scope, - start_algorithm, - algorithm_receiver, - &[scope.root_value(controller.as_value())], - )?; + let start_result = + support::invoke_algorithm(scope, start_algorithm, algorithm_receiver, &[controller])?; // Step 10: Let _startPromise_ be `a promise resolved with` _startResult_. // As in the writable controller's setup, WebIDL "a promise resolved // with" creates a new promise (it does not return a promise input @@ -3871,12 +3768,11 @@ pub(crate) fn set_up_readable_stream_default_controller( // Step 12: `Upon rejection` of _startPromise_ with reason _r_, Perform ! // `DefaultControllerError`(_controller_, _r_). // (Steps 11 and 12 are implemented by `start_promise_fulfilled` / `start_promise_rejected`.) - let payload = scope.root_value(controller.as_value()); support::react( scope, &start_promise, - Some((start_promise_fulfilled, payload)), - Some((start_promise_rejected, payload)), + Some((start_promise_fulfilled, controller)), + Some((start_promise_rejected, controller)), )?; Ok(()) } @@ -3892,40 +3788,27 @@ pub(crate) fn set_up_readable_stream_default_controller_from_underlying_source( size_algorithm: HandleValue<'_>, ) -> Result<(), ExnThrown> { // Step 1: Let _controller_ be a `new` ``ReadableStreamDefaultController``. - let controller = ReadableStreamDefaultController::new(scope)?; - // The start/pull/cancel algorithms are the raw callbacks, invoked with `this` = - // _underlyingSource_ (passed below as the algorithm receiver). An absent callback is - // represented as `undefined`, which the invoker treats as the resolved-undefined / - // constant algorithm. + let controller = ReadableStreamDefaultController::new(scope, *stream)?; + // The start/pull/cancel algorithms are the source's callbacks, invoked with `this` = + // _underlyingSource_ (passed below as the algorithm receiver). An absent callback is no + // algorithm, which the invoker treats as the resolved-undefined / constant algorithm. // Step 2: Let _startAlgorithm_ be an algorithm that returns undefined. // Step 5: If _underlyingSourceDict_["``start``"] `exists`, then set _startAlgorithm_ to an // algorithm which returns the result of `invoking` _underlyingSourceDict_["``start``"] // with argument list « _controller_ » and `callback this value` _underlyingSource_. - let start_algorithm = support::callback_member( - scope, - underlying_source_dict.start.as_ref(), - c"underlying source start must be a function", - )?; + let start_algorithm = AlgorithmArg::from_member(scope, underlying_source_dict.start.as_ref()); // Step 3: Let _pullAlgorithm_ be an algorithm that returns `a promise resolved with` undefined. // Step 6: If _underlyingSourceDict_["``pull``"] `exists`, then set _pullAlgorithm_ to an // algorithm which returns the result of `invoking` _underlyingSourceDict_["``pull``"] // with argument list « _controller_ » and `callback this value` _underlyingSource_. - let pull_algorithm = support::callback_member( - scope, - underlying_source_dict.pull.as_ref(), - c"underlying source pull must be a function", - )?; + let pull_algorithm = AlgorithmArg::from_member(scope, underlying_source_dict.pull.as_ref()); // Step 4: Let _cancelAlgorithm_ be an algorithm that returns `a promise resolved with` // undefined. // Step 7: If _underlyingSourceDict_["``cancel``"] `exists`, then set _cancelAlgorithm_ to an // algorithm which takes an argument _reason_ and returns the result of `invoking` // _underlyingSourceDict_["``cancel``"] with argument list « _reason_ » and `callback // this value` _underlyingSource_. - let cancel_algorithm = support::callback_member( - scope, - underlying_source_dict.cancel.as_ref(), - c"underlying source cancel must be a function", - )?; + let cancel_algorithm = AlgorithmArg::from_member(scope, underlying_source_dict.cancel.as_ref()); // Step 8: Perform ? `SetUpDefaultController`(_stream_, _controller_, // _startAlgorithm_, _pullAlgorithm_, _cancelAlgorithm_, _highWaterMark_, // _sizeAlgorithm_). @@ -3969,12 +3852,8 @@ pub(crate) fn readable_byte_stream_controller_call_pull_if_needed( // byte source's `pull(controller)`). let pull_algorithm = controller.data().pull_algorithm.get(scope); let receiver = controller.data().algorithm_receiver.get(scope); - let pull_promise = support::invoke_promise_algorithm( - scope, - pull_algorithm, - receiver, - &[scope.root_value(controller.as_value())], - ); + let pull_promise = + support::invoke_promise_algorithm(scope, pull_algorithm, receiver, &[controller]); // Step 7: `Upon fulfillment` of _pullPromise_, Set _controller_.`[[pulling]]` to false. If // _controller_.`[[pullAgain]]` is true, Set _controller_.`[[pullAgain]]` to false. // Perform ! `ByteStreamControllerCallPullIfNeeded`(_controller_). @@ -3982,13 +3861,14 @@ pub(crate) fn readable_byte_stream_controller_call_pull_if_needed( // `ByteStreamControllerError`(_controller_, _e_). // (Steps 7 and 8 are implemented by `byte_pull_promise_fulfilled` / `byte_pull_promise_rejected`.) if controller.data().pull_fulfilled_fn.is_none() { - let payload = controller.to_jsval(scope).unwrap(); - let fulfilled = Function::new_callback(scope, c"", 1, byte_pull_promise_fulfilled, payload) - .expect("create byte pull reaction"); - controller.data_mut().pull_fulfilled_fn = Some(Heap::from(fulfilled)); - let rejected = Function::new_callback(scope, c"", 1, byte_pull_promise_rejected, payload) - .expect("create byte pull reaction"); - controller.data_mut().pull_rejected_fn = Some(Heap::from(rejected)); + let fulfilled = + Function::new_callback(scope, c"", 1, byte_pull_promise_fulfilled, controller) + .expect("create byte pull reaction"); + controller.data_mut().pull_fulfilled_fn.set(fulfilled); + let rejected = + Function::new_callback(scope, c"", 1, byte_pull_promise_rejected, controller) + .expect("create byte pull reaction"); + controller.data_mut().pull_rejected_fn.set(rejected); } let fulfilled = controller .data() @@ -4011,12 +3891,9 @@ pub(crate) fn readable_byte_stream_controller_clear_algorithms( controller: &ReadableByteStreamController<'_>, ) { // Step 1: Set _controller_.`[[pullAlgorithm]]` to undefined. - controller.data_mut().pull_algorithm.set(value::undefined()); + controller.data_mut().pull_algorithm = Algorithm::None; // Step 2: Set _controller_.`[[cancelAlgorithm]]` to undefined. - controller - .data_mut() - .cancel_algorithm - .set(value::undefined()); + controller.data_mut().cancel_algorithm = Algorithm::None; } /// @@ -4064,8 +3941,11 @@ pub(crate) fn readable_byte_stream_controller_close( if bytes_filled % element_size != 0 { let e = make_type_error(scope, c"Insufficient bytes to fill the pending pull-into"); readable_byte_stream_controller_error(scope, controller, e); - js::exception::set_pending(scope, e, js::native::ExceptionStackBehavior::DoNotCapture); - return Err(ExnThrown); + return Err(js::exception::set_pending( + scope, + e, + js::native::ExceptionStackBehavior::DoNotCapture, + )); } } // Step 5: Perform ! `ByteStreamControllerClearAlgorithms`(_controller_). @@ -4183,13 +4063,7 @@ pub(crate) fn readable_byte_stream_controller_enqueue( // `ByteStreamControllerEnqueueDetachedPullIntoToQueue`(_controller_, // _firstPendingPullInto_). if !controller.data().pending_pull_intos.is_empty() { - let first_buffer = { - let data = controller.data(); - data.pending_pull_intos[0].buffer.get(scope) - }; - let first_buffer = first_buffer - .cast::() - .expect("pull-into descriptor buffer is an ArrayBuffer"); + let first_buffer = controller.data().pending_pull_intos[0].buffer.get(scope); if first_buffer.is_detached() { return Err(js::error::throw_type_error( scope, @@ -4200,7 +4074,7 @@ pub(crate) fn readable_byte_stream_controller_enqueue( let transferred = first_buffer.transfer(scope)?; controller.data_mut().pending_pull_intos[0] .buffer - .set(*transferred); + .set(transferred); if controller.data().pending_pull_intos[0].reader_type == ReaderType::None { readable_byte_stream_controller_enqueue_detached_pull_into_to_queue(scope, controller)?; } @@ -4295,7 +4169,7 @@ pub(crate) fn readable_byte_stream_controller_enqueue_chunk_to_queue( // Step 1: `Append` a new `readable byte stream queue entry` with `buffer` _buffer_, `byte // offset` _byteOffset_, and `byte length` _byteLength_ to _controller_.`[[queue]]`. controller.data_mut().queue.push_back(ByteQueueEntry { - buffer: Heap::from(*buffer), + buffer: Heap::from(buffer), byte_offset, byte_length, }); @@ -4324,12 +4198,11 @@ fn readable_byte_stream_controller_enqueue_cloned_chunk_to_queue( Err(_) => { let error = take_pending_or_undefined(scope); readable_byte_stream_controller_error(scope, controller, error); - js::exception::set_pending( + return Err(js::exception::set_pending( scope, error, js::native::ExceptionStackBehavior::DoNotCapture, - ); - return Err(ExnThrown); + )); } }; // Step 3: Perform ! `ByteStreamControllerEnqueueChunkToQueue`(_controller_, @@ -4346,7 +4219,7 @@ fn readable_byte_stream_controller_enqueue_detached_pull_into_to_queue( scope: &Scope<'_>, controller: &ReadableByteStreamController<'_>, ) -> Result<(), ExnThrown> { - let (reader_type, byte_offset, bytes_filled, buffer_obj) = { + let (reader_type, byte_offset, bytes_filled, buffer) = { let data = controller.data(); let head = &data.pending_pull_intos[0]; ( @@ -4363,9 +4236,6 @@ fn readable_byte_stream_controller_enqueue_detached_pull_into_to_queue( // _pullIntoDescriptor_’s `buffer`, _pullIntoDescriptor_’s `byte offset`, // _pullIntoDescriptor_’s `bytes filled`). if bytes_filled > 0 { - let buffer = buffer_obj - .cast::() - .expect("pull-into descriptor buffer is an ArrayBuffer"); readable_byte_stream_controller_enqueue_cloned_chunk_to_queue( scope, controller, @@ -4460,11 +4330,7 @@ fn descriptor_buffer<'r>( scope: &'r Scope<'_>, descriptor: &PullIntoDescriptor, ) -> js::ArrayBuffer<'r> { - descriptor - .buffer - .get(scope) - .cast::() - .expect("pull-into descriptor buffer is an ArrayBuffer") + descriptor.buffer.get(scope) } /// @@ -4536,9 +4402,6 @@ fn readable_byte_stream_controller_fill_pull_into_descriptor_from_queue( }; let bytes_to_copy = total_bytes_to_copy_remaining.min(head_byte_length); let dest_start = pull_into_descriptor.byte_offset + pull_into_descriptor.bytes_filled; - let queue_buffer = queue_buffer - .cast::() - .expect("queue entry buffer is an ArrayBuffer"); copy_data_block_bytes( descriptor_buffer, dest_start, @@ -4588,7 +4451,7 @@ pub(crate) fn readable_byte_stream_controller_fill_read_request_from_queue( // Consume the head entry: `into_parts` roots its buffer to the scope (and // drops the now-untraced `Heap` on its still-live pointer) before // `HandleQueueDrain` below can compact, so no stale pointer is left behind. - let (buffer_obj, byte_offset, byte_length) = controller + let (buffer, byte_offset, byte_length) = controller .data_mut() .queue .pop_front() @@ -4602,9 +4465,6 @@ pub(crate) fn readable_byte_stream_controller_fill_read_request_from_queue( readable_byte_stream_controller_handle_queue_drain(scope, controller); // Step 6: Let _view_ be ! `Construct`(``%Uint8Array%``, « _entry_’s `buffer`, _entry_’s // `byte offset`, _entry_’s `byte length` »). - let buffer = buffer_obj - .cast::() - .expect("queue entry buffer is an ArrayBuffer"); let view = js::Uint8Array::with_buffer(scope, buffer, byte_offset, byte_length)?; // Step 7: Perform _readRequest_’s `chunk steps`, given _view_. let view_value = scope.root_value(view.as_value()); @@ -4612,7 +4472,8 @@ pub(crate) fn readable_byte_stream_controller_fill_read_request_from_queue( .take() .unwrap() .root(scope) - .chunk_steps(scope, view_value) + .chunk_steps(scope, view_value); + Ok(()) } /// @@ -4631,7 +4492,7 @@ pub(crate) fn readable_byte_stream_controller_get_byob_request<'r>( // _view_. Set _controller_.`[[ReadableStreamBYOBRequest]]` to _byobRequest_. if controller.data().byob_request.is_none() && !controller.data().pending_pull_intos.is_empty() { - let (buffer_obj, byte_offset, bytes_filled, byte_length) = { + let (buffer, byte_offset, bytes_filled, byte_length) = { let data = controller.data(); let first = &data.pending_pull_intos[0]; ( @@ -4641,9 +4502,6 @@ pub(crate) fn readable_byte_stream_controller_get_byob_request<'r>( first.byte_length, ) }; - let buffer = buffer_obj - .cast::() - .expect("pull-into descriptor buffer is an ArrayBuffer"); let view = js::Uint8Array::with_buffer( scope, buffer, @@ -4653,9 +4511,12 @@ pub(crate) fn readable_byte_stream_controller_get_byob_request<'r>( .expect("constructing a Uint8Array over the pending pull-into"); let byob_request = ReadableStreamBYOBRequest::new(scope).expect("creating a ReadableStreamBYOBRequest"); - byob_request.data_mut().controller = Some(Heap::from(*controller)); - byob_request.data_mut().view.set(view.as_value()); - controller.data_mut().byob_request = Some(Heap::from(byob_request)); + byob_request.data_mut().controller.set(*controller); + byob_request + .data_mut() + .view + .set(view.as_array_buffer_view()); + controller.data_mut().byob_request.set(byob_request); } // Step 2: Return _controller_.`[[ReadableStreamBYOBRequest]]`. controller.data().byob_request.get(scope) @@ -4719,7 +4580,7 @@ pub(crate) fn readable_byte_stream_controller_invalidate_byob_request( // Step 2: Set _controller_.`[[ReadableStreamBYOBRequest]]`.`[[controller]]` to undefined. byob_request.data_mut().controller = None; // Step 3: Set _controller_.`[[ReadableStreamBYOBRequest]]`.`[[view]]` to null. - byob_request.data_mut().view.set(value::null()); + byob_request.data_mut().view = None; // Step 4: Set _controller_.`[[ReadableStreamBYOBRequest]]` to null. controller.data_mut().byob_request = None; } @@ -4850,8 +4711,7 @@ pub(crate) fn readable_byte_stream_controller_pull_into( .take() .unwrap() .root(scope) - .error_steps(scope, error) - .expect("read-into request error steps"); + .error_steps(scope, error); return; } }; @@ -4866,8 +4726,7 @@ pub(crate) fn readable_byte_stream_controller_pull_into( .take() .unwrap() .root(scope) - .error_steps(scope, error) - .expect("read-into request error steps"); + .error_steps(scope, error); return; } }; @@ -4883,7 +4742,7 @@ pub(crate) fn readable_byte_stream_controller_pull_into( // must stay current (and never drop stale). `take()` hands it back when a // branch appends it to the (traced) `[[pendingPullIntos]]`. let mut pull_into_descriptor = RootedTraceableBox::new(Some(PullIntoDescriptor { - buffer: Heap::from(*buffer), + buffer: Heap::from(buffer), buffer_byte_length, byte_offset, byte_length, @@ -4917,8 +4776,7 @@ pub(crate) fn readable_byte_stream_controller_pull_into( .take() .unwrap() .root(scope) - .close_steps(scope, empty_view) - .expect("read-into request close steps"); + .close_steps(scope, empty_view); return; } // Step 16: If _controller_.`[[queueTotalSize]]` > 0, If ! @@ -4947,8 +4805,7 @@ pub(crate) fn readable_byte_stream_controller_pull_into( .take() .unwrap() .root(scope) - .chunk_steps(scope, filled_view) - .expect("read-into request chunk steps"); + .chunk_steps(scope, filled_view); return; } if controller.data().close_requested { @@ -4958,8 +4815,7 @@ pub(crate) fn readable_byte_stream_controller_pull_into( .take() .unwrap() .root(scope) - .error_steps(scope, e) - .expect("read-into request error steps"); + .error_steps(scope, e); return; } } @@ -5032,7 +4888,7 @@ pub(crate) fn readable_byte_stream_controller_respond( descriptor_buffer(scope, &controller.data().pending_pull_intos[0]).transfer(scope)?; controller.data_mut().pending_pull_intos[0] .buffer - .set(*transferred); + .set(transferred); readable_byte_stream_controller_respond_internal(scope, controller, bytes_written) } @@ -5332,7 +5188,7 @@ pub(crate) fn readable_byte_stream_controller_respond_with_new_view( let transferred = view_buffer.transfer(scope)?; controller.data_mut().pending_pull_intos[0] .buffer - .set(*transferred); + .set(transferred); readable_byte_stream_controller_respond_internal(scope, controller, view_byte_length) } @@ -5409,12 +5265,12 @@ pub(crate) fn set_up_readable_byte_stream_controller( scope: &Scope<'_>, stream: &ReadableStream<'_>, controller: &ReadableByteStreamController<'_>, - start_algorithm: HandleValue<'_>, - pull_algorithm: HandleValue<'_>, - cancel_algorithm: HandleValue<'_>, + start_algorithm: AlgorithmArg<'_>, + pull_algorithm: AlgorithmArg<'_>, + cancel_algorithm: AlgorithmArg<'_>, algorithm_receiver: HandleValue<'_>, high_water_mark: f64, - auto_allocate_chunk_size: Option, + auto_allocate_chunk_size: Option, ) -> Result<(), ExnThrown> { // Step 1: Assert: _stream_.`[[controller]]` is undefined. debug_assert!(stream.data().controller.is_none()); @@ -5422,11 +5278,11 @@ pub(crate) fn set_up_readable_byte_stream_controller( // `IsInteger`(_autoAllocateChunkSize_) is true. Assert: _autoAllocateChunkSize_ is // positive. if let Some(size) = auto_allocate_chunk_size { - debug_assert_eq!(size.fract(), 0.0); - debug_assert!(size > 0.0); + debug_assert!(size > 0); } // Step 3: Set _controller_.`[[stream]]` to _stream_. - controller.data_mut().stream = Some(Heap::from(*stream)); + // (Already done by `ReadableByteStreamController::new`.) + debug_assert!(stream.eq_heap(&controller.data().stream)); // Step 4: Set _controller_.`[[pullAgain]]` and _controller_.`[[pulling]]` to false. { let mut data = controller.data_mut(); @@ -5446,15 +5302,9 @@ pub(crate) fn set_up_readable_byte_stream_controller( // Step 8: Set _controller_.`[[strategyHWM]]` to _highWaterMark_. controller.data_mut().strategy_hwm = high_water_mark; // Step 9: Set _controller_.`[[pullAlgorithm]]` to _pullAlgorithm_. - controller - .data_mut() - .pull_algorithm - .set(pull_algorithm.get()); + controller.data_mut().pull_algorithm = Algorithm::from(pull_algorithm); // Step 10: Set _controller_.`[[cancelAlgorithm]]` to _cancelAlgorithm_. - controller - .data_mut() - .cancel_algorithm - .set(cancel_algorithm.get()); + controller.data_mut().cancel_algorithm = Algorithm::from(cancel_algorithm); // (The algorithms close over `algorithm_receiver` — the underlying byte source — as their // `this` value; see the `algorithm_receiver` field.) controller @@ -5466,15 +5316,10 @@ pub(crate) fn set_up_readable_byte_stream_controller( // Step 12: Set _controller_.`[[pendingPullIntos]]` to a new empty `list`. controller.data_mut().pending_pull_intos.clear(); // Step 13: Set _stream_.`[[controller]]` to _controller_. - let controller_obj = Object::from_value(scope, controller.as_value()).map_err(|_| ExnThrown)?; - stream.data_mut().controller = Some(Heap::from(controller_obj)); + stream.data_mut().controller = Some(HeapReadableStreamController::from(*controller)); // Step 14: Let _startResult_ be the result of performing _startAlgorithm_. - let start_result = support::invoke_algorithm( - scope, - start_algorithm, - algorithm_receiver, - &[scope.root_value(controller.as_value())], - )?; + let start_result = + support::invoke_algorithm(scope, start_algorithm, algorithm_receiver, &[controller])?; // Step 15: Let _startPromise_ be `a promise resolved with` _startResult_. // As in the default controller's setup, WebIDL "a promise resolved with" creates a new // promise (it does not return a promise input as-is the way `Promise.resolve` does). @@ -5486,12 +5331,11 @@ pub(crate) fn set_up_readable_byte_stream_controller( // `ByteStreamControllerError`(_controller_, _r_). // (Steps 16 and 17 are implemented by `byte_start_promise_fulfilled` / // `byte_start_promise_rejected`.) - let payload = controller.to_jsval(scope).unwrap(); support::react( scope, &start_promise, - Some((byte_start_promise_fulfilled, payload)), - Some((byte_start_promise_rejected, payload)), + Some((byte_start_promise_fulfilled, controller)), + Some((byte_start_promise_rejected, controller)), )?; Ok(()) } @@ -5506,58 +5350,34 @@ pub(crate) fn set_up_readable_byte_stream_controller_from_underlying_source( high_water_mark: f64, ) -> Result<(), ExnThrown> { // Step 1: Let _controller_ be a `new` ``ReadableByteStreamController``. - let controller = ReadableByteStreamController::new(scope)?; - // The start/pull/cancel algorithms are the raw callbacks, invoked with `this` = - // _underlyingSource_ (passed below as the algorithm receiver). An absent callback is - // represented as `undefined`, which the invoker treats as the resolved-undefined / - // constant algorithm. + let controller = ReadableByteStreamController::new(scope, *stream)?; + // The start/pull/cancel algorithms are the source's callbacks, invoked with `this` = + // _underlyingSource_ (passed below as the algorithm receiver). An absent callback is no + // algorithm, which the invoker treats as the resolved-undefined / constant algorithm. // Step 2: Let _startAlgorithm_ be an algorithm that returns undefined. // Step 5: If _underlyingSourceDict_["``start``"] `exists`, then set _startAlgorithm_ to an // algorithm which returns the result of `invoking` _underlyingSourceDict_["``start``"] // with argument list « _controller_ » and `callback this value` _underlyingSource_. - let start_algorithm = support::callback_member( - scope, - underlying_source_dict.start.as_ref(), - c"underlying source start must be a function", - )?; + let start_algorithm = AlgorithmArg::from_member(scope, underlying_source_dict.start.as_ref()); // Step 3: Let _pullAlgorithm_ be an algorithm that returns `a promise resolved with` undefined. // Step 6: If _underlyingSourceDict_["``pull``"] `exists`, then set _pullAlgorithm_ to an // algorithm which returns the result of `invoking` _underlyingSourceDict_["``pull``"] // with argument list « _controller_ » and `callback this value` _underlyingSource_. - let pull_algorithm = support::callback_member( - scope, - underlying_source_dict.pull.as_ref(), - c"underlying source pull must be a function", - )?; + let pull_algorithm = AlgorithmArg::from_member(scope, underlying_source_dict.pull.as_ref()); // Step 4: Let _cancelAlgorithm_ be an algorithm that returns `a promise resolved with` // undefined. // Step 7: If _underlyingSourceDict_["``cancel``"] `exists`, then set _cancelAlgorithm_ to an // algorithm which takes an argument _reason_ and returns the result of `invoking` // _underlyingSourceDict_["``cancel``"] with argument list « _reason_ » and `callback // this value` _underlyingSource_. - let cancel_algorithm = support::callback_member( - scope, - underlying_source_dict.cancel.as_ref(), - c"underlying source cancel must be a function", - )?; + let cancel_algorithm = AlgorithmArg::from_member(scope, underlying_source_dict.cancel.as_ref()); // Step 8: Let _autoAllocateChunkSize_ be _underlyingSourceDict_["``autoAllocateChunkSize``"], // if it `exists`, or undefined otherwise. - // WebIDL `[EnforceRange] unsigned long long`: a non-finite value, or one that's - // negative after truncating toward zero, throws a `TypeError`. (The upper - // bound 2^64-1 is beyond `f64` precision.) Applied here because the dictionary - // member is held as `f64` rather than enforced during conversion. - let auto_allocate_chunk_size = match underlying_source_dict.auto_allocate_chunk_size { - Some(n) if !n.is_finite() || n.trunc() < 0.0 => { - return Err(js::error::throw_type_error( - scope, - c"autoAllocateChunkSize is out of range", - )); - } - Some(n) => Some(n.trunc()), - None => None, - }; + let auto_allocate_chunk_size = underlying_source_dict + .auto_allocate_chunk_size + .map(|size| size.0); // Step 9: If _autoAllocateChunkSize_ is 0, then throw a ``TypeError`` exception. - if auto_allocate_chunk_size == Some(0.0) { + if auto_allocate_chunk_size == Some(0) { return Err(js::error::throw_type_error( scope, c"autoAllocateChunkSize cannot be 0", diff --git a/crates/builtins/web-streams/src/readable/async_iterator.rs b/crates/builtins/web-streams/src/readable/async_iterator.rs index 504e1cd1..8065b24d 100644 --- a/crates/builtins/web-streams/src/readable/async_iterator.rs +++ b/crates/builtins/web-streams/src/readable/async_iterator.rs @@ -6,8 +6,8 @@ //! //! `ReadableStream.prototype.values()` and `[Symbol.asyncIterator]()` return an //! instance of this interface. Its prototype chains to `%AsyncIteratorPrototype%` -//! (via `js_proto = "AsyncIterator"`), which supplies `[Symbol.asyncIterator]` -//! returning `this`. The `next`/`return` methods implement WebIDL §3.7.10.2's +//! (set up in `add_to_global`), which supplies `[Symbol.asyncIterator]` returning +//! `this`. The `next`/`return` methods implement WebIDL §3.7.10.2's //! default async iterator semantics: calls are serialized through an "ongoing //! promise", and a finished iterator yields `{ value: undefined, done: true }`. @@ -17,6 +17,7 @@ use super::read_request::ReadRequest; use crate::algorithms::{pair_parts, pair_payload}; use core_runtime::{webidl_interface, webidl_methods}; use js::error::ExnThrown; +use js::function::cast_payload; use js::gc::handle::{Heap, OptionHeapExt}; use js::gc::scope::Scope; use js::iteration::create_iter_result; @@ -33,7 +34,7 @@ use js::{Function, Object, Promise}; #[webidl_interface(hidden, to_string_tag = "ReadableStream AsyncIterator")] pub struct ReadableStreamAsyncIterator { /// The `DefaultReader` acquired for this iteration. - pub(crate) reader: Option>, + pub(crate) reader: Heap, /// The `preventCancel` option captured at creation. #[no_trace] pub(crate) prevent_cancel: bool, @@ -55,34 +56,63 @@ pub struct ReadableStreamAsyncIterator { #[webidl_methods] impl ReadableStreamAsyncIterator { - /// Not exposed to JS (see `hidden`). Produces the default-initialized data; - /// the fields are populated by `ReadableStream.prototype.values`. - fn new() -> Self { - ReadableStreamAsyncIteratorImpl::default() + /// Not exposed to JS, called by `ReadableStream.prototype.values`. + fn new(reader: DefaultReader<'_>, prevent_cancel: bool) -> Self { + ReadableStreamAsyncIteratorImpl { + reader: Heap::from(reader), + prevent_cancel, + ..Default::default() + } } /// : `next`. #[method] fn next<'r>(&self, scope: &'r Scope<'_>) -> Result, ExnThrown> { - // Serialize on the ongoing promise: run the next steps now if none is - // pending, otherwise after the pending one settles (either way). - let prev = self.data().ongoing_promise.get(scope); - let ongoing = match prev { - Some(prev) => { - if self.data().after_ongoing_next_fn.is_none() { - let cb = Function::new_callback(scope, c"", 1, after_ongoing_next, self)?; - self.data_mut().after_ongoing_next_fn = Some(Heap::from(cb)); - } - let cb = self - .data() - .after_ongoing_next_fn - .get(scope) - .expect("created above"); - prev.then(scope, Some(*cb), Some(*cb))? + // Step 1: Let _interface_ be the `interface` for which the `asynchronous iterator prototype + // object` exists. + // Step 2: Let _thisValidationPromiseCapability_ be ! NewPromiseCapability(%Promise%). + // Step 3: Let _thisValue_ be the *this* value. + // Step 4: Let _object_ be Completion(ToObject(_thisValue_)). + // Step 5: IfAbruptRejectPromise(_object_, _thisValidationPromiseCapability_). + // Step 6: If _object_ `is a platform object`, then `perform a security check`, passing: + // the platform object _object_, the identifier "`next`", and the type + // "`method`". If this threw an exception _e_, then: Perform ! Call( + // _thisValidationPromiseCapability_.[[Reject]], undefined, « _e_ »). Return + // _thisValidationPromiseCapability_.[[Promise]]. + // Step 7: If _object_ is not a `default asynchronous iterator object` for _interface_, + // then: Let _error_ be a new TypeError. Perform ! Call( + // _thisValidationPromiseCapability_.[[Reject]], undefined, « _error_ »). Return + // _thisValidationPromiseCapability_.[[Promise]]. + // (Steps 1-7: the `Result` method style brand-checks `this` and returns a + // rejected promise on failure. There are no security checks in this runtime.) + // Step 8: Let _nextSteps_ be the following steps: (implemented in `run_next_steps`) + // Step 9: Let _ongoingPromise_ be _object_'s `ongoing promise`. + let ongoing_promise = self.data().ongoing_promise.get(scope); + // Step 10: If _ongoingPromise_ is not null, then: + let ongoing = if let Some(ongoing_promise) = ongoing_promise { + // Step 10.1: Let _afterOngoingPromiseCapability_ be ! NewPromiseCapability(%Promise%). + // Step 10.2: Let _onSettled_ be CreateBuiltinFunction(_nextSteps_, 0, "", « »). + if self.data().after_ongoing_next_fn.is_none() { + let cb = Function::new_callback(scope, c"", 1, after_ongoing_next, self)?; + self.data_mut().after_ongoing_next_fn.set(cb); } - None => run_next_steps(scope, self)?, + let on_settled = self + .data() + .after_ongoing_next_fn + .get(scope) + .expect("created above"); + // Step 10.3: Perform PerformPromiseThen(_ongoingPromise_, _onSettled_, _onSettled_, + // _afterOngoingPromiseCapability_). + // Step 10.4: Set _object_'s `ongoing promise` to + // _afterOngoingPromiseCapability_.[[Promise]]. + ongoing_promise.then(scope, Some(*on_settled), Some(*on_settled))? + } else { + // Step 11: Otherwise: + // Step 11.1: Set _object_'s `ongoing promise` to the result of running _nextSteps_. + run_next_steps(scope, self)? }; - self.data_mut().ongoing_promise = Some(Heap::from(ongoing)); + self.data_mut().ongoing_promise.set(ongoing); + // Step 12: Return _object_'s `ongoing promise`. Ok(ongoing) } @@ -94,30 +124,55 @@ impl ReadableStreamAsyncIterator { value: Option>, ) -> Result, ExnThrown> { let value = value.unwrap_or(HandleValue::undefined()); - let prev = self.data().ongoing_promise.get(scope); - let ongoing = match prev { - Some(prev) => { - let payload = pair_payload(scope, scope.root_value(self.as_value()), value)?; - let cb = Function::new_callback(scope, c"", 1, after_ongoing_return, payload)?; - prev.then(scope, Some(*cb), Some(*cb))? - } - None => run_return_steps(scope, self, value)?, + // Step 1: Let _interface_ be the `interface` for which the `asynchronous iterator prototype + // object` exists. + // Step 2: Let _returnPromiseCapability_ be ! NewPromiseCapability(%Promise%). + // Step 3: Let _thisValue_ be the *this* value. + // Step 4: Let _object_ be Completion(ToObject(_thisValue_)). + // Step 5: IfAbruptRejectPromise(_object_, _returnPromiseCapability_). + // Step 6: If _object_ `is a platform object`, then `perform a security check`, passing: + // the platform object _object_, the identifier "`return`", and the type + // "`method`". If this threw an exception _e_, then: Perform ! Call( + // _returnPromiseCapability_.[[Reject]], undefined, « _e_ »). Return + // _returnPromiseCapability_.[[Promise]]. + // Step 7: If _object_ is not a `default asynchronous iterator object` for _interface_, + // then: Let _error_ be a new TypeError. Perform ! Call( + // _returnPromiseCapability_.[[Reject]], undefined, « _error_ »). Return + // _returnPromiseCapability_.[[Promise]]. + // (Steps 1-7: the `Result` method style brand-checks `this` and returns a + // rejected promise on failure. There are no security checks in this runtime.) + // Step 8: Let _returnSteps_ be the following steps: (implemented in `run_return_steps`) + // Step 9: Let _ongoingPromise_ be _object_'s `ongoing promise`. + let ongoing_promise = self.data().ongoing_promise.get(scope); + // Step 10: If _ongoingPromise_ is not null, then: + let ongoing = if let Some(ongoing_promise) = ongoing_promise { + // Step 10.1: Let _afterOngoingPromiseCapability_ be ! NewPromiseCapability(%Promise%). + // Step 10.2: Let _onSettled_ be CreateBuiltinFunction(_returnSteps_, 0, "", « »). + let payload = pair_payload(scope, self, value)?; + let on_settled = Function::new_callback(scope, c"", 1, after_ongoing_return, payload)?; + // Step 10.3: Perform PerformPromiseThen(_ongoingPromise_, _onSettled_, _onSettled_, + // _afterOngoingPromiseCapability_). + // Step 10.4: Set _object_'s `ongoing promise` to + // _afterOngoingPromiseCapability_.[[Promise]]. + ongoing_promise.then(scope, Some(*on_settled), Some(*on_settled))? + } else { + // Step 11: Otherwise: + // Step 11.1: Set _object_'s `ongoing promise` to the result of running _returnSteps_. + run_return_steps(scope, self, value)? }; - self.data_mut().ongoing_promise = Some(Heap::from(ongoing)); - Ok(ongoing) + self.data_mut().ongoing_promise.set(ongoing); + // Step 12: Let _fulfillSteps_ be the following steps: + // Step 12.1: Return CreateIteratorResultObject(_value_, true). + // (Implemented in `return_fulfilled`.) + // Step 13: Let _onFulfilled_ be CreateBuiltinFunction(_fulfillSteps_, 1, "", « »). + let on_fulfilled = Function::new_callback(scope, c"", 1, return_fulfilled, value)?; + // Step 14: Perform PerformPromiseThen(_object_'s `ongoing promise`, _onFulfilled_, + // undefined, _returnPromiseCapability_). + // Step 15: Return _returnPromiseCapability_.[[Promise]]. + ongoing.then(scope, Some(*on_fulfilled), None) } } -fn iter_from_value<'r>( - scope: &'r Scope<'_>, - v: HandleValue<'_>, -) -> Result, ExnThrown> { - Object::from_value(scope, *v) - .map_err(|_| ExnThrown)? - .cast::() - .map_err(|_| ExnThrown) -} - /// The async iterator's `[[ongoingPromise]]` next continuation (payload = the /// iterator): run the next steps regardless of how the previous call settled. fn after_ongoing_next( @@ -125,7 +180,7 @@ fn after_ongoing_next( _args: CallbackArgs<'_>, payload: HandleValue<'_>, ) -> Result { - let iter = iter_from_value(scope, payload)?; + let iter = cast_payload::(scope, payload); Ok(run_next_steps(scope, &iter)?.as_value()) } @@ -137,20 +192,34 @@ fn after_ongoing_return( payload: HandleValue<'_>, ) -> Result { let (iter_v, value) = pair_parts(scope, payload); - let iter = iter_from_value(scope, iter_v)?; + let iter = cast_payload::(scope, iter_v); Ok(run_return_steps(scope, &iter, value)?.as_value()) } -/// WebIDL next steps: a finished iterator yields end-of-iteration; otherwise read -/// the next chunk and map the result. +/// The _nextSteps_ of the asynchronous iterator `next` method. fn run_next_steps<'r>( scope: &'r Scope<'_>, iter: &ReadableStreamAsyncIterator<'_>, ) -> Result, ExnThrown> { + // Step 1: Let _nextPromiseCapability_ be ! NewPromiseCapability(%Promise%). + // Step 2: If _object_'s `is finished` is true, then: if iter.data().is_finished { + // Step 2.1: Let _result_ be CreateIteratorResultObject(undefined, true). let result = create_iter_result(scope, HandleValue::undefined(), true)?; + // Step 2.2: Perform ! Call(_nextPromiseCapability_.[[Resolve]], undefined, « _result_ »). + // Step 2.3: Return _nextPromiseCapability_.[[Promise]]. return Promise::new_resolved_with_value(scope, result); } + // Step 3: Let _kind_ be _object_'s `kind`. + // (A value asynchronously iterable declaration has no kind to use.) + // Step 4: Let _nextPromise_ be the result of `getting the next iteration result` with + // _object_'s `target` and _object_. + // Step 5: Let _fulfillSteps_ be the following steps, given _next_: (implemented in + // `next_fulfilled`) + // Step 6: Let _onFulfilled_ be CreateBuiltinFunction(_fulfillSteps_, 1, "", « »). + // Step 7: Let _rejectSteps_ be the following steps, given _reason_: (implemented in + // `next_rejected`) + // Step 8: Let _onRejected_ be CreateBuiltinFunction(_rejectSteps_, 1, "", « »). // The unique sentinel the close steps resolve the next-iteration promise // with (recognized by identity in `next_fulfilled`) and the reaction // callbacks are per-iterator; create them on the first `next()` and reuse @@ -158,12 +227,11 @@ fn run_next_steps<'r>( if iter.data().next_fulfilled_fn.is_none() { // TODO: share the sentinel per-global. let sentinel = Object::new_plain(scope)?; - iter.data_mut().end_of_iteration = Some(Heap::from(sentinel)); - let payload = scope.root_value(iter.as_value()); - let on_f = Function::new_callback(scope, c"", 1, next_fulfilled, payload)?; - iter.data_mut().next_fulfilled_fn = Some(Heap::from(on_f)); - let on_r = Function::new_callback(scope, c"", 1, next_rejected, payload)?; - iter.data_mut().next_rejected_fn = Some(Heap::from(on_r)); + iter.data_mut().end_of_iteration.set(sentinel); + let on_f = Function::new_callback(scope, c"", 1, next_fulfilled, iter)?; + iter.data_mut().next_fulfilled_fn.set(on_f); + let on_r = Function::new_callback(scope, c"", 1, next_rejected, iter)?; + iter.data_mut().next_rejected_fn.set(on_r); } let sentinel = iter .data() @@ -181,9 +249,10 @@ fn run_next_steps<'r>( .next_rejected_fn .get(scope) .expect("created above"); - next_promise - .then(scope, Some(*on_f), Some(*on_r)) - .map_err(|_| ExnThrown) + // Step 9: Perform PerformPromiseThen(_nextPromise_, _onFulfilled_, _onRejected_, + // _nextPromiseCapability_). + // Step 10: Return _nextPromiseCapability_.[[Promise]]. + next_promise.then(scope, Some(*on_f), Some(*on_r)) } /// The fulfill steps of `next()`: build the `{ value, done }` result from the @@ -196,33 +265,46 @@ fn next_fulfilled( args: CallbackArgs<'_>, payload: HandleValue<'_>, ) -> Result { - let iter = iter_from_value(scope, payload)?; + let iter = cast_payload::(scope, payload); let sentinel = iter .data() .end_of_iteration .get(scope) .expect("sentinel is created with this callback"); let next = args.get(0); - // If _next_ is end of iteration: set the iterator finished and return - // CreateIterResultObject(undefined, true). + // Step 1: Set _object_'s `ongoing promise` to null. + iter.data_mut().ongoing_promise = None; + // Step 2: If _next_ is `end of iteration`, then: if next.get() == sentinel.as_value() { + // Step 2.1: Set _object_'s `is finished` to true. iter.data_mut().is_finished = true; + // Step 2.2: Return CreateIteratorResultObject(undefined, true). let result = create_iter_result(scope, HandleValue::undefined(), true)?; return Ok(result.as_value()); } - // Otherwise return CreateIterResultObject(next, false). + // Step 3: Otherwise, if _interface_ has a `pair asynchronously iterable declaration`: + // (`ReadableStream` has a value declaration.) + // Step 4: Otherwise: + // Step 4.1: Assert: _interface_ has a `value asynchronously iterable declaration`. + // Step 4.2: Assert: _next_ is a value of the type that appears in the declaration. + // Step 4.3: Let _value_ be _next_, `converted to a JavaScript value`. + // Step 4.4: Return CreateIteratorResultObject(_value_, false). let result = create_iter_result(scope, next, false)?; Ok(result.as_value()) } -/// The rejection of `get the next iteration result`: mark finished and rethrow. +/// The _rejectSteps_ of the asynchronous iterator `next` method. fn next_rejected( scope: &Scope<'_>, args: CallbackArgs<'_>, payload: HandleValue<'_>, ) -> Result { - let iter = iter_from_value(scope, payload)?; + let iter = cast_payload::(scope, payload); + // Step 1: Set _object_'s `ongoing promise` to null. + iter.data_mut().ongoing_promise = None; + // Step 2: Set _object_'s `is finished` to true. iter.data_mut().is_finished = true; + // Step 3: Throw _reason_. Err(js::exception::set_pending( scope, args.get(0), @@ -230,35 +312,37 @@ fn next_rejected( )) } -/// WebIDL return steps: a finished iterator resolves with `{ value, done: true }`; -/// otherwise mark finished, run the asynchronous iterator return, then wrap. +/// The _returnSteps_ of the asynchronous iterator `return` method. fn run_return_steps<'r>( scope: &'r Scope<'_>, iter: &ReadableStreamAsyncIterator<'_>, value: HandleValue<'r>, ) -> Result, ExnThrown> { + // Step 1: Let _returnPromiseCapability_ be ! NewPromiseCapability(%Promise%). + // Step 2: If _object_'s `is finished` is true, then: if iter.data().is_finished { + // Step 2.1: Let _result_ be CreateIteratorResultObject(_value_, true). let result = create_iter_result(scope, value, true)?; + // Step 2.2: Perform ! Call(_returnPromiseCapability_.[[Resolve]], undefined, + // « _result_ »). + // Step 2.3: Return _returnPromiseCapability_.[[Promise]]. return Promise::new_resolved_with_value(scope, result); } + // Step 3: Set _object_'s `is finished` to true. iter.data_mut().is_finished = true; - let return_promise = asynchronous_iterator_return(scope, iter, value)?; - let payload = pair_payload(scope, scope.root_value(iter.as_value()), value)?; - let on_f = Function::new_callback(scope, c"", 1, return_fulfilled, payload)?; - return_promise - .then(scope, Some(*on_f), None) - .map_err(|_| ExnThrown) + // Step 4: Return the result of running the `asynchronous iterator return` algorithm for + // _interface_, given _object_'s `target`, _object_, and _value_. + asynchronous_iterator_return(scope, iter, value) } -/// The fulfillment of the asynchronous iterator return: wrap the argument as -/// `{ value, done: true }` (payload = `[iterator, value]`). +/// The _fulfillSteps_ of the asynchronous iterator `return` method (payload = the +/// `return` argument). fn return_fulfilled( scope: &Scope<'_>, _args: CallbackArgs<'_>, payload: HandleValue<'_>, ) -> Result { - let (_iter_v, value) = pair_parts(scope, payload); - let result = create_iter_result(scope, value, true)?; + let result = create_iter_result(scope, payload, true)?; Ok(result.as_value()) } @@ -308,7 +392,7 @@ fn iter_reader<'r>( scope: &'r Scope<'_>, iter: &ReadableStreamAsyncIterator<'_>, ) -> DefaultReader<'r> { - iter.data().reader.get(scope).expect("reader is set") + iter.data().reader.get(scope) } // --- ReadRequest::AsyncIter step bodies (called from `read_request.rs`) ------- diff --git a/crates/builtins/web-streams/src/readable/byob_reader.rs b/crates/builtins/web-streams/src/readable/byob_reader.rs index 709543b2..a30c1f47 100644 --- a/crates/builtins/web-streams/src/readable/byob_reader.rs +++ b/crates/builtins/web-streams/src/readable/byob_reader.rs @@ -60,73 +60,68 @@ impl BYOBReader { view: ArrayBufferView<'_>, options: Option, ) -> Result, ExnThrown> { - // WebIDL: `min` is `[EnforceRange] unsigned long long`, defaulting to 1 when - // the `options` dictionary or member is absent. Apply `[EnforceRange]` here: - // a non-finite value, or one that is negative after truncating toward zero, - // rejects with a `TypeError`. (The upper bound 2^64-1 is not enforced — it is - // beyond `f64` precision and a value that large fails the length checks below - // with a `RangeError` regardless.) A conversion failure surfaces as a rejected - // promise rather than a synchronous throw, per WebIDL §3.7.7 ("Operations") - // for a promise-returning operation. - let min_f64 = options.map(|o| o.min).unwrap_or(1.0); - if !min_f64.is_finite() || min_f64.trunc() < 0.0 { - js::error::throw_type_error(scope, c"read() option min is out of range"); - return Promise::new_rejected_with_pending_error(scope); - } - let min = min_f64.trunc() as usize; + let min = options.map_or(1, |o| o.min.0) as usize; // Step 1: If _view_.[[ByteLength]] is 0, return `a promise rejected with` a ``TypeError`` // exception. if view.byte_length() == 0 { - js::error::throw_type_error(scope, c"read() view has a byte length of 0"); - return Promise::new_rejected_with_pending_error(scope); + return Err(js::error::throw_type_error( + scope, + c"read() view has a byte length of 0", + )); } // Step 2: If _view_.[[ViewedArrayBuffer]].[[ByteLength]] is 0, return `a promise rejected // with` a ``TypeError`` exception. let buffer = view.viewed_buffer(scope)?; if buffer.byte_length() == 0 { - js::error::throw_type_error(scope, c"read() view's buffer has a byte length of 0"); - return Promise::new_rejected_with_pending_error(scope); + return Err(js::error::throw_type_error( + scope, + c"read() view's buffer has a byte length of 0", + )); } // Step 3: If ! `IsDetachedBuffer`(_view_.[[ViewedArrayBuffer]]) is true, return `a promise // rejected with` a ``TypeError`` exception. if buffer.is_detached() { - js::error::throw_type_error(scope, c"read() view's buffer is detached"); - return Promise::new_rejected_with_pending_error(scope); + return Err(js::error::throw_type_error( + scope, + c"read() view's buffer is detached", + )); } // Step 4: If _options_["``min``"] is 0, return `a promise rejected with` a ``TypeError`` // exception. if min == 0 { - js::error::throw_type_error(scope, c"read() option min cannot be 0"); - return Promise::new_rejected_with_pending_error(scope); + return Err(js::error::throw_type_error( + scope, + c"read() option min cannot be 0", + )); } if view.view_kind().is_typed_array() { // Step 5: If _view_ has a [[TypedArrayName]] internal slot, If _options_["``min``"] > // _view_.[[ArrayLength]], return `a promise rejected with` a ``RangeError`` // exception. if min > view.array_length() { - js::error::throw_range_error(scope, c"read() option min exceeds the view's length"); - return Promise::new_rejected_with_pending_error(scope); + return Err(js::error::throw_range_error( + scope, + c"read() option min exceeds the view's length", + )); } } else { // Step 6: Otherwise (i.e., it is a ``DataView``), If _options_["``min``"] > // _view_.[[ByteLength]], return `a promise rejected with` a ``RangeError`` // exception. if min > view.byte_length() { - js::error::throw_range_error( + return Err(js::error::throw_range_error( scope, c"read() option min exceeds the view's byte length", - ); - return Promise::new_rejected_with_pending_error(scope); + )); } } // Step 7: If `this`.`[[stream]]` is undefined, return `a promise rejected with` a // ``TypeError`` exception. if self.data().stream.is_none() { - js::error::throw_type_error( + return Err(js::error::throw_type_error( scope, c"Cannot read from a reader that is not attached to a stream", - ); - return Promise::new_rejected_with_pending_error(scope); + )); } // Step 8: Let _promise_ be `a new promise`. let promise = Promise::new_pending(scope)?; @@ -172,11 +167,13 @@ impl BYOBReader { // Step 1: If `this`.`[[stream]]` is undefined, return `a promise rejected with` a // ``TypeError`` exception. if self.data().stream.is_none() { - js::error::throw_type_error(scope, c"Cannot cancel a reader that has no stream"); - return Promise::new_rejected_with_pending_error(scope); + return Err(js::error::throw_type_error( + scope, + c"Cannot cancel a reader that has no stream", + )); } // Step 2: Return ! `ReadableStreamReaderGenericCancel`(`this`, _reason_). - let reason = reason.unwrap_or_else(|| scope.root_value(js::value::undefined())); + let reason = reason.unwrap_or(HandleValue::undefined()); Ok(algorithms::readable_stream_reader_generic_cancel( scope, self, reason, )) diff --git a/crates/builtins/web-streams/src/readable/byob_request.rs b/crates/builtins/web-streams/src/readable/byob_request.rs index 3351975b..1e001a04 100644 --- a/crates/builtins/web-streams/src/readable/byob_request.rs +++ b/crates/builtins/web-streams/src/readable/byob_request.rs @@ -3,11 +3,11 @@ //! use super::byte_stream_controller::ReadableByteStreamControllerImpl; use core_runtime::{webidl_interface, webidl_methods}; +use js::conversion::EnforceRange; use js::error::ExnThrown; use js::gc::handle::Heap; use js::gc::scope::Scope; -use js::native::Value; -use js::prelude::{HandleValue, OptionHeapExt}; +use js::prelude::OptionHeapExt; /// #[webidl_interface] @@ -19,7 +19,7 @@ pub struct ReadableStreamBYOBRequest { /// /// A typed array representing the destination region to which the controller can write generated /// data, or null after the BYOB request has been invalidated. - pub(crate) view: Heap, + pub(crate) view: Option>, } #[webidl_methods] @@ -31,38 +31,22 @@ impl ReadableStreamBYOBRequest { /// #[getter] - fn view<'r>(&self, scope: &'r Scope<'_>) -> Option> { + fn view<'r>(&self, scope: &'r Scope<'_>) -> Option> { // WebIDL: Uint8Array // Step 1: Return `this`.`[[view]]`. - // The slot holds a `Uint8Array` or null (after invalidation); a null is surfaced as - // the WebIDL nullable's `None`. - if self.data().view.is_undefined() { - return None; - } - let view = self.data().view.get(scope); - if view.is_null() { - None - } else { - Some(view) - } + // The slot holds a `Uint8Array`, or `None` after invalidation, which the WebIDL + // nullable surfaces as null. + self.data().view.get(scope) } /// #[method] - fn respond(&self, scope: &Scope<'_>, bytes_written: f64) -> Result<(), ExnThrown> { - // WebIDL: `bytesWritten` is `[EnforceRange] unsigned long long`. Apply - // `[EnforceRange]` here (the macro converts integer parameters with - // wrapping semantics): a non-finite value, or one negative after - // truncating toward zero, throws a `TypeError` before any step runs. (The - // upper bound 2^64-1 is beyond `f64` precision; a value that large exceeds - // the view length and fails the respond steps with a `RangeError` anyway.) - if !bytes_written.is_finite() || bytes_written.trunc() < 0.0 { - return Err(js::error::throw_type_error( - scope, - c"respond() bytesWritten is out of range", - )); - } - let bytes_written = bytes_written.trunc() as u64; + fn respond( + &self, + scope: &Scope<'_>, + bytes_written: EnforceRange, + ) -> Result<(), ExnThrown> { + let bytes_written = bytes_written.0; // Step 1: If `this`.`[[controller]]` is undefined, throw a ``TypeError`` exception. let controller = match self.data().controller.get(scope) { Some(c) => c, @@ -75,10 +59,11 @@ impl ReadableStreamBYOBRequest { }; // Step 2: If ! `IsDetachedBuffer`(`this`.`[[view]]`.[[ArrayBuffer]]) is true, throw a // ``TypeError`` exception. - let view = js::Object::from_value(scope, self.data().view.get(scope).get()) - .ok() - .and_then(js::ArrayBufferView::from_object) - .expect("BYOB request view is an ArrayBufferView"); + let view = self + .data() + .view + .get(scope) + .expect("BYOB request view is set while its controller is"); if view.viewed_buffer(scope)?.is_detached() { return Err(js::error::throw_type_error( scope, @@ -103,19 +88,8 @@ impl ReadableStreamBYOBRequest { fn respond_with_new_view( &self, scope: &Scope<'_>, - view: HandleValue<'_>, /* WebIDL: ArrayBufferView */ + view: js::ArrayBufferView<'_>, ) -> Result<(), ExnThrown> { - // The WebIDL signature coerces the argument to an `ArrayBufferView`; a non-view value is a - // ``TypeError`` before the steps below run. - let view = js::Object::from_value(scope, *view) - .ok() - .and_then(js::ArrayBufferView::from_object) - .ok_or_else(|| { - js::error::throw_type_error( - scope, - c"respondWithNewView() argument is not an ArrayBufferView", - ) - })?; // Step 1: If `this`.`[[controller]]` is undefined, throw a ``TypeError`` exception. let controller = match self.data().controller.get(scope) { Some(c) => c, diff --git a/crates/builtins/web-streams/src/readable/byte_stream_controller.rs b/crates/builtins/web-streams/src/readable/byte_stream_controller.rs index 0398c7d9..d594fac5 100644 --- a/crates/builtins/web-streams/src/readable/byte_stream_controller.rs +++ b/crates/builtins/web-streams/src/readable/byte_stream_controller.rs @@ -16,8 +16,7 @@ use js::error::ExnThrown; use js::gc::handle::Heap; use js::gc::scope::Scope; use js::native::Value; -use js::prelude::{HandleValue, OptionHeapExt}; -use js::Object; +use js::prelude::HandleValue; /// A readable byte stream queue entry: a contiguous region of an /// `ArrayBuffer`. @@ -32,7 +31,7 @@ use js::Object; #[derive(Traceable, Default, js::ScopeRoot)] pub(crate) struct ByteQueueEntry { /// The `ArrayBuffer` backing this entry's bytes. - pub(crate) buffer: Heap, + pub(crate) buffer: Heap, /// The offset, in bytes, of this entry's region within `buffer`. #[no_trace] pub(crate) byte_offset: usize, @@ -44,7 +43,7 @@ pub(crate) struct ByteQueueEntry { impl<'s> StackByteQueueEntry<'s> { /// Consume the rooted entry, returning its buffer and the region's /// `(byte_offset, byte_length)`. - pub(crate) fn into_parts(self) -> (Object<'s>, usize, usize) { + pub(crate) fn into_parts(self) -> (js::ArrayBuffer<'s>, usize, usize) { (self.buffer, self.byte_offset, self.byte_length) } } @@ -78,7 +77,7 @@ pub(crate) enum ReaderType { #[derive(Traceable)] pub(crate) struct PullIntoDescriptor { /// The `ArrayBuffer` being filled. - pub(crate) buffer: Heap, + pub(crate) buffer: Heap, /// The byte length of `buffer`. #[no_trace] pub(crate) buffer_byte_length: usize, @@ -113,7 +112,7 @@ pub struct ReadableByteStreamController { /// /// A positive integer, when the automatic buffer allocation feature is enabled. In that case, /// this value specifies the size of buffer to allocate. It is undefined otherwise. - pub(crate) auto_allocate_chunk_size: Option, + pub(crate) auto_allocate_chunk_size: Option, /// /// A ReadableStreamBYOBRequest instance representing the current BYOB pull request, or null if /// there are no pending requests @@ -121,7 +120,7 @@ pub struct ReadableByteStreamController { /// /// A promise-returning algorithm, taking one argument (the cancel reason), which communicates a /// requested cancelation to the underlying byte source - pub(crate) cancel_algorithm: Heap, + pub(crate) cancel_algorithm: crate::support::Algorithm, /// /// A boolean flag indicating whether the stream has been closed by its underlying byte source, /// but still has chunks in its internal queue that have not yet been read @@ -133,10 +132,10 @@ pub struct ReadableByteStreamController { pub(crate) pull_again: bool, /// /// A promise-returning algorithm that pulls data from the underlying byte source - pub(crate) pull_algorithm: Heap, - /// The `this` value with which the pull and cancel algorithms are invoked - /// (the underlying byte source object, or undefined for a controller created - /// internally). Mirrors the default controller's `algorithm_receiver` slot. + pub(crate) pull_algorithm: crate::support::Algorithm, + /// The `this` value with which JS pull and cancel algorithms are invoked: the + /// underlying byte source object. Mirrors the default controller's + /// `algorithm_receiver` slot. pub(crate) algorithm_receiver: Heap, /// /// A boolean flag set to true while the underlying byte source’s pull algorithm is executing @@ -160,9 +159,8 @@ pub struct ReadableByteStreamController { /// the point at which the stream will apply backpressure to its underlying byte source pub(crate) strategy_hwm: f64, /// - /// The ReadableStream instance controlled. `None` only between creating the - /// bare controller and `SetUpByteStreamController` wiring it up. - pub(crate) stream: Option>, + /// The ReadableStream instance controlled + pub(crate) stream: Heap, /// The pull-reaction callbacks (`ByteStreamControllerCallPullIfNeeded` /// steps 7-8; payload = this controller), created on the first pull and /// reused for every subsequent pull. @@ -173,8 +171,11 @@ pub struct ReadableByteStreamController { #[webidl_methods] impl ReadableByteStreamController { /// - fn new() -> Self { - ReadableByteStreamControllerImpl::default() + fn new(stream: ReadableStream<'_>) -> Self { + ReadableByteStreamControllerImpl { + stream: Heap::from(stream), + ..Default::default() + } } /// @@ -203,11 +204,7 @@ impl ReadableByteStreamController { } // Step 2: If `this`.`[[stream]]`.`[[state]]` is not "`readable`", throw a ``TypeError`` // exception. - let stream = self - .data() - .stream - .get(scope) - .expect("controller has a stream"); + let stream = self.data().stream.get(scope); if stream.data().state != ReadableStreamState::Readable { return Err(js::error::throw_type_error( scope, @@ -220,19 +217,7 @@ impl ReadableByteStreamController { /// #[method] - fn enqueue( - &self, - scope: &Scope<'_>, - chunk: HandleValue<'_>, /* WebIDL: ArrayBufferView */ - ) -> Result<(), ExnThrown> { - // WebIDL coerces the argument to an `ArrayBufferView`; a non-view value - // is a ``TypeError`` before any of the steps below run. - let view = Object::from_value(scope, *chunk) - .ok() - .and_then(js::ArrayBufferView::from_object) - .ok_or_else(|| { - js::error::throw_type_error(scope, c"enqueue() argument is not an ArrayBufferView") - })?; + fn enqueue(&self, scope: &Scope<'_>, view: js::ArrayBufferView<'_>) -> Result<(), ExnThrown> { // Step 1: If _chunk_.[[ByteLength]] is 0, throw a ``TypeError`` exception. if view.byte_length() == 0 { return Err(js::error::throw_type_error( @@ -257,11 +242,7 @@ impl ReadableByteStreamController { } // Step 4: If `this`.`[[stream]]`.`[[state]]` is not "`readable`", throw a ``TypeError`` // exception. - let stream = self - .data() - .stream - .get(scope) - .expect("controller has a stream"); + let stream = self.data().stream.get(scope); if stream.data().state != ReadableStreamState::Readable { return Err(js::error::throw_type_error( scope, @@ -276,15 +257,12 @@ impl ReadableByteStreamController { #[method] fn error(&self, scope: &Scope<'_>, e: Option>) -> Result<(), ExnThrown> { // Step 1: Perform ! `ByteStreamControllerError`(`this`, _e_). - let e = e.unwrap_or_else(|| scope.root_value(js::value::undefined())); + let e = e.unwrap_or(HandleValue::undefined()); super::algorithms::readable_byte_stream_controller_error(scope, self, e); Ok(()) } - pub(crate) fn stream<'r>(&'r self, scope: &'r Scope<'_>) -> ReadableStream<'r> { - self.data() - .stream - .get(scope) - .expect("controller has a stream") + pub(crate) fn stream<'r>(&self, scope: &'r Scope<'_>) -> ReadableStream<'r> { + self.data().stream.get(scope) } } diff --git a/crates/builtins/web-streams/src/readable/default_controller.rs b/crates/builtins/web-streams/src/readable/default_controller.rs index 9eed8d31..6c1adc83 100644 --- a/crates/builtins/web-streams/src/readable/default_controller.rs +++ b/crates/builtins/web-streams/src/readable/default_controller.rs @@ -13,7 +13,7 @@ use js::error::ExnThrown; use js::gc::handle::Heap; use js::gc::scope::Scope; use js::native::Value; -use js::prelude::{HandleValue, OptionHeapExt}; +use js::prelude::HandleValue; /// #[webidl_interface] @@ -21,7 +21,7 @@ pub struct ReadableStreamDefaultController { /// /// A promise-returning algorithm, taking one argument (the cancel reason), which communicates a /// requested cancelation to the underlying source - pub(crate) cancel_algorithm: Heap, + pub(crate) cancel_algorithm: crate::support::Algorithm, /// /// A boolean flag indicating whether the stream has been closed by its underlying source, but /// still has chunks in its internal queue that have not yet been read @@ -33,7 +33,7 @@ pub struct ReadableStreamDefaultController { pub(crate) pull_again: bool, /// /// A promise-returning algorithm that pulls data from the underlying source - pub(crate) pull_algorithm: Heap, + pub(crate) pull_algorithm: crate::support::Algorithm, /// /// A boolean flag set to true while the underlying source’s pull algorithm is executing and /// the returned promise has not yet fulfilled, used to prevent reentrant calls @@ -59,16 +59,12 @@ pub struct ReadableStreamDefaultController { /// The `this` value the start/pull/cancel algorithms are invoked with. /// /// Not a spec slot: the spec models pull/cancel as algorithms closing over - /// the underlying source. We store the raw callbacks plus their receiver - /// (the underlying source object for the from-underlying-source path, or - /// `undefined` for native algorithms, which ignore `this`). + /// the underlying source. We store a JS algorithm's callback plus its + /// receiver, the underlying source object. Native algorithms don't use it. pub(crate) algorithm_receiver: Heap, /// /// The ReadableStream instance controlled - /// - /// `Option` because the controller is created before `SetUp...Controller` - /// wires it to its stream; it is always `Some` thereafter. - pub(crate) stream: Option>, + pub(crate) stream: Heap, /// The pull-reaction callbacks (`DefaultControllerCallPullIfNeeded` steps /// 7-8; payload = this controller), created on the first pull. pub(crate) pull_fulfilled_fn: Option>, @@ -77,8 +73,11 @@ pub struct ReadableStreamDefaultController { #[webidl_methods] impl ReadableStreamDefaultController { - fn new() -> Self { - ReadableStreamDefaultControllerImpl::default() + fn new(stream: ReadableStream<'_>) -> Self { + ReadableStreamDefaultControllerImpl { + stream: Heap::from(stream), + ..Default::default() + } } /// @@ -128,11 +127,8 @@ impl ReadableStreamDefaultController { Ok(()) } - pub(crate) fn stream<'r>(&'r self, scope: &'r Scope<'_>) -> ReadableStream<'r> { - self.data() - .stream - .get(scope) - .expect("controller has a stream") + pub(crate) fn stream<'r>(&self, scope: &'r Scope<'_>) -> ReadableStream<'r> { + self.data().stream.get(scope) } } diff --git a/crates/builtins/web-streams/src/readable/default_reader.rs b/crates/builtins/web-streams/src/readable/default_reader.rs index 694e4187..677dddf4 100644 --- a/crates/builtins/web-streams/src/readable/default_reader.rs +++ b/crates/builtins/web-streams/src/readable/default_reader.rs @@ -55,11 +55,10 @@ impl DefaultReader { // Step 1: If `this`.`[[stream]]` is undefined, return `a promise rejected with` a // ``TypeError`` exception. if self.data().stream.is_none() { - js::error::throw_type_error( + return Err(js::error::throw_type_error( scope, c"Cannot read from a reader that does not have an owner stream", - ); - return Promise::new_rejected_with_pending_error(scope); + )); } // Step 2: Let _promise_ be `a new promise`. let promise = Promise::new_pending(scope)?; @@ -101,11 +100,10 @@ impl DefaultReader { // Step 1: If `this`.`[[stream]]` is undefined, return `a promise rejected with` a // ``TypeError`` exception. if self.data().stream.is_none() { - js::error::throw_type_error( + return Err(js::error::throw_type_error( scope, c"Cannot cancel a reader that does not have an owner stream", - ); - return Promise::new_rejected_with_pending_error(scope); + )); } // Step 2: Return ! `ReadableStreamReaderGenericCancel`(`this`, _reason_). let reason = reason.unwrap_or(HandleValue::undefined()); diff --git a/crates/builtins/web-streams/src/readable/mod.rs b/crates/builtins/web-streams/src/readable/mod.rs index 03e9304e..c04ded5d 100644 --- a/crates/builtins/web-streams/src/readable/mod.rs +++ b/crates/builtins/web-streams/src/readable/mod.rs @@ -17,6 +17,7 @@ pub mod read_all_bytes; pub mod read_request; pub mod readable_stream; pub mod underlying_source; +pub(crate) mod unions; pub use async_iterator::ReadableStreamAsyncIterator as AsyncIterator; pub use byob_reader::BYOBReader; diff --git a/crates/builtins/web-streams/src/readable/options.rs b/crates/builtins/web-streams/src/readable/options.rs index 81ef6e0f..77ad98d3 100644 --- a/crates/builtins/web-streams/src/readable/options.rs +++ b/crates/builtins/web-streams/src/readable/options.rs @@ -3,6 +3,7 @@ //! use core_runtime::webidl_dictionary; +use js::conversion::EnforceRange; use web_globals::signals::AbortSignal; use super::enums::ReaderMode; @@ -10,14 +11,8 @@ use super::enums::ReaderMode; /// #[webidl_dictionary] pub struct BYOBReaderReadOptions { - /// WebIDL declares `min` as `[EnforceRange] unsigned long long` (default 1). - /// It is captured here as an `f64` rather than an integer so the sign and - /// non-finiteness survive: the `[EnforceRange]` conversion (which rejects a - /// negative or non-finite value with a `TypeError`) is applied in - /// `BYOBReader.read`, where the failure can surface as a - /// rejected promise per WebIDL §3.7.7 ("Operations"). - #[webidl(default = 1.0)] - pub min: f64, + #[webidl(default = EnforceRange(1))] + pub min: EnforceRange, } /// diff --git a/crates/builtins/web-streams/src/readable/read_all_bytes.rs b/crates/builtins/web-streams/src/readable/read_all_bytes.rs index 8f47c69d..8f0f0c3e 100644 --- a/crates/builtins/web-streams/src/readable/read_all_bytes.rs +++ b/crates/builtins/web-streams/src/readable/read_all_bytes.rs @@ -28,12 +28,13 @@ use crate::readable::DefaultReader; use core_runtime::{jsclass, jsmethods}; use js::conversion::FromJSVal; use js::error::ExnThrown; +use js::function::cast_payload; use js::gc::handle::Heap; use js::gc::scope::Scope; use js::native::Value; use js::prelude::{CallbackArgs, HandleValue, OptionHeapExt}; use js::value; -use js::{Function, Object, Promise, Uint8Array}; +use js::{Function, Promise, Uint8Array}; /// The completion of a read-all-bytes drain (the caller's `successSteps`): /// receives the payload value given to [`read_all_bytes`] and the assembled @@ -111,7 +112,7 @@ pub fn read_all_bytes<'r>( // The deferred-read callback, reused for every chunk. Created here (not in // the closure above) because its payload is the state object. let read_next_fn = Function::new_callback(scope, c"", 0, read_next_microtask, state)?; - state.data_mut().read_next_fn = Some(Heap::from(read_next_fn)); + state.data_mut().read_next_fn.set(read_next_fn); read_next(scope, state); Ok(promise) } @@ -150,13 +151,10 @@ pub(crate) fn read_all_bytes_chunk_steps( ) -> Result<(), ExnThrown> { // Step 1: If chunk is not a Uint8Array object, call failureSteps with a // TypeError and abort these steps. - let chunk_array = Object::from_value(scope, *chunk) - .ok() - .and_then(|object| object.cast::>().ok()); - let Some(array) = chunk_array else { + let Ok(array) = Uint8Array::from_jsval(scope, chunk, ()) else { js::error::throw_type_error(scope, c"a body stream chunk must be a Uint8Array"); let promise = state.data().promise.get(scope); - return reject_with_pending(scope, &promise); + return promise.reject_with_pending(scope); }; // Step 2: Append the bytes represented by chunk to bytes. // SAFETY: the slice is consumed immediately; appending to the Rust-side @@ -180,8 +178,7 @@ fn read_next_microtask( _args: CallbackArgs<'_>, payload: HandleValue<'_>, ) -> Result { - let state = ReadAllBytesState::from_jsval(scope, payload, ()) - .expect("read-next payload is the ReadAllBytesState"); + let state = cast_payload::(scope, payload); read_next(scope, state); Ok(value::undefined()) } @@ -202,7 +199,7 @@ pub(crate) fn read_all_bytes_close_steps( let payload = state.data().payload.get(scope); match on_complete(scope, payload, bytes) { Ok(value) => promise.resolve(scope, value), - Err(_) => reject_with_pending(scope, &promise), + Err(_) => promise.reject_with_pending(scope), } } @@ -216,11 +213,3 @@ pub(crate) fn read_all_bytes_error_steps( let promise = state.data().promise.get(scope); promise.reject(scope, e) } - -/// Reject `promise` with the current pending exception (an error a step or the -/// success callback has thrown), clearing it so it does not leak as an unhandled -/// engine exception. -fn reject_with_pending(scope: &Scope<'_>, promise: &Promise<'_>) -> Result<(), ExnThrown> { - let error = js::exception::take_pending_or_undefined(scope); - promise.reject(scope, error) -} diff --git a/crates/builtins/web-streams/src/readable/read_request.rs b/crates/builtins/web-streams/src/readable/read_request.rs index ff77ca14..87cce38b 100644 --- a/crates/builtins/web-streams/src/readable/read_request.rs +++ b/crates/builtins/web-streams/src/readable/read_request.rs @@ -10,14 +10,12 @@ //! `pipeTo`, `tee`, the async iterator) supplies its own. They are modelled as //! an enum — one variant per consumer — rather than as JS values, because the //! steps are native and the `[[readRequests]]` list must trace whatever rooted -//! state they hold. The `Traceable` derive rejects enums, so `Trace` is -//! hand-written. +//! state they hold. use js::error::ExnThrown; use js::gc::handle::Heap; use js::gc::scope::Scope; use js::iteration::create_iter_result; -use js::native::JSTracer; use js::prelude::HandleValue; use js::promise::Promise; @@ -41,7 +39,7 @@ use crate::readable::read_all_bytes::{ /// untraced-`Heap`-across-GC hazard — forcing callers to keep it traced /// (`RootedTraceableBox`) until they settle it. #[js::must_root] -#[derive(js::ScopeRoot)] +#[derive(core_runtime::Traceable, js::ScopeRoot)] pub(crate) enum ReadRequest { /// A `DefaultReader.read()` call. /// @@ -118,6 +116,7 @@ pub(crate) enum ReadRequest { /// observe it. Native { /// The consumer's step functions. + #[no_trace] steps: crate::readable::native_read::NativeReadSteps, /// The consumer's rooted state object. payload: Heap, @@ -126,16 +125,10 @@ pub(crate) enum ReadRequest { impl<'s> StackReadRequest<'s> { /// Perform the read request's `chunk steps` given `chunk`. - pub(crate) fn chunk_steps( - self, - scope: &Scope<'_>, - chunk: HandleValue<'_>, - ) -> Result<(), ExnThrown> { - match self { - StackReadRequest::Read { promise } => { - let result = create_iter_result(scope, chunk, false)?; - promise.resolve(scope, result) - } + pub(crate) fn chunk_steps(self, scope: &Scope<'_>, chunk: HandleValue<'_>) { + let result = match self { + StackReadRequest::Read { promise } => create_iter_result(scope, chunk, false) + .and_then(|result| promise.resolve(scope, result)), StackReadRequest::Tee { state } => tee_read_request_chunk_steps(scope, state, chunk), StackReadRequest::Pipe { state } => pipe_read_request_chunk_steps(scope, state, chunk), StackReadRequest::AsyncIter { promise, .. } => { @@ -146,15 +139,16 @@ impl<'s> StackReadRequest<'s> { } StackReadRequest::Consume { state } => read_all_bytes_chunk_steps(scope, state, chunk), StackReadRequest::Native { steps, payload } => (steps.chunk)(scope, payload, chunk), - } + }; + report_failed_steps(scope, result, "read request chunk steps"); } /// Perform the read request's `close steps`. - pub(crate) fn close_steps(self, scope: &Scope<'_>) -> Result<(), ExnThrown> { - match self { + pub(crate) fn close_steps(self, scope: &Scope<'_>) { + let result = match self { StackReadRequest::Read { promise } => { - let result = create_iter_result(scope, HandleValue::undefined(), true)?; - promise.resolve(scope, result) + create_iter_result(scope, HandleValue::undefined(), true) + .and_then(|result| promise.resolve(scope, result)) } StackReadRequest::Tee { state } => tee_read_request_close_steps(scope, state), // The pipe loop's close steps do nothing — shutdown is driven by the @@ -175,16 +169,13 @@ impl<'s> StackReadRequest<'s> { } StackReadRequest::Consume { state } => read_all_bytes_close_steps(scope, state), StackReadRequest::Native { steps, payload } => (steps.close)(scope, payload), - } + }; + report_failed_steps(scope, result, "read request close steps"); } /// Perform the read request's `error steps` given `e`. - pub(crate) fn error_steps( - self, - scope: &Scope<'_>, - e: HandleValue<'_>, - ) -> Result<(), ExnThrown> { - match self { + pub(crate) fn error_steps(self, scope: &Scope<'_>, e: HandleValue<'_>) { + let result = match self { StackReadRequest::Read { promise } => promise.reject(scope, e), StackReadRequest::Tee { state } => tee_read_request_error_steps(scope, state, e), // The pipe loop's error steps do nothing — shutdown is driven by the @@ -202,32 +193,8 @@ impl<'s> StackReadRequest<'s> { } StackReadRequest::Consume { state } => read_all_bytes_error_steps(scope, state, e), StackReadRequest::Native { steps, payload } => (steps.error)(scope, payload, e), - } - } -} - -// Safety: trace every GC pointer held by each variant. -unsafe impl js::heap::Trace for ReadRequest { - #[inline] - unsafe fn trace(&self, trc: *mut JSTracer) { - match self { - ReadRequest::Read { promise } => promise.trace(trc), - ReadRequest::Tee { state } => state.trace(trc), - ReadRequest::Pipe { state } => state.trace(trc), - ReadRequest::AsyncIter { - promise, - reader, - end_of_iteration, - } => { - promise.trace(trc); - reader.trace(trc); - end_of_iteration.trace(trc); - } - ReadRequest::ByteTeeDefault { state } => state.trace(trc), - ReadRequest::Consume { state } => state.trace(trc), - // `steps` holds only fn pointers; the payload is the sole GC pointer. - ReadRequest::Native { payload, .. } => payload.trace(trc), - } + }; + report_failed_steps(scope, result, "read request error steps"); } } @@ -242,7 +209,7 @@ unsafe impl js::heap::Trace for ReadRequest { /// Marked `#[js::must_root]` for the same reason as [`ReadRequest`]: its /// variants hold a `Heap`, so crown forbids holding one untraced across a GC. #[js::must_root] -#[derive(js::ScopeRoot)] +#[derive(core_runtime::Traceable, js::ScopeRoot)] pub(crate) enum ReadIntoRequest { /// A `BYOBReader.read(view)` call. /// @@ -262,52 +229,39 @@ pub(crate) enum ReadIntoRequest { /// The byte-tee state object. state: Heap, /// Whether this read-into targets branch 2 (`forBranch2`). + #[no_trace] for_branch2: bool, }, } impl<'s> StackReadIntoRequest<'s> { /// Perform the read-into request's `chunk steps` given `chunk`. - pub(crate) fn chunk_steps( - self, - scope: &Scope<'_>, - chunk: HandleValue<'_>, - ) -> Result<(), ExnThrown> { - match self { - StackReadIntoRequest::Read { promise } => { - let result = create_iter_result(scope, chunk, false)?; - promise.resolve(scope, result) - } + pub(crate) fn chunk_steps(self, scope: &Scope<'_>, chunk: HandleValue<'_>) { + let result = match self { + StackReadIntoRequest::Read { promise } => create_iter_result(scope, chunk, false) + .and_then(|result| promise.resolve(scope, result)), StackReadIntoRequest::ByteTeeByob { state, for_branch2 } => { byte_tee_byob_chunk_steps(scope, state, for_branch2, chunk) } - } + }; + report_failed_steps(scope, result, "read-into request chunk steps"); } /// Perform the read-into request's `close steps` given `chunk`. - pub(crate) fn close_steps( - self, - scope: &Scope<'_>, - chunk: HandleValue<'_>, - ) -> Result<(), ExnThrown> { - match self { - StackReadIntoRequest::Read { promise } => { - let result = create_iter_result(scope, chunk, true)?; - promise.resolve(scope, result) - } + pub(crate) fn close_steps(self, scope: &Scope<'_>, chunk: HandleValue<'_>) { + let result = match self { + StackReadIntoRequest::Read { promise } => create_iter_result(scope, chunk, true) + .and_then(|result| promise.resolve(scope, result)), StackReadIntoRequest::ByteTeeByob { state, for_branch2 } => { byte_tee_byob_close_steps(scope, state, for_branch2, chunk) } - } + }; + report_failed_steps(scope, result, "read-into request close steps"); } /// Perform the read-into request's `error steps` given `e`. - pub(crate) fn error_steps( - self, - scope: &Scope<'_>, - e: HandleValue<'_>, - ) -> Result<(), ExnThrown> { - match self { + pub(crate) fn error_steps(self, scope: &Scope<'_>, e: HandleValue<'_>) { + let result = match self { StackReadIntoRequest::Read { promise } => promise.reject(scope, e), // The byte-tee BYOB read-into request's error steps only set `reading` // to false; stream errors are forwarded via the reader's closed promise. @@ -316,17 +270,16 @@ impl<'s> StackReadIntoRequest<'s> { byte_tee_set_not_reading(scope, state); Ok(()) } - } + }; + report_failed_steps(scope, result, "read-into request error steps"); } } -// Safety: trace every GC pointer held by each variant. -unsafe impl js::heap::Trace for ReadIntoRequest { - #[inline] - unsafe fn trace(&self, trc: *mut JSTracer) { - match self { - ReadIntoRequest::Read { promise } => promise.trace(trc), - ReadIntoRequest::ByteTeeByob { state, .. } => state.trace(trc), - } +/// Read request steps have no caller to throw to, so an exception one of them +/// leaves (from a consumer's native step, or running out of memory) is reported +/// and cleared. +fn report_failed_steps(scope: &Scope<'_>, result: Result<(), ExnThrown>, context: &str) { + if result.is_err() { + js::exception::report_and_clear(scope, context); } } diff --git a/crates/builtins/web-streams/src/readable/readable_stream.rs b/crates/builtins/web-streams/src/readable/readable_stream.rs index 5cd5067a..409ecf0b 100644 --- a/crates/builtins/web-streams/src/readable/readable_stream.rs +++ b/crates/builtins/web-streams/src/readable/readable_stream.rs @@ -2,12 +2,16 @@ //! -use super::async_iterator::{ReadableStreamAsyncIterator, ReadableStreamAsyncIteratorImpl}; +use super::async_iterator::ReadableStreamAsyncIterator; use super::enums::ReadableStreamType; use super::options::{ ReadableStreamGetReaderOptions, ReadableStreamIteratorOptions, StreamPipeOptions, }; use super::underlying_source::UnderlyingSource; +use super::unions::{ + HeapReadableStreamController, HeapReadableStreamReader, ReadableStreamController, + ReadableStreamReader, +}; use super::{algorithms, ReadableByteStreamController}; use crate::algorithms::{extract_high_water_mark, extract_size_algorithm}; use crate::queuing::QueuingStrategy; @@ -18,14 +22,14 @@ use crate::readable::algorithms::{ readable_stream_default_controller_error, readable_stream_default_tee, }; use crate::readable::ReadableStreamDefaultController; +use crate::support::AlgorithmArg; use crate::transform::readable_writable_pair::ReadableWritablePair; use crate::writable::WritableStream; use core_runtime::{webidl_interface, webidl_methods}; use js::prelude::ToJSVal; use js::{ - conversion::ConversionError, conversion::FromJSVal, error::ExnThrown, gc::handle::Heap, - gc::handle::OptionHeapExt, gc::scope::Scope, native::Value, prelude::HandleValue, Object, - Promise, Uint8Array, + conversion::FromJSVal, error::ExnThrown, gc::handle::Heap, gc::handle::OptionHeapExt, + gc::scope::Scope, native::Value, prelude::HandleValue, Object, Promise, Uint8Array, }; use web_globals::signals::AbortSignal; @@ -41,8 +45,6 @@ fn pipe_options<'r>( } } -pub type ReadableStreamReader<'a> = HandleValue<'a>; // WebIDL: (DefaultReader or BYOBReader) - /// #[webidl_interface] pub struct ReadableStream { @@ -50,22 +52,16 @@ pub struct ReadableStream { /// A ReadableStreamDefaultController or ReadableByteStreamController created with the ability to /// control the state and queue of this stream /// - /// Polymorphic (default-or-byte controller), so stored as an `Object` and - /// downcast with `Object::cast` at the use site. `None` until set up. - pub(crate) controller: Option>, - /// - /// A boolean flag set to true when the stream is transferred - pub(crate) detached: bool, + /// `None` until `SetUpReadableStreamDefaultController` or `SetUpReadableByteStreamController` + /// runs. + pub(crate) controller: Option, /// /// A boolean flag set to true when the stream has been read from or canceled pub(crate) disturbed: bool, /// /// A DefaultReader or BYOBReader instance, if the stream is locked /// to a reader, or undefined if it is not - /// - /// Polymorphic (default-or-BYOB reader), so stored as an `Object` and - /// downcast with `Object::cast` at the use site. `None` when unlocked. - pub(crate) reader: Option>, + pub(crate) reader: Option, /// /// A string containing the stream’s current state, used internally; one of "readable", /// "closed", or "errored" @@ -131,32 +127,7 @@ impl ReadableStream { } }; let underlying_source_dict = - UnderlyingSource::from_jsval(scope, underlying_source_value, ()).map_err(|_| { - if js::exception::get_pending(scope).is_err() { - js::error::throw_type_error(scope, c"Invalid underlying source"); - } - ExnThrown - })?; - // Step 2 (continued): converting the dictionary validates that each present - // callback member is callable, before the step-4 byte-stream checks. - // TODO: consider introducing a `Callable` type that WebIDL type checking can use. - crate::support::ensure_callback_members_callable( - scope, - &[ - ( - underlying_source_dict.cancel.as_ref(), - c"underlying source cancel must be a function", - ), - ( - underlying_source_dict.pull.as_ref(), - c"underlying source pull must be a function", - ), - ( - underlying_source_dict.start.as_ref(), - c"underlying source start must be a function", - ), - ], - )?; + UnderlyingSource::from_jsval_throwing(scope, underlying_source_value, ())?; // Step 3: Perform ! `InitializeReadableStream`(`this`). algorithms::initialize_readable_stream(self); // Step 4: If _underlyingSourceDict_["``type``"] is "``bytes``": ... @@ -168,18 +139,7 @@ impl ReadableStream { // _highWaterMark_ be ? `ExtractHighWaterMark`(_strategy_, 0). Perform ? // `SetUpByteStreamControllerFromUnderlyingSource`(`this`, _underlyingSource_, // _underlyingSourceDict_, _highWaterMark_). - if let Some(size) = strategy.as_ref().and_then(|s| s.size.as_ref()) { - // `size` is a `QueuingStrategySize` callback: WebIDL converts it when binding the - // `strategy` argument and throws a `TypeError` if it is not callable, before this - // RangeError. The codebase defers callback callability checks to their use sites, - // so replicate the conversion-time `TypeError` here. (TASKLOG: a validating - // callback-conversion type would centralize this for all callback members.) - if !size.is_callable() { - return Err(js::error::throw_type_error( - scope, - c"queuing strategy size must be a function", - )); - } + if strategy.as_ref().is_some_and(|s| s.size.is_some()) { return Err(js::error::throw_range_error( scope, c"a byte stream's queuing strategy must not have a size function", @@ -199,7 +159,7 @@ impl ReadableStream { // `ExtractHighWaterMark`(_strategy_, 1). Perform ? // `SetUpDefaultControllerFromUnderlyingSource`(`this`, // _underlyingSource_, _underlyingSourceDict_, _highWaterMark_, _sizeAlgorithm_). - let size_algorithm = scope.root_value(extract_size_algorithm(scope, &strategy)?); + let size_algorithm = extract_size_algorithm(scope, &strategy)?; let high_water_mark = extract_high_water_mark(scope, &strategy, 1.0)?; algorithms::set_up_readable_stream_default_controller_from_underlying_source( scope, @@ -211,8 +171,8 @@ impl ReadableStream { ) } - /// Create a `ReadableStream` driven by the provided `pull`/`cancel` algorithms (JS - /// functions, typically native callbacks). + /// Create a `ReadableStream` driven by the provided `pull`/`cancel` algorithms, typically + /// native callbacks. /// /// `pull` is called with the stream's controller as its argument when more data is wanted. /// `cancel` is called with the cancel reason. @@ -227,13 +187,13 @@ impl ReadableStream { pub fn new_native<'r>( scope: &'r Scope<'_>, underlying_source: impl ToJSVal<'r>, - pull: HandleValue<'_>, - cancel: HandleValue<'_>, + pull: AlgorithmArg<'r>, + cancel: AlgorithmArg<'r>, ) -> Result, ExnThrown> { let underlying_source = underlying_source.to_jsval_throwing(scope)?; let stream = create_readable_stream( scope, - HandleValue::undefined(), + AlgorithmArg::None, pull, cancel, 0.0, @@ -255,8 +215,9 @@ impl ReadableStream { scope: &'r Scope<'_>, bytes: &[u8], ) -> Result, ExnThrown> { + let none = AlgorithmArg::None; let undef = HandleValue::undefined(); - let stream = create_readable_stream(scope, undef, undef, undef, 1.0, undef)?; + let stream = create_readable_stream(scope, none, none, none, 1.0, undef)?; let controller = stream .default_controller(scope) .expect("stream has a default controller"); @@ -312,8 +273,10 @@ impl ReadableStream { // Step 1: If ! `IsReadableStreamLocked`(`this`) is true, return `a promise rejected with` a // ``TypeError`` exception. if self.is_locked() { - js::error::throw_type_error(scope, c"Cannot cancel a stream that already has a reader"); - return Promise::new_rejected_with_pending_error(scope); + return Err(js::error::throw_type_error( + scope, + c"Cannot cancel a stream that already has a reader", + )); } // Step 2: Return ! `ReadableStreamCancel`(`this`, _reason_). let reason = reason.unwrap_or(HandleValue::undefined()); @@ -332,12 +295,12 @@ impl ReadableStream { let mode = options.and_then(|o| o.mode); if mode.is_none() { let reader = algorithms::acquire_readable_stream_default_reader(scope, self)?; - return Ok(scope.root_value(reader.as_value())); + return Ok(ReadableStreamReader::Default(reader)); } // Step 2: Assert: _options_["``mode``"] is "``byob``". // Step 3: Return ? `AcquireBYOBReader`(`this`). let reader = algorithms::acquire_readable_stream_byob_reader(scope, self)?; - Ok(scope.root_value(reader.as_value())) + Ok(ReadableStreamReader::Byob(reader)) } /// @@ -389,39 +352,23 @@ impl ReadableStream { &self, scope: &'r Scope<'_>, destination: WritableStream<'_>, - options: Option>, + options: Option>, ) -> Result, ExnThrown> { - // `pipeTo` returns a promise, so per WebIDL a failed coercion of the - // `options` dictionary (e.g. an invalid `signal`) must reject the - // returned promise rather than throw synchronously. The dictionary is - // therefore coerced here, inside the method, instead of as a typed - // parameter. - let options = match options { - Some(v) if !v.is_undefined() => match StreamPipeOptions::from_jsval(scope, v, ()) { - Ok(o) => Some(o), - Err(e) => { - // A non-object value (e.g. an invalid `signal`) fails without - // a pending exception; surface it as a TypeError so the - // returned promise rejects with one. - if let ConversionError::Failure(msg) = e { - js::error::throw_type_error(scope, msg.as_ref()); - } - return Promise::new_rejected_with_pending_error(scope); - } - }, - _ => None, - }; // Step 1: If ! `IsReadableStreamLocked`(`this`) is true, return `a promise rejected with` a // ``TypeError`` exception. if self.is_locked() { - js::error::throw_type_error(scope, c"cannot pipe from a locked ReadableStream"); - return Promise::new_rejected_with_pending_error(scope); + return Err(js::error::throw_type_error( + scope, + c"cannot pipe from a locked ReadableStream", + )); } // Step 2: If ! `IsWritableStreamLocked`(_destination_) is true, return `a promise rejected // with` a ``TypeError`` exception. if crate::writable::algorithms::is_writable_stream_locked(&destination) { - js::error::throw_type_error(scope, c"cannot pipe to a locked WritableStream"); - return Promise::new_rejected_with_pending_error(scope); + return Err(js::error::throw_type_error( + scope, + c"cannot pipe to a locked WritableStream", + )); } // Step 3: Let _signal_ be _options_["``signal``"] if it `exists`, or undefined otherwise. let (prevent_close, prevent_abort, prevent_cancel, signal) = pipe_options(&options); @@ -482,15 +429,12 @@ impl ReadableStream { // Step 1: Let _reader_ be ? `AcquireDefaultReader`(`this`). let reader = algorithms::acquire_readable_stream_default_reader(scope, self)?; // Step 2: Let _iterator_ be a `new` ``ReadableStreamAsyncIterator``. - let iterator = - js::class::create_instance_with::(scope, |_| { - ReadableStreamAsyncIteratorImpl::default() - })?; // Step 3: Set _iterator_'s reader to _reader_. - iterator.data_mut().reader = Some(Heap::from(reader)); // Step 4: Let _preventCancel_ be _options_["``preventCancel``"]. // Step 5: Set _iterator_'s prevent cancel to _preventCancel_. - iterator.data_mut().prevent_cancel = options.map(|o| o.prevent_cancel).unwrap_or(false); + // (`ReadableStreamAsyncIterator::new` performs steps 3 and 5.) + let prevent_cancel = options.is_some_and(|o| o.prevent_cancel); + let iterator = ReadableStreamAsyncIterator::new(scope, reader, prevent_cancel)?; // Step 6: Return _iterator_. Ok(iterator) } @@ -533,36 +477,45 @@ impl ReadableStream { } pub fn error(&self, scope: &Scope<'_>, reason: HandleValue<'_>) { - if let Some(controller) = self.default_controller(scope) { - readable_stream_default_controller_error(scope, &controller, reason); - } else { - let controller = self.byte_controller(scope).unwrap(); - readable_byte_stream_controller_error(scope, &controller, reason); + match self.controller(scope) { + ReadableStreamController::Default(controller) => { + readable_stream_default_controller_error(scope, &controller, reason) + } + ReadableStreamController::Byte(controller) => { + readable_byte_stream_controller_error(scope, &controller, reason) + } } } - pub(crate) fn default_controller<'r>( - &'r self, - scope: &'r Scope<'_>, - ) -> Option> { + /// The stream's `[[controller]]`. Panics if the stream is not set up yet. + pub(crate) fn controller<'r>(&self, scope: &'r Scope<'_>) -> ReadableStreamController<'r> { self.data() .controller - .get(scope) + .as_ref() .expect("stream has a controller") - .cast::() - .ok() + .get(scope) + } + + /// The stream's `[[controller]]` if it is a `ReadableStreamDefaultController`. + pub fn default_controller<'r>( + &self, + scope: &'r Scope<'_>, + ) -> Option> { + match self.controller(scope) { + ReadableStreamController::Default(controller) => Some(controller), + ReadableStreamController::Byte(_) => None, + } } + /// The stream's `[[controller]]` if it is a `ReadableByteStreamController`. pub(crate) fn byte_controller<'r>( &self, scope: &'r Scope<'_>, ) -> Option> { - self.data() - .controller - .get(scope) - .expect("stream has a controller") - .cast::() - .ok() + match self.controller(scope) { + ReadableStreamController::Byte(controller) => Some(controller), + ReadableStreamController::Default(_) => None, + } } /// The native source object backing this stream, if any (see @@ -577,7 +530,7 @@ impl ReadableStream { /// Record `source` as this stream's native byte source. pub fn set_native_source(&self, source: &Object<'_>) { - self.data_mut().native_byte_source = Some(Heap::from(*source)); + self.data_mut().native_byte_source.set(*source); } /// For streams piped to an identity `TransformStream` whose readable end has not @@ -609,12 +562,4 @@ impl ReadableStream { let demand = transform.data().backpressure_change_promise.get(scope); demand } - - pub fn controller(&self, scope: &'s Scope<'_>) -> Option> { - self.data().controller.get(scope) - } - - pub fn reader(&self, scope: &'s Scope<'_>) -> Option> { - self.data().reader.get(scope) - } } diff --git a/crates/builtins/web-streams/src/readable/underlying_source.rs b/crates/builtins/web-streams/src/readable/underlying_source.rs index 1ae98dd5..6efd4021 100644 --- a/crates/builtins/web-streams/src/readable/underlying_source.rs +++ b/crates/builtins/web-streams/src/readable/underlying_source.rs @@ -4,7 +4,8 @@ use super::enums::ReadableStreamType; use core_runtime::webidl_dictionary; -use js::Object; +use js::conversion::EnforceRange; +use js::Callable; /// #[webidl_dictionary] @@ -13,18 +14,14 @@ pub struct UnderlyingSource<'a> { pub pull: Option>, pub cancel: Option>, pub r#type: Option, - // WebIDL `[EnforceRange] unsigned long long`. Held as `f64` so the sign and - // non-finiteness survive the dictionary conversion (the macro converts - // integers with wrapping semantics); `[EnforceRange]` is applied in - // `set_up_readable_byte_stream_controller_from_underlying_source`. - pub auto_allocate_chunk_size: Option, + pub auto_allocate_chunk_size: Option>, } /// WebIDL callback `UnderlyingSourceStartCallback`: (controller: ReadableStreamController) -> HandleValue<'_> -pub type StartCallback<'s> = Object<'s>; +pub type StartCallback<'s> = Callable<'s>; /// WebIDL callback `UnderlyingSourcePullCallback`: (controller: ReadableStreamController) -> Promise<'_> -pub type PullCallback<'s> = Object<'s>; +pub type PullCallback<'s> = Callable<'s>; /// WebIDL callback `UnderlyingSourceCancelCallback`: (reason: Option>) -> Promise<'_> -pub type CancelCallback<'s> = Object<'s>; +pub type CancelCallback<'s> = Callable<'s>; diff --git a/crates/builtins/web-streams/src/readable/unions.rs b/crates/builtins/web-streams/src/readable/unions.rs new file mode 100644 index 00000000..cf96ae64 --- /dev/null +++ b/crates/builtins/web-streams/src/readable/unions.rs @@ -0,0 +1,146 @@ +// SPDX-License-Identifier: Apache-2.0-WITH-LLVM-exception + +//! The `ReadableStreamController` and `ReadableStreamReader` WebIDL union types, +//! each in a rooted form and a traced `Heap*` form for internal slots. + +use super::algorithms::GenericReader; +use super::byob_reader::BYOBReaderImpl; +use super::byte_stream_controller::ReadableByteStreamControllerImpl; +use super::default_controller::ReadableStreamDefaultControllerImpl; +use super::default_reader::DefaultReaderImpl; +use super::{ + BYOBReader, DefaultReader, ReadableByteStreamController, ReadableStream, + ReadableStreamDefaultController, +}; +use js::conversion::{ConversionError, ToJSVal}; +use js::gc::handle::Heap; +use js::gc::scope::Scope; +use js::{Object, Promise}; + +/// +#[derive(Clone, Copy)] +pub(crate) enum ReadableStreamController<'s> { + Default(ReadableStreamDefaultController<'s>), + Byte(ReadableByteStreamController<'s>), +} + +/// A stream's `[[controller]]` slot value. +#[js::must_root] +#[derive(core_runtime::Traceable)] +pub(crate) enum HeapReadableStreamController { + Default(Heap), + Byte(Heap), +} + +impl HeapReadableStreamController { + pub(crate) fn get<'s>(&self, scope: &'s Scope<'_>) -> ReadableStreamController<'s> { + match self { + Self::Default(c) => ReadableStreamController::Default(c.get(scope)), + Self::Byte(c) => ReadableStreamController::Byte(c.get(scope)), + } + } +} + +impl From> for HeapReadableStreamController { + fn from(controller: ReadableStreamDefaultController<'_>) -> Self { + Self::Default(Heap::from(controller)) + } +} + +impl From> for HeapReadableStreamController { + fn from(controller: ReadableByteStreamController<'_>) -> Self { + Self::Byte(Heap::from(controller)) + } +} + +/// +#[derive(Clone, Copy)] +pub(crate) enum ReadableStreamReader<'s> { + Default(DefaultReader<'s>), + Byob(BYOBReader<'s>), +} + +impl<'s> ReadableStreamReader<'s> { + pub(crate) fn as_object(&self) -> Object<'s> { + match self { + Self::Default(r) => r.as_object(), + Self::Byob(r) => r.as_object(), + } + } +} + +impl<'s> ToJSVal<'s> for ReadableStreamReader<'s> { + fn to_jsval_raw(&self, scope: &'s Scope<'_>) -> Result { + self.as_object().to_jsval_raw(scope) + } +} + +impl GenericReader for ReadableStreamReader<'_> { + fn generic_stream<'r>(&self, scope: &'r Scope<'_>) -> Option> { + match self { + Self::Default(r) => r.generic_stream(scope), + Self::Byob(r) => r.generic_stream(scope), + } + } + fn set_generic_stream(&self, stream: &ReadableStream<'_>) { + match self { + Self::Default(r) => r.set_generic_stream(stream), + Self::Byob(r) => r.set_generic_stream(stream), + } + } + fn clear_generic_stream(&self) { + match self { + Self::Default(r) => r.clear_generic_stream(), + Self::Byob(r) => r.clear_generic_stream(), + } + } + fn generic_closed_promise<'r>(&self, scope: &'r Scope<'_>) -> Promise<'r> { + match self { + Self::Default(r) => r.generic_closed_promise(scope), + Self::Byob(r) => r.generic_closed_promise(scope), + } + } + fn set_generic_closed_promise(&self, promise: Promise<'_>) { + match self { + Self::Default(r) => r.set_generic_closed_promise(promise), + Self::Byob(r) => r.set_generic_closed_promise(promise), + } + } + fn as_generic(&self) -> ReadableStreamReader<'_> { + *self + } +} + +/// A stream's `[[reader]]` slot value, or the byte tee's current reader. +#[js::must_root] +#[derive(core_runtime::Traceable)] +pub(crate) enum HeapReadableStreamReader { + Default(Heap), + Byob(Heap), +} + +/// A null default-reader `Heap`, for setup-style constructors that set the slot before the object +/// is exposed. Calling `get()` on it panics. +impl Default for HeapReadableStreamReader { + fn default() -> Self { + Self::Default(Heap::default()) + } +} + +impl HeapReadableStreamReader { + pub(crate) fn get<'s>(&self, scope: &'s Scope<'_>) -> ReadableStreamReader<'s> { + match self { + Self::Default(r) => ReadableStreamReader::Default(r.get(scope)), + Self::Byob(r) => ReadableStreamReader::Byob(r.get(scope)), + } + } +} + +impl From> for HeapReadableStreamReader { + fn from(reader: ReadableStreamReader<'_>) -> Self { + match reader { + ReadableStreamReader::Default(r) => Self::Default(Heap::from(r)), + ReadableStreamReader::Byob(r) => Self::Byob(Heap::from(r)), + } + } +} diff --git a/crates/builtins/web-streams/src/support.rs b/crates/builtins/web-streams/src/support.rs index bee7e670..4df25e9d 100644 --- a/crates/builtins/web-streams/src/support.rs +++ b/crates/builtins/web-streams/src/support.rs @@ -1,83 +1,175 @@ // SPDX-License-Identifier: Apache-2.0-WITH-LLVM-exception -//! Non-spec plumbing shared by the stream algorithms: invoking the JS-callable -//! algorithms stored on controllers, attaching native promise reactions, and +//! Non-spec plumbing shared by the stream algorithms: the algorithms stored on +//! controllers and how they are invoked, attaching native promise reactions, and //! building iterator-result objects. -use std::ffi::CStr; - use js::error::ExnThrown; -use js::function::Callback; +use js::function::{Callback, CallbackArgs}; +use js::gc::handle::Heap; use js::gc::scope::Scope; +use js::native::Value; use js::prelude::HandleValue; use js::prelude::ToJSVal; -use js::Function; -use js::{Object, Promise}; +use js::Promise; +use js::{Callable, Function}; + +/// A stream algorithm, as the `SetUp*` and `Create*` operations take it. +#[derive(Clone, Copy)] +pub enum AlgorithmArg<'s> { + /// No algorithm. A promise-returning one resolves with undefined, any other + /// returns undefined. + None, + /// A JS callable, invoked with the controller's algorithm receiver as `this`. + Js(HandleValue<'s>), + /// A native callback, invoked directly with its payload. + Native(Callback, HandleValue<'s>), +} + +impl<'s> AlgorithmArg<'s> { + /// A native algorithm running `callback` with `payload`. + pub fn native( + scope: &'s Scope<'_>, + callback: Callback, + payload: impl ToJSVal<'s>, + ) -> Result { + Ok(Self::Native(callback, payload.to_jsval_throwing(scope)?)) + } + + /// The algorithm for a WebIDL callback dictionary member: none when the + /// member is absent, the callable otherwise. + pub(crate) fn from_member(scope: &'s Scope<'_>, member: Option<&Callable<'_>>) -> Self { + match member { + Some(callable) => Self::Js(scope.root_value(callable.as_value())), + None => Self::None, + } + } +} + +/// A stream algorithm stored on a controller. See [`AlgorithmArg`]. +#[js::must_root] +#[derive(core_runtime::Traceable, Default)] +pub(crate) enum Algorithm { + #[default] + None, + Js(Heap), + Native { + #[no_trace] + callback: Callback, + payload: Heap, + }, +} + +impl Algorithm { + pub(crate) fn get<'s>(&self, scope: &'s Scope<'_>) -> AlgorithmArg<'s> { + match self { + Self::None => AlgorithmArg::None, + Self::Js(callable) => AlgorithmArg::Js(callable.get(scope)), + Self::Native { callback, payload } => { + AlgorithmArg::Native(*callback, payload.get(scope)) + } + } + } +} + +impl From> for Algorithm { + fn from(algorithm: AlgorithmArg<'_>) -> Self { + match algorithm { + AlgorithmArg::None => Self::None, + AlgorithmArg::Js(callable) => Self::Js(Heap::from(callable.get())), + AlgorithmArg::Native(callback, payload) => Self::Native { + callback, + payload: Heap::from(payload.get()), + }, + } + } +} + +/// Call a native algorithm's `callback` with `payload` and `args`. +fn call_native<'r>( + scope: &'r Scope<'_>, + callback: Callback, + payload: HandleValue<'_>, + args: &[impl ToJSVal<'r>], +) -> Result, ExnThrown> { + let mut values: smallvec::SmallVec<[HandleValue<'r>; 2]> = + smallvec::SmallVec::with_capacity(args.len()); + for arg in args { + values.push(arg.to_jsval_throwing(scope)?); + } + Ok(scope.root_value(callback( + scope, + CallbackArgs::from_values(&values), + payload, + )?)) +} /// Invoke a stored promise-returning stream algorithm (a controller's pull or /// cancel algorithm) following WebIDL "invoke a Promise-returning operation": /// -/// - an absent algorithm (stored as `undefined`) returns a promise resolved -/// with undefined; +/// - an absent algorithm returns a promise resolved with undefined; /// - a synchronous throw becomes a rejected promise; /// - otherwise the call result is coerced to a promise. /// -/// `receiver` is the `this` value (the underlying source for the -/// from-underlying-source path, or `undefined` for native algorithms). +/// `receiver` is the `this` value of a JS algorithm: the underlying source, +/// sink or transformer. pub(crate) fn invoke_promise_algorithm<'r>( scope: &'r Scope<'_>, - algorithm: HandleValue<'r>, + algorithm: AlgorithmArg<'r>, receiver: HandleValue<'r>, - args: &[HandleValue<'r>], + args: &[impl ToJSVal<'r>], ) -> Promise<'r> { - if algorithm.is_undefined() { - // An absent algorithm resolves with undefined. This branch runs once - // per pull for a source without `pull` and once per chunk for a sink - // without `write`, and its promise is only ever reacted to internally, - // so the per-global reused instance serves without allocating. - return Promise::shared_resolved_undefined(scope) - .expect("failed to get shared resolved promise"); - } - match Function::call(scope, receiver, algorithm, args) { - Ok(result) => { - if receiver.is_undefined() { - // A native internal algorithm (tee, `Create*`, the transform - // source/sink natives): its result already *is* the algorithm's - // promise, which the controller reacts to directly. Coerce as-is - // (no extra adoption tick), matching the spec's `uponPromise`. + let result = match algorithm { + AlgorithmArg::None => { + // This branch runs once per pull for a source without `pull` and + // once per chunk for a sink without `write`, and its promise is only + // ever reacted to internally, so the per-global reused instance + // serves without allocating. + return Promise::shared_resolved_undefined(scope) + .expect("failed to get shared resolved promise"); + } + // A native algorithm's result is already the algorithm's promise, which + // the controller reacts to directly. It is coerced as-is, without an extra + // adoption tick, matching the spec's `uponPromise`. + AlgorithmArg::Native(callback, payload) => { + call_native(scope, callback, payload, args).map(|result| { Promise::call_original_resolve(scope, result).expect("Promise.resolve failed") - } else { - // A WebIDL operation invocation (a user source/sink/transformer - // method, called with the dictionary as `this`). WebIDL's - // `Promise` conversion of the result is `NewPromiseCapability` - // + `Resolve` ("a promise resolved with"), which creates a *new* - // promise rather than returning a promise result as-is. When the - // method returns a promise (e.g. an async transformer method), - // adopting it adds a microtask tick — observable in cancel and - // erroring orderings. + }) + } + // A WebIDL operation invocation (a user source/sink/transformer method, + // called with the dictionary as `this`). WebIDL's `Promise` conversion + // of the result is `NewPromiseCapability` + `Resolve` ("a promise + // resolved with"), which creates a new promise rather than returning a + // promise result as-is. When the method returns a promise (e.g. an async + // transformer method), adopting it adds a microtask tick, which is + // observable in cancel and erroring orderings. + AlgorithmArg::Js(callable) => { + Function::call(scope, receiver, callable, args).map(|result| { Promise::new_resolved_with_value(scope, result) .expect("failed to create resolved promise") - } + }) } - Err(_) => Promise::new_rejected_with_pending_error(scope) - .expect("failed to create rejected promise"), - } + }; + result.unwrap_or_else(|_| { + Promise::new_rejected_with_pending_error(scope).expect("failed to create rejected promise") + }) } /// Invoke a non-promise-returning stream algorithm (a controller's start -/// algorithm, or a strategy size algorithm) with `this` = `receiver`. Returns -/// the raw call result; a synchronous throw propagates as `Err`. An absent -/// algorithm (`undefined`) yields `undefined`. +/// algorithm) with `this` = `receiver` for a JS algorithm. Returns the raw call +/// result. A synchronous throw propagates as `Err`, and no algorithm yields +/// `undefined`. pub(crate) fn invoke_algorithm<'r>( scope: &'r Scope<'_>, - algorithm: HandleValue<'r>, + algorithm: AlgorithmArg<'r>, receiver: HandleValue<'r>, args: &[impl ToJSVal<'r>], ) -> Result, ExnThrown> { - if algorithm.is_undefined() { - return Ok(HandleValue::undefined()); + match algorithm { + AlgorithmArg::None => Ok(HandleValue::undefined()), + AlgorithmArg::Native(callback, payload) => call_native(scope, callback, payload, args), + AlgorithmArg::Js(callable) => Function::call(scope, receiver, callable, args), } - Function::call(scope, receiver, algorithm, args) } /// Attach native fulfillment/rejection reactions to `promise`, each carrying a @@ -92,11 +184,11 @@ pub(crate) fn invoke_algorithm<'r>( /// otherwise produce a spurious unhandled rejection. Attaching the reaction also /// marks `promise` itself handled, which is correct — the stream is consuming /// it. -pub(crate) fn react( - scope: &Scope<'_>, +pub(crate) fn react<'r, P: ToJSVal<'r>>( + scope: &'r Scope<'_>, promise: &Promise<'_>, - on_fulfilled: Option<(Callback, HandleValue<'_>)>, - on_rejected: Option<(Callback, HandleValue<'_>)>, + on_fulfilled: Option<(Callback, P)>, + on_rejected: Option<(Callback, P)>, ) -> Result<(), ExnThrown> { let fulfilled = match on_fulfilled { Some((cb, payload)) => Some(Function::new_callback(scope, c"", 1, cb, payload)?), @@ -113,46 +205,10 @@ pub(crate) fn react( ) } -/// Resolve a WebIDL callback dictionary member to the value stored as the -/// corresponding algorithm: `undefined` when the member is absent (the invoker -/// treats that as the default/constant algorithm), or the callable itself. -/// -/// The callback dictionary members are held as plain [`Object`]s (so a bound -/// function or callable proxy is accepted), so the "callback function" type's -/// callability check is not applied during dictionary conversion — it is applied -/// here: a present member that is not callable is a `TypeError` (`message`). -pub(crate) fn callback_member<'r>( - scope: &'r Scope<'_>, - member: Option<&Object<'_>>, - message: &CStr, -) -> Result, ExnThrown> { - match member { - None => Ok(HandleValue::undefined()), - Some(obj) if obj.is_callable() => Ok(scope.root_value(obj.as_value())), - Some(_) => Err(js::error::throw_type_error(scope, message)), - } -} - -/// Validate that each present callback dictionary member is callable. -/// -/// WebIDL converts a dictionary's callback-function members as part of converting -/// the dictionary, throwing a `TypeError` for a non-callable value. The macro -/// holds these members as plain [`Object`]s and defers callability to the use -/// sites (see [`callback_member`]); a stream constructor applies the check up -/// front — before its later steps, e.g. the `type`/`readableType` `RangeError` — -/// so a non-callable member surfaces the spec's `TypeError` rather than a later -/// error. `members` are listed in lexicographic order of the member identifiers -/// to match WebIDL's dictionary-conversion order. -pub(crate) fn ensure_callback_members_callable( - scope: &Scope<'_>, - members: &[(Option<&Object<'_>>, &CStr)], -) -> Result<(), ExnThrown> { - for &(member, message) in members { - if let Some(obj) = member { - if !obj.is_callable() { - return Err(js::error::throw_type_error(scope, message)); - } - } +/// Report and clear the exception a failed operation left pending, for callers +/// that have no caller of their own to propagate it to. +pub(crate) fn report_failure(scope: &Scope<'_>, result: Result, context: &str) { + if result.is_err() { + js::exception::report_and_clear(scope, context); } - Ok(()) } diff --git a/crates/builtins/web-streams/src/transform/algorithms.rs b/crates/builtins/web-streams/src/transform/algorithms.rs index 290e826f..5e25836e 100644 --- a/crates/builtins/web-streams/src/transform/algorithms.rs +++ b/crates/builtins/web-streams/src/transform/algorithms.rs @@ -4,54 +4,32 @@ use js::error::ExnThrown; use js::exception::take_pending_or_undefined; -use js::gc::handle::Heap; +use js::function::cast_payload; use js::gc::scope::Scope; use js::native::Value; use js::prelude::{CallbackArgs, HandleValue, OptionHeapExt}; use js::value; use js::Function; -use js::{Object, Promise}; +use js::Promise; use super::transform_stream::TransformStream; use super::transform_stream_default_controller::TransformStreamDefaultController; use super::transformer::Transformer; -use crate::algorithms::{cast_payload, make_type_error, pair_parts, pair_payload}; +use crate::algorithms::{make_type_error, pair_parts, pair_payload}; use crate::readable::algorithms::{ create_readable_stream, readable_stream_default_controller_can_close_or_enqueue, readable_stream_default_controller_close, readable_stream_default_controller_enqueue, readable_stream_default_controller_error, readable_stream_default_controller_get_desired_size, readable_stream_default_controller_has_backpressure, }; -use crate::readable::default_controller::ReadableStreamDefaultController; use crate::readable::readable_stream::{ReadableStream, ReadableStreamState}; use crate::support; +use crate::support::{Algorithm, AlgorithmArg}; use crate::writable::algorithms::{ create_writable_stream, writable_stream_default_controller_error_if_needed, }; use crate::writable::writable_stream::{WritableStream, WritableStreamState}; -// --------------------------------------------------------------------------- -// Private accessors bridging the polymorphic `[[controller]]`/`[[reader]]` -// object slots to the concrete default-stream newtypes. -// --------------------------------------------------------------------------- - -/// The stream's `[[controller]]` as a default controller. Panics if the -/// controller is unset or is not a `ReadableStreamDefaultController`; both are -/// invariants on the readable default path. -fn stream_default_controller<'r>( - scope: &'r Scope<'_>, - stream: &ReadableStream<'_>, -) -> ReadableStreamDefaultController<'r> { - let obj: Object<'r> = stream - .data() - .controller - .as_ref() - .expect("stream has a controller") - .get(scope); - obj.cast::() - .expect("controller is a ReadableStreamDefaultController") -} - // --------------------------------------------------------------------------- // Transform-stream accessors, native algorithm callbacks, and reactions. // --------------------------------------------------------------------------- @@ -64,9 +42,8 @@ fn ts_controller<'r>( stream .data() .controller - .as_ref() - .expect("transform stream has a controller") .get(scope) + .expect("transform stream has a controller") } /// The transform stream's `[[readable]]`. @@ -84,12 +61,7 @@ fn ts_controller_stream<'r>( scope: &'r Scope<'_>, controller: &TransformStreamDefaultController<'_>, ) -> TransformStream<'r> { - controller - .data() - .stream - .as_ref() - .expect("controller has a stream") - .get(scope) + controller.data().stream.get(scope) } /// The controller's `[[finishPromise]]` (asserted set). @@ -100,23 +72,14 @@ fn ts_finish_promise<'r>( controller .data() .finish_promise - .as_ref() - .expect("finish promise is set") .get(scope) + .expect("finish promise is set") } // Native algorithm callbacks built by `InitializeTransformStream` (payload = the // transform stream, except the start algorithm whose payload is the start // promise). Each returns the underlying transform algorithm's promise as a value. -/// A `TransformStreamDefaultController` value usable as a reaction payload. -fn ts_controller_value<'r>( - scope: &'r Scope<'_>, - controller: &TransformStreamDefaultController<'_>, -) -> HandleValue<'r> { - scope.root_value(controller.as_value()) -} - /// The desired size of the transform controller's readable side (the /// `TransformStreamDefaultController.desiredSize` getter). pub(crate) fn transform_controller_readable_desired_size( @@ -125,7 +88,9 @@ pub(crate) fn transform_controller_readable_desired_size( ) -> Option { let stream = ts_controller_stream(scope, controller); let readable = ts_readable(scope, &stream); - let readable_controller = stream_default_controller(scope, &readable); + let readable_controller = readable + .default_controller(scope) + .expect("readable side has a default controller"); readable_stream_default_controller_get_desired_size(scope, &readable_controller) } @@ -211,8 +176,11 @@ fn ts_perform_transform_rejected( let r = args.get(0); // Perform ! TransformStreamError(stream, r). Throw r. transform_stream_error(scope, &stream, r); - js::exception::set_pending(scope, r, js::native::ExceptionStackBehavior::DoNotCapture); - Err(ExnThrown) + Err(js::exception::set_pending( + scope, + r, + js::native::ExceptionStackBehavior::DoNotCapture, + )) } /// `TransformStreamDefaultSinkWriteAlgorithm` step 3 fulfillment steps @@ -235,12 +203,11 @@ fn ts_sink_write_after_backpressure( // If state is "erroring", throw writable's storedError. if state == WritableStreamState::Erroring { let stored_error = writable.data().stored_error.get(scope); - js::exception::set_pending( + return Err(js::exception::set_pending( scope, stored_error, js::native::ExceptionStackBehavior::DoNotCapture, - ); - return Err(ExnThrown); + )); } debug_assert_eq!(state, WritableStreamState::Writable); // Return ! TransformStreamDefaultControllerPerformTransform(controller, chunk). @@ -262,7 +229,9 @@ fn ts_sink_close_fulfilled( let stored_error = readable.data().stored_error.get(scope); finish.reject(scope, stored_error).expect("reject finish"); } else { - let readable_controller = stream_default_controller(scope, &readable); + let readable_controller = readable + .default_controller(scope) + .expect("readable side has a default controller"); readable_stream_default_controller_close(scope, &readable_controller); finish.resolve(scope, HandleValue::undefined())?; } @@ -279,7 +248,9 @@ fn ts_sink_close_rejected( let stream = ts_controller_stream(scope, &controller); let readable = ts_readable(scope, &stream); let r = args.get(0); - let readable_controller = stream_default_controller(scope, &readable); + let readable_controller = readable + .default_controller(scope) + .expect("readable side has a default controller"); readable_stream_default_controller_error(scope, &readable_controller, r); ts_finish_promise(scope, &controller) .reject(scope, r) @@ -303,7 +274,9 @@ fn ts_sink_abort_fulfilled( let stored_error = readable.data().stored_error.get(scope); finish.reject(scope, stored_error).expect("reject finish"); } else { - let readable_controller = stream_default_controller(scope, &readable); + let readable_controller = readable + .default_controller(scope) + .expect("readable side has a default controller"); readable_stream_default_controller_error(scope, &readable_controller, reason); finish.resolve(scope, HandleValue::undefined())?; } @@ -322,7 +295,9 @@ fn ts_sink_abort_rejected( let stream = ts_controller_stream(scope, &controller); let readable = ts_readable(scope, &stream); let r = args.get(0); - let readable_controller = stream_default_controller(scope, &readable); + let readable_controller = readable + .default_controller(scope) + .expect("readable side has a default controller"); readable_stream_default_controller_error(scope, &readable_controller, r); ts_finish_promise(scope, &controller) .reject(scope, r) @@ -387,23 +362,17 @@ pub(crate) fn initialize_transform_stream( readable_high_water_mark: f64, readable_size_algorithm: HandleValue<'_>, ) -> Result<(), ExnThrown> { - let stream_payload = scope.root_value(stream.as_value()); // Step 1: Let _startAlgorithm_ be an algorithm that returns _startPromise_. - let start_promise_value = scope.root_value(start_promise.as_value()); - let start = Function::new_callback(scope, c"", 0, ts_start_native, start_promise_value)?; - let start = scope.root_value(start.as_value()); + let start = AlgorithmArg::native(scope, ts_start_native, start_promise)?; // Step 2: Let _writeAlgorithm_ be the following steps, taking a _chunk_ argument: Return ! // `TransformStreamDefaultSinkWriteAlgorithm`(_stream_, _chunk_). - let write = Function::new_callback(scope, c"", 2, ts_sink_write_native, stream_payload)?; - let write = scope.root_value(write.as_value()); + let write = AlgorithmArg::native(scope, ts_sink_write_native, *stream)?; // Step 3: Let _abortAlgorithm_ be the following steps, taking a _reason_ argument: Return ! // `TransformStreamDefaultSinkAbortAlgorithm`(_stream_, _reason_). - let abort = Function::new_callback(scope, c"", 1, ts_sink_abort_native, stream_payload)?; - let abort = scope.root_value(abort.as_value()); + let abort = AlgorithmArg::native(scope, ts_sink_abort_native, *stream)?; // Step 4: Let _closeAlgorithm_ be the following steps: Return ! // `TransformStreamDefaultSinkCloseAlgorithm`(_stream_). - let close = Function::new_callback(scope, c"", 0, ts_sink_close_native, stream_payload)?; - let close = scope.root_value(close.as_value()); + let close = AlgorithmArg::native(scope, ts_sink_close_native, *stream)?; // Step 5: Set _stream_.`[[writable]]` to ! `CreateWritableStream`(_startAlgorithm_, // _writeAlgorithm_, _closeAlgorithm_, _abortAlgorithm_, _writableHighWaterMark_, // _writableSizeAlgorithm_). @@ -419,12 +388,10 @@ pub(crate) fn initialize_transform_stream( stream.data_mut().writable.set(writable); // Step 6: Let _pullAlgorithm_ be the following steps: Return ! // `TransformStreamDefaultSourcePullAlgorithm`(_stream_). - let pull = Function::new_callback(scope, c"", 0, ts_source_pull_native, stream_payload)?; - let pull = scope.root_value(pull.as_value()); + let pull = AlgorithmArg::native(scope, ts_source_pull_native, *stream)?; // Step 7: Let _cancelAlgorithm_ be the following steps, taking a _reason_ argument: Return ! // `TransformStreamDefaultSourceCancelAlgorithm`(_stream_, _reason_). - let cancel = Function::new_callback(scope, c"", 1, ts_source_cancel_native, stream_payload)?; - let cancel = scope.root_value(cancel.as_value()); + let cancel = AlgorithmArg::native(scope, ts_source_cancel_native, *stream)?; // Step 8: Set _stream_.`[[readable]]` to ! `CreateReadableStream`(_startAlgorithm_, // _pullAlgorithm_, _cancelAlgorithm_, _readableHighWaterMark_, // _readableSizeAlgorithm_). @@ -462,7 +429,9 @@ pub(crate) fn transform_stream_error( // `DefaultControllerError`(_stream_.`[[readable]]`.`[[controller]]`, // _e_). let readable = ts_readable(scope, stream); - let readable_controller = stream_default_controller(scope, &readable); + let readable_controller = readable + .default_controller(scope) + .expect("readable side has a default controller"); readable_stream_default_controller_error(scope, &readable_controller, e); // Step 2: Perform ! `TransformStreamErrorWritableAndUnblockWrite`(_stream_, _e_). transform_stream_error_writable_and_unblock_write(scope, stream, e); @@ -500,18 +469,16 @@ pub(crate) fn transform_stream_set_backpressure( debug_assert_ne!(stream.data().backpressure, backpressure); // Step 2: If _stream_.`[[backpressureChangePromise]]` is not undefined, `resolve` // stream.`[[backpressureChangePromise]]` with undefined. - if stream.data().backpressure_change_promise.is_some() { - let promise = stream - .data() - .backpressure_change_promise - .as_ref() - .unwrap() - .get(scope); + let promise = stream.data().backpressure_change_promise.get(scope); + if let Some(promise) = promise { promise.resolve(scope, HandleValue::undefined()).unwrap(); } // Step 3: Set _stream_.`[[backpressureChangePromise]]` to `a new promise`. let new_promise = Promise::new_pending(scope).expect("new promise"); - stream.data_mut().backpressure_change_promise = Some(Heap::from(new_promise)); + stream + .data_mut() + .backpressure_change_promise + .set(new_promise); // Step 4: Set _stream_.`[[backpressure]]` to _backpressure_. stream.data_mut().backpressure = backpressure; } @@ -532,33 +499,25 @@ pub(crate) fn set_up_transform_stream_default_controller( _scope: &Scope<'_>, stream: &TransformStream<'_>, controller: &TransformStreamDefaultController<'_>, - transform_algorithm: HandleValue<'_>, - flush_algorithm: HandleValue<'_>, - cancel_algorithm: HandleValue<'_>, + transform_algorithm: AlgorithmArg<'_>, + flush_algorithm: AlgorithmArg<'_>, + cancel_algorithm: AlgorithmArg<'_>, algorithm_receiver: HandleValue<'_>, ) { // Step 1: Assert: _stream_ `implements` ``TransformStream``. // Step 2: Assert: _stream_.`[[controller]]` is undefined. debug_assert!(stream.data().controller.is_none()); // Step 3: Set _controller_.`[[stream]]` to _stream_. - controller.data_mut().stream = Some(Heap::from(*stream)); + // (Already done by `TransformStreamDefaultController::new`.) + debug_assert!(stream.eq_heap(&controller.data().stream)); // Step 4: Set _stream_.`[[controller]]` to _controller_. - stream.data_mut().controller = Some(Heap::from(*controller)); + stream.data_mut().controller.set(*controller); // Step 5: Set _controller_.`[[transformAlgorithm]]` to _transformAlgorithm_. - controller - .data_mut() - .transform_algorithm - .set(transform_algorithm.get()); + controller.data_mut().transform_algorithm = Algorithm::from(transform_algorithm); // Step 6: Set _controller_.`[[flushAlgorithm]]` to _flushAlgorithm_. - controller - .data_mut() - .flush_algorithm - .set(flush_algorithm.get()); + controller.data_mut().flush_algorithm = Algorithm::from(flush_algorithm); // Step 7: Set _controller_.`[[cancelAlgorithm]]` to _cancelAlgorithm_. - controller - .data_mut() - .cancel_algorithm - .set(cancel_algorithm.get()); + controller.data_mut().cancel_algorithm = Algorithm::from(cancel_algorithm); // (The algorithms close over `algorithm_receiver` — the transformer — as their `this`.) controller .data_mut() @@ -575,7 +534,7 @@ pub(crate) fn set_up_transform_stream_default_controller_from_transformer( transformer_dict: &Transformer<'_>, ) -> Result<(), ExnThrown> { // Step 1: Let _controller_ be a `new` ``TransformStreamDefaultController``. - let controller = TransformStreamDefaultController::new(scope)?; + let controller = TransformStreamDefaultController::new(scope, *stream)?; // Step 2: Let _transformAlgorithm_ be the following steps, taking a _chunk_ argument: Let // _result_ be `TransformStreamDefaultControllerEnqueue`(_controller_, _chunk_). If // _result_ is an abrupt completion, return `a promise rejected with` @@ -585,46 +544,22 @@ pub(crate) fn set_up_transform_stream_default_controller_from_transformer( // _transformerDict_["``transform``"] with argument list « _chunk_, _controller_ » and // `callback this value` _transformer_. let transform_algorithm = match transformer_dict.transform.as_ref() { - Some(transform) if transform.is_callable() => scope.root_value(transform.as_value()), - Some(_) => { - return Err(js::error::throw_type_error( - scope, - c"transformer transform must be a function", - )); - } - None => { - let controller_value = ts_controller_value(scope, &controller); - let default = Function::new_callback( - scope, - c"", - 2, - ts_default_transform_native, - controller_value, - )?; - scope.root_value(default.as_value()) - } + Some(transform) => AlgorithmArg::Js(scope.root_value(transform.as_value())), + None => AlgorithmArg::native(scope, ts_default_transform_native, controller)?, }; // Step 3: Let _flushAlgorithm_ be an algorithm which returns `a promise resolved with` // undefined. // Step 6: If _transformerDict_["``flush``"] `exists`, set _flushAlgorithm_ to an algorithm // which returns the result of `invoking` _transformerDict_["``flush``"] with argument // list « _controller_ » and `callback this value` _transformer_. - let flush_algorithm = support::callback_member( - scope, - transformer_dict.flush.as_ref(), - c"transformer flush must be a function", - )?; + let flush_algorithm = AlgorithmArg::from_member(scope, transformer_dict.flush.as_ref()); // Step 4: Let _cancelAlgorithm_ be an algorithm which returns `a promise resolved with` // undefined. // Step 7: If _transformerDict_["``cancel``"] `exists`, set _cancelAlgorithm_ to an algorithm // which takes an argument _reason_ and returns the result of `invoking` // _transformerDict_["``cancel``"] with argument list « _reason_ » and `callback this // value` _transformer_. - let cancel_algorithm = support::callback_member( - scope, - transformer_dict.cancel.as_ref(), - c"transformer cancel must be a function", - )?; + let cancel_algorithm = AlgorithmArg::from_member(scope, transformer_dict.cancel.as_ref()); // Step 8: Perform ! `SetUpTransformStreamDefaultController`(_stream_, _controller_, // _transformAlgorithm_, _flushAlgorithm_, _cancelAlgorithm_). set_up_transform_stream_default_controller( @@ -645,20 +580,11 @@ pub(crate) fn transform_stream_default_controller_clear_algorithms( controller: &TransformStreamDefaultController<'_>, ) { // Step 1: Set _controller_.`[[transformAlgorithm]]` to undefined. - controller - .data_mut() - .transform_algorithm - .set(value::undefined()); + controller.data_mut().transform_algorithm = Algorithm::None; // Step 2: Set _controller_.`[[flushAlgorithm]]` to undefined. - controller - .data_mut() - .flush_algorithm - .set(value::undefined()); + controller.data_mut().flush_algorithm = Algorithm::None; // Step 3: Set _controller_.`[[cancelAlgorithm]]` to undefined. - controller - .data_mut() - .cancel_algorithm - .set(value::undefined()); + controller.data_mut().cancel_algorithm = Algorithm::None; } /// @@ -672,7 +598,9 @@ pub(crate) fn transform_stream_default_controller_enqueue( let stream = ts_controller_stream(scope, controller); // Step 2: Let _readableController_ be _stream_.`[[readable]]`.`[[controller]]`. let readable = ts_readable(scope, &stream); - let readable_controller = stream_default_controller(scope, &readable); + let readable_controller = readable + .default_controller(scope) + .expect("readable side has a default controller"); // Step 3: If ! `DefaultControllerCanCloseOrEnqueue`(_readableController_) is // false, throw a ``TypeError`` exception. if !readable_stream_default_controller_can_close_or_enqueue(scope, &readable_controller) { @@ -690,12 +618,11 @@ pub(crate) fn transform_stream_default_controller_enqueue( let error = take_pending_or_undefined(scope); transform_stream_error_writable_and_unblock_write(scope, &stream, error); let stored_error = readable.data().stored_error.get(scope); - js::exception::set_pending( + return Err(js::exception::set_pending( scope, stored_error, js::native::ExceptionStackBehavior::DoNotCapture, - ); - return Err(ExnThrown); + )); } // Step 6: Let _backpressure_ be ! // `DefaultControllerHasBackpressure`(_readableController_). @@ -746,18 +673,16 @@ pub(crate) fn transform_stream_default_controller_perform_transform<'r>( // (The rejection callback's payload — the controller — never changes, so it // is created on the first transform and reused for every subsequent chunk.) if controller.data().transform_rejected_fn.is_none() { - let payload = ts_controller_value(scope, controller); let on_rejected = - Function::new_callback(scope, c"", 1, ts_perform_transform_rejected, payload) + Function::new_callback(scope, c"", 1, ts_perform_transform_rejected, controller) .expect("cb"); - controller.data_mut().transform_rejected_fn = Some(Heap::from(on_rejected)); + controller.data_mut().transform_rejected_fn.set(on_rejected); } let on_rejected = controller .data() .transform_rejected_fn - .as_ref() - .expect("created above") - .get(scope); + .get(scope) + .expect("created above"); transform_promise .then(scope, None, Some(*on_rejected)) .expect("then") @@ -773,7 +698,9 @@ pub(crate) fn transform_stream_default_controller_terminate( let stream = ts_controller_stream(scope, controller); // Step 2: Let _readableController_ be _stream_.`[[readable]]`.`[[controller]]`. let readable = ts_readable(scope, &stream); - let readable_controller = stream_default_controller(scope, &readable); + let readable_controller = readable + .default_controller(scope) + .expect("readable side has a default controller"); // Step 3: Perform ! `DefaultControllerClose`(_readableController_). readable_stream_default_controller_close(scope, &readable_controller); // Step 4: Let _error_ be a ``TypeError`` exception indicating that the stream has been @@ -815,11 +742,13 @@ pub(crate) fn transform_stream_default_sink_write_algorithm<'r>( // so only the returned derived promise is allocated per chunk. controller.data().pending_write_chunk.set(*chunk); if controller.data().write_after_backpressure_fn.is_none() { - let payload = ts_controller_value(scope, &controller); let on_fulfilled = - Function::new_callback(scope, c"", 1, ts_sink_write_after_backpressure, payload) + Function::new_callback(scope, c"", 1, ts_sink_write_after_backpressure, controller) .expect("cb"); - controller.data_mut().write_after_backpressure_fn = Some(Heap::from(on_fulfilled)); + controller + .data_mut() + .write_after_backpressure_fn + .set(on_fulfilled); } let on_fulfilled = controller .data() @@ -851,7 +780,7 @@ pub(crate) fn transform_stream_default_sink_abort_algorithm<'r>( // Step 3: Let _readable_ be _stream_.`[[readable]]`. (Used by the reaction.) // Step 4: Let _controller_.`[[finishPromise]]` be a new promise. let finish = Promise::new_pending(scope).expect("new promise"); - controller.data_mut().finish_promise = Some(Heap::from(finish)); + controller.data_mut().finish_promise.set(finish); // Step 5: Let _cancelPromise_ be the result of performing _controller_.`[[cancelAlgorithm]]`, // passing _reason_. let cancel_algorithm = controller.data().cancel_algorithm.get(scope); @@ -870,8 +799,7 @@ pub(crate) fn transform_stream_default_sink_abort_algorithm<'r>( // _controller_.`[[finishPromise]]` with _r_. // (Implemented by `ts_sink_abort_fulfilled` / `ts_sink_abort_rejected`, carrying // [controller, reason].) - let controller_value = ts_controller_value(scope, &controller); - let payload = pair_payload(scope, controller_value, reason).expect("payload"); + let payload = pair_payload(scope, controller, reason).expect("payload"); support::react( scope, &cancel_promise, @@ -899,17 +827,13 @@ pub(crate) fn transform_stream_default_sink_close_algorithm<'r>( // Step 3: Let _readable_ be _stream_.`[[readable]]`. (Used by the reaction.) // Step 4: Let _controller_.`[[finishPromise]]` be a new promise. let finish = Promise::new_pending(scope).expect("new promise"); - controller.data_mut().finish_promise = Some(Heap::from(finish)); + controller.data_mut().finish_promise.set(finish); // Step 5: Let _flushPromise_ be the result of performing _controller_.`[[flushAlgorithm]]`. // The from-transformer flush callback is invoked with « controller ». let flush_algorithm = controller.data().flush_algorithm.get(scope); let receiver = controller.data().algorithm_receiver.get(scope); - let flush_promise = support::invoke_promise_algorithm( - scope, - flush_algorithm, - receiver, - &[scope.root_value(controller.as_value())], - ); + let flush_promise = + support::invoke_promise_algorithm(scope, flush_algorithm, receiver, &[controller]); // Step 6: Perform ! `TransformStreamDefaultControllerClearAlgorithms`(_controller_). transform_stream_default_controller_clear_algorithms(&controller); // Step 7: `React` to _flushPromise_: If _flushPromise_ was fulfilled, then: If @@ -921,12 +845,11 @@ pub(crate) fn transform_stream_default_sink_close_algorithm<'r>( // `DefaultControllerError`(_readable_.`[[controller]]`, _r_). `Reject` // _controller_.`[[finishPromise]]` with _r_. // (Implemented by `ts_sink_close_fulfilled` / `ts_sink_close_rejected`.) - let payload = ts_controller_value(scope, &controller); support::react( scope, &flush_promise, - Some((ts_sink_close_fulfilled, payload)), - Some((ts_sink_close_rejected, payload)), + Some((ts_sink_close_fulfilled, controller)), + Some((ts_sink_close_rejected, controller)), ) .expect("react"); // Step 8: Return _controller_.`[[finishPromise]]`. @@ -950,7 +873,7 @@ pub(crate) fn transform_stream_default_source_cancel_algorithm<'r>( // Step 3: Let _writable_ be _stream_.`[[writable]]`. (Used by the reaction.) // Step 4: Let _controller_.`[[finishPromise]]` be a new promise. let finish = Promise::new_pending(scope).expect("new promise"); - controller.data_mut().finish_promise = Some(Heap::from(finish)); + controller.data_mut().finish_promise.set(finish); // Step 5: Let _cancelPromise_ be the result of performing _controller_.`[[cancelAlgorithm]]`, // passing _reason_. let cancel_algorithm = controller.data().cancel_algorithm.get(scope); @@ -971,8 +894,7 @@ pub(crate) fn transform_stream_default_source_cancel_algorithm<'r>( // _controller_.`[[finishPromise]]` with _r_. // (Implemented by `ts_source_cancel_fulfilled` / `ts_source_cancel_rejected`, carrying // [controller, reason].) - let controller_value = ts_controller_value(scope, &controller); - let payload = pair_payload(scope, controller_value, reason).expect("payload"); + let payload = pair_payload(scope, controller, reason).expect("payload"); support::react( scope, &cancel_promise, @@ -1000,7 +922,6 @@ pub(crate) fn transform_stream_default_source_pull_algorithm<'r>( stream .data() .backpressure_change_promise - .as_ref() - .expect("backpressureChangePromise is set") .get(scope) + .expect("backpressureChangePromise is set") } diff --git a/crates/builtins/web-streams/src/transform/transform_stream.rs b/crates/builtins/web-streams/src/transform/transform_stream.rs index 0ab90683..c3ec18da 100644 --- a/crates/builtins/web-streams/src/transform/transform_stream.rs +++ b/crates/builtins/web-streams/src/transform/transform_stream.rs @@ -16,7 +16,7 @@ use crate::writable::WritableStream; use core_runtime::{webidl_interface, webidl_methods}; use js::conversion::FromJSVal; use js::error::ExnThrown; -use js::gc::handle::Heap; +use js::gc::handle::{Heap, OptionHeapExt}; use js::gc::scope::Scope; use js::prelude::HandleValue; use js::Promise; @@ -41,9 +41,6 @@ pub struct TransformStream { /// `Option`: `InitializeTransformStream` sets it to undefined, then /// `SetUpTransformStreamDefaultController` populates it. pub(crate) controller: Option>, - /// - /// A boolean flag set to true when the stream is transferred - pub(crate) detached: bool, /// /// The ReadableStream instance controlled by this object pub(crate) readable: Heap, @@ -82,37 +79,7 @@ impl TransformStream { v } }; - let transformer_dict = - Transformer::from_jsval(scope, transformer_value, ()).map_err(|_| { - if js::exception::get_pending(scope).is_err() { - js::error::throw_type_error(scope, c"Invalid transformer"); - } - ExnThrown - })?; - // Step 2 (continued): converting the dictionary validates that each present - // callback member is callable (a `TypeError`), before the step-3/4 - // `readableType`/`writableType` `RangeError`s. - crate::support::ensure_callback_members_callable( - scope, - &[ - ( - transformer_dict.cancel.as_ref(), - c"transformer cancel must be a function", - ), - ( - transformer_dict.flush.as_ref(), - c"transformer flush must be a function", - ), - ( - transformer_dict.start.as_ref(), - c"transformer start must be a function", - ), - ( - transformer_dict.transform.as_ref(), - c"transformer transform must be a function", - ), - ], - )?; + let transformer_dict = Transformer::from_jsval_throwing(scope, transformer_value, ())?; // Step 3: If _transformerDict_["``readableType``"] `exists`, throw a ``RangeError`` // exception. if transformer_dict.readable_type.is_some() { @@ -132,13 +99,11 @@ impl TransformStream { // Step 5: Let _readableHighWaterMark_ be ? `ExtractHighWaterMark`(_readableStrategy_, 0). let readable_high_water_mark = extract_high_water_mark(scope, &readable_strategy, 0.0)?; // Step 6: Let _readableSizeAlgorithm_ be ! `ExtractSizeAlgorithm`(_readableStrategy_). - let readable_size_algorithm = - scope.root_value(extract_size_algorithm(scope, &readable_strategy)?); + let readable_size_algorithm = extract_size_algorithm(scope, &readable_strategy)?; // Step 7: Let _writableHighWaterMark_ be ? `ExtractHighWaterMark`(_writableStrategy_, 1). let writable_high_water_mark = extract_high_water_mark(scope, &writable_strategy, 1.0)?; // Step 8: Let _writableSizeAlgorithm_ be ! `ExtractSizeAlgorithm`(_writableStrategy_). - let writable_size_algorithm = - scope.root_value(extract_size_algorithm(scope, &writable_strategy)?); + let writable_size_algorithm = extract_size_algorithm(scope, &writable_strategy)?; // Step 9: Let _startPromise_ be `a new promise`. let start_promise = Promise::new_pending(scope)?; // Step 10: Perform ! `InitializeTransformStream`(`this`, _startPromise_, @@ -165,22 +130,12 @@ impl TransformStream { // the result of `invoking` _transformerDict_["``start``"] with argument list « // `this`.`[[controller]]` » and `callback this value` _transformer_. if let Some(start) = transformer_dict.start.as_ref() { - if !start.is_callable() { - return Err(js::error::throw_type_error( - scope, - c"transformer start must be a function", - )); - } let controller: TransformStreamDefaultController<'_> = self .data() .controller - .as_ref() - .expect("controller is set") - .get(scope); - let controller_value = scope.root_value(controller.as_value()); - let start_value = scope.root_value(start.as_value()); - let result = - js::Function::call(scope, transformer_value, start_value, &[controller_value])?; + .get(scope) + .expect("controller is set"); + let result = js::Function::call(scope, transformer_value, start, &[controller])?; start_promise.resolve(scope, result)?; } else { // Step 13: Otherwise, `resolve` _startPromise_ with undefined. @@ -211,7 +166,8 @@ impl TransformStream { .writable .get(scope) .data_mut() - .identity_transform = Some(Heap::from(*self)); + .identity_transform + .set(*self); } Ok(()) } diff --git a/crates/builtins/web-streams/src/transform/transform_stream_default_controller.rs b/crates/builtins/web-streams/src/transform/transform_stream_default_controller.rs index 5ad8c2da..e2abfa99 100644 --- a/crates/builtins/web-streams/src/transform/transform_stream_default_controller.rs +++ b/crates/builtins/web-streams/src/transform/transform_stream_default_controller.rs @@ -3,7 +3,7 @@ //! use super::algorithms; -use super::transform_stream::TransformStreamImpl; +use super::transform_stream::{TransformStream, TransformStreamImpl}; use core_runtime::{webidl_interface, webidl_methods}; use js::error::ExnThrown; use js::gc::handle::Heap; @@ -17,7 +17,7 @@ pub struct TransformStreamDefaultController { /// /// A promise-returning algorithm, taking one argument (the reason for cancellation), which /// communicates a requested cancellation to the transformer - pub(crate) cancel_algorithm: Heap, + pub(crate) cancel_algorithm: crate::support::Algorithm, /// /// A promise which resolves on completion of either the [[cancelAlgorithm]] or the /// [[flushAlgorithm]]. If this field is unpopulated (that is, undefined), then neither of those @@ -25,19 +25,17 @@ pub struct TransformStreamDefaultController { pub(crate) finish_promise: Option>, /// /// A promise-returning algorithm which communicates a requested close to the transformer - pub(crate) flush_algorithm: Heap, - /// The `this` value the transform/flush/cancel algorithms are invoked with (the - /// transformer object, or `undefined` for native algorithms). Not a spec slot. + pub(crate) flush_algorithm: crate::support::Algorithm, + /// The `this` value JS transform/flush/cancel algorithms are invoked with: the + /// transformer object. Not a spec slot. pub(crate) algorithm_receiver: Heap, /// /// The TransformStream instance controlled - /// - /// `Option`: set by `SetUpTransformStreamDefaultController` after creating. - pub(crate) stream: Option>, + pub(crate) stream: Heap, /// /// A promise-returning algorithm, taking one argument (the chunk to transform), which requests /// the transformer perform its transformation - pub(crate) transform_algorithm: Heap, + pub(crate) transform_algorithm: crate::support::Algorithm, /// The transform-rejection callback (`TransformStreamDefaultControllerPerformTransform` /// step 2; payload = this controller), created on the first transform and /// reused for every subsequent chunk. `None` until the first transform. @@ -52,8 +50,11 @@ pub struct TransformStreamDefaultController { #[webidl_methods] impl TransformStreamDefaultController { - fn new() -> Self { - TransformStreamDefaultControllerImpl::default() + fn new(stream: TransformStream<'_>) -> Self { + TransformStreamDefaultControllerImpl { + stream: Heap::from(stream), + ..Default::default() + } } /// diff --git a/crates/builtins/web-streams/src/transform/transformer.rs b/crates/builtins/web-streams/src/transform/transformer.rs index be650ac4..1cbcf92d 100644 --- a/crates/builtins/web-streams/src/transform/transformer.rs +++ b/crates/builtins/web-streams/src/transform/transformer.rs @@ -3,7 +3,7 @@ //! use core_runtime::webidl_dictionary; -use js::{prelude::HandleValue, Object}; +use js::{prelude::HandleValue, Callable}; /// #[webidl_dictionary] @@ -17,13 +17,13 @@ pub struct Transformer<'a> { } /// WebIDL callback `TransformerStartCallback`: (controller: TransformStreamDefaultController<'_>) -> HandleValue<'_> -pub type StartCallback<'s> = Object<'s>; +pub type StartCallback<'s> = Callable<'s>; /// WebIDL callback `TransformerFlushCallback`: (controller: TransformStreamDefaultController<'_>) -> Promise<'_> -pub type FlushCallback<'s> = Object<'s>; +pub type FlushCallback<'s> = Callable<'s>; /// WebIDL callback `TransformerTransformCallback`: (chunk: HandleValue<'_>, controller: TransformStreamDefaultController<'_>) -> Promise<'_> -pub type TransformCallback<'s> = Object<'s>; +pub type TransformCallback<'s> = Callable<'s>; /// WebIDL callback `TransformerCancelCallback`: (reason: HandleValue<'_>) -> Promise<'_> -pub type CancelCallback<'s> = Object<'s>; +pub type CancelCallback<'s> = Callable<'s>; diff --git a/crates/builtins/web-streams/src/writable/algorithms.rs b/crates/builtins/web-streams/src/writable/algorithms.rs index 18866739..5251ac00 100644 --- a/crates/builtins/web-streams/src/writable/algorithms.rs +++ b/crates/builtins/web-streams/src/writable/algorithms.rs @@ -4,21 +4,24 @@ use js::error::ExnThrown; use js::exception::take_pending_or_undefined; +use js::function::cast_payload; +use js::function::EmptyArgs; use js::gc::handle::Heap; use js::gc::scope::Scope; use js::heap::RootedTraceableBox; use js::native::Value; -use js::prelude::{CallbackArgs, HandleValue, OptionHeapExt}; +use js::prelude::{CallbackArgs, HandleValue, OptionHeapExt, ToJSVal}; use js::{value, Function, Promise}; use web_globals::events::algorithms::ScriptStackState; use web_globals::signals::abort_controller::AbortController; use crate::algorithms::{ - cast_payload, dequeue_value, enqueue_value_with_size, is_non_negative_number, make_type_error, - pair_parts, pair_payload, reset_queue, resolved_undefined_promise, + dequeue_value, enqueue_value_with_size, is_non_negative_number, make_type_error, pair_parts, + pair_payload, reset_queue, resolved_undefined_promise, }; use crate::queuing::{QueueWithSizes, ValueWithSize}; use crate::support; +use crate::support::{Algorithm, AlgorithmArg}; use crate::writable::underlying_sink::UnderlyingSink; use crate::writable::writable_stream::{ PendingAbortRequest, PromiseSlot, WritableStream, WritableStreamImpl, WritableStreamState, @@ -35,7 +38,7 @@ fn writable_stream_writer<'r>( scope: &'r Scope<'_>, stream: &WritableStream<'_>, ) -> Option> { - Some(stream.data().writer.as_ref()?.get(scope)) + stream.data().writer.get(scope) } /// The writer's `[[stream]]`, or `None` once released. @@ -43,7 +46,7 @@ pub(crate) fn writer_stream<'r>( scope: &'r Scope<'_>, writer: &WritableStreamDefaultWriter<'_>, ) -> Option> { - Some(writer.data().stream.as_ref()?.get(scope)) + writer.data().stream.get(scope) } /// `SetUpWritableStreamDefaultController` step 17. @@ -147,14 +150,10 @@ fn ws_close_promise_rejected( /// cleared). fn abort_reaction_payload<'r>( scope: &'r Scope<'_>, - stream: &WritableStream<'_>, - abort_promise: &Promise<'_>, + stream: &WritableStream<'r>, + abort_promise: &Promise<'r>, ) -> Result, ExnThrown> { - pair_payload( - scope, - scope.root_value(stream.as_value()), - scope.root_value(abort_promise.as_value()), - ) + pair_payload(scope, stream, abort_promise) } /// Unpack the `[stream, abortPromise]` abort-reaction payload. @@ -194,7 +193,7 @@ fn ws_abort_promise_rejected( } // --------------------------------------------------------------------------- -// Transform-stream accessors, native algorithm callbacks, and reactions. +// Controller internal methods, native algorithm callbacks, and reactions. // --------------------------------------------------------------------------- /// @@ -242,12 +241,12 @@ pub(crate) fn acquire_writable_stream_default_writer<'r>( #[allow(clippy::too_many_arguments)] pub(crate) fn create_writable_stream<'r>( scope: &'r Scope<'_>, - start_algorithm: HandleValue<'_>, - write_algorithm: HandleValue<'_>, - close_algorithm: HandleValue<'_>, - abort_algorithm: HandleValue<'_>, + start_algorithm: AlgorithmArg<'r>, + write_algorithm: AlgorithmArg<'r>, + close_algorithm: AlgorithmArg<'r>, + abort_algorithm: AlgorithmArg<'r>, high_water_mark: f64, - size_algorithm: HandleValue<'_>, + size_algorithm: impl ToJSVal<'r>, ) -> Result, ExnThrown> { // Step 1: Assert: ! `IsNonNegativeNumber`(_highWaterMark_) is true. debug_assert!(is_non_negative_number(high_water_mark)); @@ -258,7 +257,7 @@ pub(crate) fn create_writable_stream<'r>( // Step 3: Perform ! `InitializeWritableStream`(_stream_). initialize_writable_stream(&stream); // Step 4: Let _controller_ be a `new` ``WritableStreamDefaultController``. - let controller = WritableStreamDefaultController::new(scope)?; + let controller = WritableStreamDefaultController::new(scope, stream)?; // Step 5: Perform ? `SetUpWritableStreamDefaultController`(_stream_, _controller_, // _startAlgorithm_, _writeAlgorithm_, _closeAlgorithm_, _abortAlgorithm_, // _highWaterMark_, _sizeAlgorithm_). @@ -274,7 +273,7 @@ pub(crate) fn create_writable_stream<'r>( abort_algorithm, receiver, high_water_mark, - size_algorithm, + size_algorithm.to_jsval_throwing(scope)?, )?; // Step 6: Return _stream_. Ok(stream) @@ -326,9 +325,9 @@ pub(crate) fn set_up_writable_stream_default_writer( )); } // Step 2: Set _writer_.`[[stream]]` to _stream_. - writer.data_mut().stream = Some(Heap::from(*stream)); + writer.data_mut().stream.set(*stream); // Step 3: Set _stream_.`[[writer]]` to _writer_. - stream.data_mut().writer = Some(Heap::from(*writer)); + stream.data_mut().writer.set(*writer); // Step 4: Let _state_ be _stream_.`[[state]]`. let state = stream.data().state; match state { @@ -1148,13 +1147,11 @@ pub(crate) fn writable_stream_default_writer_write<'r>( let chunk_size = writable_stream_default_controller_get_chunk_size(scope, &controller, chunk); // Step 5: If _stream_ is not equal to _writer_.`[[stream]]`, return `a promise rejected with` a // ``TypeError`` exception. - // SAFETY: `as_ptr` is read only to compare object identity against the just-fetched, rooted - // `stream`; no allocation happens between reading it and the comparison. let still_owned = writer .data() .stream .as_ref() - .is_some_and(|s| unsafe { s.as_ptr() == stream.as_raw() }); + .is_some_and(|s| stream.eq_heap(s)); if !still_owned { js::error::throw_type_error(scope, c"Cannot write to a stream using a released writer"); return Promise::new_rejected_with_pending_error(scope).expect("rejected promise"); @@ -1201,10 +1198,10 @@ pub(crate) fn set_up_writable_stream_default_controller( scope: &Scope<'_>, stream: &WritableStream<'_>, controller: &WritableStreamDefaultController<'_>, - start_algorithm: HandleValue<'_>, - write_algorithm: HandleValue<'_>, - close_algorithm: HandleValue<'_>, - abort_algorithm: HandleValue<'_>, + start_algorithm: AlgorithmArg<'_>, + write_algorithm: AlgorithmArg<'_>, + close_algorithm: AlgorithmArg<'_>, + abort_algorithm: AlgorithmArg<'_>, algorithm_receiver: HandleValue<'_>, high_water_mark: f64, size_algorithm: HandleValue<'_>, @@ -1213,9 +1210,10 @@ pub(crate) fn set_up_writable_stream_default_controller( // Step 2: Assert: _stream_.`[[controller]]` is undefined. debug_assert!(stream.data().controller.is_none()); // Step 3: Set _controller_.`[[stream]]` to _stream_. - controller.data_mut().stream = Some(Heap::from(*stream)); + // (Already done by `WritableStreamDefaultController::new`.) + debug_assert!(stream.eq_heap(&controller.data().stream)); // Step 4: Set _stream_.`[[controller]]` to _controller_. - stream.data_mut().controller = Some(Heap::from(*controller)); + stream.data_mut().controller.set(*controller); // Step 5: Perform ! `ResetQueue`(_controller_). reset_queue(&mut *controller.data_mut()); // Step 6: Set _controller_.`[[abortController]]` to a new ``AbortController``. @@ -1231,20 +1229,11 @@ pub(crate) fn set_up_writable_stream_default_controller( // Step 9: Set _controller_.`[[strategyHWM]]` to _highWaterMark_. controller.data_mut().strategy_hwm = high_water_mark; // Step 10: Set _controller_.`[[writeAlgorithm]]` to _writeAlgorithm_. - controller - .data_mut() - .write_algorithm - .set(write_algorithm.get()); + controller.data_mut().write_algorithm = Algorithm::from(write_algorithm); // Step 11: Set _controller_.`[[closeAlgorithm]]` to _closeAlgorithm_. - controller - .data_mut() - .close_algorithm - .set(close_algorithm.get()); + controller.data_mut().close_algorithm = Algorithm::from(close_algorithm); // Step 12: Set _controller_.`[[abortAlgorithm]]` to _abortAlgorithm_. - controller - .data_mut() - .abort_algorithm - .set(abort_algorithm.get()); + controller.data_mut().abort_algorithm = Algorithm::from(abort_algorithm); // (The algorithms close over `algorithm_receiver` — the underlying sink — as their `this`.) controller .data_mut() @@ -1257,12 +1246,8 @@ pub(crate) fn set_up_writable_stream_default_controller( writable_stream_update_backpressure(scope, stream, backpressure); // Step 15: Let _startResult_ be the result of performing _startAlgorithm_. (This may throw an // exception.) - let start_result = support::invoke_algorithm( - scope, - start_algorithm, - algorithm_receiver, - &[scope.root_value(controller.as_value())], - )?; + let start_result = + support::invoke_algorithm(scope, start_algorithm, algorithm_receiver, &[controller])?; // Step 16: Let _startPromise_ be `a promise resolved with` _startResult_. // WebIDL "a promise resolved with" always creates a *new* promise (it // does not return the value as-is the way `Promise.resolve` does for @@ -1278,12 +1263,11 @@ pub(crate) fn set_up_writable_stream_default_controller( // "`writable`" or "`erroring`". Set _controller_.`[[started]]` to true. Perform ! // `WritableStreamDealWithRejection`(_stream_, _r_). // (Steps 17 and 18 are implemented by `ws_start_promise_fulfilled` / `ws_start_promise_rejected`.) - let payload = scope.root_value(controller.as_value()); support::react( scope, &start_promise, - Some((ws_start_promise_fulfilled, payload)), - Some((ws_start_promise_rejected, payload)), + Some((ws_start_promise_fulfilled, controller)), + Some((ws_start_promise_rejected, controller)), )?; Ok(()) } @@ -1299,52 +1283,36 @@ pub(crate) fn set_up_writable_stream_default_controller_from_underlying_sink( size_algorithm: HandleValue<'_>, ) -> Result<(), ExnThrown> { // Step 1: Let _controller_ be a `new` ``WritableStreamDefaultController``. - let controller = WritableStreamDefaultController::new(scope)?; - // The write/close/abort/start algorithms are the raw callbacks, invoked with `this` = - // _underlyingSink_ (passed below as the algorithm receiver); an absent callback is - // `undefined`, which the invoker treats as the resolved-undefined algorithm. + let controller = WritableStreamDefaultController::new(scope, *stream)?; + // The write/close/abort/start algorithms are the sink's callbacks, invoked with `this` = + // _underlyingSink_ (passed below as the algorithm receiver). An absent callback is no + // algorithm, which the invoker treats as the resolved-undefined algorithm. // Step 2: Let _startAlgorithm_ be an algorithm that returns undefined. // Step 6: If _underlyingSinkDict_["``start``"] `exists`, then set _startAlgorithm_ to an // algorithm which returns the result of `invoking` _underlyingSinkDict_["``start``"] // with argument list « _controller_ », exception behavior "`rethrow`", and `callback // this value` _underlyingSink_. - let start_algorithm = support::callback_member( - scope, - underlying_sink_dict.start.as_ref(), - c"underlying sink start must be a function", - )?; + let start_algorithm = AlgorithmArg::from_member(scope, underlying_sink_dict.start.as_ref()); // Step 3: Let _writeAlgorithm_ be an algorithm that returns `a promise resolved with` // undefined. // Step 7: If _underlyingSinkDict_["``write``"] `exists`, then set _writeAlgorithm_ to an // algorithm which takes an argument _chunk_ and returns the result of `invoking` // _underlyingSinkDict_["``write``"] with argument list « _chunk_, _controller_ » and // `callback this value` _underlyingSink_. - let write_algorithm = support::callback_member( - scope, - underlying_sink_dict.write.as_ref(), - c"underlying sink write must be a function", - )?; + let write_algorithm = AlgorithmArg::from_member(scope, underlying_sink_dict.write.as_ref()); // Step 4: Let _closeAlgorithm_ be an algorithm that returns `a promise resolved with` // undefined. // Step 8: If _underlyingSinkDict_["``close``"] `exists`, then set _closeAlgorithm_ to an // algorithm which returns the result of `invoking` _underlyingSinkDict_["``close``"] // with argument list «» and `callback this value` _underlyingSink_. - let close_algorithm = support::callback_member( - scope, - underlying_sink_dict.close.as_ref(), - c"underlying sink close must be a function", - )?; + let close_algorithm = AlgorithmArg::from_member(scope, underlying_sink_dict.close.as_ref()); // Step 5: Let _abortAlgorithm_ be an algorithm that returns `a promise resolved with` // undefined. // Step 9: If _underlyingSinkDict_["``abort``"] `exists`, then set _abortAlgorithm_ to an // algorithm which takes an argument _reason_ and returns the result of `invoking` // _underlyingSinkDict_["``abort``"] with argument list « _reason_ » and `callback // this value` _underlyingSink_. - let abort_algorithm = support::callback_member( - scope, - underlying_sink_dict.abort.as_ref(), - c"underlying sink abort must be a function", - )?; + let abort_algorithm = AlgorithmArg::from_member(scope, underlying_sink_dict.abort.as_ref()); // Step 10: Perform ? `SetUpWritableStreamDefaultController`(_stream_, _controller_, // _startAlgorithm_, _writeAlgorithm_, _closeAlgorithm_, _abortAlgorithm_, // _highWaterMark_, _sizeAlgorithm_). @@ -1398,12 +1366,10 @@ pub(crate) fn writable_stream_default_controller_advance_queue_if_needed( // Step 8: Let _value_ be ! `PeekQueueValue`(_controller_). // Step 9: If _value_ is the `close sentinel`, perform ! // `WritableStreamDefaultControllerProcessClose`(_controller_). - let is_close_sentinel = controller - .data() - .queue - .front() - .map(|entry| entry.is_close_sentinel) - .unwrap_or(false); + let is_close_sentinel = matches!( + controller.data().queue.front(), + Some(ValueWithSize::CloseSentinel) + ); if is_close_sentinel { writable_stream_default_controller_process_close(scope, controller); } else { @@ -1420,20 +1386,11 @@ pub(crate) fn writable_stream_default_controller_clear_algorithms( controller: &WritableStreamDefaultController<'_>, ) { // Step 1: Set _controller_.`[[writeAlgorithm]]` to undefined. - controller - .data_mut() - .write_algorithm - .set(value::undefined()); + controller.data_mut().write_algorithm = Algorithm::None; // Step 2: Set _controller_.`[[closeAlgorithm]]` to undefined. - controller - .data_mut() - .close_algorithm - .set(value::undefined()); + controller.data_mut().close_algorithm = Algorithm::None; // Step 3: Set _controller_.`[[abortAlgorithm]]` to undefined. - controller - .data_mut() - .abort_algorithm - .set(value::undefined()); + controller.data_mut().abort_algorithm = Algorithm::None; // Step 4: Set _controller_.`[[strategySizeAlgorithm]]` to undefined. controller .data_mut() @@ -1448,13 +1405,11 @@ pub(crate) fn writable_stream_default_controller_close( controller: &WritableStreamDefaultController<'_>, ) { // Step 1: Perform ! `EnqueueValueWithSize`(_controller_, `close sentinel`, 0). - // The close sentinel is represented by the `is_close_sentinel` flag on the queue - // entry; size 0 leaves the total queue size unchanged. - controller.data_mut().queue.push_back(ValueWithSize { - value: Heap::default(), - size: 0.0, - is_close_sentinel: true, - }); + // Size 0 leaves the total queue size unchanged. + controller + .data_mut() + .queue + .push_back(ValueWithSize::CloseSentinel); // Step 2: Perform ! `WritableStreamDefaultControllerAdvanceQueueIfNeeded`(_controller_). writable_stream_default_controller_advance_queue_if_needed(scope, controller); } @@ -1512,22 +1467,24 @@ pub(crate) fn writable_stream_default_controller_get_chunk_size( // Step 1: If _controller_.`[[strategySizeAlgorithm]]` is undefined, then: if size_algorithm.is_undefined() { // Step 1.1: Assert: _controller_.`[[stream]]`.`[[state]]` is not "`writable`". - // `[[strategySizeAlgorithm]]` is only set to undefined by - // `…ClearAlgorithms`, which runs once the stream leaves "writable" - // (`ExtractSizeAlgorithm` always installs a callable, never undefined). - debug_assert!(controller.stream(scope).data().state != WritableStreamState::Writable); + // (`undefined` also stands for `ExtractSizeAlgorithm`'s "algorithm that + // returns 1" here, so the assertion only holds for a cleared algorithm.) // Step 1.2: Return 1. return 1.0; } // Step 2: Let _returnValue_ be the result of performing // _controller_.`[[strategySizeAlgorithm]]`, passing in _chunk_, and interpreting the // result as a `completion record`. - let return_value = - support::invoke_algorithm(scope, size_algorithm, HandleValue::undefined(), &[chunk]) - .and_then(|v| { - use js::conversion::FromJSVal; - f64::from_jsval_throwing(scope, v, ()) - }); + let return_value = support::invoke_algorithm( + scope, + support::AlgorithmArg::Js(size_algorithm), + HandleValue::undefined(), + &[chunk], + ) + .and_then(|v| { + use js::conversion::FromJSVal; + f64::from_jsval_throwing(scope, v, ()) + }); match return_value { // Step 4: Return _returnValue_.[[Value]]. Ok(size) => size, @@ -1569,7 +1526,7 @@ pub(crate) fn writable_stream_default_controller_process_close( let close_algorithm = controller.data().close_algorithm.get(scope); let receiver = controller.data().algorithm_receiver.get(scope); let sink_close_promise = - support::invoke_promise_algorithm(scope, close_algorithm, receiver, &[]); + support::invoke_promise_algorithm(scope, close_algorithm, receiver, EmptyArgs); // Step 6: Perform ! `WritableStreamDefaultControllerClearAlgorithms`(_controller_). writable_stream_default_controller_clear_algorithms(controller); // Step 7: `Upon fulfillment` of _sinkClosePromise_, Perform ! @@ -1577,15 +1534,11 @@ pub(crate) fn writable_stream_default_controller_process_close( // Step 8: `Upon rejection` of _sinkClosePromise_ with reason _reason_, Perform ! // `WritableStreamFinishInFlightCloseWithError`(_stream_, _reason_). // (Steps 7 and 8 are implemented by `ws_close_promise_fulfilled` / `ws_close_promise_rejected`.) - let payload = { - let stream: &WritableStream<'_> = &stream; - scope.root_value(stream.as_value()) - }; support::react( scope, &sink_close_promise, - Some((ws_close_promise_fulfilled, payload)), - Some((ws_close_promise_rejected, payload)), + Some((ws_close_promise_fulfilled, stream)), + Some((ws_close_promise_rejected, stream)), ) .expect("attach close reactions"); } @@ -1628,13 +1581,13 @@ pub(crate) fn writable_stream_default_controller_process_write( // (Steps 4 and 5 are implemented by `ws_write_promise_fulfilled` / // `ws_write_promise_rejected`.) if controller.data().write_fulfilled_fn.is_none() { - let payload = scope.root_value(controller.as_value()); - let fulfilled = Function::new_callback(scope, c"", 1, ws_write_promise_fulfilled, payload) + let fulfilled = + Function::new_callback(scope, c"", 1, ws_write_promise_fulfilled, controller) + .expect("create write reaction"); + controller.data_mut().write_fulfilled_fn.set(fulfilled); + let rejected = Function::new_callback(scope, c"", 1, ws_write_promise_rejected, controller) .expect("create write reaction"); - controller.data_mut().write_fulfilled_fn = Some(Heap::from(fulfilled)); - let rejected = Function::new_callback(scope, c"", 1, ws_write_promise_rejected, payload) - .expect("create write reaction"); - controller.data_mut().write_rejected_fn = Some(Heap::from(rejected)); + controller.data_mut().write_rejected_fn.set(rejected); } let fulfilled = controller .data() @@ -1697,5 +1650,9 @@ pub(crate) fn peek_queue_value<'r>( // Step 3: Let _valueWithSize_ be _container_.[[queue]][0]. let value_with_size = container.queue().front().unwrap(); // Step 4: Return _valueWithSize_’s `value`. - value_with_size.value.get(scope) + // (The close sentinel's value is never read, so it peeks as undefined.) + match value_with_size { + ValueWithSize::Chunk { value, .. } => value.get(scope), + ValueWithSize::CloseSentinel => HandleValue::undefined(), + } } diff --git a/crates/builtins/web-streams/src/writable/default_controller.rs b/crates/builtins/web-streams/src/writable/default_controller.rs index 7b6574e8..fb8ef48d 100644 --- a/crates/builtins/web-streams/src/writable/default_controller.rs +++ b/crates/builtins/web-streams/src/writable/default_controller.rs @@ -23,14 +23,14 @@ pub struct WritableStreamDefaultController { /// /// A promise-returning algorithm, taking one argument (the abort reason), which communicates a /// requested abort to the underlying sink - pub(crate) abort_algorithm: Heap, + pub(crate) abort_algorithm: crate::support::Algorithm, /// /// An AbortController that can be used to abort the pending write or close operation when the /// stream is aborted. pub(crate) abort_controller: Heap, /// /// A promise-returning algorithm which communicates a requested close to the underlying sink - pub(crate) close_algorithm: Heap, + pub(crate) close_algorithm: crate::support::Algorithm, /// /// A list representing the stream’s internal queue of chunks pub(crate) queue: VecDeque, @@ -49,19 +49,17 @@ pub struct WritableStreamDefaultController { /// An algorithm to calculate the size of enqueued chunks, as part of the stream’s queuing /// strategy pub(crate) strategy_size_algorithm: Heap, - /// The `this` value the write/close/abort algorithms are invoked with (the - /// underlying sink, or `undefined` for native algorithms). Not a spec slot; - /// see the readable controller's `algorithm_receiver`. + /// The `this` value JS write/close/abort algorithms are invoked with: the + /// underlying sink. Not a spec slot, see the readable controller's + /// `algorithm_receiver`. pub(crate) algorithm_receiver: Heap, /// /// The WritableStream instance controlled - /// - /// `Option`: set by `SetUp...Controller` after the controller is created. - pub(crate) stream: Option>, + pub(crate) stream: Heap, /// /// A promise-returning algorithm, taking one argument (the chunk to write), which writes data to /// the underlying sink - pub(crate) write_algorithm: Heap, + pub(crate) write_algorithm: crate::support::Algorithm, /// The sink-write-reaction callbacks (`WritableStreamDefaultControllerProcessWrite` /// steps 4-5; payload = this controller), created on the first write and /// reused for every subsequent chunk. `None` until the first write. @@ -71,9 +69,10 @@ pub struct WritableStreamDefaultController { #[webidl_methods] impl WritableStreamDefaultController { - fn new(&self, scope: &Scope<'_>) -> Result<(), ExnThrown> { - self.data_mut() - .abort_controller + fn new(&self, scope: &Scope<'_>, stream: WritableStream<'_>) -> Result<(), ExnThrown> { + let data = self.data_mut(); + data.stream.set(stream); + data.abort_controller .set(AbortController::new(scope).expect("AbortController can only fail due to OOM")); Ok(()) } @@ -98,17 +97,13 @@ impl WritableStreamDefaultController { return Ok(()); } // Step 3: Perform ! `WritableStreamDefaultControllerError`(`this`, _e_). - let e = e.unwrap_or_else(|| scope.root_value(js::value::undefined())); + let e = e.unwrap_or(HandleValue::undefined()); algorithms::writable_stream_default_controller_error(scope, self, e); Ok(()) } - pub(crate) fn stream<'r>(&'r self, scope: &'r Scope<'_>) -> WritableStream<'r> { - self.data() - .stream - .as_ref() - .expect("controller has a stream") - .get(scope) + pub(crate) fn stream<'r>(&self, scope: &'r Scope<'_>) -> WritableStream<'r> { + self.data().stream.get(scope) } } diff --git a/crates/builtins/web-streams/src/writable/default_writer.rs b/crates/builtins/web-streams/src/writable/default_writer.rs index 604d04b5..d27c3eb6 100644 --- a/crates/builtins/web-streams/src/writable/default_writer.rs +++ b/crates/builtins/web-streams/src/writable/default_writer.rs @@ -76,8 +76,10 @@ impl WritableStreamDefaultWriter { // Step 1: If `this`.`[[stream]]` is undefined, return `a promise rejected with` a // ``TypeError`` exception. if self.data().stream.is_none() { - js::error::throw_type_error(scope, c"Cannot abort a stream using a released writer"); - return Promise::new_rejected_with_pending_error(scope); + return Err(js::error::throw_type_error( + scope, + c"Cannot abort a stream using a released writer", + )); } // Step 2: Return ! `DefaultWriterAbort`(`this`, _reason_). Ok(algorithms::writable_stream_default_writer_abort( @@ -97,18 +99,19 @@ impl WritableStreamDefaultWriter { let stream = match algorithms::writer_stream(scope, self) { Some(stream) => stream, None => { - js::error::throw_type_error( + return Err(js::error::throw_type_error( scope, c"Cannot close a stream using a released writer", - ); - return Promise::new_rejected_with_pending_error(scope); + )); } }; // Step 3: If ! `WritableStreamCloseQueuedOrInFlight`(_stream_) is true, return `a promise // rejected with` a ``TypeError`` exception. if algorithms::writable_stream_close_queued_or_in_flight(&stream) { - js::error::throw_type_error(scope, c"Cannot close an already-closing stream"); - return Promise::new_rejected_with_pending_error(scope); + return Err(js::error::throw_type_error( + scope, + c"Cannot close an already-closing stream", + )); } // Step 4: Return ! `DefaultWriterClose`(`this`). Ok(algorithms::writable_stream_default_writer_close( @@ -140,8 +143,10 @@ impl WritableStreamDefaultWriter { // Step 1: If `this`.`[[stream]]` is undefined, return `a promise rejected with` a // ``TypeError`` exception. if self.data().stream.is_none() { - js::error::throw_type_error(scope, c"Cannot write to a stream using a released writer"); - return Promise::new_rejected_with_pending_error(scope); + return Err(js::error::throw_type_error( + scope, + c"Cannot write to a stream using a released writer", + )); } // Step 2: Return ! `DefaultWriterWrite`(`this`, _chunk_). let chunk = chunk.unwrap_or(HandleValue::undefined()); diff --git a/crates/builtins/web-streams/src/writable/underlying_sink.rs b/crates/builtins/web-streams/src/writable/underlying_sink.rs index fc76c426..0bee807a 100644 --- a/crates/builtins/web-streams/src/writable/underlying_sink.rs +++ b/crates/builtins/web-streams/src/writable/underlying_sink.rs @@ -3,7 +3,7 @@ //! use core_runtime::webidl_dictionary; -use js::{prelude::HandleValue, Object}; +use js::{prelude::HandleValue, Callable}; /// #[webidl_dictionary] @@ -16,13 +16,13 @@ pub struct UnderlyingSink<'a> { } /// WebIDL callback `UnderlyingSinkStartCallback`: (controller: WritableStreamDefaultController<'_>) -> HandleValue<'_> -pub type StartCallback<'s> = Object<'s>; +pub type StartCallback<'s> = Callable<'s>; /// WebIDL callback `UnderlyingSinkWriteCallback`: (chunk: HandleValue<'_>, controller: WritableStreamDefaultController<'_>) -> Promise<'_> -pub type WriteCallback<'s> = Object<'s>; +pub type WriteCallback<'s> = Callable<'s>; /// WebIDL callback `UnderlyingSinkCloseCallback`: () -> Promise<'_> -pub type CloseCallback<'s> = Object<'s>; +pub type CloseCallback<'s> = Callable<'s>; /// WebIDL callback `UnderlyingSinkAbortCallback`: (reason: Option>) -> Promise<'_> -pub type AbortCallback<'s> = Object<'s>; +pub type AbortCallback<'s> = Callable<'s>; diff --git a/crates/builtins/web-streams/src/writable/writable_stream.rs b/crates/builtins/web-streams/src/writable/writable_stream.rs index e63335b4..8264a89b 100644 --- a/crates/builtins/web-streams/src/writable/writable_stream.rs +++ b/crates/builtins/web-streams/src/writable/writable_stream.rs @@ -10,12 +10,11 @@ use super::underlying_sink::UnderlyingSink; use crate::algorithms::extract_high_water_mark; use crate::algorithms::extract_size_algorithm; use crate::queuing::QueuingStrategy; -use crate::support; use crate::writable::WritableStreamDefaultController; use core_runtime::{webidl_interface, webidl_methods}; use js::conversion::FromJSVal; use js::error::ExnThrown; -use js::gc::handle::Heap; +use js::gc::handle::{Heap, OptionHeapExt}; use js::gc::scope::Scope; use js::native::Value; use js::prelude::HandleValue; @@ -31,17 +30,9 @@ use web_globals::events::algorithms::ScriptStackState; /// normally; once moved out via `take`/`pop_front` it must be consumed /// immediately (`into_promise`) or kept in a `RootedTraceableBox`. #[js::must_root] -#[derive(Default, js::ScopeRoot)] +#[derive(core_runtime::Traceable, Default, js::ScopeRoot)] pub(crate) struct PromiseSlot(Heap); -// Safety: the only GC pointer is the inner Heap. -unsafe impl js::heap::Trace for PromiseSlot { - #[inline] - unsafe fn trace(&self, trc: *mut js::native::JSTracer) { - self.0.trace(trc); - } -} - impl PromiseSlot { /// Wrap a promise in a slot. pub(crate) fn new(promise: Promise<'_>) -> Self { @@ -99,9 +90,6 @@ pub struct WritableStream { /// `Option` because the stream is created before `SetUp...Controller` wires /// it; always `Some` thereafter. pub(crate) controller: Option>, - /// - /// A boolean flag set to true when the stream is transferred - pub(crate) detached: bool, /// /// A slot set to the promise for the current in-flight write operation while the underlying /// sink’s write algorithm is executing and has not yet fulfilled, used to prevent reentrant @@ -180,37 +168,8 @@ impl WritableStream { v } }; - let underlying_sink_dict = UnderlyingSink::from_jsval(scope, underlying_sink_value, ()) - .map_err(|_| { - if js::exception::get_pending(scope).is_err() { - js::error::throw_type_error(scope, c"Invalid underlying sink"); - } - ExnThrown - })?; - // Step 2 (continued): converting the dictionary validates that each present - // callback member is callable (a `TypeError`), before the step-3 `type` - // `RangeError`. - support::ensure_callback_members_callable( - scope, - &[ - ( - underlying_sink_dict.abort.as_ref(), - c"underlying sink abort must be a function", - ), - ( - underlying_sink_dict.close.as_ref(), - c"underlying sink close must be a function", - ), - ( - underlying_sink_dict.start.as_ref(), - c"underlying sink start must be a function", - ), - ( - underlying_sink_dict.write.as_ref(), - c"underlying sink write must be a function", - ), - ], - )?; + let underlying_sink_dict = + UnderlyingSink::from_jsval_throwing(scope, underlying_sink_value, ())?; // Step 3: If _underlyingSinkDict_["``type``"] `exists`, throw a ``RangeError`` exception. // This is to allow us to add new potential types in the future, without // backward-compatibility concerns. @@ -223,7 +182,7 @@ impl WritableStream { // Step 4: Perform ! `InitializeWritableStream`(`this`). algorithms::initialize_writable_stream(self); // Step 5: Let _sizeAlgorithm_ be ! `ExtractSizeAlgorithm`(_strategy_). - let size_algorithm = scope.root_value(extract_size_algorithm(scope, &strategy)?); + let size_algorithm = extract_size_algorithm(scope, &strategy)?; // Step 6: Let _highWaterMark_ be ? `ExtractHighWaterMark`(_strategy_, 1). let high_water_mark = extract_high_water_mark(scope, &strategy, 1.0)?; // Step 7: Perform ? `SetUpWritableStreamDefaultControllerFromUnderlyingSink`(`this`, @@ -255,8 +214,10 @@ impl WritableStream { // Step 1: If ! `IsWritableStreamLocked`(`this`) is true, return `a promise rejected with` a // ``TypeError`` exception. if algorithms::is_writable_stream_locked(self) { - js::error::throw_type_error(scope, c"Cannot abort a stream that already has a writer"); - return Promise::new_rejected_with_pending_error(scope); + return Err(js::error::throw_type_error( + scope, + c"Cannot abort a stream that already has a writer", + )); } // Step 2: Return ! `WritableStreamAbort`(`this`, _reason_). Ok(algorithms::writable_stream_abort( @@ -273,14 +234,18 @@ impl WritableStream { // Step 1: If ! `IsWritableStreamLocked`(`this`) is true, return `a promise rejected with` a // ``TypeError`` exception. if algorithms::is_writable_stream_locked(self) { - js::error::throw_type_error(scope, c"Cannot close a stream that already has a writer"); - return Promise::new_rejected_with_pending_error(scope); + return Err(js::error::throw_type_error( + scope, + c"Cannot close a stream that already has a writer", + )); } // Step 2: If ! `WritableStreamCloseQueuedOrInFlight`(`this`) is true, return `a promise // rejected with` a ``TypeError`` exception. if algorithms::writable_stream_close_queued_or_in_flight(self) { - js::error::throw_type_error(scope, c"Cannot close an already-closing stream"); - return Promise::new_rejected_with_pending_error(scope); + return Err(js::error::throw_type_error( + scope, + c"Cannot close an already-closing stream", + )); } // Step 3: Return ! `WritableStreamClose`(`this`). Ok(algorithms::writable_stream_close(scope, self)) @@ -302,8 +267,7 @@ impl WritableStream { ) -> WritableStreamDefaultController<'r> { self.data() .controller - .as_ref() - .expect("stream has a controller") .get(scope) + .expect("stream has a controller") } } diff --git a/crates/builtins/web-streams/tests/async_iterator.rs b/crates/builtins/web-streams/tests/async_iterator.rs index 728d2756..37da9cee 100644 --- a/crates/builtins/web-streams/tests/async_iterator.rs +++ b/crates/builtins/web-streams/tests/async_iterator.rs @@ -10,24 +10,17 @@ // This file contains nothing platform-specific, so skip it on wasm32. #![cfg(not(target_arch = "wasm32"))] -use core_runtime::config::RuntimeConfig; -use core_runtime::event_loop::run_microtasks; -use core_runtime::runtime::{clear_global_initializers, register_global_initializer, Runtime}; -use js::conversion::FromJSVal; -use js::error::ExnThrown; +use core_runtime::runtime::{clear_global_initializers, register_global_initializer}; +use core_runtime::test_util::eval_out_with_setup; fn run(code: &str) -> String { - clear_global_initializers(); - register_global_initializer(web_streams::add_to_global); - let rt = Runtime::init(&RuntimeConfig::default()); - let scope = rt.default_global(); - if js::compile::evaluate_with_filename(&scope, code, "test.js", 1).is_err() { - panic!("evaluation threw: {:?}", ExnThrown::capture(&scope)); - } - run_microtasks(&scope); - let out = js::compile::evaluate_with_filename(&scope, "globalThis.__out", "out.js", 1) - .expect("reading __out threw"); - String::from_jsval(&scope, out, ()).unwrap() + eval_out_with_setup( + || { + clear_global_initializers(); + register_global_initializer(web_streams::add_to_global); + }, + code, + ) } /// `for await` drains all of the stream's chunks. @@ -148,3 +141,32 @@ fn iterator_prototype_shape() { "#); assert_eq!(out, "true,next,return,true"); } + +/// `next()` clears the ongoing promise when the previous call settles, so a call +/// made after that runs the next steps directly instead of chaining behind the +/// settled promise. Both calls then settle after the same number of ticks. +#[test] +fn settled_next_does_not_delay_the_following_call() { + let out = run(r#" + globalThis.__out = "pending"; + const rs = new ReadableStream({ start(c) { c.enqueue(1); c.enqueue(2); } }); + const it = rs.values(); + function ticksUntil(p) { + let done = false; + p.then(() => { done = true; }); + return new Promise(resolve => { + let n = 0; + (function spin() { + if (done) { resolve(n); } else { n++; Promise.resolve().then(spin); } + })(); + }); + } + (async () => { + const first = await ticksUntil(it.next()); + const second = await ticksUntil(it.next()); + globalThis.__out = `${first},${second}`; + })(); + "#); + let (first, second) = out.split_once(',').expect("two tick counts"); + assert_eq!(first, second, "ticks: {out}"); +} diff --git a/crates/builtins/web-streams/tests/body.rs b/crates/builtins/web-streams/tests/body.rs index 99f33abb..4352e5bf 100644 --- a/crates/builtins/web-streams/tests/body.rs +++ b/crates/builtins/web-streams/tests/body.rs @@ -13,9 +13,8 @@ #![cfg(not(target_arch = "wasm32"))] use core_runtime::config::RuntimeConfig; -use core_runtime::event_loop::run_microtasks; use core_runtime::runtime::{clear_global_initializers, register_global_initializer, Runtime}; -use js::conversion::FromJSVal; +use core_runtime::test_util::eval_and_read_out; use web_streams::readable::ReadableStream; /// Create a stream from `bytes`, expose it as `globalThis.__stream`, then evaluate @@ -23,23 +22,14 @@ use web_streams::readable::ReadableStream; fn run_with_stream(bytes: &[u8], code: &str) -> String { clear_global_initializers(); register_global_initializer(web_streams::add_to_global); - let rt = Runtime::init(&RuntimeConfig::default()); + let rt = Runtime::init(&RuntimeConfig::default()).expect("runtime init"); let scope = rt.default_global(); let global = scope.global(); let stream = ReadableStream::from_bytes(&scope, bytes).expect("create stream from bytes"); global - .set_property(&scope, c"__stream", scope.root_value(stream.as_value())) + .set_property(&scope, c"__stream", stream) .expect("expose __stream"); - if js::compile::evaluate_with_filename(&scope, code, "test.js", 1).is_err() { - panic!( - "evaluation threw: {:?}", - js::error::ExnThrown::capture(&scope) - ); - } - run_microtasks(&scope); - let out = js::compile::evaluate_with_filename(&scope, "globalThis.__out", "out.js", 1) - .expect("reading __out threw"); - String::from_jsval(&scope, out, ()).unwrap() + eval_and_read_out(&scope, code) } /// A non-empty Rust-created stream drains to exactly its original bytes, then diff --git a/crates/builtins/web-streams/tests/pipe_to.rs b/crates/builtins/web-streams/tests/pipe_to.rs index de598f70..c7707a02 100644 --- a/crates/builtins/web-streams/tests/pipe_to.rs +++ b/crates/builtins/web-streams/tests/pipe_to.rs @@ -11,25 +11,18 @@ // This file contains nothing platform-specific, so skip it on wasm32. #![cfg(not(target_arch = "wasm32"))] -use core_runtime::config::RuntimeConfig; -use core_runtime::event_loop::run_microtasks; -use core_runtime::runtime::{clear_global_initializers, register_global_initializer, Runtime}; -use js::conversion::FromJSVal; -use js::error::ExnThrown; +use core_runtime::runtime::{clear_global_initializers, register_global_initializer}; +use core_runtime::test_util::eval_out_with_setup; fn run(code: &str) -> String { - clear_global_initializers(); - register_global_initializer(web_globals::add_to_global); - register_global_initializer(web_streams::add_to_global); - let rt = Runtime::init(&RuntimeConfig::default()); - let scope = rt.default_global(); - if js::compile::evaluate_with_filename(&scope, code, "test.js", 1).is_err() { - panic!("evaluation threw: {:?}", ExnThrown::capture(&scope)); - } - run_microtasks(&scope); - let out = js::compile::evaluate_with_filename(&scope, "globalThis.__out", "out.js", 1) - .expect("reading __out threw"); - String::from_jsval(&scope, out, ()).unwrap() + eval_out_with_setup( + || { + clear_global_initializers(); + register_global_initializer(web_globals::add_to_global); + register_global_initializer(web_streams::add_to_global); + }, + code, + ) } /// A WritableStream that records written chunks and whether it was closed. @@ -172,3 +165,26 @@ fn abort_signal_not_suppressed_by_stop_immediate_propagation() { )); assert_eq!(out, "rejected:stop|aborted:true"); } + +/// Aborting a pipe runs author code synchronously: the destination controller's +/// `signal` listeners. A listener that enqueues into the source re-enters the +/// pipe's pending read request, which must not find the pipe state borrowed. +#[test] +fn abort_listener_enqueueing_into_source_does_not_panic() { + let out = run(r#" + globalThis.__out = "pending"; + let sourceController; + const rs = new ReadableStream({ start(c) { sourceController = c; } }); + const ws = new WritableStream({ + start(c) { + c.signal.addEventListener("abort", () => sourceController.enqueue("late")); + }, + }); + const ac = new AbortController(); + const p = rs.pipeTo(ws, { signal: ac.signal }); + Promise.resolve().then(() => ac.abort(new Error("stop"))); + p.then(() => { globalThis.__out = "fulfilled"; }, + e => { globalThis.__out = "rejected:" + e.message; }); + "#); + assert_eq!(out, "rejected:stop"); +} diff --git a/crates/builtins/web-streams/tests/readable_byob.rs b/crates/builtins/web-streams/tests/readable_byob.rs index 4dbd08ed..14debebd 100644 --- a/crates/builtins/web-streams/tests/readable_byob.rs +++ b/crates/builtins/web-streams/tests/readable_byob.rs @@ -18,25 +18,18 @@ // This file contains nothing platform-specific, so skip it on wasm32. #![cfg(not(target_arch = "wasm32"))] -use core_runtime::config::RuntimeConfig; -use core_runtime::event_loop::run_microtasks; -use core_runtime::runtime::{clear_global_initializers, register_global_initializer, Runtime}; -use js::conversion::FromJSVal; -use js::error::ExnThrown; +use core_runtime::runtime::{clear_global_initializers, register_global_initializer}; +use core_runtime::test_util::eval_out_with_setup; /// Evaluate `code`, drain microtasks, and return `String(globalThis.__out)`. fn run(code: &str) -> String { - clear_global_initializers(); - register_global_initializer(web_streams::add_to_global); - let rt = Runtime::init(&RuntimeConfig::default()); - let scope = rt.default_global(); - if js::compile::evaluate_with_filename(&scope, code, "test.js", 1).is_err() { - panic!("evaluation threw: {:?}", ExnThrown::capture(&scope)); - } - run_microtasks(&scope); - let out = js::compile::evaluate_with_filename(&scope, "globalThis.__out", "out.js", 1) - .expect("reading __out threw"); - String::from_jsval(&scope, out, ()).unwrap() + eval_out_with_setup( + || { + clear_global_initializers(); + register_global_initializer(web_streams::add_to_global); + }, + code, + ) } /// A BYOB `read(view)`: the source fills the provided view through diff --git a/crates/builtins/web-streams/tests/readable_byte.rs b/crates/builtins/web-streams/tests/readable_byte.rs index e28828d8..a73fb70d 100644 --- a/crates/builtins/web-streams/tests/readable_byte.rs +++ b/crates/builtins/web-streams/tests/readable_byte.rs @@ -17,25 +17,18 @@ // This file contains nothing platform-specific, so skip it on wasm32. #![cfg(not(target_arch = "wasm32"))] -use core_runtime::config::RuntimeConfig; -use core_runtime::event_loop::run_microtasks; -use core_runtime::runtime::{clear_global_initializers, register_global_initializer, Runtime}; -use js::conversion::FromJSVal; -use js::error::ExnThrown; +use core_runtime::runtime::{clear_global_initializers, register_global_initializer}; +use core_runtime::test_util::eval_out_with_setup; /// Evaluate `code`, drain microtasks, and return `String(globalThis.__out)`. fn run(code: &str) -> String { - clear_global_initializers(); - register_global_initializer(web_streams::add_to_global); - let rt = Runtime::init(&RuntimeConfig::default()); - let scope = rt.default_global(); - if js::compile::evaluate_with_filename(&scope, code, "test.js", 1).is_err() { - panic!("evaluation threw: {:?}", ExnThrown::capture(&scope)); - } - run_microtasks(&scope); - let out = js::compile::evaluate_with_filename(&scope, "globalThis.__out", "out.js", 1) - .expect("reading __out threw"); - String::from_jsval(&scope, out, ()).unwrap() + eval_out_with_setup( + || { + clear_global_initializers(); + register_global_initializer(web_streams::add_to_global); + }, + code, + ) } /// A byte chunk enqueued during `start` is queued, then delivered to a `read()` diff --git a/crates/builtins/web-streams/tests/readable_default.rs b/crates/builtins/web-streams/tests/readable_default.rs index 5f3d0218..7a18058d 100644 --- a/crates/builtins/web-streams/tests/readable_default.rs +++ b/crates/builtins/web-streams/tests/readable_default.rs @@ -15,25 +15,18 @@ // This file contains nothing platform-specific, so skip it on wasm32. #![cfg(not(target_arch = "wasm32"))] -use core_runtime::config::RuntimeConfig; -use core_runtime::event_loop::run_microtasks; -use core_runtime::runtime::{clear_global_initializers, register_global_initializer, Runtime}; -use js::conversion::FromJSVal; -use js::error::ExnThrown; +use core_runtime::runtime::{clear_global_initializers, register_global_initializer}; +use core_runtime::test_util::eval_out_with_setup; /// Evaluate `code`, drain microtasks, and return `String(globalThis.__out)`. fn run(code: &str) -> String { - clear_global_initializers(); - register_global_initializer(|scope, global| web_streams::add_to_global(scope, global)); - let rt = Runtime::init(&RuntimeConfig::default()); - let scope = rt.default_global(); - if js::compile::evaluate_with_filename(&scope, code, "test.js", 1).is_err() { - panic!("evaluation threw: {:?}", ExnThrown::capture(&scope)); - } - run_microtasks(&scope); - let out = js::compile::evaluate_with_filename(&scope, "globalThis.__out", "out.js", 1) - .expect("reading __out threw"); - String::from_jsval(&scope, out, ()).unwrap() + eval_out_with_setup( + || { + clear_global_initializers(); + register_global_initializer(|scope, global| web_streams::add_to_global(scope, global)); + }, + code, + ) } /// Bound functions are accepted as WebIDL callback-type members: a bound `pull` diff --git a/crates/builtins/web-streams/tests/stream_from.rs b/crates/builtins/web-streams/tests/stream_from.rs index 1276616b..123cb5bf 100644 --- a/crates/builtins/web-streams/tests/stream_from.rs +++ b/crates/builtins/web-streams/tests/stream_from.rs @@ -10,24 +10,17 @@ // This file contains nothing platform-specific, so skip it on wasm32. #![cfg(not(target_arch = "wasm32"))] -use core_runtime::config::RuntimeConfig; -use core_runtime::event_loop::run_microtasks; -use core_runtime::runtime::{clear_global_initializers, register_global_initializer, Runtime}; -use js::conversion::FromJSVal; -use js::error::ExnThrown; +use core_runtime::runtime::{clear_global_initializers, register_global_initializer}; +use core_runtime::test_util::eval_out_with_setup; fn run(code: &str) -> String { - clear_global_initializers(); - register_global_initializer(web_streams::add_to_global); - let rt = Runtime::init(&RuntimeConfig::default()); - let scope = rt.default_global(); - if js::compile::evaluate_with_filename(&scope, code, "test.js", 1).is_err() { - panic!("evaluation threw: {:?}", ExnThrown::capture(&scope)); - } - run_microtasks(&scope); - let out = js::compile::evaluate_with_filename(&scope, "globalThis.__out", "out.js", 1) - .expect("reading __out threw"); - String::from_jsval(&scope, out, ()).unwrap() + eval_out_with_setup( + || { + clear_global_initializers(); + register_global_initializer(web_streams::add_to_global); + }, + code, + ) } const DRAIN: &str = r#" diff --git a/crates/builtins/web-streams/tests/tee_default.rs b/crates/builtins/web-streams/tests/tee_default.rs index 7a6fd644..027c7491 100644 --- a/crates/builtins/web-streams/tests/tee_default.rs +++ b/crates/builtins/web-streams/tests/tee_default.rs @@ -9,24 +9,17 @@ // This file contains nothing platform-specific, so skip it on wasm32. #![cfg(not(target_arch = "wasm32"))] -use core_runtime::config::RuntimeConfig; -use core_runtime::event_loop::run_microtasks; -use core_runtime::runtime::{clear_global_initializers, register_global_initializer, Runtime}; -use js::conversion::FromJSVal; -use js::error::ExnThrown; +use core_runtime::runtime::{clear_global_initializers, register_global_initializer}; +use core_runtime::test_util::eval_out_with_setup; fn run(code: &str) -> String { - clear_global_initializers(); - register_global_initializer(web_streams::add_to_global); - let rt = Runtime::init(&RuntimeConfig::default()); - let scope = rt.default_global(); - if js::compile::evaluate_with_filename(&scope, code, "test.js", 1).is_err() { - panic!("evaluation threw: {:?}", ExnThrown::capture(&scope)); - } - run_microtasks(&scope); - let out = js::compile::evaluate_with_filename(&scope, "globalThis.__out", "out.js", 1) - .expect("reading __out threw"); - String::from_jsval(&scope, out, ()).unwrap() + eval_out_with_setup( + || { + clear_global_initializers(); + register_global_initializer(web_streams::add_to_global); + }, + code, + ) } const DRAIN: &str = r#" diff --git a/crates/builtins/web-streams/tests/transform_default.rs b/crates/builtins/web-streams/tests/transform_default.rs index 5f140c64..fd686563 100644 --- a/crates/builtins/web-streams/tests/transform_default.rs +++ b/crates/builtins/web-streams/tests/transform_default.rs @@ -12,27 +12,20 @@ // This file contains nothing platform-specific, so skip it on wasm32. #![cfg(not(target_arch = "wasm32"))] -use core_runtime::config::RuntimeConfig; -use core_runtime::event_loop::run_microtasks; -use core_runtime::runtime::{clear_global_initializers, register_global_initializer, Runtime}; -use js::conversion::FromJSVal; -use js::error::ExnThrown; +use core_runtime::runtime::{clear_global_initializers, register_global_initializer}; +use core_runtime::test_util::eval_out_with_setup; fn run(code: &str) -> String { - clear_global_initializers(); - register_global_initializer(|scope, global| { - web_globals::add_to_global(scope, global); - web_streams::add_to_global(scope, global); - }); - let rt = Runtime::init(&RuntimeConfig::default()); - let scope = rt.default_global(); - if js::compile::evaluate_with_filename(&scope, code, "test.js", 1).is_err() { - panic!("evaluation threw: {:?}", ExnThrown::capture(&scope)); - } - run_microtasks(&scope); - let out = js::compile::evaluate_with_filename(&scope, "globalThis.__out", "out.js", 1) - .expect("reading __out threw"); - String::from_jsval(&scope, out, ()).unwrap() + eval_out_with_setup( + || { + clear_global_initializers(); + register_global_initializer(|scope, global| { + web_globals::add_to_global(scope, global); + web_streams::add_to_global(scope, global); + }); + }, + code, + ) } /// A transformer that uppercases each chunk: written chunks come out transformed diff --git a/crates/builtins/web-streams/tests/writable_default.rs b/crates/builtins/web-streams/tests/writable_default.rs index 5a09d7db..c2e19638 100644 --- a/crates/builtins/web-streams/tests/writable_default.rs +++ b/crates/builtins/web-streams/tests/writable_default.rs @@ -12,28 +12,21 @@ // This file contains nothing platform-specific, so skip it on wasm32. #![cfg(not(target_arch = "wasm32"))] -use core_runtime::config::RuntimeConfig; -use core_runtime::event_loop::run_microtasks; -use core_runtime::runtime::{clear_global_initializers, register_global_initializer, Runtime}; -use js::conversion::FromJSVal; -use js::error::ExnThrown; +use core_runtime::runtime::{clear_global_initializers, register_global_initializer}; +use core_runtime::test_util::eval_out_with_setup; /// Evaluate `code`, drain microtasks, and return `String(globalThis.__out)`. fn run(code: &str) -> String { - clear_global_initializers(); - register_global_initializer(|scope, global| { - web_globals::add_to_global(scope, global); - web_streams::add_to_global(scope, global); - }); - let rt = Runtime::init(&RuntimeConfig::default()); - let scope = rt.default_global(); - if js::compile::evaluate_with_filename(&scope, code, "test.js", 1).is_err() { - panic!("evaluation threw: {:?}", ExnThrown::capture(&scope)); - } - run_microtasks(&scope); - let out = js::compile::evaluate_with_filename(&scope, "globalThis.__out", "out.js", 1) - .expect("reading __out threw"); - String::from_jsval(&scope, out, ()).unwrap() + eval_out_with_setup( + || { + clear_global_initializers(); + register_global_initializer(|scope, global| { + web_globals::add_to_global(scope, global); + web_streams::add_to_global(scope, global); + }); + }, + code, + ) } /// Chunks written through a writer reach the sink's `write` algorithm in order, diff --git a/crates/builtins/web-url/src/algorithms.rs b/crates/builtins/web-url/src/algorithms.rs index 5b399eaf..4647afcd 100644 --- a/crates/builtins/web-url/src/algorithms.rs +++ b/crates/builtins/web-url/src/algorithms.rs @@ -23,3 +23,19 @@ pub(crate) fn api_url_parser(url: &str, base: Option<&str>) -> Option { }; basic_url_parser(url, parsed_base.as_ref()) } + +/// +pub(crate) fn urlencoded_parse(input: &str) -> Vec<(String, String)> { + form_urlencoded::parse(input.as_bytes()) + .into_owned() + .collect() +} + +/// +pub(crate) fn urlencoded_serialize(tuples: &[(String, String)]) -> String { + let mut serializer = form_urlencoded::Serializer::new(String::new()); + for (name, value) in tuples { + serializer.append_pair(name, value); + } + serializer.finish() +} diff --git a/crates/builtins/web-url/src/lib.rs b/crates/builtins/web-url/src/lib.rs index 147dcdc4..390a80c3 100644 --- a/crates/builtins/web-url/src/lib.rs +++ b/crates/builtins/web-url/src/lib.rs @@ -13,5 +13,8 @@ pub fn add_to_global(scope: &Scope<'_>, global: Object<'_>) { url_search_params::URLSearchParams::add_to_global(scope, global); url_search_params_iterator::URLSearchParamsIterator::add_to_global(scope, global); url_search_params::install_symbol_iterator(scope); - url_search_params_iterator::install_symbol_iterator(scope); + js::class::inherit_from_iterator_prototype::< + url_search_params_iterator::URLSearchParamsIteratorImpl, + >(scope) + .expect("setting a class prototype's prototype can only fail due to OOM"); } diff --git a/crates/builtins/web-url/src/url.rs b/crates/builtins/web-url/src/url.rs index 6ac92227..aab4b334 100644 --- a/crates/builtins/web-url/src/url.rs +++ b/crates/builtins/web-url/src/url.rs @@ -5,6 +5,7 @@ use super::url_search_params::URLSearchParams; use crate::algorithms; use core_runtime::{webidl_interface, webidl_methods}; +use js::class::Ref; use js::error::{throw_type_error, ExnThrown}; use js::exception; use js::gc::handle::{Heap, OptionHeapExt}; @@ -35,13 +36,11 @@ impl URL<'_> { /// #[getter] - fn href(&self) -> String { + fn href(&self) -> Ref<'_, str> { // Step 1: Return the serialization of this’s URL. - self.data() - .url - .as_ref() - .map(|url| url.as_str().to_string()) - .unwrap_or_default() + Ref::map(self.data(), |data| { + data.url.as_ref().map(|url| url.as_str()).unwrap_or("") + }) } /// @@ -57,7 +56,8 @@ impl URL<'_> { self.data_mut().url = Some(parsed_url.clone()); // Step 4: Empty `this`’s `query object`’s `list`. - if let Some(query_object) = self.data().query_object.get(scope) { + let query_object = self.data().query_object.get(scope); + if let Some(query_object) = query_object { query_object.data_mut().list.clear(); // Step 5: Let _query_ be `this`’s `URL`’s `query`. @@ -66,9 +66,7 @@ impl URL<'_> { // Step 6: If _query_ is non-null, then set `this`’s `query object`’s `list` to the // result of `parsing` _query_. if let Some(query) = query { - query_object.data_mut().list = form_urlencoded::parse(query.as_bytes()) - .into_owned() - .collect(); + query_object.data_mut().list = algorithms::urlencoded_parse(query); } } @@ -88,160 +86,143 @@ impl URL<'_> { /// #[getter] - fn protocol(&self) -> String { + fn protocol(&self) -> Ref<'_, str> { // Step 1: Return this’s URL’s scheme, followed by U+003A (:). - self.data() - .url - .as_ref() - .map(|url| url::quirks::protocol(url).to_string()) - .unwrap_or_default() + Ref::map(self.data(), |data| { + data.url.as_ref().map(url::quirks::protocol).unwrap_or("") + }) } /// #[setter] - fn set_protocol(&self, _scope: &Scope<'_>, value: String) -> Result<(), ExnThrown> { + fn set_protocol(&self, value: String) { // Step 1: Basic URL parse the given value, followed by U+003A (:), with this’s URL as url // and scheme start state as state override. let mut url_data = self.data_mut(); let Some(url) = url_data.url.as_mut() else { - return Ok(()); + return; }; let _ = url::quirks::set_protocol(url, &value); - Ok(()) } /// #[getter] - fn username(&self) -> String { + fn username(&self) -> Ref<'_, str> { // Step 1: Return this’s URL’s username. - self.data() - .url - .as_ref() - .map(|url| url.username().to_string()) - .unwrap_or_default() + Ref::map(self.data(), |data| { + data.url.as_ref().map(|url| url.username()).unwrap_or("") + }) } /// #[setter] - fn set_username(&self, _scope: &Scope<'_>, value: String) -> Result<(), ExnThrown> { + fn set_username(&self, value: String) { // Step 1: If `this`’s `URL` `cannot have a username/password/port`, then return. let mut url_data = self.data_mut(); let Some(url) = url_data.url.as_mut() else { - return Ok(()); + return; }; if url.cannot_be_a_base() || url.host_str().is_none() { - return Ok(()); + return; } // Step 2: `Set the username` given `this`’s `URL` and the given value. let _ = url::quirks::set_username(url, &value); - Ok(()) } /// #[getter] - fn password(&self) -> String { + fn password(&self) -> Ref<'_, str> { // Step 1: Return this’s URL’s password. - self.data() - .url - .as_ref() - .map(|url| url::quirks::password(url).to_string()) - .unwrap_or_default() + Ref::map(self.data(), |data| { + data.url.as_ref().map(url::quirks::password).unwrap_or("") + }) } /// #[setter] - fn set_password(&self, _scope: &Scope<'_>, value: String) -> Result<(), ExnThrown> { + fn set_password(&self, value: String) { // Step 1: If `this`’s `URL` `cannot have a username/password/port`, then return. let mut url_data = self.data_mut(); let Some(url) = url_data.url.as_mut() else { - return Ok(()); + return; }; if url.cannot_be_a_base() || url.host_str().is_none() { - return Ok(()); + return; } // Step 2: `Set the password` given `this`’s `URL` and the given value. let _ = url::quirks::set_password(url, &value); - Ok(()) } /// #[getter] - fn host(&self) -> String { + fn host(&self) -> Ref<'_, str> { // Step 1: Let _url_ be `this`’s `URL`. // Step 2: If _url_’s `host` is null, then return the empty string. // Step 3: If _url_’s `port` is null, return _url_’s `host`, `serialized`. // Step 4: Return _url_’s `host`, `serialized`, followed by U+003A (:) and _url_’s // `port`, `serialized`. - self.data() - .url - .as_ref() - .map(|url| url::quirks::host(url).to_string()) - .unwrap_or_default() + Ref::map(self.data(), |data| { + data.url.as_ref().map(url::quirks::host).unwrap_or("") + }) } /// #[setter] - fn set_host(&self, _scope: &Scope<'_>, value: String) -> Result<(), ExnThrown> { + fn set_host(&self, value: String) { // Step 1: If `this`'s `URL` has an `opaque path`, then return. let mut url_data = self.data_mut(); let Some(url) = url_data.url.as_mut() else { - return Ok(()); + return; }; // Step 2: `Basic URL parse` the given value with `this`'s `URL` as `_url_` and `host // state` as `_state override_`. let _ = url::quirks::set_host(url, &value); - Ok(()) } /// #[getter] - fn hostname(&self) -> String { + fn hostname(&self) -> Ref<'_, str> { // Step 1: If `this`’s `URL`’s `host` is null, then return the empty string. // Step 2: Return `this`’s `URL`’s `host`, `serialized`. - self.data() - .url - .as_ref() - .map(|url| url::quirks::hostname(url).to_string()) - .unwrap_or_default() + Ref::map(self.data(), |data| { + data.url.as_ref().map(url::quirks::hostname).unwrap_or("") + }) } /// #[setter] - fn set_hostname(&self, _scope: &Scope<'_>, value: String) -> Result<(), ExnThrown> { + fn set_hostname(&self, value: String) { // Step 1: If `this`’s `URL` has an `opaque path`, then return. let mut url_data = self.data_mut(); let Some(url) = url_data.url.as_mut() else { - return Ok(()); + return; }; // Step 2: `Basic URL parse` the given value with `this`’s `URL` as `_url_` and `hostname // state` as `_state override_`. let _ = url::quirks::set_hostname(url, &value); - Ok(()) } /// #[getter] - fn port(&self) -> String { + fn port(&self) -> Ref<'_, str> { // Step 1: If `this`’s `URL`’s `port` is null, then return the empty string. // Step 2: Return `this`’s `URL`’s `port`, `serialized`. - self.data() - .url - .as_ref() - .map(|url| url::quirks::port(url).to_string()) - .unwrap_or_default() + Ref::map(self.data(), |data| { + data.url.as_ref().map(url::quirks::port).unwrap_or("") + }) } /// #[setter] - fn set_port(&self, _scope: &Scope<'_>, value: String) -> Result<(), ExnThrown> { + fn set_port(&self, value: String) { // Step 1: If `this`’s `URL` `cannot have a username/password/port`, then return. let mut url_data = self.data_mut(); let Some(url) = url_data.url.as_mut() else { - return Ok(()); + return; }; // Step 2: If the given value is the empty string, then set `this`’s `URL`’s `port` to @@ -249,47 +230,41 @@ impl URL<'_> { // Step 3: Otherwise, `basic URL parse` the given value with `this`’s `URL` as `_url_` and // `port state` as `_state override_`. let _ = url::quirks::set_port(url, &value); - Ok(()) } /// #[getter] - fn pathname(&self) -> String { + fn pathname(&self) -> Ref<'_, str> { // Step 1: Return the result of URL path serializing this’s URL. - self.data() - .url - .as_ref() - .map(|url| url.path().to_string()) - .unwrap_or_default() + Ref::map(self.data(), |data| { + data.url.as_ref().map(|url| url.path()).unwrap_or("") + }) } /// #[setter] - fn set_pathname(&self, _scope: &Scope<'_>, value: String) -> Result<(), ExnThrown> { + fn set_pathname(&self, value: String) { // Step 1: If `this`’s `URL` has an `opaque path`, then return. let mut url_data = self.data_mut(); let Some(url) = url_data.url.as_mut() else { - return Ok(()); + return; }; // Step 2: `Empty` `this`’s `URL`’s `path`. // Step 3: `Basic URL parse` the given value with `this`'s `URL` as `_url_` and `path // start state` as `_state override_`. url::quirks::set_pathname(url, &value); - Ok(()) } /// #[getter] - fn search(&self) -> String { + fn search(&self) -> Ref<'_, str> { // Step 1: If `this`’s `URL`’s `query` is either null or the empty string, then return // the empty string. // Step 2: Return U+003F (?), followed by `this`’s `URL`’s `query`. - self.data() - .url - .as_ref() - .map(|url| url::quirks::search(url).to_string()) - .unwrap_or_default() + Ref::map(self.data(), |data| { + data.url.as_ref().map(url::quirks::search).unwrap_or("") + }) } /// @@ -313,13 +288,12 @@ impl URL<'_> { } // Step 6: Set `this`’s `query object`’s `list` to the result of `parsing` _input_. - if let Some(query_object) = self.data().query_object.get(scope) { + let query_object = self.data().query_object.get(scope); + if let Some(query_object) = query_object { if value.is_empty() { query_object.data_mut().list.clear(); } else { - query_object.data_mut().list = form_urlencoded::parse(input.as_bytes()) - .into_owned() - .collect(); + query_object.data_mut().list = algorithms::urlencoded_parse(input); } } Ok(()) @@ -337,46 +311,43 @@ impl URL<'_> { /// #[getter] - fn hash(&self) -> String { + fn hash(&self) -> Ref<'_, str> { // Step 1: If `this`’s `URL`’s `fragment` is either null or the empty string, then // return the empty string. // Step 2: Return U+0023 (#), followed by `this`’s `URL`’s `fragment`. - self.data() - .url - .as_ref() - .map(|url| url::quirks::hash(url).to_string()) - .unwrap_or_default() + Ref::map(self.data(), |data| { + data.url.as_ref().map(url::quirks::hash).unwrap_or("") + }) } /// #[setter] - fn set_hash(&self, _scope: &Scope<'_>, value: String) -> Result<(), ExnThrown> { + fn set_hash(&self, value: String) { // Step 1: If the given value is the empty string, then set `this`’s `URL`’s `fragment` // to null and return. // Step 2: Let _input_ be the given value with a single leading U+0023 (#) removed, if any. // Step 3: Set `this`’s `URL`’s `fragment` to the empty string. let mut url_data = self.data_mut(); let Some(url) = url_data.url.as_mut() else { - return Ok(()); + return; }; url::quirks::set_hash(url, &value); - Ok(()) } /// #[allow(clippy::wrong_self_convention)] #[method(name = "toJSON")] - fn to_json(&self, scope: &Scope<'_>) -> Result { + fn to_json(&self) -> Ref<'_, str> { // Step 1: Return the serialization of this’s URL. - let _ = scope; - Ok(self.href()) + self.href() } + /// : `href` is the interface's stringifier. #[allow(clippy::wrong_self_convention)] #[method(name = "toString")] - fn to_string(&self, scope: &Scope<'_>) -> Result { - let _ = scope; - Ok(self.href()) + fn to_string(&self) -> Ref<'_, str> { + // Return the serialization of `this`'s URL. + self.href() } /// @@ -400,13 +371,12 @@ impl URL<'_> { /// #[static_method] - fn can_parse(scope: &Scope<'_>, url: String, base: Option) -> Result { + fn can_parse(url: String, base: Option) -> bool { // Step 1: Let _parsedURL_ be the result of running the `API URL parser` on _url_ with // _base_, if given. // Step 2: If _parsedURL_ is failure, then return false. // Step 3: Return true. - let _ = scope; - Ok(algorithms::api_url_parser(&url, base.as_deref()).is_some()) + algorithms::api_url_parser(&url, base.as_deref()).is_some() } } @@ -417,9 +387,7 @@ impl URL<'_> { url_record: url::Url, ) -> Result<(), ExnThrown> { let query = url_record.query().unwrap_or(""); - let query_list: Vec<(String, String)> = form_urlencoded::parse(query.as_bytes()) - .into_owned() - .collect(); + let query_list: Vec<(String, String)> = algorithms::urlencoded_parse(query); let query_object = js::class::create_instance_with::< crate::url_search_params::URLSearchParamsImpl, @@ -427,9 +395,6 @@ impl URL<'_> { list: query_list, url_object: None, })?; - let query_object = query_object - .cast::() - .map_err(|_| throw_type_error(scope, c"Failed to create URLSearchParams object"))?; self.data_mut().url = Some(url_record); self.data_mut().query_object = Some(query_object.into()); diff --git a/crates/builtins/web-url/src/url_search_params.rs b/crates/builtins/web-url/src/url_search_params.rs index 46e5ae7e..c6817653 100644 --- a/crates/builtins/web-url/src/url_search_params.rs +++ b/crates/builtins/web-url/src/url_search_params.rs @@ -2,15 +2,15 @@ //! -use crate::url_search_params_iterator::{ - IteratorKind, URLSearchParamsIterator, URLSearchParamsIteratorImpl, -}; +use crate::url_search_params_iterator::URLSearchParamsIterator; use core_runtime::{webidl_interface, webidl_methods, webidl_union}; use js::conversion::{Record, ToJSVal}; -use js::error::{throw_type_error, ExnThrown, TypeError}; +use js::error::{ExnThrown, TypeError}; use js::gc::handle::Heap; use js::gc::scope::Scope; +use js::iteration::IterationKind; use js::prelude::HandleValue; +use js::Callable; /// WebIDL: `(sequence> or record or USVString)` /// — the union accepted by the [`URLSearchParams` constructor]. @@ -35,19 +35,26 @@ impl URLSearchParams<'_> { /// #[constructor] fn new(&self, init: Option) -> Result<(), TypeError> { + // Step 1 (for a string _init_) is below. // Step 2: `Initialize` `this` with _init_. + // (inlined) let list = match init { None => Vec::new(), + // [inlined] Step 1: If _init_ is a `sequence`, then `for each` _innerSequence_ of + // _init_: Some(URLSearchParamsInit::Pairs(pairs)) => { let mut list = Vec::with_capacity(pairs.len()); for pair in pairs { - // Each inner sequence must contain exactly two items. + // [inlined] Step 1.1: If _innerSequence_'s `size` is not 2, then `throw` a + // ``TypeError``. if pair.len() != 2 { return Err(TypeError( "Each URLSearchParams sequence element must contain exactly two items" .into(), )); } + // [inlined] Step 1.2: `Append` (_innerSequence_[0], _innerSequence_[1]) to + // _query_'s `list`. let mut iter = pair.into_iter(); let name = iter.next().unwrap(); let value = iter.next().unwrap(); @@ -55,14 +62,19 @@ impl URLSearchParams<'_> { } list } + // [inlined] Step 2: Otherwise, if _init_ is a `record`, then `for each` _name_ → + // _value_ of _init_, `append` (_name_, _value_) to _query_'s `list`. Some(URLSearchParamsInit::Record(record)) => record.into_iter().collect(), + // [inlined] Step 3: Otherwise: + // [inlined] Step 3.1: Assert: _init_ is a string. Some(URLSearchParamsInit::Str(mut s)) => { // Step 1: If _init_ is a string and starts with U+003F (?), then remove // the first code point from _init_. if s.starts_with('?') { s.remove(0); } - form_urlencoded::parse(s.as_bytes()).into_owned().collect() + // [inlined] Step 3.2: Set _query_'s `list` to the result of `parsing` _init_. + crate::algorithms::urlencoded_parse(&s) } }; @@ -116,65 +128,45 @@ impl URLSearchParams<'_> { /// #[method] - pub fn get(&self, scope: &Scope<'_>, name: String) -> Result, ExnThrown> { + pub fn get(&self, name: String) -> Option { // Step 1: Return the value of the first tuple whose name is name in this’s list, if there // is such a tuple; otherwise null. - let _ = scope; - Ok(self - .data() + self.data() .list .iter() .find(|(tuple_name, _)| tuple_name == &name) - .map(|(_, value)| value.clone())) + .map(|(_, value)| value.clone()) } /// #[method] - pub fn get_all(&self, scope: &Scope<'_>, name: String) -> Result, ExnThrown> { + pub fn get_all(&self, name: String) -> Vec { // Step 1: Return the values of all tuples whose name is name in this’s list, in list // order; otherwise the empty sequence. - let _ = scope; - Ok(self - .data() + self.data() .list .iter() .filter(|(tuple_name, _)| tuple_name == &name) .map(|(_, value)| value.clone()) - .collect()) + .collect() } /// #[method] - pub fn has( - &self, - scope: &Scope<'_>, - name: String, - value: Option, - ) -> Result { + pub fn has(&self, name: String, value: Option) -> bool { // Step 1: If _value_ is given and there is a `tuple` whose name is _name_ and value is // _value_ in `this`’s `list`, then return true. // Step 2: If _value_ is not given and there is a `tuple` whose name is _name_ in `this`’s // `list`, then return true. - let _ = scope; - if let Some(value) = value { - return Ok(self - .data() + // Step 3: Return false. + let data = self.data(); + match value { + Some(value) => data .list .iter() - .any(|(tuple_name, tuple_value)| tuple_name == &name && tuple_value == &value)); - } - - if self - .data() - .list - .iter() - .any(|(tuple_name, _)| tuple_name == &name) - { - return Ok(true); + .any(|(tuple_name, tuple_value)| tuple_name == &name && tuple_value == &value), + None => data.list.iter().any(|(tuple_name, _)| tuple_name == &name), } - - // Step 3: Return false. - Ok(false) } /// @@ -228,59 +220,56 @@ impl URLSearchParams<'_> { self.update(scope) } + /// : `forEach`. #[method] fn for_each( &self, scope: &Scope<'_>, - callback: HandleValue, + callback: Callable<'_>, this_arg: Option, ) -> Result<(), ExnThrown> { - if !callback.is_object() { - return Err(throw_type_error(scope, c"callback must be a function")); - } - - let mut index = 0usize; - while index < self.data().list.len() { - let (name, value) = self.data().list[index].clone(); - let value_js = value.as_str().to_jsval_throwing(scope)?; - let name_js = name.as_str().to_jsval_throwing(scope)?; - let self_js = scope.root_value(self.as_value()); - - js::Function::call( - scope, - this_arg.unwrap_or(HandleValue::undefined()), - callback, - &[value_js, name_js, self_js], - )?; - index += 1; - } - - Ok(()) + let this_arg = this_arg.unwrap_or(HandleValue::undefined()); + js::iteration::for_each_pair(scope, *self, callback, this_arg, |i| { + let data = self.data(); + let Some((name, value)) = data.list.get(i) else { + return Ok(None); + }; + let name = name.as_str().to_jsval_throwing(scope)?; + let value = value.as_str().to_jsval_throwing(scope)?; + Ok(Some((name, value))) + }) } + /// #[allow(clippy::wrong_self_convention)] #[method] - pub fn to_string(&self, _scope: &Scope<'_>) -> Result { - let mut serializer = form_urlencoded::Serializer::new(String::new()); - for (name, value) in &self.data().list { - serializer.append_pair(name, value); - } - Ok(serializer.finish()) + pub fn to_string(&self) -> String { + // Return the `serialization` of `this`'s `list`. + crate::algorithms::urlencoded_serialize(&self.data().list) } + /// : `entries`. #[method] fn entries<'r>(&self, scope: &'r Scope<'_>) -> Result, ExnThrown> { - self.create_iterator(scope, IteratorKind::Entries) + // Return a newly created `default iterator object` for _definition_, with _jsValue_ as + // its `target`, "`key+value`" as its `kind`, and `index` set to 0. + URLSearchParamsIterator::new(scope, *self, IterationKind::KeyValue) } + /// : `keys`. #[method] fn keys<'r>(&self, scope: &'r Scope<'_>) -> Result, ExnThrown> { - self.create_iterator(scope, IteratorKind::Keys) + // Return a newly created `default iterator object` for _definition_, with _jsValue_ as + // its `target`, "`key`" as its `kind`, and `index` set to 0. + URLSearchParamsIterator::new(scope, *self, IterationKind::Key) } + /// : `values`. #[method] fn values<'r>(&self, scope: &'r Scope<'_>) -> Result, ExnThrown> { - self.create_iterator(scope, IteratorKind::Values) + // Return a newly created `default iterator object` for _definition_, with _jsValue_ as + // its `target`, "`value`" as its `kind`, and `index` set to 0. + URLSearchParamsIterator::new(scope, *self, IterationKind::Value) } } @@ -294,25 +283,26 @@ pub fn install_symbol_iterator(scope: &Scope<'_>) { } impl URLSearchParams<'_> { + /// pub(crate) fn update(&self, scope: &Scope<'_>) -> Result<(), ExnThrown> { let data = self.data(); + // Step 1: If _query_'s `URL object` is null, then return. let Some(url_object_heap) = data.url_object.as_ref() else { return Ok(()); }; let url_object = url_object_heap.get(scope); - let mut serializer = form_urlencoded::Serializer::new(String::new()); - for (name, value) in &data.list { - serializer.append_pair(name, value); - } - let serialized = serializer.finish(); + // Step 2: Let _serializedQuery_ be the `serialization` of _query_'s `list`. + let serialized = crate::algorithms::urlencoded_serialize(&data.list); + // Step 3: If _serializedQuery_ is the empty string, then set _serializedQuery_ to null. let serialized_query = if serialized.is_empty() { None } else { Some(serialized) }; + // Step 4: Set _query_'s `URL object`'s `URL`'s `query` to _serializedQuery_. if let Some(url) = url_object.data_mut().url.as_mut() { // Per the URL spec's opaque-path state, a trailing U+0020 SPACE before a query // delimiter is stored literally, but must be percent-encoded as %20 when the query @@ -330,18 +320,4 @@ impl URLSearchParams<'_> { } Ok(()) } - - fn create_iterator<'r>( - &self, - scope: &'r Scope<'_>, - kind: IteratorKind, - ) -> Result, ExnThrown> { - js::class::create_instance_with::(scope, |_| { - URLSearchParamsIteratorImpl { - params: (*self).into(), - index: 0, - kind, - } - }) - } } diff --git a/crates/builtins/web-url/src/url_search_params_iterator.rs b/crates/builtins/web-url/src/url_search_params_iterator.rs index bde53446..93467262 100644 --- a/crates/builtins/web-url/src/url_search_params_iterator.rs +++ b/crates/builtins/web-url/src/url_search_params_iterator.rs @@ -9,85 +9,55 @@ use crate::url_search_params::{URLSearchParams, URLSearchParamsImpl}; use core_runtime::{webidl_interface, webidl_methods}; -use js::class::{get_iterator_prototype, get_prototype_for}; -use js::conversion::ToJSVal; use js::error::ExnThrown; use js::gc::handle::Heap; use js::gc::scope::Scope; -use js::Object; +use js::iteration::{create_iter_result, pair_iterator_result, IterationKind}; +use js::prelude::HandleValue; -/// Which kind of values the iterator produces. -#[derive(Clone, Copy, Default)] -pub enum IteratorKind { - /// Yields `[name, value]` pairs. - #[default] - Entries, - /// Yields names only. - Keys, - /// Yields values only. - Values, -} - -/// -#[webidl_interface(name = "URLSearchParams Iterator")] +/// +#[webidl_interface(hidden, name = "URLSearchParams Iterator")] pub struct URLSearchParamsIterator { /// Reference to the URLSearchParams being iterated. pub(crate) params: Heap, /// Current position in the list. pub(crate) index: usize, - /// What kind of values to yield. #[no_trace] - pub(crate) kind: IteratorKind, + pub(crate) kind: IterationKind, } #[webidl_methods] impl URLSearchParamsIterator { + fn new(params: URLSearchParams, kind: IterationKind) -> Self { + Self { + params: Heap::from(params), + index: 0, + kind, + } + } + /// #[method] fn next<'a>(&self, scope: &'a Scope<'a>) -> Result, ExnThrown> { - let result = js::Object::new(scope, None)?; + // Steps 1-5: Implemented in the `#[method]` receiver check. + // Step 6: Let _index_ be _object_'s `index`. let index = self.data().index; + // Step 7: Let _kind_ be _object_'s `kind`. let kind = self.data().kind; - let params: URLSearchParams = self.data().params.get(scope); - - if let Some((name, value)) = params.data().list.get(index).cloned() { - let js_value = match kind { - IteratorKind::Entries => { - let arr = js::Array::new(scope, 2)?; - let name_val = name.as_str().to_jsval_throwing(scope)?; - let val_val = value.as_str().to_jsval_throwing(scope)?; - arr.set_element(scope, 0, name_val)?; - arr.set_element(scope, 1, val_val)?; - scope.root_value(arr.as_value()) - } - IteratorKind::Keys => name.as_str().to_jsval_throwing(scope)?, - IteratorKind::Values => value.as_str().to_jsval_throwing(scope)?, - }; - self.data_mut().index = index + 1; - result.set_property(scope, c"value", js_value)?; - result.set_property(scope, c"done", false)?; - } else { - result.set_property(scope, c"value", js::value::undefined())?; - result.set_property(scope, c"done", true)?; - } - - Ok(result) - } -} - -/// Chain the `URLSearchParamsIterator` prototype under `%IteratorPrototype%` to satisfy -/// the interface's `iterable<>` declaration. -pub fn install_symbol_iterator(scope: &Scope<'_>) { - let proto = unsafe { - Object::from_raw( - scope, - get_prototype_for::(scope) - .expect("URLSearchParamsIterator class not registered"), - ) - .expect("URLSearchParamsIterator prototype is null") - }; - - if let Ok(iterator_proto) = get_iterator_prototype(scope) { - let _ = proto.set_prototype(scope, iterator_proto.handle()); + // Step 8: Let _values_ be _object_'s `target`'s `value pairs to iterate over`. + let params = self.data().params.get(scope); + let params_data = params.data(); + let values = ¶ms_data.list; + // Step 9: Let _len_ be the length of _values_. + // Step 10: If _index_ is greater than or equal to _len_, then return + // CreateIteratorResultObject(undefined, true). + // Step 11: Let _pair_ be the entry in _values_ at index _index_. + let Some((name, value)) = values.get(index) else { + return create_iter_result(scope, HandleValue::undefined(), true); + }; + // Step 12: Set _object_'s index to _index_ + 1. + self.data_mut().index = index + 1; + // Step 13: Return the `iterator result` for _pair_ and _kind_. + pair_iterator_result(scope, name.as_str(), value.as_str(), kind) } } diff --git a/crates/builtins/web-url/tests/url.rs b/crates/builtins/web-url/tests/url.rs index ba78a306..93321969 100644 --- a/crates/builtins/web-url/tests/url.rs +++ b/crates/builtins/web-url/tests/url.rs @@ -554,3 +554,15 @@ fn usp_delete_on_opaque_path_with_fragment_preserves_trailing_space_encoding() { "data:space %20#test" ); } + +#[test] +fn url_search_params_iterator_is_not_a_global() { + assert_eq!( + eval("typeof globalThis['URLSearchParams Iterator']"), + "undefined" + ); + assert_eq!( + eval("Object.prototype.toString.call(new URLSearchParams('a=1').entries())"), + "[object URLSearchParams Iterator]" + ); +} diff --git a/crates/core-runtime/src/config.rs b/crates/core-runtime/src/config.rs index 0b5e94a0..138fa8f1 100644 --- a/crates/core-runtime/src/config.rs +++ b/crates/core-runtime/src/config.rs @@ -37,6 +37,13 @@ pub struct RuntimeConfig { #[arg(short = 'd', long = "debug")] pub debugging: bool, + /// Record the JavaScript stack each promise and async call was created on, and show it as + /// async parent frames in `Error.stack`. + /// + /// Off by default, since it captures a stack for every promise. Always on for globals that have a debugger attached. + #[arg(long = "async-stacks")] + pub async_stacks: bool, + /// Use classic (non-module) script mode. #[arg(long = "legacy-script")] pub legacy_script: bool, @@ -188,11 +195,6 @@ impl RuntimeConfig { Ok(()) } - /// The effective content script source — either from --eval or from the file path. - pub fn content_script(&self) -> Option<&str> { - self.eval_script.as_deref() - } - /// The base directory for resolving module imports. /// /// For `--eval` scripts this is the current working directory. Otherwise, @@ -525,6 +527,7 @@ mod tests { assert!(config.module_mode()); assert!(!config.verbose); assert!(!config.debugging); + assert!(!config.async_stacks); assert!(!config.wpt_mode); } diff --git a/crates/core-runtime/src/event_loop.rs b/crates/core-runtime/src/event_loop.rs index cd6b9670..ee466d61 100644 --- a/crates/core-runtime/src/event_loop.rs +++ b/crates/core-runtime/src/event_loop.rs @@ -54,7 +54,7 @@ pub mod timer; use platform::clock::Instant; use std::cell::{Cell, RefCell}; -use std::collections::HashMap; +use std::collections::{BTreeSet, HashMap}; use std::rc::Rc; use std::time::Duration; @@ -235,6 +235,76 @@ struct TaskEntry { deadline: Option, } +impl TaskEntry { + /// The entry's position in [`TaskQueue::ready`]: non-timer tasks first in allocation + /// order, then timers by deadline, ties broken by allocation order. + fn ready_key(&self) -> (Option, u64) { + (self.deadline, self.id.0) + } +} + +/// The live tasks, indexed so that lookups by id, the next timer, and the ready tasks in +/// dispatch order are found without scanning. +#[derive(Default)] +struct TaskQueue { + /// Every live task by id. + entries: HashMap, + /// The ready tasks, by [`TaskEntry::ready_key`]. + ready: BTreeSet<(Option, u64)>, + /// The timers that are not ready yet, by deadline then id. + timers: BTreeSet<(Instant, u64)>, +} + +impl TaskQueue { + fn insert(&mut self, entry: TaskEntry) { + if entry.ready { + self.ready.insert(entry.ready_key()); + } else if let Some(deadline) = entry.deadline { + self.timers.insert((deadline, entry.id.0)); + } + self.entries.insert(entry.id, entry); + } + + fn remove(&mut self, id: TaskId) -> Option { + let entry = self.entries.remove(&id)?; + if entry.ready { + self.ready.remove(&entry.ready_key()); + } else if let Some(deadline) = entry.deadline { + self.timers.remove(&(deadline, id.0)); + } + Some(entry) + } + + /// Mark `id` ready. Returns whether a task not yet ready was found. + fn mark_ready(&mut self, id: TaskId) -> bool { + let Some(entry) = self.entries.get_mut(&id) else { + return false; + }; + if entry.ready { + return false; + } + entry.ready = true; + if let Some(deadline) = entry.deadline { + self.timers.remove(&(deadline, id.0)); + } + self.ready.insert(entry.ready_key()); + true + } + + /// Mark every timer whose deadline is at or before `now` ready, returning how many. + fn expire_timers(&mut self, now: Instant) -> usize { + let mut count = 0; + while let Some(&(deadline, id)) = self.timers.first() { + if deadline > now { + break; + } + self.mark_ready(TaskId(id)); + count += 1; + } + count + } +} + // --------------------------------------------------------------------------- // EventLoop // --------------------------------------------------------------------------- @@ -267,15 +337,14 @@ pub struct EventLoop { loop_id: u64, /// Monotonically increasing counter for generating unique [`TaskId`]s. next_id: Cell, - /// All live tasks. Order is not significant — tasks are looked up by - /// [`TaskId`]. - tasks: RefCell>, + /// All live tasks. + tasks: RefCell, /// HTML's per-loop "map of setTimeout and setInterval IDs": the timer ids /// JS sees, mapped to the internal task they control. js_timers: RefCell>, /// Scratch buffer for [`step`](Self::step)'s per-batch dispatch list, /// reused across steps so the hot path stays allocation-free. - batch_buf: RefCell, TaskId)>>, + batch_buf: RefCell>, /// Set by [`request_stop`](Self::request_stop) to end this loop after its current step, /// overriding all other signals of event loop activity, such as pending async tasks and /// timers, or active interest. @@ -298,7 +367,7 @@ impl EventLoop { Self { loop_id: NEXT_LOOP_ID.fetch_add(1, std::sync::atomic::Ordering::Relaxed), next_id: Cell::new(0), - tasks: RefCell::new(Vec::new()), + tasks: RefCell::new(TaskQueue::default()), js_timers: RefCell::new(HashMap::new()), batch_buf: RefCell::new(Vec::new()), stop_requested: Cell::new(false), @@ -315,7 +384,7 @@ impl EventLoop { /// Returns the [`TaskId`] assigned to this task. pub fn queue(&self, task: Box) -> TaskId { let id = self.next_task_id(); - self.tasks.borrow_mut().push(TaskEntry { + self.tasks.borrow_mut().insert(TaskEntry { id, task, ready: false, @@ -330,7 +399,7 @@ impl EventLoop { /// external event (e.g. resolved promises, `queueMicrotask` work). pub fn queue_ready(&self, task: Box) -> TaskId { let id = self.next_task_id(); - self.tasks.borrow_mut().push(TaskEntry { + self.tasks.borrow_mut().insert(TaskEntry { id, task, ready: true, @@ -347,7 +416,7 @@ impl EventLoop { /// similar, use a deadline of `Instant::now()`. pub fn queue_timer(&self, task: Box, deadline: Instant) -> TaskId { let id = self.next_task_id(); - self.tasks.borrow_mut().push(TaskEntry { + self.tasks.borrow_mut().insert(TaskEntry { id, task, ready: false, @@ -364,13 +433,7 @@ impl EventLoop { /// in the queue (already run, or currently being executed by the driver) /// is left alone. pub fn cancel_if_queued(&self, id: TaskId) -> bool { - let mut tasks = self.tasks.borrow_mut(); - if let Some(pos) = tasks.iter().position(|e| e.id == id) { - tasks.swap_remove(pos); - true - } else { - false - } + self.tasks.borrow_mut().remove(id).is_some() } /// Queue a `setTimeout`/`setInterval` task under a fresh JS timer id. @@ -401,7 +464,7 @@ impl EventLoop { if self.js_timers.borrow().get(&timer_id) != Some(&id) { return; } - self.tasks.borrow_mut().push(TaskEntry { + self.tasks.borrow_mut().insert(TaskEntry { id, task, ready: false, @@ -450,9 +513,7 @@ impl EventLoop { /// Has no effect if the task ID is not found (the task may have /// already been cancelled or run). pub fn signal_ready(&self, id: TaskId) { - let mut tasks = self.tasks.borrow_mut(); - if let Some(entry) = tasks.iter_mut().find(|e| e.id == id) { - entry.ready = true; + if self.tasks.borrow_mut().mark_ready(id) { self.notify.notify(1); } } @@ -465,12 +526,8 @@ impl EventLoop { /// tasks in allocation order, then expired timers by deadline. pub fn pop_ready(&self) -> Option<(TaskId, Box)> { let mut tasks = self.tasks.borrow_mut(); - let (pos, _) = tasks - .iter() - .enumerate() - .filter(|(_, e)| e.ready) - .min_by_key(|(_, e)| (e.deadline, e.id.0))?; - let entry = tasks.swap_remove(pos); + let &(_, id) = tasks.ready.first()?; + let entry = tasks.remove(TaskId(id)).expect("ready tasks are live"); Some((entry.id, entry.task)) } @@ -480,17 +537,7 @@ impl EventLoop { /// Returns the number of timers that became ready. // TODO: consider merging with `time_to_next_timer` and returning the tasks directly. pub fn advance_timers(&self) -> usize { - let now = Instant::now(); - let mut count = 0; - for entry in self.tasks.borrow_mut().iter_mut() { - if let Some(deadline) = entry.deadline { - if !entry.ready && deadline <= now { - entry.ready = true; - count += 1; - } - } - } - count + self.tasks.borrow_mut().expire_timers(Instant::now()) } /// Returns the duration until the next timer fires, or `None` if @@ -500,38 +547,28 @@ impl EventLoop { /// is already expired and [`advance_timers`](Self::advance_timers) /// should be called. pub fn time_to_next_timer(&self) -> Option { - let now = Instant::now(); - self.tasks - .borrow() - .iter() - .filter_map(|e| { - if !e.ready { - e.deadline.map(|d| d.saturating_duration_since(now)) - } else { - None - } - }) - .min() + let &(deadline, _) = self.tasks.borrow().timers.first()?; + Some(deadline.saturating_duration_since(Instant::now())) } /// Returns `true` if there are any tasks (ready or not) in the queue. pub fn has_pending(&self) -> bool { - !self.tasks.borrow().is_empty() + !self.tasks.borrow().entries.is_empty() } /// Returns `true` if at least one task is in the ready state. pub fn has_ready(&self) -> bool { - self.tasks.borrow().iter().any(|e| e.ready) + !self.tasks.borrow().ready.is_empty() } /// Returns the number of tasks currently queued (ready or not). pub fn len(&self) -> usize { - self.tasks.borrow().len() + self.tasks.borrow().entries.len() } /// Returns `true` if the event loop has no tasks. pub fn is_empty(&self) -> bool { - self.tasks.borrow().is_empty() + self.tasks.borrow().entries.is_empty() } // ----------------------------------------------------------------------- @@ -663,6 +700,28 @@ pub async unsafe fn run_until_evaluated( } } +/// Settle `completed` async-promise futures with `event_loop` active, so a reaction (a timer, or +/// releasing the loop's interest, e.g. a FetchEvent's `respondWith` resolving from a `fetch`) runs +/// against this loop rather than no loop or another request's. +/// +/// # Safety +/// +/// As for [`run_until`]. +unsafe fn settle_completed_futures( + raw_cx: *mut js::native::RawJSContext, + event_loop: &EventLoop, + completed: Vec, +) { + if completed.is_empty() { + return; + } + with_event_loop(event_loop, |_| { + // SAFETY: guaranteed by this function's caller. + let scope = unsafe { js::gc::scope::RootScope::from_current_realm(raw_cx) }; + js::promise::settle_completed_futures(&scope, completed); + }); +} + /// Runs the event loop in an async loop, calling `should_stop` each turn, and returns when it /// returns true. /// @@ -724,12 +783,8 @@ pub async unsafe fn run_until( std::task::Poll::Ready(js::promise::poll_pending_futures(owner, cx)) }) .await; - if !completed.is_empty() { - with_event_loop(event_loop, |_| unsafe { - let scope = js::gc::scope::RootScope::from_current_realm(raw_cx); - js::promise::settle_completed_futures(&scope, completed); - }); - } + // SAFETY: guaranteed by this function's caller. + unsafe { settle_completed_futures(raw_cx, event_loop, completed) }; continue; } StepOutcome::Done | StepOutcome::Idle => { @@ -771,15 +826,8 @@ pub async unsafe fn run_until( ) .await; - // Settle any completed futures with this loop active, so a reaction (a timer, or - // releasing the loop's interest, e.g. a FetchEvent's respondWith resolving from a - // `fetch`) runs against this loop rather than no loop or another request's. - if !completed.is_empty() { - with_event_loop(event_loop, |_| unsafe { - let scope = js::gc::scope::RootScope::from_current_realm(raw_cx); - js::promise::settle_completed_futures(&scope, completed); - }); - } + // SAFETY: guaranteed by this function's caller. + unsafe { settle_completed_futures(raw_cx, event_loop, completed) }; } } } @@ -795,9 +843,7 @@ pub async unsafe fn run_until( /// allowing the event loop to continue. This is called by [`EventLoop::step`] /// when a task or microtask throws. fn handle_and_clear_exception(scope: &Scope<'_>) { - let e = ExnThrown::capture(scope); - eprintln!("[event_loop] Uncaught exception: {e}"); - js::exception::clear(scope); + js::exception::report_and_clear(scope, "event_loop"); } impl EventLoop { @@ -822,31 +868,20 @@ impl EventLoop { // polled and the platform reactor turns. // // Ready non-timer tasks go first in allocation order, then expired timers by deadline, - // ties broken the same way. The batch buffer is reused to keep the path allocation-free. + // ties broken the same way, which is the order of the ready set. The batch buffer is + // reused to keep the path allocation-free. let mut batch = self.batch_buf.take(); - batch.extend( - self.tasks - .borrow() - .iter() - .filter(|entry| entry.ready) - .map(|entry| (entry.deadline, entry.id)), - ); - batch.sort_unstable_by_key(|&(deadline, id)| (deadline, id.0)); + batch.extend(self.tasks.borrow().ready.iter().map(|&(_, id)| TaskId(id))); let mut ran_any = false; - for &(_, id) in &batch { + for &id in &batch { // Take the task out before running it, ending the borrow: the // task's JS can re-enter this loop (setTimeout, clearTimeout) // through `with_active_event_loop`. Re-locate by id — an earlier // task in the batch may have cancelled this one. - let entry = { - let mut tasks = self.tasks.borrow_mut(); - let Some(pos) = tasks.iter().position(|entry| entry.id == id && entry.ready) else { - continue; - }; - tasks.swap_remove(pos) + let Some(entry) = self.tasks.borrow_mut().remove(id) else { + continue; }; if entry.task.run(scope, id).is_err() { - eprintln!("[event_loop] Task error (id={:?})", id); handle_and_clear_exception(scope); } ran_any = true; @@ -884,7 +919,7 @@ impl EventLoop { /// allocation points, and no `tasks` borrow is held across a call into JS, so the borrow here /// cannot conflict. pub unsafe fn trace(&self, trc: *mut JSTracer) { - for entry in self.tasks.borrow().iter() { + for entry in self.tasks.borrow().entries.values() { entry.task.trace(trc); } } diff --git a/crates/core-runtime/src/event_loop/timer.rs b/crates/core-runtime/src/event_loop/timer.rs index fa67ef44..d9691bce 100644 --- a/crates/core-runtime/src/event_loop/timer.rs +++ b/crates/core-runtime/src/event_loop/timer.rs @@ -15,11 +15,11 @@ use platform::clock::Instant; use std::time::Duration; -use js::conversion::ToJSVal; use js::error::{throw_error, ExnThrown}; use js::gc::handle::Heap; use js::heap::RootedTraceableBox; use js::native::Value; +use js::prelude::HandleValue; use js::gc::scope::Scope; @@ -29,7 +29,7 @@ use super::{with_active_event_loop, Task, TaskId}; /// a string to evaluate as a classic script when the timer fires. enum TimerHandler { Function { - callback: RootedTraceableBox>, + callback: RootedTraceableBox>, /// The `setTimeout`/`setInterval` arguments after the timeout, passed /// to the callback on every invocation (HTML timer initialization /// steps: "invoke handler given arguments"). @@ -79,12 +79,8 @@ pub struct TimerTask { impl TimerTask { /// Create a timer task with a function handler and its call arguments. - /// - /// # Safety - /// - /// `callback` must be a valid JS function object. - unsafe fn function( - callback: Object, + fn function( + callback: js::Callable<'_>, args: Vec>>, interval: Option, nesting_level: u32, @@ -184,140 +180,79 @@ impl Task for TimerTask { // Timer global functions (setTimeout, setInterval, etc.) // --------------------------------------------------------------------------- -// TODO: move these to a separate crate under `builtins`, and use `#[jsglobals]`. - -use js::native::RawJSContext; -use js::{value, Object}; - -use js::gc::scope::RootScope; - /// Install `setTimeout`, `setInterval`, `clearTimeout`, and `clearInterval` /// on a global object. -/// -/// # Safety -/// -/// - `scope` must have an active realm. -/// - `global` must be the realm's global object. -pub unsafe fn install_timer_globals(scope: &Scope<'_>, global: js::Object<'_>) { - let set_timeout = c"setTimeout"; - let set_interval = c"setInterval"; - let clear_timeout = c"clearTimeout"; - let clear_interval = c"clearInterval"; - - js::Function::define( - scope, - global.handle(), - set_timeout, - Some(js_set_timeout), - 1, - 0, - ) - .unwrap(); - js::Function::define( - scope, - global.handle(), - set_interval, - Some(js_set_interval), - 1, - 0, - ) - .unwrap(); - js::Function::define( - scope, - global.handle(), - clear_timeout, - Some(js_clear_timeout), - 1, - 0, - ) - .unwrap(); - js::Function::define( - scope, - global.handle(), - clear_interval, - Some(js_clear_interval), - 1, - 0, - ) - .unwrap(); +pub fn install_timer_globals(scope: &Scope<'_>, global: js::Object<'_>) { + timer_globals::add_to_global(scope, global); } -/// `setTimeout(callback, delay?)` — schedule a one-shot timer. -/// -/// Returns a numeric timer ID that can be passed to `clearTimeout`. -unsafe extern "C" fn js_set_timeout( - raw_cx: *mut RawJSContext, - argc: u32, - vp: *mut js::native::Value, -) -> bool { - queue_timer_from_js(raw_cx, argc, vp, false) +/// WebIDL `TimerHandler = (Function or DOMString)`. +#[js::macros::webidl_union] +pub enum TimerHandlerArg<'a> { + Function(js::Callable<'a>), + String(String), } -/// `setInterval(callback, delay?)` — schedule a repeating timer. -/// -/// Returns a numeric timer ID that can be passed to `clearInterval`. -unsafe extern "C" fn js_set_interval( - raw_cx: *mut RawJSContext, - argc: u32, - vp: *mut js::native::Value, -) -> bool { - queue_timer_from_js(raw_cx, argc, vp, true) -} +/// The timer methods of `WindowOrWorkerGlobalScope`. +#[js::macros::jsglobals] +mod timer_globals { + use js::error::ExnThrown; + use js::gc::scope::Scope; + use js::prelude::HandleValue; + + /// + pub fn set_timeout( + scope: &Scope<'_>, + handler: super::TimerHandlerArg<'_>, + timeout: Option, + arguments: RestArgs>, + ) -> Result { + // Return the result of running the `timer initialization steps` given `this`, _handler_, + // _timeout_, _arguments_, and false. + super::timer_initialization_steps(scope, handler, timeout.unwrap_or(0), &arguments, false) + } -/// Common implementation for `setTimeout` and `setInterval`. -unsafe fn queue_timer_from_js( - raw_cx: *mut RawJSContext, - argc: u32, - vp: *mut js::native::Value, - repeating: bool, -) -> bool { - let scope = RootScope::from_current_realm(raw_cx); - let args = js::native::CallArgs::from_vp(vp, argc); - - // Argument 0: handler (required) — WebIDL `TimerHandler = (Function or DOMString)`. A callable is - // invoked when the timer fires; anything else is coerced to a string and evaluated as code (the - // `setTimeout("code", ms)` form). - if argc == 0 { - throw_error(&scope, "setTimeout/setInterval requires a handler argument"); - return false; + /// + pub fn set_interval( + scope: &Scope<'_>, + handler: super::TimerHandlerArg<'_>, + timeout: Option, + arguments: RestArgs>, + ) -> Result { + // Return the result of running the `timer initialization steps` given `this`, _handler_, + // _timeout_, _arguments_, and true. + super::timer_initialization_steps(scope, handler, timeout.unwrap_or(0), &arguments, true) } - let arg0 = args.get(0); - let callable = arg0 - .is_object() - .then(|| Object::from_value(&scope, *arg0).ok()) - .flatten() - .filter(|object| object.is_callable()); - - let code = match &callable { - Some(_) => None, - None => { - use js::conversion::FromJSVal; - match String::from_jsval(&scope, js::native::Handle::from_raw(arg0), ()) { - Ok(code) => Some(code), - Err(error) => { - error.throw(&scope); - return false; - } - } - } - }; - // Argument 1: delay in milliseconds (optional, default 0). - // - let delay_ms = if argc > 1 { - use js::conversion::{ConversionBehavior, FromJSVal}; - let delay_handle = js::native::Handle::from_raw(args.get(1)); - match i32::from_jsval(&scope, delay_handle, ConversionBehavior::Default) { - Ok(delay) => delay.max(0) as u64, - Err(error) => { - error.throw(&scope); - return false; - } - } - } else { - 0 + /// + pub fn clear_timeout(id: Option) { + // `remove` `this`'s `map of setTimeout and setInterval IDs`[_id_]. + super::with_active_event_loop(|el| el.clear_js_timer(id.unwrap_or(0))); + } + + /// + pub fn clear_interval(id: Option) { + // `remove` `this`'s `map of setTimeout and setInterval IDs`[_id_]. + super::with_active_event_loop(|el| el.clear_js_timer(id.unwrap_or(0))); + } +} + +/// , for `setTimeout` (`repeat` false) +/// and `setInterval` (`repeat` true). +fn timer_initialization_steps( + scope: &Scope<'_>, + handler: TimerHandlerArg<'_>, + timeout: i32, + arguments: &js::class::RestArgs>, + repeat: bool, +) -> Result { + // A function handler is invoked when the timer fires, and a string one is evaluated as code. + let (callable, code) = match handler { + TimerHandlerArg::Function(callable) => (Some(callable), None), + TimerHandlerArg::String(code) => (None, Some(code)), }; - let delay = Duration::from_millis(delay_ms); + + let delay = Duration::from_millis(timeout.max(0) as u64); // HTML timer initialization steps: "Let nesting level be the task's timer nesting level" // (the currently running timer task's, or 0), clamp the timeout once nested deeper than @@ -328,21 +263,22 @@ unsafe fn queue_timer_from_js( let deadline = Instant::now() + delay; - let interval = repeating.then_some(delay); + let interval = repeat.then_some(delay); // HTML timer initialization steps: "Let arguments be... the rest of the arguments" // For the handler form, everything after the timeout is forwarded to the callback on every // invocation. For the string form, additional arguments are ignored. let extra_args: Vec>> = match &callable { - Some(_) => (2..argc) - .map(|i| RootedTraceableBox::new(Heap::from(*args.get(i)))) + Some(_) => arguments + .iter() + .map(|arg| RootedTraceableBox::new(Heap::from(arg.get()))) .collect(), None => Vec::new(), }; // Queue on the current event loop. let timer_id = with_active_event_loop(|el| { - el.queue_js_timer(&scope, deadline, |timer_id| match callable { + el.queue_js_timer(scope, deadline, |timer_id| match callable { Some(callback) => Box::new(TimerTask::function( callback, extra_args, @@ -351,66 +287,14 @@ unsafe fn queue_timer_from_js( timer_id, )), None => { - // Not callable: run the handler's string as code. - // TODO: can we do the eval once instead of on every fire? + // Not callable: run the handler's string as code. The HTML timer initialization + // steps create a new classic script from it each time the timer fires, so the + // string is kept rather than a compiled script. let code = code.expect("non-callable handler was converted to a string above"); Box::new(TimerTask::code(code, interval, task_nesting, timer_id)) } }) }); - match timer_id { - Some(timer_id) => { - args.rval().set(timer_id.to_jsval(&scope).unwrap().get()); - true - } - None => { - throw_error(&scope, "No active event loop"); - false - } - } -} - -/// `clearTimeout(id)` / `clearInterval(id)` — cancel a timer. -unsafe extern "C" fn js_clear_timeout( - raw_cx: *mut RawJSContext, - argc: u32, - vp: *mut js::native::Value, -) -> bool { - clear_timer_from_js(raw_cx, argc, vp) -} - -unsafe extern "C" fn js_clear_interval( - raw_cx: *mut RawJSContext, - argc: u32, - vp: *mut js::native::Value, -) -> bool { - clear_timer_from_js(raw_cx, argc, vp) -} - -unsafe fn clear_timer_from_js( - raw_cx: *mut RawJSContext, - argc: u32, - vp: *mut js::native::Value, -) -> bool { - let scope = RootScope::from_current_realm(raw_cx); - let args = js::native::CallArgs::from_vp(vp, argc); - let id = if argc > 0 { - use js::conversion::{ConversionBehavior, FromJSVal}; - let id_handle = js::native::Handle::from_raw(args.get(0)); - match u64::from_jsval(&scope, id_handle, ConversionBehavior::Default) { - Ok(id) => id, - Err(error) => { - error.throw(&scope); - return false; - } - } - } else { - 0 - }; - - with_active_event_loop(|el| el.clear_js_timer(id)); - - args.rval().set(value::undefined()); - true + timer_id.ok_or_else(|| throw_error(scope, "No active event loop")) } diff --git a/crates/core-runtime/src/invocation.rs b/crates/core-runtime/src/invocation.rs index a70cff1b..2b59f6e1 100644 --- a/crates/core-runtime/src/invocation.rs +++ b/crates/core-runtime/src/invocation.rs @@ -45,11 +45,6 @@ impl InvocationState { &self.event_loop } - /// Returns a mutable reference to this invocation's event loop. - pub fn event_loop_mut(&mut self) -> &mut EventLoop { - &mut self.event_loop - } - /// Trace all GC-managed objects in this invocation's event loop. /// /// # Safety @@ -205,8 +200,8 @@ impl OwnedInvocation { } /// The invocation state (e.g. to drive its event loop). - pub fn state_mut(&mut self) -> &mut InvocationState { - &mut self.state + pub fn state(&self) -> &InvocationState { + &self.state } } diff --git a/crates/core-runtime/src/lib.rs b/crates/core-runtime/src/lib.rs index 273b79ed..0695ac86 100644 --- a/crates/core-runtime/src/lib.rs +++ b/crates/core-runtime/src/lib.rs @@ -70,12 +70,6 @@ pub fn content_script(config: &config::RuntimeConfig) -> Result<(String, String) Ok((source, path.clone())) } -/// Initialize the runtime and evaluate the script (the synchronous portion of an -/// invocation), draining initial microtasks. Returns the runtime and its -/// invocation, with the invocation **unregistered** from the GC tracer: it is -/// registered only for the eval phase here, then unregistered before being moved -/// out (the registry holds a raw pointer that the move would invalidate). The -/// caller re-registers at the final location, drives the loop, then unregisters. /// Evaluate a content script into `scope` — as a module or as a classic script, per `module_mode` /// — with `event_loop` active, and drain the microtasks it queued. /// @@ -147,6 +141,12 @@ impl ScriptEvaluation { } } +/// Initialize the runtime and evaluate the script (the synchronous portion of an +/// invocation), draining initial microtasks. Returns the runtime and its +/// invocation, with the invocation unregistered from the GC tracer. It is +/// registered only for the eval phase here, then unregistered before being moved +/// out, since the registry holds a raw pointer that the move would invalidate. The +/// caller re-registers at the final location, drives the loop, then unregisters. fn init_and_eval( config: config::RuntimeConfig, ) -> Result< @@ -157,7 +157,7 @@ fn init_and_eval( ), String, > { - let runtime = Runtime::init(&config); + let runtime = Runtime::init(&config)?; let (source, filename) = content_script(&config)?; @@ -241,41 +241,6 @@ pub fn setup_for_serve( init_and_eval(config) } -/// Extract and print the pending JS exception, if any. -/// -/// # Safety -/// -/// Called from within an active realm context. -pub unsafe fn report_pending_exception(scope: &js::gc::scope::Scope<'_>) { - use js::exception; - - if !exception::is_pending(scope) { - eprintln!("Error: script execution failed (no exception details available)"); - return; - } - - let exc_val = match exception::get_pending(scope) { - Ok(v) => v, - Err(_) => { - eprintln!("Error: script execution failed (could not retrieve exception)"); - return; - } - }; - exception::clear(scope); - - // Try to convert the exception to a string. - // TODO: use mozjs's better abstractions for this. - match js::JSString::from_value(scope, exc_val) { - Ok(js_str) => match js_str.to_utf8(scope) { - Ok(msg) => eprintln!("Error: {}", msg), - Err(_) => eprintln!("Error: script execution failed"), - }, - Err(_) => { - eprintln!("Error: script execution failed"); - } - } -} - #[cfg(test)] mod tests { use super::config::RuntimeConfig; diff --git a/crates/core-runtime/src/module.rs b/crates/core-runtime/src/module.rs index 069af1a6..7869955e 100644 --- a/crates/core-runtime/src/module.rs +++ b/crates/core-runtime/src/module.rs @@ -30,7 +30,7 @@ //! } //! //! // Register and use: -//! let rt = Runtime::init(&config); +//! let rt = Runtime::init(&config).expect("runtime init"); //! let scope = rt.default_global(); //! unsafe { //! register_module::(&scope); @@ -173,7 +173,10 @@ unsafe fn trace_module_registry(state: &ModuleState, trc: *mut JSTracer) { /// /// `data` must be the `Runtime` address [`init_module_gc_tracer`] was given. unsafe extern "C" fn trace_module_registry_cb(trc: *mut JSTracer, data: *mut c_void) { + // SAFETY: the caller passes the `Runtime` address the tracer was registered with, which + // `init_module_gc_tracer`'s caller keeps valid while the tracer is registered. let rt = unsafe { &*(data as *const crate::Runtime) }; + // SAFETY: SpiderMonkey calls a roots tracer with a valid tracer. unsafe { trace_module_registry(&rt.modules, trc) }; } @@ -182,14 +185,25 @@ unsafe extern "C" fn trace_module_registry_cb(trc: *mut JSTracer, data: *mut c_v /// Called automatically by `Runtime::init` — only needed when using a /// raw mozjs `Runtime` directly. /// `rt` is handed back to the tracer, which reads the registry out of it. -pub fn init_module_gc_tracer(cx: &mut js::native::JSContext, rt: *const crate::Runtime) { +/// +/// # Safety +/// +/// `rt` must stay valid until [`remove_module_gc_tracer`] removes the tracer. +pub unsafe fn init_module_gc_tracer(cx: &mut js::native::JSContext, rt: *const crate::Runtime) { + // SAFETY: the callback is a valid function, and the caller keeps `rt` valid for as long as + // the tracer is registered. unsafe { js::gc::add_extra_gc_roots_tracer(cx, Some(trace_module_registry_cb), rt as *mut c_void) }; } -/// Remove the module registry GC root tracer. `rt` must match the registration. -pub fn remove_module_gc_tracer(cx: &js::native::JSContext, rt: *const crate::Runtime) { +/// Remove the module registry GC root tracer. +/// +/// # Safety +/// +/// `rt` must be the address passed to [`init_module_gc_tracer`]. +pub unsafe fn remove_module_gc_tracer(cx: &js::native::JSContext, rt: *const crate::Runtime) { + // SAFETY: the callback is the registered one, and the caller passes the registered `rt`. unsafe { js::gc::remove_extra_gc_roots_tracer(cx, Some(trace_module_registry_cb), rt as *mut c_void) }; @@ -268,16 +282,12 @@ unsafe extern "C" fn module_load_hook( let scope = unsafe { RootScope::from_current_realm(cx) }; // Extract the specifier string from the ModuleRequest object - let specifier_str = - unsafe { js::module_raw::GetModuleRequestSpecifier(cx as _, module_request) }; - if specifier_str.is_null() { + // SAFETY: SpiderMonkey passes the hook a rooted `module_request` handle. + let request = unsafe { GCHandle::from_raw(module_request) }; + let Some(specifier_str) = js::module::get_module_request_specifier(&scope, request) else { return false; - } - - let specifier = match jsstring_to_string(&scope, specifier_str) { - Some(s) => s, - None => return false, }; + let specifier = jsstr_to_string(&scope, specifier_str); // 1. Check the module registry for an exact match. let cached = registry(|reg| { @@ -293,16 +303,19 @@ unsafe extern "C" fn module_load_hook( let base_dir = referrer_base_dir(&scope, referrer); match resolve_file_module(&scope, &specifier, base_dir) { Ok(obj) => obj, - Err(msg) => { + Err(ModuleLoadError::Failed(msg)) => { let c_msg = CString::new(msg).unwrap_or_else(|_| c"Module resolution failed".into()); js::error::report_error_ascii(&scope, &c_msg); return false; } + // The compile error, with its location, is already pending. + Err(ModuleLoadError::CompileThrew) => return false, } } }; + // SAFETY: this is the load hook, passing its own arguments, and this is its only call. unsafe { js::module::finish_loading_imported_module( &scope, @@ -324,6 +337,7 @@ fn referrer_base_dir( referrer: js::native::RawHandle<*mut js::native::JSScript>, ) -> Option { if !referrer.get().is_null() { + // SAFETY: `referrer` is the load hook's rooted `referrer` argument. let private = js::module::get_script_private(scope, unsafe { GCHandle::from_raw(referrer) }); if private.is_string() { @@ -347,7 +361,7 @@ fn resolve_file_module<'r>( scope: &'r Scope, specifier: &str, base_dir: Option, -) -> Result, String> { +) -> Result, ModuleLoadError> { let base_dir = base_dir .ok_or_else(|| format!("Module '{}' not found (no base path configured)", specifier))?; @@ -394,12 +408,14 @@ fn resolve_file_module<'r>( let mut src = transform_str_to_source_text(&source); // SAFETY: `options` and `src` are valid for the duration of this call. let module = unsafe { js::module::compile_module(scope, options.ptr, &mut src) } - .map_err(|_| format!("Failed to compile module '{}'", canonical_path.display()))?; + .map_err(|_| ModuleLoadError::CompileThrew)?; // Store the canonical path in the module private: the load hook reads it // to resolve this module's own relative imports against its directory. let path_str = js::JSString::from_str(scope, &canonical_key) .map_err(|_| "Failed to allocate module path string".to_string())?; + // SAFETY: `module` is the rooted module object `compile_module` returned, and `path_str` is + // rooted. unsafe { SetModulePrivate(module.as_raw(), &path_str.as_value()) }; registry(|reg| { @@ -414,6 +430,20 @@ fn resolve_file_module<'r>( Ok(module) } +/// Why [`resolve_file_module`] could not produce a module. +enum ModuleLoadError { + /// Resolving or reading the module failed. The message describes why. + Failed(String), + /// Compiling the module threw, and its exception is pending. + CompileThrew, +} + +impl From for ModuleLoadError { + fn from(message: String) -> Self { + ModuleLoadError::Failed(message) + } +} + /// Resolve `.` and `..` components lexically, without touching the filesystem, so /// two lexically-equivalent paths produce the same module-cache key. Used as the /// fallback key when [`std::fs::canonicalize`] is unavailable (it can fail under @@ -521,6 +551,7 @@ pub unsafe fn register_module(scope: &Scope<'_>) -> bool { let options = js::compile::options(scope, filename, 1); let mut src = transform_str_to_source_text(&source); + // SAFETY: `options` and `src` are live locals. let module = match unsafe { js::module::compile_module(scope, options.ptr, &mut src) } { Ok(m) => m, Err(_) => return false, @@ -534,8 +565,7 @@ pub unsafe fn register_module(scope: &Scope<'_>) -> bool { reg.borrow_mut().insert( T::NAME.to_string(), ModuleEntry { - // SAFETY: module was just compiled and is non-null. - module_obj: unsafe { Heap::from_raw(module.as_raw()) }, + module_obj: Heap::from(module), }, ); }); @@ -544,7 +574,7 @@ pub unsafe fn register_module(scope: &Scope<'_>) -> bool { // remove the registry entry again so a later import of the module fails // to resolve instead of finding a half-initialized module whose value // exports are undefined. - let populated = unsafe { link_evaluate_and_populate::(scope, module, &declarations) }; + let populated = link_evaluate_and_populate::(scope, module, &declarations); if !populated { registry(|reg| { reg.borrow_mut().remove(T::NAME); @@ -555,16 +585,12 @@ pub unsafe fn register_module(scope: &Scope<'_>) -> bool { /// Steps 4.-6. of [`register_module`]: link and evaluate the compiled module, /// then populate its environment with the native exports. -/// -/// # Safety -/// -/// Same contract as [`register_module`]. -unsafe fn link_evaluate_and_populate( +fn link_evaluate_and_populate( scope: &Scope<'_>, module: Object<'_>, declarations: &[ModuleExport], ) -> bool { - let env = match unsafe { link_evaluate_and_get_env(scope, module) } { + let env = match link_evaluate_and_get_env(scope, module) { Some(env) => env, None => return false, }; @@ -582,14 +608,14 @@ unsafe fn link_evaluate_and_populate( Ok(f) => f, Err(_) => return false, }; - let func_val = scope.root_value(func.as_value()); - if env.set_property(scope, &c_name, func_val).is_err() { + if env.set_property(scope, &c_name, func).is_err() { return false; } } } // Let the module implementation set value exports + // SAFETY: `env` is the module's environment object. unsafe { T::evaluate(scope, env.handle()) } } @@ -598,14 +624,9 @@ unsafe fn link_evaluate_and_populate( /// `export var ...` initializations), and return its environment object, ready /// for the caller to populate. /// -/// # Safety -/// /// `module` must be a freshly compiled, registered module object on `scope`'s /// realm. -unsafe fn link_evaluate_and_get_env<'s>( - scope: &'s Scope<'_>, - module: Object<'_>, -) -> Option> { +fn link_evaluate_and_get_env<'s>(scope: &'s Scope<'_>, module: Object<'_>) -> Option> { // 4. Load the dependency graph, then link if js::module::load_requested_modules(scope, module).is_err() { return None; @@ -620,12 +641,8 @@ unsafe fn link_evaluate_and_get_env<'s>( } // 6. Get the module environment - unsafe { - Object::from_raw( - scope, - js::module_raw::GetModuleEnvironment(scope.cx_mut(), module.handle()), - ) - } + js::module::get_environment(scope, module.handle()) + .and_then(|env| Object::from_handle(scope.root_object(env))) } /// Register a module under an arbitrary specifier, exporting the given named @@ -670,6 +687,7 @@ pub unsafe fn register_synthetic_module( let mut src = transform_str_to_source_text(&source); // Synthetic modules carry no path in their module private: the generated // `export var` source has no imports of its own to resolve. + // SAFETY: `options` and `src` are live locals. let module = unsafe { js::module::compile_module(scope, options.ptr, &mut src) }?; // 3. Store in the registry under the owned specifier before linking, so the @@ -678,15 +696,14 @@ pub unsafe fn register_synthetic_module( reg.borrow_mut().insert( name.to_string(), ModuleEntry { - // SAFETY: module was just compiled and is non-null. - module_obj: unsafe { Heap::from_raw(module.as_raw()) }, + module_obj: Heap::from(module), }, ); }); // 4.-6. Link, evaluate, and populate the environment with the export values. // On any failure, remove the registry entry again. - let result = unsafe { link_evaluate_and_populate_values(scope, module, exports) }; + let result = link_evaluate_and_populate_values(scope, module, exports); if result.is_err() { registry(|reg| { reg.borrow_mut().remove(name); @@ -697,16 +714,12 @@ pub unsafe fn register_synthetic_module( /// Steps 4.-6. of [`register_synthetic_module`]: link and evaluate the compiled /// module, then assign each export binding its provided value. -/// -/// # Safety -/// -/// Same contract as [`register_synthetic_module`]. -unsafe fn link_evaluate_and_populate_values( +fn link_evaluate_and_populate_values( scope: &Scope<'_>, module: Object<'_>, exports: &[(&str, HandleValue)], ) -> Result<(), ExnThrown> { - let env = unsafe { link_evaluate_and_get_env(scope, module) }.ok_or(ExnThrown)?; + let env = link_evaluate_and_get_env(scope, module).ok_or(ExnThrown)?; for (export_name, value) in exports { let c_name = CString::new(*export_name).map_err(|_| ExnThrown)?; @@ -740,6 +753,7 @@ pub unsafe fn register_source_module( let filename = CString::new(name).map_err(|_| ExnThrown)?; let options = js::compile::options(scope, filename, 1); let mut src = transform_str_to_source_text(source); + // SAFETY: `options` and `src` are live locals. let module = unsafe { js::module::compile_module(scope, options.ptr, &mut src) }?; // Register before loading so the load hook (and the module's own imports) @@ -748,8 +762,7 @@ pub unsafe fn register_source_module( reg.borrow_mut().insert( name.to_string(), ModuleEntry { - // SAFETY: module was just compiled and is non-null. - module_obj: unsafe { Heap::from_raw(module.as_raw()) }, + module_obj: Heap::from(module), }, ); }); @@ -777,8 +790,7 @@ pub unsafe fn register_source_module( /// /// # Safety /// -/// - `cx` must be a valid `JSContext` pointer. -/// - [`init_module_loader`] must have been called first. +/// [`init_module_loader`] must have been called first. // TODO: mark this as safe: it's no less safe than `evaluate_with_filename`, really pub unsafe fn evaluate_module<'s>( scope: &'s Scope<'_>, @@ -789,6 +801,7 @@ pub unsafe fn evaluate_module<'s>( let options = js::compile::options(scope, c_filename, 1); let mut src = transform_str_to_source_text(source); + // SAFETY: `options` and `src` are live locals. let module = unsafe { js::module::compile_module(scope, options.ptr, &mut src) } .map_err(|_| ExnThrown)?; @@ -808,6 +821,8 @@ pub unsafe fn evaluate_module<'s>( }; let path_str = js::JSString::from_str(scope, &abs.to_string_lossy()).map_err(|_| ExnThrown)?; + // SAFETY: `module` is the rooted module object `compile_module` returned, and `path_str` is + // rooted. unsafe { SetModulePrivate(module.as_raw(), &path_str.as_value()) }; } } @@ -823,8 +838,7 @@ pub unsafe fn evaluate_module<'s>( /// /// # Safety /// -/// - `cx` must be valid. -/// - `env` must be a valid module environment object. +/// `env` must be a valid module environment object. pub unsafe fn set_module_export<'s, V: ToJSVal<'s> + ?Sized>( scope: &'s Scope<'_>, env: HandleObject, @@ -832,12 +846,8 @@ pub unsafe fn set_module_export<'s, V: ToJSVal<'s> + ?Sized>( value: &V, ) -> bool { let c_name = CString::new(name).unwrap(); - let val = match value.to_jsval(scope) { - Ok(v) => v, - Err(_) => return false, - }; let env_obj = js::Object::from_handle(env).expect("module environment object is null"); - env_obj.set_property(scope, &c_name, val).is_ok() + env_obj.set_property(scope, &c_name, value).is_ok() } #[cfg(test)] @@ -880,7 +890,7 @@ mod tests { let config = RuntimeConfig::from_args(["starling", "-e", "42"].iter().map(|s| s.to_string())) .unwrap(); - Runtime::init(&config) + Runtime::init(&config).expect("runtime init") } /// Helper: read back a globalThis property as f64. diff --git a/crates/core-runtime/src/runtime.rs b/crates/core-runtime/src/runtime.rs index 5d45ccdd..0851e672 100644 --- a/crates/core-runtime/src/runtime.rs +++ b/crates/core-runtime/src/runtime.rs @@ -2,18 +2,13 @@ use std::{ cell::{Cell, RefCell, UnsafeCell}, - env, ffi::c_void, - process, ptr::NonNull, rc::Rc, sync::{Mutex, OnceLock}, }; -use crate::{ - config::RuntimeConfig, event_loop, invocation::InvocationRegistry, module, - report_pending_exception, -}; +use crate::{config::RuntimeConfig, event_loop, invocation::InvocationRegistry, module}; use js::{ engine::{JSEngine, JSEngineHandle, MozJSRuntime, RealmOptions}, gc::{ @@ -146,20 +141,6 @@ static ENGINE: OnceLock> = OnceLock::new(); unsafe extern "C" { fn atexit(func: unsafe extern "C" fn()) -> std::os::raw::c_int; - fn _exit(status: std::os::raw::c_int) -> !; -} - -/// Exit the process with `code` without running C `atexit` handlers or C++ -/// static destructors. -/// -/// Used on error paths that fire while a [`Runtime`] is still alive, avoiding -/// failing asserts inside of SpiderMonkey. -fn exit_without_cleanup(code: std::os::raw::c_int) -> ! { - use std::io::Write; - let _ = std::io::stdout().flush(); - let _ = std::io::stderr().flush(); - // SAFETY: `_exit` is always safe to call; it terminates the process. - unsafe { _exit(code) } } /// `atexit` callback: takes the `JSEngine` out of the global and drops it, @@ -242,22 +223,6 @@ impl std::fmt::Debug for Runtime { } impl Runtime { - pub fn init_from_env() -> Rc { - let config = RuntimeConfig::from_env().unwrap_or_else(|e| { - eprintln!("Error loading runtime config: {}", e); - process::exit(1); - }); - Self::init(&config) - } - - pub fn init_from_args() -> Rc { - let config = RuntimeConfig::from_args(env::args()).unwrap_or_else(|e| { - eprintln!("Error loading runtime config: {}", e); - process::exit(1); - }); - Self::init(&config) - } - /// Get a mutable reference to the inner MozJS runtime. /// /// # Safety @@ -275,11 +240,16 @@ impl Runtime { /// The runtime owns the SpiderMonkey context and all global objects created /// in that context. The caller is responsible for keeping the `Rc` /// alive for as long as the runtime is needed. - pub fn init(config: &RuntimeConfig) -> Rc { + /// + /// Returns `Err` with a description if the configured initializer script can't + /// be read, throws, or leaves asynchronous work behind. + pub fn init(config: &RuntimeConfig) -> Result, String> { crate::config::set_enforce_fetch_restrictions(config.enforce_fetch_restrictions()); + // SAFETY: no parent runtime is given, which is the only condition. let mut mozjs_rt = unsafe { MozJSRuntime::create_with_internal_job_queues(engine_handle(), None) }; js::gc::init(mozjs_rt.cx()); + js::context::set_async_stack_capture(mozjs_rt.cx(), !config.async_stacks); let rt = Rc::new(Self { mozjs_rt: UnsafeCell::new(mozjs_rt), @@ -310,7 +280,8 @@ impl Runtime { // Register GC tracer for the module registry so cached module // objects are properly traced. - module::init_module_gc_tracer(rt.mozjs_rt_mut().cx(), self_ptr as *const Self); + // SAFETY: as for the runtime tracer above, `Drop` removes it. + unsafe { module::init_module_gc_tracer(rt.mozjs_rt_mut().cx(), self_ptr as *const Self) }; // Ensure `FinalizationRegistry` callbacks get run. crate::finalization::install(rt.mozjs_rt_mut().cx()); @@ -319,9 +290,9 @@ impl Runtime { // Create the default global and register builtins. drop(rt.new_global()); - rt.run_initializer_script(config); + rt.run_initializer_script(config)?; - rt + Ok(rt) } /// Create a new global object (and realm), install all registered global initializers on it, @@ -347,9 +318,7 @@ impl Runtime { self.default_global.set(scope.global()); } - unsafe { - event_loop::timer::install_timer_globals(&scope, scope.global()); - } + event_loop::timer::install_timer_globals(&scope, scope.global()); // Call any registered global initializers (e.g., web-globals, WPT builtins). // Snapshot the list first: an initializer (or JS it runs) may itself @@ -373,16 +342,14 @@ impl Runtime { RootScope::new_with_realm(self.mozjs_rt_mut().cx(), global) } - fn run_initializer_script(&self, config: &RuntimeConfig) { + fn run_initializer_script(&self, config: &RuntimeConfig) -> Result<(), String> { // Run initializer script if provided (always as legacy script). let Some(ref init_path) = config.initializer_script_path else { - return; + return Ok(()); }; let scope = self.default_global(); - let init_source = std::fs::read_to_string(init_path).unwrap_or_else(|e| { - eprintln!("Error reading initializer script '{}': {}", init_path, e); - exit_without_cleanup(1); - }); + let init_source = std::fs::read_to_string(init_path) + .map_err(|e| format!("Error reading initializer script '{init_path}': {e}"))?; let filename = init_path.as_str(); // The initializer runs with its own event loop active, so scheduling APIs such as @@ -401,22 +368,23 @@ impl Runtime { failed }); if failed { - eprintln!("Error evaluating initializer script '{init_path}':"); - unsafe { report_pending_exception(&scope) }; - exit_without_cleanup(1); + return Err(format!( + "Error evaluating initializer script '{init_path}': {}", + js::error::ExnThrown::capture(&scope) + )); } // Nothing drives this loop after initialization: leftover async work // (a live timer, an in-flight promise-backed operation) would be // silently dropped, so make it a hard error instead. if invocation.event_loop().is_alive() { - eprintln!( + return Err(format!( "Error: initializer script '{init_path}' left asynchronous work \ (timers or pending operations) behind. Initializer scripts must \ complete synchronously" - ); - exit_without_cleanup(1); + )); } + Ok(()) } /// Returns the `JSRuntime` object. @@ -490,6 +458,9 @@ impl Runtime { impl Drop for Runtime { fn drop(&mut self) { + // Futures left by loops that were never canceled, and those spawned with no loop active, + // have to go while the context is alive. + js::promise::cancel_all_pending_futures(); // Clear module state while tracers are still registered. // Heap::drop fires GC write barriers which can trigger GC under // GC zeal — the module tracer must still be registered. @@ -503,14 +474,16 @@ impl Drop for Runtime { // The class registry is owned by the global object and cleaned // up by its finalize hook — no explicit clearing needed. let self_ptr = self as *const Self as *mut c_void; + // SAFETY: `init` registered both tracers with this callback and this address, which is + // the `Rc` allocation `self` lives in. unsafe { js::gc::remove_extra_gc_roots_tracer( self.mozjs_rt().cx_no_gc(), Some(trace_runtime_cb), self_ptr, ); + module::remove_module_gc_tracer(self.mozjs_rt().cx_no_gc(), self_ptr as *const Self); } - module::remove_module_gc_tracer(self.mozjs_rt().cx_no_gc(), self_ptr as *const Self); // Only if this is still the current runtime: a second one created on // this thread has already replaced the pointer with its own. if std::ptr::eq(CURRENT.get(), self) { @@ -532,7 +505,8 @@ impl Drop for Runtime { /// - `data` must point to a live `Runtime` instance. #[js::allow_unrooted] unsafe extern "C" fn trace_runtime_cb(trc: *mut JSTracer, data: *mut c_void) { - let rt = &*(data as *const Runtime); + // SAFETY: the caller guarantees `data` points to a live `Runtime`. + let rt = unsafe { &*(data as *const Runtime) }; rt.default_global.trace(trc); // Trace all event loops across registered invocations. // @@ -541,6 +515,7 @@ unsafe extern "C" fn trace_runtime_cb(trc: *mut JSTracer, data: *mut c_void) { // on the caller's stack aren't being actively used — no aliasing hazard. // A normal `borrow()` would panic when the invocation registry is // already borrowed mutably. + // SAFETY: as described above, nothing uses a borrow of the registry while the GC traces. let invocations = unsafe { &*rt.invocations.as_ptr() }; invocations.trace(trc); } diff --git a/crates/core-runtime/src/test_util.rs b/crates/core-runtime/src/test_util.rs index d8ba0427..d25220a7 100644 --- a/crates/core-runtime/src/test_util.rs +++ b/crates/core-runtime/src/test_util.rs @@ -2,9 +2,9 @@ //! Shared test helpers for `core-runtime` in-crate tests. -use js::{conversion::FromJSVal, error::ExnThrown}; +use js::{conversion::FromJSVal, error::ExnThrown, gc::scope::Scope}; -use crate::{config::RuntimeConfig, runtime::Runtime}; +use crate::{config::RuntimeConfig, event_loop::run_microtasks, runtime::Runtime}; /// Create a temp directory that works on both native and wasm targets. /// @@ -28,7 +28,7 @@ pub fn test_tempdir() -> tempfile::TempDir { /// Run setup, create a runtime, evaluate JS code, and convert the result to a string. pub fn eval_with_setup(setup: impl FnOnce(), code: &str) -> String { setup(); - let rt = Runtime::init(&RuntimeConfig::default()); + let rt = Runtime::init(&RuntimeConfig::default()).expect("runtime init"); let scope = rt.default_global(); match js::compile::evaluate_with_filename(&scope, code, "test.js", 1) { Ok(val) => String::from_jsval(&scope, val, ()).unwrap(), @@ -42,7 +42,28 @@ pub fn eval_with_setup(setup: impl FnOnce(), code: &str) -> String { /// Run setup, create a runtime, and check whether JS code throws. pub fn throws_with_setup(setup: impl FnOnce(), code: &str) -> bool { setup(); - let rt = Runtime::init(&RuntimeConfig::default()); + let rt = Runtime::init(&RuntimeConfig::default()).expect("runtime init"); let scope = rt.default_global(); js::compile::evaluate_with_filename(&scope, code, "test.js", 1).is_err() } + +/// Evaluate `code` on `scope`, drain microtasks, and return `String(globalThis.__out)`. +/// +/// Panics if `code` throws. +pub fn eval_and_read_out(scope: &Scope<'_>, code: &str) -> String { + if js::compile::evaluate_with_filename(scope, code, "test.js", 1).is_err() { + panic!("evaluation threw: {:?}", ExnThrown::capture(scope)); + } + run_microtasks(scope); + let out = js::compile::evaluate_with_filename(scope, "globalThis.__out", "out.js", 1) + .expect("reading __out threw"); + String::from_jsval(scope, out, ()).unwrap() +} + +/// Run setup, create a runtime, and return [`eval_and_read_out`] for `code`. +pub fn eval_out_with_setup(setup: impl FnOnce(), code: &str) -> String { + setup(); + let rt = Runtime::init(&RuntimeConfig::default()).expect("runtime init"); + let scope = rt.default_global(); + eval_and_read_out(&scope, code) +} diff --git a/crates/core-runtime/tests/accessor_return_styles.rs b/crates/core-runtime/tests/accessor_return_styles.rs index a93db575..beaa68c0 100644 --- a/crates/core-runtime/tests/accessor_return_styles.rs +++ b/crates/core-runtime/tests/accessor_return_styles.rs @@ -13,8 +13,10 @@ use core_runtime::jsclass; use core_runtime::jsmethods; -use core_runtime::test_util::{eval_with_setup, throws_with_setup}; +use core_runtime::test_util::{eval_out_with_setup, eval_with_setup, throws_with_setup}; use js::error::TypeError; +use js::gc::scope::Scope; +use js::Promise; #[jsclass] struct Cell { @@ -41,6 +43,16 @@ impl Cell { self.data_mut().value = v; Ok(v) } + + #[method] + fn fail_async<'r>(&self, _scope: &'r Scope<'_>) -> Result, TypeError> { + Err(TypeError("method failure".into())) + } + + #[getter] + fn failing_promise<'r>(&self, _scope: &'r Scope<'_>) -> Result, TypeError> { + Err(TypeError("getter failure".into())) + } } fn setup() { @@ -75,3 +87,20 @@ fn setter_err_leaves_value_unchanged() { "9" ); } + +/// A `Result` operation or attribute turns an `Err` of any +/// `ThrowException` type into a promise rejected with that error. +#[test] +fn result_promise_rejects_with_the_thrown_error() { + assert_eq!( + eval_out_with_setup( + setup, + "const c = new Cell(); + Promise.all([ + c.failAsync().catch(e => `${e.constructor.name}:${e.message}`), + c.failingPromise.catch(e => `${e.constructor.name}:${e.message}`), + ]).then(v => { globalThis.__out = v.join(','); });" + ), + "TypeError:method failure,TypeError:getter failure" + ); +} diff --git a/crates/core-runtime/tests/associated_memory.rs b/crates/core-runtime/tests/associated_memory.rs index a2fc2098..c8cd0bbf 100644 --- a/crates/core-runtime/tests/associated_memory.rs +++ b/crates/core-runtime/tests/associated_memory.rs @@ -46,8 +46,7 @@ impl Accounted { #[method] fn account(&self, bytes: i32) { let mut data = self.data_mut(); - // SAFETY: `self` is a rooted handle to the object owning this private data. - unsafe { data.accounted.set(self.as_raw(), bytes as usize) }; + data.accounted.set(self.as_object(), bytes as usize); } #[getter] @@ -128,7 +127,7 @@ fn associated_memory_survives_relocation() { core_runtime::runtime::register_global_initializer(|scope, global| { Accounted::add_to_global(scope, global); }); - let rt = Runtime::init(&RuntimeConfig::default()); + let rt = Runtime::init(&RuntimeConfig::default()).expect("runtime init"); let scope = rt.default_global(); eval( @@ -163,7 +162,7 @@ fn destructors_chain_to_the_parent_class() { core_runtime::runtime::register_global_initializer(|scope, global| { Grandchild::add_to_global(scope, global); }); - let rt = Runtime::init(&RuntimeConfig::default()); + let rt = Runtime::init(&RuntimeConfig::default()).expect("runtime init"); let scope = rt.default_global(); eval( diff --git a/crates/core-runtime/tests/async_promise.rs b/crates/core-runtime/tests/async_promise.rs index 17c994e5..3d43d612 100644 --- a/crates/core-runtime/tests/async_promise.rs +++ b/crates/core-runtime/tests/async_promise.rs @@ -62,7 +62,7 @@ fn block_on_event_loop(scope: &Scope<'_>, el: &mut EventLoop) { fn async_promise_resolves_and_rejects_via_event_loop() { clear_global_initializers(); register_global_initializer(AsyncTest::add_to_global); - let rt = Runtime::init(&core_runtime::config::RuntimeConfig::default()); + let rt = Runtime::init(&core_runtime::config::RuntimeConfig::default()).expect("runtime init"); let scope = rt.default_global(); let mut el = EventLoop::new(); @@ -108,7 +108,7 @@ fn zero_delay_interval_does_not_starve_async_futures() { // promise never settles, and this test livelocks. clear_global_initializers(); register_global_initializer(AsyncTest::add_to_global); - let rt = Runtime::init(&core_runtime::config::RuntimeConfig::default()); + let rt = Runtime::init(&core_runtime::config::RuntimeConfig::default()).expect("runtime init"); let scope = rt.default_global(); let mut el = EventLoop::new(); @@ -148,7 +148,7 @@ fn long_handler_timer_chain_does_not_starve_async_futures() { // a starved future into an assertion failure instead of a livelock. clear_global_initializers(); register_global_initializer(AsyncTest::add_to_global); - let rt = Runtime::init(&core_runtime::config::RuntimeConfig::default()); + let rt = Runtime::init(&core_runtime::config::RuntimeConfig::default()).expect("runtime init"); let scope = rt.default_global(); let mut el = EventLoop::new(); @@ -183,3 +183,32 @@ fn long_handler_timer_chain_does_not_starve_async_futures() { let v = js::compile::evaluate_with_filename(&scope, "globalThis.__out", "", 1).unwrap(); assert_eq!(String::from_jsval(&scope, v, ()).unwrap(), "done:7"); } + +/// A future spawned with no event loop active belongs to no single loop: cancelling one loop's +/// futures leaves it in place, and another loop still drives it to completion. +#[test] +fn cancelling_one_loop_keeps_unowned_futures_for_the_others() { + clear_global_initializers(); + register_global_initializer(AsyncTest::add_to_global); + let rt = Runtime::init(&core_runtime::config::RuntimeConfig::default()).expect("runtime init"); + let scope = rt.default_global(); + let cancelled = EventLoop::new(); + let mut driving = EventLoop::new(); + + js::compile::evaluate_with_filename( + &scope, + r#" + globalThis.__out = "pending"; + new AsyncTest().delayed(7).then(v => { globalThis.__out = String(v); }); + "#, + "", + 1, + ) + .expect("evaluation failed"); + + cancelled.cancel_pending_futures(); + block_on_event_loop(&scope, &mut driving); + + let v = js::compile::evaluate_with_filename(&scope, "globalThis.__out", "", 1).unwrap(); + assert_eq!(String::from_jsval(&scope, v, ()).unwrap(), "7"); +} diff --git a/crates/core-runtime/tests/async_stacks.rs b/crates/core-runtime/tests/async_stacks.rs new file mode 100644 index 00000000..37e06f41 --- /dev/null +++ b/crates/core-runtime/tests/async_stacks.rs @@ -0,0 +1,55 @@ +// SPDX-License-Identifier: Apache-2.0-WITH-LLVM-exception + +//! Async stack capture: off by default, on with `--async-stacks`, and on in realms a `Debugger` +//! observes. + +// This file contains nothing platform-specific, so skip it on wasm32. +#![cfg(not(target_arch = "wasm32"))] + +use core_runtime::config::RuntimeConfig; +use core_runtime::runtime::Runtime; +use core_runtime::test_util::eval_and_read_out; + +/// Stores the stack of an error created after an `await` in `globalThis.__out`. +const ASYNC_STACK: &str = " + async function inner() { await null; return new Error().stack; } + async function outer() { return await inner(); } + outer().then(stack => { globalThis.__out = stack; }); +"; + +fn async_stack(config: &str) -> String { + let rt = Runtime::init(&RuntimeConfig::from_arg_string(config).unwrap()).expect("runtime init"); + let scope = rt.default_global(); + eval_and_read_out(&scope, ASYNC_STACK) +} + +#[test] +fn async_stacks_are_off_by_default() { + let stack = async_stack(""); + assert!(stack.starts_with("inner@"), "{stack}"); + assert!(!stack.contains("async*"), "{stack}"); +} + +#[test] +fn async_stacks_flag_records_async_parent_frames() { + let stack = async_stack("--async-stacks"); + assert!(stack.contains("async*outer@"), "{stack}"); +} + +#[test] +fn debuggee_realms_record_async_parent_frames() { + let rt = Runtime::init(&RuntimeConfig::default()).expect("runtime init"); + { + let debugger_scope = rt.new_global(); + js::debug::define_debugger_object(&debugger_scope, debugger_scope.global().handle()) + .expect("defining Debugger"); + eval_and_read_out( + &debugger_scope, + "globalThis.dbg = new Debugger(); dbg.addAllGlobalsAsDebuggees(); \ + globalThis.__out = dbg.getDebuggees().length", + ); + } + let scope = rt.default_global(); + let stack = eval_and_read_out(&scope, ASYNC_STACK); + assert!(stack.contains("async*outer@"), "{stack}"); +} diff --git a/crates/core-runtime/tests/const_naming.rs b/crates/core-runtime/tests/const_naming.rs index 39e9f8a8..b4f2fa92 100644 --- a/crates/core-runtime/tests/const_naming.rs +++ b/crates/core-runtime/tests/const_naming.rs @@ -84,7 +84,7 @@ fn eval(code: &str) -> String { /// does. fn eval_module(body: &str) -> String { setup(); - let rt = Runtime::init(&RuntimeConfig::default()); + let rt = Runtime::init(&RuntimeConfig::default()).expect("runtime init"); let scope = rt.default_global(); let source = format!("import * as m from \"constModule\";\nglobalThis._result = {body};"); // SAFETY: `scope` outlives the evaluation, and the module registry was @@ -211,7 +211,7 @@ fn setup_derived_names() { /// Import from `specifier` and read `ANSWER` back as a string. fn eval_import(specifier: &str) -> String { setup_derived_names(); - let rt = Runtime::init(&RuntimeConfig::default()); + let rt = Runtime::init(&RuntimeConfig::default()).expect("runtime init"); let scope = rt.default_global(); let source = format!("import {{ ANSWER }} from \"{specifier}\";\nglobalThis._result = ANSWER;"); // SAFETY: `scope` outlives the evaluation, and the module registry was diff --git a/crates/core-runtime/tests/event_loop.rs b/crates/core-runtime/tests/event_loop.rs index 7b64375a..9aac3573 100644 --- a/crates/core-runtime/tests/event_loop.rs +++ b/crates/core-runtime/tests/event_loop.rs @@ -70,13 +70,11 @@ impl Task for CounterTask { #[test] fn test_event_loop() { - let rt = Runtime::init(&core_runtime::config::RuntimeConfig::default()); + let rt = Runtime::init(&core_runtime::config::RuntimeConfig::default()).expect("runtime init"); let scope = rt.default_global(); // Install timer globals for timer tests. - unsafe { - install_timer_globals(&scope, scope.global()); - } + install_timer_globals(&scope, scope.global()); // ---- Test 1: Basic EventLoop operations ---- { @@ -548,11 +546,13 @@ fn test_event_loop() { with_event_loop(&el, |_| { let ok = js::compile::evaluate_with_filename( &scope, + // `late` is registered last so that its deadline follows the + // others' however long each `setTimeout` call itself takes. "globalThis._fireOrder = []; \ - setTimeout(function() { globalThis._fireOrder.push('late'); }, 40); \ setTimeout(function() { globalThis._fireOrder.push('early'); }, 10); \ globalThis._goner = setTimeout(function() { globalThis._fireOrder.push('goner'); }, 10); \ setTimeout(function() { globalThis._fireOrder.push('early2'); }, 10); \ + setTimeout(function() { globalThis._fireOrder.push('late'); }, 40); \ clearTimeout(globalThis._goner);", "", 1, diff --git a/crates/core-runtime/tests/heap_value.rs b/crates/core-runtime/tests/heap_value.rs index 4387cb53..631a8465 100644 --- a/crates/core-runtime/tests/heap_value.rs +++ b/crates/core-runtime/tests/heap_value.rs @@ -26,7 +26,7 @@ use js::Object; #[test] fn heap_value_survives_move_and_compacting_gc() { - let rt = Runtime::init(&RuntimeConfig::default()); + let rt = Runtime::init(&RuntimeConfig::default()).expect("runtime init"); let scope = rt.default_global(); // Allocate a nursery object and give it an identifying property. diff --git a/crates/core-runtime/tests/implicit_ctor.rs b/crates/core-runtime/tests/implicit_ctor.rs index 08027c0f..aa738f66 100644 --- a/crates/core-runtime/tests/implicit_ctor.rs +++ b/crates/core-runtime/tests/implicit_ctor.rs @@ -192,7 +192,7 @@ fn explicit_constructor_still_works() { #[test] fn rust_side_new_works() { setup(); - let rt = Runtime::init(&RuntimeConfig::default()); + let rt = Runtime::init(&RuntimeConfig::default()).expect("runtime init"); let scope = rt.default_global(); let obj = RustOnly::new(&scope).unwrap(); assert_eq!(obj.data().n, 7); @@ -204,7 +204,7 @@ fn rust_side_new_works() { #[test] fn rust_side_renamed_ctor_works() { setup(); - let rt = Runtime::init(&RuntimeConfig::default()); + let rt = Runtime::init(&RuntimeConfig::default()).expect("runtime init"); let scope = rt.default_global(); let obj = RenamedCtor::from_parts(&scope, 3).unwrap(); assert_eq!(obj.data().n, 3); @@ -214,7 +214,7 @@ fn rust_side_renamed_ctor_works() { #[test] fn rust_side_multiple_factories() { setup(); - let rt = Runtime::init(&RuntimeConfig::default()); + let rt = Runtime::init(&RuntimeConfig::default()).expect("runtime init"); let scope = rt.default_global(); assert_eq!(ManyFactories::new(&scope).unwrap().data().n, 0); assert_eq!(ManyFactories::from_n(&scope, 6).unwrap().data().n, 6); @@ -232,7 +232,7 @@ fn rust_side_factory_alongside_js_constructor() { assert_eq!(js, "5:undefined"); setup(); - let rt = Runtime::init(&RuntimeConfig::default()); + let rt = Runtime::init(&RuntimeConfig::default()).expect("runtime init"); let scope = rt.default_global(); assert_eq!(CtorPlusFactory::new(&scope, 5).unwrap().data().n, 5); assert_eq!(CtorPlusFactory::doubled(&scope, 5).unwrap().data().n, 10); @@ -242,7 +242,7 @@ fn rust_side_factory_alongside_js_constructor() { #[test] fn rust_side_setup_new_works() { setup(); - let rt = Runtime::init(&RuntimeConfig::default()); + let rt = Runtime::init(&RuntimeConfig::default()).expect("runtime init"); let scope = rt.default_global(); let obj = RustOnlySetup::new(&scope, 11).unwrap(); assert_eq!(obj.data().n, 11); diff --git a/crates/core-runtime/tests/instance_value_unforgeable.rs b/crates/core-runtime/tests/instance_value_unforgeable.rs index e58a3637..50101896 100644 --- a/crates/core-runtime/tests/instance_value_unforgeable.rs +++ b/crates/core-runtime/tests/instance_value_unforgeable.rs @@ -38,9 +38,31 @@ impl Widget { } } +/// A subclass with an unforgeable accessor of its own, which also inherits `kind`. +#[jsclass(extends = Widget)] +struct Gadget { + parent: WidgetImpl, +} + +#[jsmethods] +impl Gadget { + #[constructor] + fn construct() -> Self { + Self { + parent: WidgetImpl::construct(), + } + } + + #[getter(unforgeable)] + fn level(&self) -> i32 { + 3 + } +} + fn setup() { core_runtime::runtime::register_global_initializer(|scope, global| { Widget::add_to_global(scope, global); + Gadget::add_to_global(scope, global); }); } @@ -67,3 +89,55 @@ fn unforgeable_accessor_is_not_on_prototype() { "true" ); } + +#[test] +fn instances_share_one_getter_function() { + assert_eq!( + eval_with_setup( + setup, + "const get = o => Object.getOwnPropertyDescriptor(o, 'kind').get; \ + const a = new Widget(); \ + get(a) === get(new Widget()) && get(a) === get(a.dup()) && get(a).name" + ), + "get kind" + ); +} + +#[test] +fn unforgeable_accessor_is_enumerable_and_non_configurable() { + assert_eq!( + eval_with_setup( + setup, + "const d = Object.getOwnPropertyDescriptor(new Widget(), 'kind'); \ + [d.enumerable, d.configurable, d.set].join()" + ), + "true,false," + ); +} + +#[test] +fn js_subclass_instances_get_the_unforgeable_accessor() { + assert_eq!( + eval_with_setup( + setup, + "class Sub extends Widget { extra() { return 1; } } \ + const s = new Sub(); \ + [Object.getOwnPropertyNames(s).join(), s.kind, s.extra(), \ + Object.getPrototypeOf(s) === Sub.prototype].join()" + ), + "kind,42,1,true" + ); +} + +#[test] +fn rust_subclass_instances_get_inherited_and_own_unforgeable_accessors() { + assert_eq!( + eval_with_setup( + setup, + "const g = new Gadget(); \ + [Object.getOwnPropertyNames(g).sort().join(), g.kind, g.level, \ + Object.getOwnPropertyNames(new Widget()).join()].join('|')" + ), + "kind,level|42|3|kind" + ); +} diff --git a/crates/core-runtime/tests/js_api.rs b/crates/core-runtime/tests/js_api.rs index f2f7793d..76952976 100644 --- a/crates/core-runtime/tests/js_api.rs +++ b/crates/core-runtime/tests/js_api.rs @@ -96,7 +96,7 @@ fn test_jserror_display() { #[test] fn test_js_api_with_runtime() { - let rt = Runtime::init(&RuntimeConfig::default()); + let rt = Runtime::init(&RuntimeConfig::default()).expect("runtime init"); let scope = rt.default_global(); // --- String operations --- @@ -526,10 +526,9 @@ fn test_js_api_with_runtime() { ) .expect("new_callback should succeed"); - let fun_val = scope.root_value(fun.as_value()); scope .global() - .set_property(&scope, c"fortyTwo", fun_val) + .set_property(&scope, c"fortyTwo", fun) .expect("set_property"); let result = @@ -553,10 +552,9 @@ fn test_js_api_with_runtime() { ) .expect("new_callback should succeed"); - let fun_val = scope.root_value(fun.as_value()); scope .global() - .set_property(&scope, c"add", fun_val) + .set_property(&scope, c"add", fun) .expect("set_property"); let result = @@ -576,10 +574,9 @@ fn test_js_api_with_runtime() { ) .expect("new_callback should succeed"); - let fun_val = scope.root_value(fun.as_value()); scope .global() - .set_property(&scope, c"fail", fun_val) + .set_property(&scope, c"fail", fun) .expect("set_property"); let tc = TryCatch::new(&scope); @@ -600,10 +597,9 @@ fn test_js_api_with_runtime() { ) .expect("new_callback should succeed"); - let fun_val = scope.root_value(fun.as_value()); scope .global() - .set_property(&scope, c"argInfo", fun_val) + .set_property(&scope, c"argInfo", fun) .expect("set_property"); let result = js::compile::evaluate(&scope, "argInfo(1, 2, 3)").expect("evaluate"); @@ -626,7 +622,7 @@ fn test_queue_microtask() { static FIRST_ARG_WAS_UNDEFINED: Cell = const { Cell::new(false) }; } - let rt = Runtime::init(&RuntimeConfig::default()); + let rt = Runtime::init(&RuntimeConfig::default()).expect("runtime init"); let scope = rt.default_global(); let record = js::Function::new_callback( @@ -678,3 +674,90 @@ fn test_queue_microtask() { let joined = String::from_jsval(&scope, joined, ()).unwrap(); assert_eq!(joined, "abc"); } + +/// `ArrayBuffer::transfer` moves the contents into a new buffer and detaches the original, for +/// empty buffers too. +#[test] +fn array_buffer_transfer_moves_the_contents() { + let rt = Runtime::init(&RuntimeConfig::default()).expect("runtime init"); + let scope = rt.default_global(); + + let value = js::compile::evaluate(&scope, "new Uint8Array([1, 2, 3, 4]).buffer").unwrap(); + let source = js::ArrayBuffer::from_jsval(&scope, value, ()).unwrap(); + let moved = source.transfer(&scope).unwrap(); + assert!(source.is_detached()); + assert_eq!(moved.byte_length(), 4); + // SAFETY: nothing allocates while the slice is read. + assert_eq!(unsafe { moved.bytes() }, &[1, 2, 3, 4]); + + let empty = js::ArrayBuffer::new(&scope, 0).unwrap(); + let moved_empty = empty.transfer(&scope).unwrap(); + assert!(empty.is_detached()); + assert_eq!(moved_empty.byte_length(), 0); +} + +/// Strings created from Rust text hold the same UTF-16 code units as the text, whether it is +/// ASCII, Latin-1, other BMP characters, or characters outside the BMP, and whether the +/// non-ASCII part starts at the beginning or after an ASCII prefix longer than the conversion +/// buffers' inline capacity. +#[test] +fn strings_from_rust_text_hold_its_utf16_code_units() { + let rt = Runtime::init(&RuntimeConfig::default()).expect("runtime init"); + let scope = rt.default_global(); + + let long_prefix = "a".repeat(300); + let samples = [ + String::new(), + "plain ascii".to_string(), + "héllo wörld ÿ".to_string(), + "é".to_string(), + "日本語 ✓".to_string(), + "emoji 🦀 and ☃".to_string(), + format!("{long_prefix}é"), + format!("{long_prefix}日"), + format!("{long_prefix}🦀"), + ]; + for text in &samples { + let expected: Vec = text.encode_utf16().collect(); + let from_str = js::string::Str::from_str(&scope, text).unwrap(); + let converted = js::string::Str::from_value(&scope, text.as_str()).unwrap(); + for s in [from_str, converted] { + assert_eq!(s.len(), expected.len(), "{text:?}"); + let units: Vec = (0..s.len()) + .map(|i| s.char_at(&scope, i).unwrap()) + .collect(); + assert_eq!(units, expected, "{text:?}"); + assert_eq!(s.to_utf8(&scope).unwrap(), *text); + } + } +} + +/// Converting a JS string to UTF-8 flattens ropes, encodes Latin-1 characters after any ASCII +/// prefix as two bytes each, and replaces unpaired surrogates with U+FFFD. +#[test] +fn strings_convert_to_utf8_from_either_representation() { + let rt = Runtime::init(&RuntimeConfig::default()).expect("runtime init"); + let scope = rt.default_global(); + + let long_prefix = "a".repeat(300); + let cases = [ + ("''", String::new()), + ("'ÿé'", "ÿé".to_string()), + ("'abc ÿé'", "abc ÿé".to_string()), + ("'\\uD800x\\uDC00'", "\u{FFFD}x\u{FFFD}".to_string()), + ("'\\uD83E\\uDD80'", "🦀".to_string()), + ( + "let a = 'a'.repeat(300); a + String.fromCharCode(0xe9)", + format!("{long_prefix}é"), + ), + ( + "let b = 'a'.repeat(300); b + String.fromCharCode(0x65e5)", + format!("{long_prefix}日"), + ), + ]; + for (source, expected) in &cases { + let value = js::compile::evaluate(&scope, source).unwrap(); + let s = js::string::Str::from_value(&scope, value).unwrap(); + assert_eq!(s.to_utf8(&scope).unwrap(), *expected, "{source}"); + } +} diff --git a/crates/core-runtime/tests/multi_global_registry.rs b/crates/core-runtime/tests/multi_global_registry.rs index f4101de8..8b71251c 100644 --- a/crates/core-runtime/tests/multi_global_registry.rs +++ b/crates/core-runtime/tests/multi_global_registry.rs @@ -41,7 +41,7 @@ fn displaced_global_registry_survives_compacting_gc() { core_runtime::runtime::register_global_initializer(|scope, global| { Probe::add_to_global(scope, global); }); - let rt = Runtime::init(&RuntimeConfig::default()); + let rt = Runtime::init(&RuntimeConfig::default()).expect("runtime init"); // Realm A: the initial default global. let scope_a = rt.default_global(); diff --git a/crates/core-runtime/tests/promise_args.rs b/crates/core-runtime/tests/promise_args.rs index a7ff076b..01b59f31 100644 --- a/crates/core-runtime/tests/promise_args.rs +++ b/crates/core-runtime/tests/promise_args.rs @@ -141,7 +141,7 @@ fn eval_then_drain(code: &str, probe: &str) -> String { clear_global_initializers(); register_global_initializer(Payload::add_to_global); register_global_initializer(PromiseArgs::add_to_global); - let rt = Runtime::init(&RuntimeConfig::default()); + let rt = Runtime::init(&RuntimeConfig::default()).expect("runtime init"); let scope = rt.default_global(); if js::compile::evaluate_with_filename(&scope, code, "test.js", 1).is_err() { diff --git a/crates/core-runtime/tests/rest_args.rs b/crates/core-runtime/tests/rest_args.rs index 6298e890..e8cb86d1 100644 --- a/crates/core-runtime/tests/rest_args.rs +++ b/crates/core-runtime/tests/rest_args.rs @@ -371,7 +371,7 @@ mod handle_value_tests { use js::gc::SetGCZeal; setup(); - let rt = Runtime::init(&RuntimeConfig::default()); + let rt = Runtime::init(&RuntimeConfig::default()).expect("runtime init"); let scope = rt.default_global(); // Mode 14 (Compact): every GC compacts, moving heap objects. @@ -577,7 +577,7 @@ mod free_fn_tests { fn eval_module(body: &str) -> String { setup(); - let rt = Runtime::init(&RuntimeConfig::default()); + let rt = Runtime::init(&RuntimeConfig::default()).expect("runtime init"); let scope = rt.default_global(); let source = format!("import * as m from \"restModule\";\nglobalThis._result = {body};"); // SAFETY: `scope` outlives the evaluation, and the module registry was @@ -666,11 +666,9 @@ mod free_fn_tests { // ============================================================================ mod promise_return_tests { - use core_runtime::config::RuntimeConfig; - use core_runtime::event_loop::run_microtasks; - use core_runtime::runtime::{clear_global_initializers, register_global_initializer, Runtime}; + use core_runtime::runtime::{clear_global_initializers, register_global_initializer}; + use core_runtime::test_util::eval_out_with_setup; use core_runtime::{jsclass, jsmethods}; - use js::conversion::FromJSVal; use js::error::ExnThrown; use js::gc::scope::Scope; use js::Promise; @@ -710,19 +708,15 @@ mod promise_return_tests { /// Evaluate `code`, drain microtasks, and return `String(globalThis.__out)`. fn run(code: &str) -> String { - clear_global_initializers(); - register_global_initializer(|scope, global| { - Waiter::add_to_global(scope, global); - }); - let rt = Runtime::init(&RuntimeConfig::default()); - let scope = rt.default_global(); - if js::compile::evaluate_with_filename(&scope, code, "test.js", 1).is_err() { - panic!("evaluation threw: {:?}", ExnThrown::capture(&scope)); - } - run_microtasks(&scope); - let out = js::compile::evaluate_with_filename(&scope, "globalThis.__out", "out.js", 1) - .expect("reading __out threw"); - String::from_jsval(&scope, out, ()).unwrap() + eval_out_with_setup( + || { + clear_global_initializers(); + register_global_initializer(|scope, global| { + Waiter::add_to_global(scope, global); + }); + }, + code, + ) } #[test] diff --git a/crates/core-runtime/tests/scope.rs b/crates/core-runtime/tests/scope.rs index 303a32d3..1205623e 100644 --- a/crates/core-runtime/tests/scope.rs +++ b/crates/core-runtime/tests/scope.rs @@ -14,7 +14,7 @@ use js::value; #[test] fn test_scope_rooting() { - let rt = Runtime::init(&RuntimeConfig::default()); + let rt = Runtime::init(&RuntimeConfig::default()).expect("runtime init"); let scope = rt.default_global(); // --- Basic rooting --- diff --git a/crates/core-runtime/tests/scope_gc_zeal.rs b/crates/core-runtime/tests/scope_gc_zeal.rs index 2c7e550c..f1528949 100644 --- a/crates/core-runtime/tests/scope_gc_zeal.rs +++ b/crates/core-runtime/tests/scope_gc_zeal.rs @@ -39,7 +39,7 @@ unsafe fn reset_zeal(scope: &js::gc::scope::Scope<'_>) { #[test] fn test_scope_gc_zeal() { - let rt = Runtime::init(&RuntimeConfig::default()); + let rt = Runtime::init(&RuntimeConfig::default()).expect("runtime init"); let scope = rt.default_global(); unsafe { @@ -360,13 +360,27 @@ fn test_scope_gc_zeal() { let arr = Array::with_contents(&inner, &[sv, iv]).unwrap(); assert_eq!(arr.length(&inner).unwrap(), 2); let got = arr.get_element(&inner, 0).unwrap(); - let got_str = js::JSString::from_handle( - inner.root_string(std::ptr::NonNull::new(got.to_string()).unwrap()), + let got_str = js::JSString::from_non_null( + &inner, + std::ptr::NonNull::new(got.to_string()).unwrap(), ); assert_eq!(got_str.to_utf8(&inner).unwrap(), "compact_contents"); assert_eq!(arr.get_element(&inner, 1).unwrap().to_int32(), 7); } + // Each `&str` element allocates a fresh string when converted, so the + // strings converted first must already be traced while the later + // ones allocate. + { + let words = ["alpha", "beta", "gamma", "delta"]; + let arr = Array::with_contents(&inner, &words).unwrap(); + for (i, word) in words.iter().enumerate() { + let got = arr.get_element(&inner, i as u32).unwrap(); + let got = js::JSString::from_value(&inner, got).unwrap(); + assert_eq!(got.to_utf8(&inner).unwrap(), *word); + } + } + reset_zeal(&inner); } @@ -462,3 +476,60 @@ fn test_scope_gc_zeal() { } } } + +/// Roots survive a compacting GC when a parent scope roots values while a nested scope is alive, +/// when scopes drop in a different order than they were created in, and when a scope's roots +/// span several pages of the root stack. +#[test] +fn scope_roots_survive_nesting_out_of_order_drops_and_page_spans() { + let rt = Runtime::init(&RuntimeConfig::default()).expect("runtime init"); + let scope = rt.default_global(); + let compact = |s: &js::gc::scope::Scope<'_>| { + gc::prepare_for_full_gc(s); + gc::non_incremental_gc(s, GCOptions::Shrink, GCReason::API); + }; + + // The parent roots a value while a nested scope is alive, and again after it dropped. + let before = js::JSString::from_str(&scope, "before").unwrap(); + let during; + { + let inner = scope.inner_scope(); + let inner_value = js::JSString::from_str(&inner, "inner").unwrap(); + during = js::JSString::from_str(&scope, "during").unwrap(); + compact(&inner); + assert_eq!(inner_value.to_utf8(&inner).unwrap(), "inner"); + } + let after = js::JSString::from_str(&scope, "after").unwrap(); + compact(&scope); + assert_eq!(before.to_utf8(&scope).unwrap(), "before"); + assert_eq!(during.to_utf8(&scope).unwrap(), "during"); + assert_eq!(after.to_utf8(&scope).unwrap(), "after"); + + // A scope drops while a scope created after it is still alive. + // SAFETY: a realm is entered, and both scopes drop before `scope` is used again. + unsafe { + let raw_cx = scope.raw_cx_no_gc(); + let first = js::gc::scope::RootScope::from_current_realm(raw_cx); + let second = js::gc::scope::RootScope::from_current_realm(raw_cx); + let first_value = js::JSString::from_str(&first, "first").unwrap(); + let second_value = js::JSString::from_str(&second, "second").unwrap(); + assert_eq!(first_value.to_utf8(&first).unwrap(), "first"); + drop(first); + compact(&second); + assert_eq!(second_value.to_utf8(&second).unwrap(), "second"); + } + let later = js::JSString::from_str(&scope, "later").unwrap(); + compact(&scope); + assert_eq!(later.to_utf8(&scope).unwrap(), "later"); + assert_eq!(after.to_utf8(&scope).unwrap(), "after"); + + // More roots than fit in one page of the root stack. + let inner = scope.inner_scope(); + let values: Vec<_> = (0..300) + .map(|i| js::JSString::from_str(&inner, &format!("value {i}")).unwrap()) + .collect(); + compact(&inner); + for (i, value) in values.iter().enumerate() { + assert_eq!(value.to_utf8(&inner).unwrap(), format!("value {i}")); + } +} diff --git a/crates/core-runtime/tests/scope_root_enum.rs b/crates/core-runtime/tests/scope_root_enum.rs index 1dbcfa94..13b01a99 100644 --- a/crates/core-runtime/tests/scope_root_enum.rs +++ b/crates/core-runtime/tests/scope_root_enum.rs @@ -29,7 +29,7 @@ enum Reply { #[test] fn scope_root_enum_unit_variant_roots() { - let rt = Runtime::init(&RuntimeConfig::default()); + let rt = Runtime::init(&RuntimeConfig::default()).expect("runtime init"); let scope = rt.default_global(); let rooted = Reply::Empty.root(&scope); diff --git a/crates/core-runtime/tests/traceable_enum.rs b/crates/core-runtime/tests/traceable_enum.rs index 1a179ab7..94a7cf8d 100644 --- a/crates/core-runtime/tests/traceable_enum.rs +++ b/crates/core-runtime/tests/traceable_enum.rs @@ -58,7 +58,7 @@ const _: fn() = || _assert_trace::>(); #[test] fn traceable_enum_traces_variant_fields() { - let rt = Runtime::init(&RuntimeConfig::default()); + let rt = Runtime::init(&RuntimeConfig::default()).expect("runtime init"); let scope = rt.default_global(); let obj = Object::new(&scope, None).unwrap(); diff --git a/crates/core-runtime/tests/typedef_macros.rs b/crates/core-runtime/tests/typedef_macros.rs index 94134c8e..358ee04a 100644 --- a/crates/core-runtime/tests/typedef_macros.rs +++ b/crates/core-runtime/tests/typedef_macros.rs @@ -97,6 +97,50 @@ mod js_proto_tests { fn stack_is_nonempty() { assert_eq!(eval("new CustomError('x').stack.length > 0"), "true"); } + + /// The stack is the one at construction, formatted when it is first read. + #[test] + fn stack_is_captured_at_construction() { + assert_eq!( + eval( + "function make() { return new CustomError('x'); } + function read(e) { return e.stack; } + const s = read(make()); + [s.includes('make@'), s.includes('read@')].join()" + ), + "true,false" + ); + } + + /// Like `Error.prototype.stack`, `stack` is an accessor on the prototype: instances have no + /// own `stack` until one is assigned, and the getter returns `undefined` for other objects. + #[test] + fn stack_is_a_prototype_accessor() { + assert_eq!( + eval( + "const e = new CustomError('x'); + const d = Object.getOwnPropertyDescriptor(CustomError.prototype, 'stack'); + const own = Object.getOwnPropertyDescriptor(e, 'stack'); + e.stack = 'replaced'; + const after = Object.getOwnPropertyDescriptor(e, 'stack'); + [typeof d.get, typeof d.set, d.enumerable, d.configurable, own, + after.value, after.enumerable, after.writable, + d.get.call(CustomError.prototype), d.get.call({})].join()" + ), + "function,function,false,true,,replaced,true,true,," + ); + } + + #[test] + fn stack_getter_throws_on_primitives() { + assert_eq!( + eval( + "const get = Object.getOwnPropertyDescriptor(CustomError.prototype, 'stack').get; + try { get.call(1); 'no throw' } catch (e) { e.constructor.name }" + ), + "TypeError" + ); + } } mod constant_tests { @@ -1005,7 +1049,7 @@ mod implicit_factory_tests { core_runtime::runtime::register_global_initializer(|scope, global| { Checked::add_to_global(scope, global); }); - let rt = Runtime::init(&RuntimeConfig::default()); + let rt = Runtime::init(&RuntimeConfig::default()).expect("runtime init"); let scope = rt.default_global(); let ok = Checked::checked(&scope, 7).expect("factory should succeed"); diff --git a/crates/js/Cargo.toml b/crates/js/Cargo.toml index 775c2276..59575349 100644 --- a/crates/js/Cargo.toml +++ b/crates/js/Cargo.toml @@ -21,6 +21,7 @@ intl = ['mozjs/intl'] workspace = true [dependencies] +encoding_rs.workspace = true indexmap = "2.13" # `libz-rs` (pure Rust) rather than mozjs's default `libz-sys`, whose C zlib has no # wasm32-wasip3 build. `jit` and `intl` are mozjs defaults that `default-features = false` would @@ -32,4 +33,6 @@ mozjs = { git = "https://github.com/tschneidereit/mozjs.git", branch = "main", d ] } mozjs_sys = { git = "https://github.com/tschneidereit/mozjs.git", branch = "main" } num-traits = "0.2" +rustc-hash = "2.1" +smallvec.workspace = true starling-macro.workspace = true diff --git a/crates/js/src/array.rs b/crates/js/src/array.rs index 22ebf7bc..501dd14a 100644 --- a/crates/js/src/array.rs +++ b/crates/js/src/array.rs @@ -8,9 +8,8 @@ //! handle type. It implements `Deref` to [`Stack`](crate::Object), //! so all property and prototype methods are available directly. -use std::os::raw::c_uint; - use crate::builtins::JSType; +use crate::conversion::ToJSVal; use crate::gc::handle::Stack; use crate::gc::scope::Scope; use crate::value::ValueArrayRooter; @@ -49,10 +48,10 @@ impl<'s> Stack<'s, Array> { /// Create a new array pre-populated with the given values. pub fn with_contents( scope: &'s Scope<'s>, - contents: &[HandleValue], + contents: &[impl ToJSVal<'s>], ) -> Result { - let mut contents_root = ValueArrayRooter::new(scope, contents)?; - let contents = contents_root.root(scope); + let mut contents_root = ValueArrayRooter::new(contents.len()); + let contents = contents_root.root(scope, contents)?; let obj = unsafe { wrappers2::NewArrayObject(scope.cx_mut(), &contents.handles()) }; unsafe { Self::from_mozjs_rval(scope, obj) } } @@ -71,20 +70,6 @@ impl<'s> Stack<'s, Array> { ExnThrown::check(ok) } - /// Define an element by index with attribute flags. - pub fn define_element( - &self, - scope: &Scope<'_>, - index: u32, - value: HandleValue, - attrs: c_uint, - ) -> Result<(), ExnThrown> { - let ok = unsafe { - wrappers2::JS_DefineElement(scope.cx_mut(), self.handle(), index, value, attrs) - }; - ExnThrown::check(ok) - } - /// Check whether an object is an `Array`. pub fn is_array(scope: &Scope<'_>, obj: HandleObject) -> Result { let mut result = false; @@ -103,3 +88,5 @@ impl<'s> Stack<'s, Array> { } crate::gc::handle::deref_to_object!(Array); + +crate::gc::handle::from_jsval_via_cast!(Array, c"Value isn't an Array"); diff --git a/crates/js/src/builtins.rs b/crates/js/src/builtins.rs index bb95d283..b4ef9e44 100644 --- a/crates/js/src/builtins.rs +++ b/crates/js/src/builtins.rs @@ -41,9 +41,8 @@ pub trait JSType: Sized + 'static { /// The `JSClass` pointer that identifies objects of this type. /// - /// Used by [`Stack::is`](Stack::is) and - /// [`Stack::from_object`](Stack::from_object) for type-checked - /// conversions. + /// Used by [`Stack::is`](Stack::is) and [`Stack::cast`](Stack::cast) for + /// type-checked conversions. fn js_class() -> *const crate::class_spec::JSClass; /// Test whether `obj` is an instance of this type. @@ -121,6 +120,7 @@ pub(crate) fn class_tag() -> usize { /// # Safety /// /// - `obj` must be a valid, non-null JS object pointer. +#[inline] pub unsafe fn get_class_tag(obj: *mut JSObject) -> usize { crate::object::get_object_class(obj) as usize } @@ -130,6 +130,7 @@ pub unsafe fn get_class_tag(obj: *mut JSObject) -> usize { /// Returns `true` if `concrete_tag == target_tag`, or if the concrete type /// has the target in its ancestor set. Also returns `true` if `target_tag` /// is Object's JSClass tag, since every JS object is-an Object. +#[inline] pub fn is_derived_from_type(concrete_tag: usize, target_tag: usize) -> bool { if concrete_tag == target_tag { return true; diff --git a/crates/js/src/class.rs b/crates/js/src/class.rs index a92781d1..a993a732 100644 --- a/crates/js/src/class.rs +++ b/crates/js/src/class.rs @@ -7,14 +7,13 @@ //! It also provides the [`ClassDef`] trait and supporting infrastructure //! for defining JavaScript classes backed by Rust structs. +use rustc_hash::{FxBuildHasher, FxHashMap}; use std::any::TypeId; #[cfg(debug_assertions)] use std::cell::Cell; use std::cell::RefCell; -use std::collections::hash_map::DefaultHasher; -use std::collections::{HashMap, HashSet}; +use std::collections::HashMap; use std::ffi::{c_void, CStr, CString}; -use std::hash::BuildHasherDefault; use std::marker::PhantomData; use std::os::raw::c_char; use std::ptr::{self, NonNull}; @@ -161,6 +160,17 @@ pub fn get_iterator_prototype<'s>(scope: &'s Scope<'_>) -> Result, Ex unsafe { Object::from_mozjs_rval(scope, obj) } } +/// Set the `[[Prototype]]` of `T`'s prototype object to `%Iterator.prototype%`, as WebIDL +/// requires of an iterator prototype object +/// (). +/// +/// Panics if `T` is not registered in the current global. +pub fn inherit_from_iterator_prototype(scope: &Scope<'_>) -> Result<(), ExnThrown> { + let proto = get_prototype_object_for::(scope).expect("class is registered"); + let iterator_proto = get_iterator_prototype(scope)?; + proto.set_prototype(scope, iterator_proto.handle()) +} + /// Get the prototype for a standard class by `JSProtoKey`. pub fn get_class_prototype<'s>( scope: &'s Scope<'_>, @@ -207,13 +217,7 @@ pub fn fire_on_new_global_object(scope: &Scope<'_>, global: HandleObject) { /// This checks the object's direct class — not the prototype chain. /// Returns `false` for null objects or objects of different classes. /// -/// Unlike `JS_InstanceOf`, this does **not** throw on failure: pass `null` -/// for the `args` parameter to suppress the TypeError. -/// -/// # Safety -/// -/// `obj` must be a valid rooted object handle. `clasp` must point to a -/// valid `JSClass` that will remain valid for the duration of the call. +/// Unlike a throwing `JS_InstanceOf` call, this does not throw on failure. pub fn instance_of(scope: &Scope<'_>, obj: HandleObject, clasp: &JSClass) -> bool { // Safety: JS_InstanceOf with a null CallArgs pointer performs a // non-throwing check: it returns true if `obj` has `clasp` as its @@ -237,10 +241,6 @@ pub fn instance_of(scope: &Scope<'_>, obj: HandleObject, clasp: &JSClass) -> boo // proc macros. // ============================================================================ -// The code below was originally in core-runtime/src/class.rs. All of its -// imports reference `crate::*` (i.e. the `js` crate). No external -// dependencies on core-runtime are needed. - // ============================================================================ // Marker types // ============================================================================ @@ -249,32 +249,35 @@ impl<'s, T: JSType + ClassDef> Stack<'s, T> { /// Borrow the private Rust data, returning a guard that dereferences to /// `&T`. /// - /// Returns `None` if the object doesn't have private data of type `T`. + /// A `Stack` is only created for an instance of `T` or of a class + /// derived from `T`, so the data is always present. /// /// # Panics /// /// In debug builds, panics if the data is already mutably borrowed. That /// happens when a JS reentry borrowed this object's data while a /// [`data_mut`](Self::data_mut) guard was live. - pub fn data(&self) -> Option> { - unsafe { borrow::(self.handle.get()) } + #[inline] + pub fn data(&self) -> Ref<'_, T> { + unsafe { borrow::(self.handle.get()) }.expect(INSTANCE_HAS_DATA) } /// Mutably borrow the private Rust data, returning a guard that /// dereferences to `&mut T`. /// - /// Returns `None` if the object doesn't have private data of type `T`. - /// /// # Panics /// /// In debug builds, panics if the data is already borrowed (shared or /// mutable). That happens when a JS reentry borrowed this object's data /// while a borrow was live. See [`data`](Self::data). - pub fn data_mut(&self) -> Option> { - unsafe { borrow_mut::(self.handle.get()) } + #[inline] + pub fn data_mut(&self) -> RefMut<'_, T> { + unsafe { borrow_mut::(self.handle.get()) }.expect(INSTANCE_HAS_DATA) } } +const INSTANCE_HAS_DATA: &str = "a `Stack` holds an instance of `T` with private data"; + // Blanket impl: every ClassDef is automatically a JSType. impl JSType for T { type Rooted<'s> = ::Rooted<'s>; @@ -470,6 +473,12 @@ const PRIVATE_DATA_SLOT: u32 = 0; #[cfg(debug_assertions)] const BORROW_FLAG_SLOT: u32 = 1; +/// The reserved slot of an instance of a class with [`ClassDef::HAS_ERROR_DATA`] that holds its +/// stack: the `SavedFrame` [`capture_stack_from_error`](capture_stack_from_error) +/// captured, replaced by the formatted string when the `stack` getter first reads it. Empty if no +/// stack was captured. +pub const ERROR_STACK_SLOT: u32 = MIN_CLASS_RESERVED_SLOTS; + /// Minimum number of reserved slots required for a class instance. /// /// Slot 0 ([`PRIVATE_DATA_SLOT`]) holds the boxed Rust data. In debug builds, @@ -558,29 +567,24 @@ fn acquire_mut(flag: &Cell) { /// /// # Safety /// -/// `obj` must be a valid JS object created with at least -/// [`MIN_CLASS_RESERVED_SLOTS`] reserved slots. +/// `obj` must be a valid JS object. +#[inline] unsafe fn borrow<'a, T: ClassDef>(obj: *mut JSObject) -> Option> { + let ptr = unsafe { private_data_ptr::(obj, step_to_parent)? }; // Check (and panic on conflict) before materializing the `&T`, so the // reference is never created while a mutable borrow is live. #[cfg(debug_assertions)] let flag = { - let flag = unsafe { get_borrow_flag(obj)? }; + let flag = + unsafe { get_borrow_flag(obj) }.expect("objects with private data have a borrow flag"); acquire_shared(flag); flag }; - match unsafe { get_private_or_ancestor::(obj) } { - Some(value) => Some(Ref { - value, - #[cfg(debug_assertions)] - flag, - }), - None => { - #[cfg(debug_assertions)] - flag.set(flag.get() - 1); - None - } - } + Some(Ref { + value: unsafe { &*(ptr as *const T) }, + #[cfg(debug_assertions)] + flag, + }) } /// Take a mutable borrow of `obj`'s private data of type `T`. @@ -591,25 +595,21 @@ unsafe fn borrow<'a, T: ClassDef>(obj: *mut JSObject) -> Option> { /// # Safety /// /// Same as [`borrow`]. +#[inline] unsafe fn borrow_mut<'a, T: ClassDef>(obj: *mut JSObject) -> Option> { + let ptr = unsafe { private_data_ptr::(obj, step_to_parent_mut)? }; #[cfg(debug_assertions)] let flag = { - let flag = unsafe { get_borrow_flag(obj)? }; + let flag = + unsafe { get_borrow_flag(obj) }.expect("objects with private data have a borrow flag"); acquire_mut(flag); flag }; - match unsafe { get_private_or_ancestor_mut::(obj) } { - Some(value) => Some(RefMut { - value, - #[cfg(debug_assertions)] - flag, - }), - None => { - #[cfg(debug_assertions)] - flag.set(UNUSED); - None - } - } + Some(RefMut { + value: unsafe { &mut *(ptr as *mut T) }, + #[cfg(debug_assertions)] + flag, + }) } /// A shared guard over private data, released when dropped. @@ -758,7 +758,7 @@ pub fn class_tag() -> usize { /// - The returned reference is only valid as long as the JS object is alive and /// no mutable reference is taken simultaneously. pub unsafe fn get_private<'a, T: 'static>(obj: *mut JSObject) -> Option<&'a T> { - let val = unsafe { crate::object::get_reserved_slot(obj, PRIVATE_DATA_SLOT) }; + let val = unsafe { crate::object::get_native_reserved_slot(obj, PRIVATE_DATA_SLOT) }; if val.is_undefined() { return None; } @@ -776,7 +776,7 @@ pub unsafe fn get_private<'a, T: 'static>(obj: *mut JSObject) -> Option<&'a T> { /// - `obj` must be a valid JS object with private data of type `T` stored via [`set_private`]. /// - No other references to the data may exist simultaneously. pub unsafe fn get_private_mut<'a, T: 'static>(obj: *mut JSObject) -> Option<&'a mut T> { - let val = unsafe { crate::object::get_reserved_slot(obj, PRIVATE_DATA_SLOT) }; + let val = unsafe { crate::object::get_native_reserved_slot(obj, PRIVATE_DATA_SLOT) }; if val.is_undefined() { return None; } @@ -796,7 +796,7 @@ pub unsafe fn get_private_mut<'a, T: 'static>(obj: *mut JSObject) -> Option<&'a /// - `obj` must be a valid JS object with private data of type `T` stored via [`set_private`]. /// - Must only be called once (typically from the GC finalize callback). pub unsafe fn drop_private(obj: *mut JSObject) { - let val = unsafe { crate::object::get_reserved_slot(obj, PRIVATE_DATA_SLOT) }; + let val = unsafe { crate::object::get_native_reserved_slot(obj, PRIVATE_DATA_SLOT) }; if !val.is_undefined() { let ptr = val.to_private() as *mut T; if !ptr.is_null() { @@ -809,7 +809,7 @@ pub unsafe fn drop_private(obj: *mut JSObject) { // Drop the borrow flag installed by `set_private`. #[cfg(debug_assertions)] unsafe { - let flag_val = crate::object::get_reserved_slot(obj, BORROW_FLAG_SLOT); + let flag_val = crate::object::get_native_reserved_slot(obj, BORROW_FLAG_SLOT); if !flag_val.is_undefined() { let flag_ptr = flag_val.to_private() as *mut Cell; if !flag_ptr.is_null() { @@ -836,7 +836,7 @@ const CLASS_REGISTRY_SLOT: u32 = MIN_CLASS_RESERVED_SLOTS + 1; /// Stored as private data in [`CLASS_REGISTRY_SLOT`] of the global object. #[crate::allow_unrooted_interior] struct ClassRegistry { - map: HashMap>>, + map: FxHashMap>>, /// Per-global shared objects keyed by an arbitrary `usize` (by convention a /// native function pointer unique to the call site), so the same object is /// reused for every caller on a given global. Used for functions whose spec @@ -844,13 +844,17 @@ struct ClassRegistry { /// the streams queuing-strategy `size` functions) and for internal /// plumbing objects reused across calls (e.g. a resolved promise serving /// as a microtask trigger). Traced like prototypes. - shared_objects: HashMap>>, + shared_objects: FxHashMap>>, /// Per-global monotonic `u64` counters keyed by an arbitrary `usize`. Used /// where a spec requires ids unique across a whole global rather than per /// call site — e.g. HTML's setTimeout/setInterval ids, which must not /// collide across the concurrent event loops that share one global. Plain /// integers, so no GC tracing. - counters: HashMap, + counters: FxHashMap, + /// Per-global interned property names keyed by an arbitrary `usize` (by + /// convention the address of a `static` unique to the call site), so a hot + /// property name is interned once per global. Traced like prototypes. + property_atoms: FxHashMap>>, /// Type-erased GC glue for the global's private data, /// installed by [`set_global_private`]. /// @@ -868,9 +872,10 @@ struct GlobalPrivateGlue { impl ClassRegistry { fn new() -> Self { Self { - map: HashMap::new(), - shared_objects: HashMap::new(), - counters: HashMap::new(), + map: FxHashMap::default(), + shared_objects: FxHashMap::default(), + counters: FxHashMap::default(), + property_atoms: FxHashMap::default(), global_private: None, } } @@ -917,6 +922,9 @@ impl ClassRegistry { for heap in self.shared_objects.values() { heap.trace(trc); } + for heap in self.property_atoms.values() { + heap.trace(trc); + } } } @@ -924,7 +932,7 @@ impl ClassRegistry { /// /// Returns `None` if the slot is unset (e.g. on a non-StarlingMonkey global). unsafe fn get_class_registry(global: *mut JSObject) -> Option<&'static ClassRegistry> { - let val = crate::object::get_reserved_slot(global, CLASS_REGISTRY_SLOT); + let val = crate::object::get_native_reserved_slot(global, CLASS_REGISTRY_SLOT); if val.is_undefined() { return None; } @@ -937,7 +945,7 @@ unsafe fn get_class_registry(global: *mut JSObject) -> Option<&'static ClassRegi /// Read the `ClassRegistry` pointer from a global, creating one if absent. unsafe fn get_or_init_class_registry(global: *mut JSObject) -> &'static mut ClassRegistry { - let val = crate::object::get_reserved_slot(global, CLASS_REGISTRY_SLOT); + let val = crate::object::get_native_reserved_slot(global, CLASS_REGISTRY_SLOT); if !val.is_undefined() { let ptr = val.to_private() as *mut ClassRegistry; if !ptr.is_null() { @@ -962,16 +970,16 @@ pub(crate) struct ClassTls { } /// A map over keys this crate generates itself, so it needs no random seeding. -/// `BuildHasherDefault` is const-constructible, which keeps [`crate::tls`]'s key +/// `FxBuildHasher` is const-constructible, which keeps [`crate::tls`]'s key /// `const`-initialized. -type TagMap = HashMap>; +type TagMap = HashMap; impl ClassTls { pub(crate) const fn new() -> Self { Self { live_registries: RefCell::new(Vec::new()), - inheritance: RefCell::new(HashMap::with_hasher(BuildHasherDefault::new())), - spec_tables: RefCell::new(HashMap::with_hasher(BuildHasherDefault::new())), + inheritance: RefCell::new(HashMap::with_hasher(FxBuildHasher)), + spec_tables: RefCell::new(HashMap::with_hasher(FxBuildHasher)), } } } @@ -1047,7 +1055,7 @@ static STARLING_GLOBAL_OPS: JSClassOps = JSClassOps { /// Destructor for Starling's global class — drops the global's /// class's private data (if any) and the class registry. unsafe extern "C" fn finalize_starling_global(gc: *mut GCContext, obj: *mut JSObject) { - let val = crate::object::get_reserved_slot(obj, CLASS_REGISTRY_SLOT); + let val = crate::object::get_native_reserved_slot(obj, CLASS_REGISTRY_SLOT); if !val.is_undefined() { let ptr = val.to_private() as *mut ClassRegistry; if !ptr.is_null() { @@ -1145,8 +1153,8 @@ pub fn get_prototype_object_for<'s, T: 'static>(scope: &'s Scope<'_>) -> Option< /// Information about a class's direct parent for inheritance support. struct InheritanceInfo { parent_tag: usize, - /// Precomputed set of all ancestor type tags (parent, grandparent, ...). - ancestors: HashSet, + /// All ancestor type tags (parent, grandparent, ...). + ancestors: Vec, accessor: unsafe fn(*const c_void) -> *const c_void, accessor_mut: unsafe fn(*mut c_void) -> *mut c_void, } @@ -1176,9 +1184,8 @@ fn register_parent_info_raw( inheritance_registry(|reg| { let mut map = reg.borrow_mut(); - // Build the ancestor set: parent + parent's ancestors (if any). - let mut ancestors = HashSet::new(); - ancestors.insert(parent_tag); + // Build the ancestor list: parent + parent's ancestors (if any). + let mut ancestors = vec![parent_tag]; if let Some(parent_info) = map.get(&parent_tag) { ancestors.extend(&parent_info.ancestors); } @@ -1241,8 +1248,14 @@ fn register_global_parent() { } /// Get the raw private data pointer from slot 0 without type interpretation. -unsafe fn get_raw_private(obj: *mut JSObject) -> Option<*const c_void> { - let val = unsafe { crate::object::get_reserved_slot(obj, PRIVATE_DATA_SLOT) }; +/// +/// # Safety +/// +/// `obj` must be a valid object of a class defined by a [`ClassDef`] or of the +/// [`STARLING_GLOBAL_CLASS`]. +#[inline] +unsafe fn get_raw_private(obj: *mut JSObject) -> Option<*mut c_void> { + let val = unsafe { crate::object::get_native_reserved_slot(obj, PRIVATE_DATA_SLOT) }; if val.is_undefined() { return None; } @@ -1250,7 +1263,7 @@ unsafe fn get_raw_private(obj: *mut JSObject) -> Option<*const c_void> { if ptr.is_null() { return None; } - Some(ptr) + Some(ptr as *mut c_void) } /// Get the per-object borrow flag from slot 1. @@ -1259,11 +1272,10 @@ unsafe fn get_raw_private(obj: *mut JSObject) -> Option<*const c_void> { /// /// # Safety /// -/// `obj` must be a valid JS object created with at least -/// [`MIN_CLASS_RESERVED_SLOTS`] reserved slots. +/// `obj` must be a valid object of a class defined by a [`ClassDef`]. #[cfg(debug_assertions)] unsafe fn get_borrow_flag<'a>(obj: *mut JSObject) -> Option<&'a Cell> { - let val = unsafe { crate::object::get_reserved_slot(obj, BORROW_FLAG_SLOT) }; + let val = unsafe { crate::object::get_native_reserved_slot(obj, BORROW_FLAG_SLOT) }; if val.is_undefined() { return None; } @@ -1274,86 +1286,88 @@ unsafe fn get_borrow_flag<'a>(obj: *mut JSObject) -> Option<&'a Cell Some(&*ptr) } -/// Inheritance-aware immutable private data access. +/// A pointer to `obj`'s private data of type `T`: `obj`'s own data if `obj` is of +/// `T`'s class, or the `T` embedded in the data of a class derived from `T`. /// -/// If the object's concrete type matches T, returns a direct reference. -/// If the concrete type derives from T (via HasParent chain), walks the -/// parent accessor chain to find the T reference within the concrete data. +/// Returns `None` if `obj` is neither, or has no private data (e.g. a prototype +/// object). `step` maps a pointer to a class's data to a pointer to its +/// parent's data. /// /// # Safety /// -/// - `obj` must be a valid JS object with private data stored via [`set_private`]. -/// - The returned reference is only valid as long as the JS object is alive. -pub unsafe fn get_private_or_ancestor<'a, T: ClassDef>(obj: *mut JSObject) -> Option<&'a T> { - // Guard: if the object doesn't have enough reserved slots, it can't be - // one of our class instances (which always have at least PRIVATE_DATA_SLOT). - if crate::object::reserved_slot_count(obj) < MIN_CLASS_RESERVED_SLOTS { - return None; - } - +/// `obj` must be a valid JS object. +#[inline] +unsafe fn private_data_ptr( + obj: *mut JSObject, + step: fn(&InheritanceInfo, *mut c_void) -> *mut c_void, +) -> Option<*mut c_void> { let concrete_tag = get_class_tag(obj); let target_tag = class_tag::(); - if concrete_tag == target_tag { - // Direct match - return get_private::(obj); + return get_raw_private(obj); } + ancestor_data_ptr(obj, concrete_tag, target_tag, step) +} - // Walk the parent chain - let data_ptr = get_raw_private(obj)?; +/// The non-generic part of [`private_data_ptr`] for an object whose class +/// is not the target class. +unsafe fn ancestor_data_ptr( + obj: *mut JSObject, + concrete_tag: usize, + target_tag: usize, + step: fn(&InheritanceInfo, *mut c_void) -> *mut c_void, +) -> Option<*mut c_void> { inheritance_registry(|reg| { let map = reg.borrow(); - let mut current_tag = concrete_tag; - let mut current_ptr = data_ptr; - + // Only classes defined by a `ClassDef` and the Starling global class are + // registered, so this also guarantees the slot layout `get_raw_private` reads. + let mut info = map.get(&concrete_tag)?; + let mut ptr = get_raw_private(obj)?; loop { - let info = map.get(¤t_tag)?; - current_ptr = (info.accessor)(current_ptr); - current_tag = info.parent_tag; - if current_tag == target_tag { - return Some(&*(current_ptr as *const T)); + ptr = step(info, ptr); + if info.parent_tag == target_tag { + return Some(ptr); } + info = map.get(&info.parent_tag)?; } }) } +fn step_to_parent(info: &InheritanceInfo, ptr: *mut c_void) -> *mut c_void { + // SAFETY: `ptr` points to the private data of `info`'s class. + unsafe { (info.accessor)(ptr) as *mut c_void } +} + +fn step_to_parent_mut(info: &InheritanceInfo, ptr: *mut c_void) -> *mut c_void { + // SAFETY: as above. + unsafe { (info.accessor_mut)(ptr) } +} + +/// Inheritance-aware immutable private data access. +/// +/// If the object's concrete type matches T, returns a direct reference. +/// If the concrete type derives from T (via HasParent chain), walks the +/// parent accessor chain to find the T reference within the concrete data. +/// Returns `None` for any other object, and for objects without private data. +/// +/// # Safety +/// +/// - `obj` must be a valid JS object. +/// - The returned reference is only valid as long as the JS object is alive. +pub unsafe fn get_private_or_ancestor<'a, T: ClassDef>(obj: *mut JSObject) -> Option<&'a T> { + private_data_ptr::(obj, step_to_parent).map(|ptr| &*(ptr as *const T)) +} + /// Inheritance-aware mutable private data access. /// /// # Safety /// -/// - `obj` must be a valid JS object with private data stored via [`set_private`]. +/// - `obj` must be a valid JS object. /// - No other references to the data may exist simultaneously. pub unsafe fn get_private_or_ancestor_mut<'a, T: ClassDef>( obj: *mut JSObject, ) -> Option<&'a mut T> { - // Guard: if the object doesn't have enough reserved slots, it can't be - // one of our class instances (which always have at least PRIVATE_DATA_SLOT). - if crate::object::reserved_slot_count(obj) < MIN_CLASS_RESERVED_SLOTS { - return None; - } - - let concrete_tag = get_class_tag(obj); - let target_tag = class_tag::(); - - if concrete_tag == target_tag { - return get_private_mut::(obj); - } - - let data_ptr = get_raw_private(obj)? as *mut c_void; - inheritance_registry(|reg| { - let map = reg.borrow(); - let mut current_tag = concrete_tag; - let mut current_ptr = data_ptr; - - loop { - let info = map.get(¤t_tag)?; - current_ptr = (info.accessor_mut)(current_ptr); - current_tag = info.parent_tag; - if current_tag == target_tag { - return Some(&mut *(current_ptr as *mut T)); - } - } - }) + private_data_ptr::(obj, step_to_parent_mut).map(|ptr| &mut *(ptr as *mut T)) } // ============================================================================ @@ -1541,11 +1555,8 @@ pub trait ClassDef: Sized + Trace + 'static { /// on the JS object. /// /// This runs inside `generic_constructor`, after `set_private`, with access - /// to the JS object and the original constructor arguments. Use this for - /// initialization steps that require the JS object reference (e.g., setting - /// up child objects that back-reference the parent). - /// - /// Use `#[post_init]` in `#[jsmethods]` to define this. + /// to the JS object and the original constructor arguments. The class macros + /// implement it to run a setup-style constructor's body. fn post_init(_scope: &Scope<'_>, _obj: Object<'_>, _args: &CallArgs) -> Result<(), ExnThrown> { Ok(()) } @@ -1555,14 +1566,35 @@ pub trait ClassDef: Sized + Trace + 'static { /// /// WebIDL §3.7.4 requires unforgeable members to live on each instance /// rather than the prototype, with a getter shared across instances. The - /// proc macro generates an override for interfaces that declare - /// `#[getter(unforgeable)]`; it installs its own attributes and then chains - /// to the parent's via `extends`. Runs inside `generic_constructor` after - /// `post_init`. The default is a no-op. - fn install_unforgeable(_scope: &Scope<'_>, _obj: Object<'_>) -> Result<(), ExnThrown> { + /// accessors are copied from a per-global template object created by + /// [`ClassDef::define_unforgeable`] on first use. `obj` must be an object of + /// this class without any own properties, so `generic_constructor` and + /// [`try_create_instance_with`] call this right after allocating it. Does + /// nothing for classes without unforgeable accessors. + fn install_unforgeable(scope: &Scope<'_>, obj: Object<'_>) -> Result<(), ExnThrown> { + if Self::has_unforgeable() { + install_unforgeable_from_template::(scope, obj) + } else { + Ok(()) + } + } + + /// Define this interface's `[LegacyUnforgeable]` accessors, including + /// inherited ones, on `obj`. + /// + /// The proc macro generates an override that chains to the parent's via + /// `extends` and then defines the interface's own `#[getter(unforgeable)]` + /// accessors. The default is a no-op. + fn define_unforgeable(_scope: &Scope<'_>, _obj: Object<'_>) -> Result<(), ExnThrown> { Ok(()) } + /// Whether this interface or one of its ancestors declares + /// `[LegacyUnforgeable]` accessors. + fn has_unforgeable() -> bool { + false + } + /// Debug assertion that all bare `Heap` fields have been initialized. /// /// Called automatically after construction + post_init completes. @@ -1819,6 +1851,10 @@ pub unsafe fn register_class<'s, T: ClassDef>( define_to_string_tag(scope, proto.handle(), T::TO_STRING_TAG); } + if T::HAS_ERROR_DATA { + define_error_stack_accessor::(scope, proto); + } + // Install constants on the constructor (and optionally the prototype). if !constants.is_empty() { let attrs = (crate::class_spec::JSPROP_READONLY @@ -1890,6 +1926,11 @@ unsafe extern "C" fn generic_constructor( None => return false, }; + // Install `[LegacyUnforgeable]` own accessors (e.g. Event.isTrusted). + if T::install_unforgeable(&scope, obj).is_err() { + return false; + } + // Call the Rust constructor match T::constructor(&scope, &args) { Ok(instance) => { @@ -1902,11 +1943,6 @@ unsafe extern "C" fn generic_constructor( return false; } - // Install `[LegacyUnforgeable]` own accessors (e.g. Event.isTrusted). - if T::install_unforgeable(&scope, obj).is_err() { - return false; - } - #[cfg(debug_assertions)] if let Some(data) = get_private::(obj.handle().get()) { data.debug_assert_fully_initialized(); @@ -2126,20 +2162,7 @@ pub unsafe fn get_stack_arg<'s, T: StackType<'s>>( )); } let val = *args.get(index); - if !val.is_object() { - let msg = CString::new(format!( - "argument {} is not an instance of {}", - index, - T::Inner::NAME, - )) - .unwrap_or_else(|_| c"argument is not an object".into()); - return Err(crate::error::throw_type_error(scope, &msg)); - } - let obj = val.to_object(); - let concrete_tag = crate::object::get_object_class(obj) as usize; - let target_tag = class_tag::(); - // Reject builtins' prototype objects, which match the class tag but carry no private data. - if !is_derived_from_type(concrete_tag, target_tag) || get_raw_private(obj).is_none() { + if !val.is_object() || !::is_instance(val.to_object()) { let msg = CString::new(format!( "argument {} is not an instance of {}", index, @@ -2148,6 +2171,7 @@ pub unsafe fn get_stack_arg<'s, T: StackType<'s>>( .unwrap_or_else(|_| c"argument is not the expected class".into()); return Err(crate::error::throw_type_error(scope, &msg)); } + let obj = val.to_object(); let nn = NonNull::new(obj).unwrap(); Ok(unsafe { T::from_handle_unchecked(scope.root_object(nn)) }) } @@ -2167,6 +2191,7 @@ pub unsafe fn get_stack_arg<'s, T: StackType<'s>>( /// # Safety /// /// - `args` must be from a valid `JSNative` call. +#[inline] unsafe fn resolve_webidl_this( scope: &Scope<'_>, args: &CallArgs, @@ -2231,6 +2256,7 @@ pub unsafe fn get_this_data_mut<'a, T: ClassDef>( /// /// - The CallArgs must be from a valid JSNative call. #[doc(hidden)] +#[inline] pub unsafe fn get_this<'s, T: StackType<'s>>( scope: &'s Scope<'s>, args: &CallArgs, @@ -2249,7 +2275,7 @@ where } // The prototype object shares the same JSClass as instances but has no // private data (set_private is only called during construction). Reject - // it here so callers can safely use data()/data_mut() via unwrap_unchecked. + // it here so callers can use data()/data_mut(), which panic on missing private data. if get_private::(obj).is_none() { return Err(crate::error::throw_type_error( scope, @@ -2272,9 +2298,10 @@ pub unsafe fn set_return<'s, T: ToJSVal<'s>>( args: &CallArgs, value: &T, ) -> bool { - match value.to_jsval(scope) { + // The converted value is stored into the rooted `rval` slot before anything else can run. + match value.to_jsval_raw(scope) { Ok(val) => { - args.rval().set(val.get()); + args.rval().set(val); true } Err(e) => { @@ -2348,10 +2375,14 @@ autoref_reg!( /// interface declares `#[getter(unforgeable)]`. #[doc(hidden)] pub trait __UnforgeableRegistrar { + fn has(&self) -> bool; fn install(&self, scope: &Scope<'_>, obj: Object<'_>) -> Result<(), ExnThrown>; } impl __UnforgeableRegistrar for &__UnforgeableReg { + fn has(&self) -> bool { + false + } fn install(&self, _scope: &Scope<'_>, _obj: Object<'_>) -> Result<(), ExnThrown> { Ok(()) } @@ -2449,8 +2480,8 @@ impl __ConstantRegistrar for &__ConstantReg { } /// Trait for post-construction initialization via autoref specialization. -/// The blanket impl on `&__PostInitReg` is a no-op; `#[jsmethods]` provides -/// the real impl on `__PostInitReg` directly when `#[post_init]` is used. +/// The blanket impl on `&__PostInitReg` is a no-op. The class macros provide +/// the real impl on `__PostInitReg` for a setup-style constructor. #[doc(hidden)] pub trait __PostInitRegistrar { fn post_init( @@ -2472,12 +2503,23 @@ impl __PostInitRegistrar for &__PostInitReg { } } +/// The prototype registered for `T` on the current global. Throws a `TypeError` if `T` is not +/// registered. +fn registered_prototype<'s, T: ClassDef>(scope: &'s Scope<'_>) -> Result, ExnThrown> { + get_prototype_object_for::(scope).ok_or_else(|| { + let message = CString::new(format!("{} is not registered on this global", T::NAME)) + .expect("class names contain no NUL"); + crate::error::throw_type_error(scope, &message) + }) +} + /// Create a JS object backed by a Rust value constructed by `init`. /// /// The closure receives the newly allocated (but empty) JS object and /// returns the Rust data to store in it. Because the JS object is /// allocated *before* `init` runs, any `Heap` fields created inside /// the closure are safe from GC hazards: the object is already rooted. +/// The object's `[LegacyUnforgeable]` accessors are installed before `init` runs. pub fn create_instance_with<'s, T: ClassDef>( scope: &'s Scope<'_>, init: impl FnOnce(Object<'s>) -> T, @@ -2492,15 +2534,10 @@ pub fn try_create_instance_with<'s, T: ClassDef>( scope: &'s Scope<'_>, init: impl FnOnce(Object<'s>) -> Result, ) -> Result, ExnThrown> { - let global = scope.global(); - let proto = match get_prototype::(global) { - // SAFETY: builtins' prototypes are always valid objects. - Some(p) => unsafe { Object::from_raw(scope, p) }.unwrap(), - None => return Err(ExnThrown), // TODO: Actually throw an error here. - }; - + let proto = registered_prototype::(scope)?; let class = T::class(); let obj = Object::new_with_proto(scope, class, proto)?; + T::install_unforgeable(scope, obj)?; let data = init(obj)?; unsafe { set_private(obj.as_raw(), data); @@ -2545,6 +2582,37 @@ pub fn get_or_init_shared_object<'s>( Ok(obj) } +/// Return, and create on the first call in a given global, an interned property key for `name`. +/// +/// `name` must not be an array index, whose property key is an integer rather than a string. +pub fn get_or_init_property_id<'s>( + scope: &'s Scope<'_>, + name: &'static CStr, +) -> Result, ExnThrown> { + let key = ptr::from_ref(name).addr(); + // SAFETY: the registry belongs to the current global, which is alive. + let cached = unsafe { + get_class_registry(scope.global().as_raw()) + .and_then(|r| r.property_atoms.get(&key).map(|h| h.get())) + }; + let atom = match cached { + Some(atom) => atom, + None => { + let name = + crate::string::Str::from_str(scope, name.to_str().expect("names are UTF-8"))?; + let id = crate::id::string_to_id(scope, name.handle())?; + assert!(id.is_string(), "property name `{name:?}` is an array index"); + // A string property key's bits are the interned string's address. + let atom = id.asBits_ as *mut crate::native::JSString; + // SAFETY: as above. + let registry = unsafe { get_or_init_class_registry(scope.global().as_raw()) }; + registry.property_atoms.insert(key, MozHeap::boxed(atom)); + atom + } + }; + Ok(scope.root_id(mozjs_sys::jsid::StringId(atom))) +} + /// Get a per-global shared function, keyed by `key` (typically the native /// callback's function pointer cast to `usize`), creating it via `init` and /// caching it on first use. @@ -2602,7 +2670,7 @@ pub fn next_global_counter(scope: &Scope<'_>, key: usize) -> u64 { /// non-configurable property on `obj`, using a getter function shared across /// all instances (cached per global, keyed by the native pointer). /// -/// Used by the proc-macro-generated `ClassDef::install_unforgeable`. +/// Used by the proc-macro-generated `ClassDef::define_unforgeable`. pub fn define_unforgeable_accessor( scope: &Scope<'_>, obj: Object<'_>, @@ -2610,39 +2678,166 @@ pub fn define_unforgeable_accessor( getter: crate::native::JSNative, ) -> Result<(), ExnThrown> { let key = getter.map(|f| f as usize).unwrap_or(0); - let name_str = name.to_str().map_err(|_| ExnThrown)?; + let name_str = name.to_str().expect("accessor names are ASCII"); // Reuse the cached getter if present; otherwise create it once and store it. let getter_fn = get_or_init_shared_function(scope, key, |scope| { // WebIDL names accessor getter functions "get ". let getter_name = - std::ffi::CString::new(format!("get {name_str}")).map_err(|_| ExnThrown)?; + CString::new(format!("get {name_str}")).expect("accessor names contain no NUL"); crate::Function::new(scope, getter, 0, 0, &getter_name) })?; - rooted!(in(unsafe { scope.raw_cx_no_gc() }) let desc = crate::native::PropertyDescriptor { - _bitfield_align_1: [0; 0], - _bitfield_1: crate::native::PropertyDescriptor::new_bitfield_1( - true, // hasConfigurable - false, // configurable (unforgeable) - true, // hasEnumerable - true, // enumerable - false, // hasWritable - false, // writable - false, // hasValue - true, // hasGetter - false, // hasSetter - false, // resolving - ), - getter_: getter_fn.as_raw(), - setter_: ptr::null_mut(), - value_: mozjs::jsval::UndefinedValue(), - }); + // An enumerable, non-configurable accessor with no setter. + let attrs = (crate::class_spec::JSPROP_ENUMERATE | crate::class_spec::JSPROP_PERMANENT) + as std::ffi::c_uint; + let ok = unsafe { + wrappers2::JS_DefineProperty2( + scope.cx_mut(), + obj.handle(), + name.as_ptr(), + getter_fn.handle(), + crate::native::HandleObject::null(), + attrs, + ) + }; + ExnThrown::check(ok) +} - let js_str = crate::string::Str::from_str(scope, name_str)?; - let id = crate::id::string_to_id(scope, js_str.handle())?; - rooted!(in(unsafe { scope.raw_cx_no_gc() }) let id_rooted = id); - obj.define_property_by_id(scope, id_rooted.handle(), desc.handle()) +/// Copy `T`'s `[LegacyUnforgeable]` accessors onto `obj` from a template object cached per +/// global under `T`'s class tag. +/// +/// The template is an object of `T`'s class and prototype without private data, with the +/// accessors defined by [`ClassDef::define_unforgeable`]. Copying its properties reuses its shape, +/// which is much cheaper than defining each accessor on every instance. `obj` must be an object of +/// `T`'s class without any own properties. +fn install_unforgeable_from_template( + scope: &Scope<'_>, + obj: Object<'_>, +) -> Result<(), ExnThrown> { + debug_assert!(std::ptr::eq( + unsafe { crate::object::get_object_class(obj.as_raw()) }, + T::class() + )); + let template = get_or_init_shared_object(scope, class_tag::(), |scope| { + let proto = registered_prototype::(scope)?; + let template = Object::new_with_proto(scope, T::class(), proto)?; + T::define_unforgeable(scope, template)?; + Ok(template) + })?; + // SAFETY: both objects are native objects of `T`'s class in the current compartment, and + // `obj` has no own properties. + let ok = unsafe { + wrappers2::JS_InitializePropertiesFromCompatibleNativeObject( + scope.cx_mut(), + obj.handle(), + template.handle(), + ) + }; + ExnThrown::check(ok) +} + +// --------------------------------------------------------------------------- +// Error stacks +// --------------------------------------------------------------------------- + +/// Define `stack` on the prototype of an error-data class `T` as an accessor like +/// `Error.prototype.stack`: non-enumerable, configurable, with a getter that formats the stack an +/// instance captured and a setter that defines an own data property. +/// +/// # Panics +/// +/// Panics if a function allocation or the property definition fails. +fn define_error_stack_accessor(scope: &Scope<'_>, proto: Object<'_>) { + let getter = crate::Function::new(scope, Some(error_stack_getter::), 0, 0, c"get stack") + .expect("failed to create the stack getter"); + let setter = crate::Function::new(scope, Some(error_stack_setter), 1, 0, c"set stack") + .expect("failed to create the stack setter"); + let ok = unsafe { + wrappers2::JS_DefineProperty2( + scope.cx_mut(), + proto.handle(), + c"stack".as_ptr(), + getter.handle(), + setter.handle(), + 0, + ) + }; + assert!(ok, "failed to define the stack accessor"); +} + +/// The `stack` getter of error-data class `T`. Returns the stack the instance captured, formatted +/// on the first read, or `undefined` for any other object. Throws a `TypeError` if `this` is not +/// an object. +unsafe extern "C" fn error_stack_getter( + cx: *mut RawJSContext, + argc: u32, + vp: *mut Value, +) -> bool { + // SAFETY: SpiderMonkey guarantees cx is valid and a realm is entered during a native call. + let scope = RootScope::from_current_realm(cx); + let args = CallArgs::from_vp(vp, argc); + let this = args.thisv(); + if !this.is_object() { + crate::error::throw_type_error(&scope, c"stack getter called on a non-object"); + return false; + } + let obj = this.to_object(); + args.rval().set(value::undefined()); + if !is_derived_from_type(get_class_tag(obj), class_tag::()) { + return true; + } + // SAFETY: `obj` is an instance of `T` or a class derived from it, so it has the slot. + let stored = unsafe { crate::object::get_native_reserved_slot(obj, ERROR_STACK_SLOT) }; + if !stored.is_object() { + args.rval().set(stored); + return true; + } + let raw = scope.cx_mut().raw_cx(); + rooted!(in(raw) let frame = stored.to_object()); + rooted!(in(raw) let mut formatted = ptr::null_mut::()); + let formatted_ok = unsafe { + crate::stack::build_stack_string( + &scope, + std::ptr::null_mut(), + frame.handle(), + formatted.handle_mut(), + 0, + mozjs::jsapi::StackFormat::Default, + ) + }; + if formatted_ok.is_err() { + return false; + } + let formatted = unsafe { mozjs::jsval::StringValue(&*formatted.get()) }; + // SAFETY: as above. + unsafe { crate::object::set_reserved_slot(obj, ERROR_STACK_SLOT, &formatted) }; + args.rval().set(formatted); + true +} + +/// The `stack` setter of error-data classes: defines `stack` as an own, enumerable, writable and +/// configurable data property of `this`, as `Error.prototype.stack`'s setter does. Throws a +/// `TypeError` if `this` is not an object. +unsafe extern "C" fn error_stack_setter(cx: *mut RawJSContext, argc: u32, vp: *mut Value) -> bool { + // SAFETY: SpiderMonkey guarantees cx is valid and a realm is entered during a native call. + let scope = RootScope::from_current_realm(cx); + let args = CallArgs::from_vp(vp, argc); + let this = args.thisv(); + if !this.is_object() { + crate::error::throw_type_error(&scope, c"stack setter called on a non-object"); + return false; + } + let obj = Object::from_raw(&scope, this.to_object()).expect("objects are non-null"); + let attrs = crate::class_spec::JSPROP_ENUMERATE as std::ffi::c_uint; + if obj + .define_property(&scope, c"stack", *args.get(0), attrs) + .is_err() + { + return false; + } + args.rval().set(value::undefined()); + true } // --------------------------------------------------------------------------- @@ -2668,8 +2863,7 @@ pub fn define_to_string_tag( let tag_key = crate::symbol::get_well_known_key(scope, SymbolCode::toStringTag); let tag_str = crate::string::Str::from_str(scope, tag_value) .expect("failed to create toStringTag string"); - // SAFETY: tag_str is a live JSString* from `from_str` above, valid in the current scope. - let str_val = unsafe { value::from_string_raw(tag_str.as_raw()) }; + let str_val = tag_str.as_value(); rooted!(in(unsafe { scope.raw_cx_no_gc() }) let desc = crate::native::PropertyDescriptor { _bitfield_align_1: [0; 0], @@ -2690,10 +2884,9 @@ pub fn define_to_string_tag( value_: str_val, }); - rooted!(in(unsafe { scope.raw_cx_no_gc() }) let tag_id = tag_key); let proto_obj = Object::from_handle(proto).expect("prototype is null"); proto_obj - .define_property_by_id(scope, tag_id.handle(), desc.handle()) + .define_property_by_id(scope, scope.root_id(tag_key), desc.handle()) .expect("failed to define Symbol.toStringTag"); } @@ -2704,11 +2897,11 @@ pub fn define_to_string_tag( /// Used for symbol-keyed method aliases, such as an `async iterable<>` /// declaration installing `@@asyncIterator` as an alias of the `values` /// method (WebIDL §3.7.10.2). -pub fn define_well_known_symbol_property( - scope: &Scope<'_>, +pub fn define_well_known_symbol_property<'s>( + scope: &'s Scope<'_>, obj: Object<'_>, which: SymbolCode, - value: crate::prelude::HandleValue<'_>, + value: impl ToJSVal<'s>, ) -> Result<(), ExnThrown> { let key = crate::symbol::get_well_known_key(scope, which); @@ -2728,11 +2921,10 @@ pub fn define_well_known_symbol_property( ), getter_: ptr::null_mut(), setter_: ptr::null_mut(), - value_: value.get(), + value_: value.to_jsval_raw_throwing(scope)?, }); - rooted!(in(unsafe { scope.raw_cx_no_gc() }) let id = key); - obj.define_property_by_id(scope, id.handle(), desc.handle()) + obj.define_property_by_id(scope, scope.root_id(key), desc.handle()) } /// Adds an alias to the property given by `property` under the symbol `symbol` to the @@ -2742,5 +2934,6 @@ pub fn add_symbol_alias(scope: &Scope, property: &CStr, symbol: Sym let values = proto .get_property(scope, property) .expect("property not found"); - let _ = define_well_known_symbol_property(scope, proto, symbol, values); + define_well_known_symbol_property(scope, proto, symbol, values) + .expect("defining the symbol alias"); } diff --git a/crates/js/src/collections/map.rs b/crates/js/src/collections/map.rs index 4424a7f7..dac76631 100644 --- a/crates/js/src/collections/map.rs +++ b/crates/js/src/collections/map.rs @@ -145,3 +145,5 @@ impl<'s> Stack<'s, Map> { } crate::gc::handle::deref_to_object!(Map); + +crate::gc::handle::from_jsval_via_cast!(Map, c"Value isn't a Map"); diff --git a/crates/js/src/collections/set.rs b/crates/js/src/collections/set.rs index a67fcebe..629053ee 100644 --- a/crates/js/src/collections/set.rs +++ b/crates/js/src/collections/set.rs @@ -122,3 +122,5 @@ impl<'s> Stack<'s, Set> { } crate::gc::handle::deref_to_object!(Set); + +crate::gc::handle::from_jsval_via_cast!(Set, c"Value isn't a Set"); diff --git a/crates/js/src/collections/weak_map.rs b/crates/js/src/collections/weak_map.rs index dff33c74..069297ec 100644 --- a/crates/js/src/collections/weak_map.rs +++ b/crates/js/src/collections/weak_map.rs @@ -83,3 +83,5 @@ impl<'s> Stack<'s, WeakMap> { } crate::gc::handle::deref_to_object!(WeakMap); + +crate::gc::handle::from_jsval_via_cast!(WeakMap, c"Value isn't a WeakMap"); diff --git a/crates/js/src/context.rs b/crates/js/src/context.rs index b6abdeb9..b10a5a92 100644 --- a/crates/js/src/context.rs +++ b/crates/js/src/context.rs @@ -42,6 +42,19 @@ pub unsafe fn options_mut<'a>(scope: &'a Scope<'_>) -> &'a mut ContextOptions { unsafe { &mut *wrappers2::ContextOptionsRef(scope.cx()) } } +/// Enable recording the JavaScript stack each promise and async call was created on. SpiderMonkey +/// shows the recorded stacks as async parent frames in `Error.stack` and in debuggers. +/// +/// With `debuggee_only`, stacks are recorded only in realms a `Debugger` observes. Otherwise they +/// are recorded in every realm. +pub fn set_async_stack_capture(cx: &mut mozjs::context::JSContext, debuggee_only: bool) { + // SAFETY: ContextOptionsRef returns a non-null pointer to options that + // live as long as the JSContext, and `cx` is borrowed exclusively. + let options = unsafe { &mut *wrappers2::ContextOptionsRef(cx) }; + options.set_asyncStack_(true); + options.set_asyncStackCaptureDebuggeeOnly_(debuggee_only); +} + /// Get the private data pointer associated with the context. pub fn get_private(scope: &Scope<'_>) -> *mut std::os::raw::c_void { unsafe { wrappers2::JS_GetContextPrivate(scope.cx()) } diff --git a/crates/js/src/conversion.rs b/crates/js/src/conversion.rs index 2b3615cb..112859e7 100644 --- a/crates/js/src/conversion.rs +++ b/crates/js/src/conversion.rs @@ -29,15 +29,12 @@ #![deny(missing_docs)] -use mozjs::jsapi::AssertSameCompartment; -use mozjs::jsapi::JS_DefineElement; use mozjs::jsapi::JS; +use mozjs::jsapi::JSPROP_ENUMERATE; use mozjs::jsapi::{ForOfIterator, ForOfIterator_NonIterableBehavior}; -use mozjs::jsapi::{JSObject, JSString, PropertyDescriptor, RootedObject, RootedValue}; -use mozjs::jsapi::{JS_NewStringCopyUTF8N, JSPROP_ENUMERATE}; +use mozjs::jsapi::{JSString, PropertyDescriptor, RootedObject, RootedValue}; use mozjs::jsval::{ - BooleanValue, DoubleValue, Int32Value, JSVal, ObjectOrNullValue, StringValue, SymbolValue, - UInt32Value, UndefinedValue, + BooleanValue, DoubleValue, Int32Value, JSVal, StringValue, UInt32Value, UndefinedValue, }; use mozjs::rooted; use mozjs::rust::{ @@ -249,6 +246,28 @@ pub enum ConversionBehavior { Clamp, } +/// A WebIDL `[EnforceRange]` integer. +/// +/// Converting a value that is not finite, or whose truncation lies outside `T`'s +/// WebIDL range, throws a `TypeError` instead of wrapping. +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub struct EnforceRange(pub T); + +impl<'s, 'v, T> FromJSVal<'s, 'v> for EnforceRange +where + T: FromJSVal<'s, 'v, Config = ConversionBehavior>, +{ + type Config = (); + + fn from_jsval( + scope: &'s Scope<'s>, + val: HandleValue<'v>, + _: (), + ) -> Result { + T::from_jsval(scope, val, ConversionBehavior::EnforceRange).map(EnforceRange) + } +} + /// Try to cast the number to a smaller type, but /// if it doesn't fit, it will return an error. // https://searchfox.org/mozilla-esr128/rev/1aa97f9d67f7a7231e62af283eaa02a6b31380e1/dom/bindings/PrimitiveConversions.h#166 @@ -725,8 +744,49 @@ impl<'s> ToJSVal<'s> for Finite { /// Converts a `JSString` into a `String`, regardless of used encoding. pub fn jsstr_to_string(scope: &Scope<'_>, jsstr: NonNull) -> String { - // SAFETY: the scope provides a valid context, and `jsstr` is non-null. - unsafe { mozjs::conversions::jsstr_to_string(scope.cx_mut(), jsstr) } + let jsstr = jsstr.as_ptr(); + let mut length = 0; + // SAFETY: `jsstr` is a live string. The accessors flatten a rope and return its characters, + // which stay in place until the next call into SpiderMonkey, and only `encoding_rs` reads them + // before then. Each conversion writes valid UTF-8 into the prefix it reports. + unsafe { + if mozjs::jsapi::JS_DeprecatedStringHasLatin1Chars(jsstr) { + let chars = mozjs::rust::wrappers2::JS_GetLatin1StringCharsAndLength( + scope.cx(), + jsstr, + &mut length, + ); + assert!(!chars.is_null()); + let chars = std::slice::from_raw_parts(chars, length); + // An ASCII prefix is copied as is, and each character after it takes at most two + // bytes, the size `convert_latin1_to_utf8` requires. + let ascii = encoding_rs::Encoding::ascii_valid_up_to(chars); + if ascii == length { + return String::from_utf8_unchecked(chars.to_vec()); + } + let mut utf8 = Vec::with_capacity(ascii + (length - ascii) * 2); + utf8.extend_from_slice(&chars[..ascii]); + utf8.resize(utf8.capacity(), 0); + let written = + encoding_rs::mem::convert_latin1_to_utf8(&chars[ascii..], &mut utf8[ascii..]); + utf8.truncate(ascii + written); + String::from_utf8_unchecked(utf8) + } else { + let chars = mozjs::rust::wrappers2::JS_GetTwoByteStringCharsAndLength( + scope.cx(), + jsstr, + &mut length, + ); + assert!(!chars.is_null()); + let chars = std::slice::from_raw_parts(chars, length); + // `convert_utf16_to_utf8` requires three bytes per code unit, and replaces unpaired + // surrogates with U+FFFD. + let mut utf8 = vec![0; length * 3]; + let written = encoding_rs::mem::convert_utf16_to_utf8(chars, &mut utf8); + utf8.truncate(written); + String::from_utf8_unchecked(utf8) + } + } } // https://heycam.github.io/webidl/#es-USVString @@ -734,11 +794,7 @@ impl<'s> ToJSVal<'s> for str { #[inline] #[deny(unsafe_op_in_unsafe_fn)] fn to_jsval_raw(&self, scope: &'s Scope<'_>) -> Result { - // Spidermonkey will automatically only copy latin1 - // or similar if the given encoding can be small enough. - // So there is no need to distinguish between ascii only or similar. - let s = Utf8Chars::from(self); - let jsstr = unsafe { JS_NewStringCopyUTF8N(scope.cx_mut().raw_cx(), &*s as *const _) }; + let jsstr = crate::string::new_js_string(scope, self); if jsstr.is_null() { return Err(ConversionError::ExnPending); } @@ -847,17 +903,7 @@ impl<'s, T: ToJSVal<'s>> ToJSVal<'s> for [T] { // which we don't currently have an API for. let val = obj.to_jsval(scope)?; - if !unsafe { - JS_DefineElement( - scope.cx_mut().raw_cx(), - array.handle().into(), - index as u32, - val.into(), - JSPROP_ENUMERATE as u32, - ) - } { - return Err(ConversionError::ExnPending); - } + array.define_element(scope, index as u32, val, JSPROP_ENUMERATE as u32)?; } Ok(array.as_value()) @@ -890,11 +936,8 @@ impl<'s, T: ToJSVal<'s>> ToJSVal<'s> for Vec { /// Returns `Ok(false)` for non-objects. Returns `Err(ExnPending)` if reading /// the `@@iterator` property throws. pub fn is_iterable_value(scope: &Scope<'_>, val: HandleValue<'_>) -> Result { - if !val.is_object() { + let Ok(obj) = crate::Object::from_value(scope, val) else { return Ok(false); - } - let obj = unsafe { - crate::Object::from_raw(scope, val.to_object()).ok_or(ConversionError::ExnPending)? }; let iter_key = crate::symbol::get_well_known_key(scope, crate::native::SymbolCode::iterator); let iter_id = scope.root_id(iter_key); @@ -1136,9 +1179,7 @@ where )); } - let obj = unsafe { - crate::Object::from_raw(scope, val.to_object()).ok_or(ConversionError::ExnPending)? - }; + let obj = crate::Object::from_value(scope, val)?; // Step 2: Let result be a new empty instance of record. let mut result = IndexMap::new(); @@ -1310,10 +1351,7 @@ impl FromJSVal<'_, '_> for AsyncSequence { c"Value is not an object (expected async iterable)".into(), )); } - - let obj = unsafe { - crate::Object::from_raw(scope, val.to_object()).ok_or(ConversionError::ExnPending)? - }; + let obj = crate::Object::from_value(scope, val)?; // GetMethod(V, P): Get the property — no [[HasProperty]] pre-check, // and a Get error propagates — treat null/undefined as absent, and @@ -1323,14 +1361,7 @@ impl FromJSVal<'_, '_> for AsyncSequence { let key = crate::symbol::get_well_known_key(scope, code); let id = scope.root_id(key); let method_val = obj.get_property_by_id(scope, id)?; - if method_val.is_null_or_undefined() { - return Ok(None); - } - if !method_val.is_object() || !unsafe { JS::IsCallable(method_val.to_object()) } { - throw_type_error(scope, c"iterator method is not callable"); - return Err(ConversionError::ExnPending); - } - Ok(Some(method_val.get())) + Ok(crate::iteration::ensure_is_method(scope, method_val)?.map(|m| m.get())) }; // Let method be ? GetMethod(V, %Symbol.asyncIterator%). @@ -1357,72 +1388,4 @@ impl FromJSVal<'_, '_> for AsyncSequence { } } -// https://heycam.github.io/webidl/#es-object -impl<'s> ToJSVal<'s> for *mut JSObject { - #[inline] - fn to_jsval_raw(&self, _scope: &'s Scope<'_>) -> Result { - Ok(ObjectOrNullValue(*self)) - } -} - -// https://heycam.github.io/webidl/#es-object -impl<'s> ToJSVal<'s> for ptr::NonNull { - #[inline] - fn to_jsval_raw(&self, _scope: &'s Scope<'_>) -> Result { - Ok(ObjectOrNullValue(self.as_ptr())) - } -} - -// https://heycam.github.io/webidl/#es-object -impl<'s> ToJSVal<'s> for MozHeap<*mut JSObject> { - #[inline] - fn to_jsval_raw(&self, _scope: &'s Scope<'_>) -> Result { - Ok(ObjectOrNullValue(self.get())) - } -} - -// https://heycam.github.io/webidl/#es-object -impl FromJSVal<'_, '_> for *mut JSObject { - type Config = (); - #[inline] - fn from_jsval( - scope: &Scope<'_>, - val: HandleValue, - _option: (), - ) -> Result<*mut JSObject, ConversionError> { - if !val.is_object() { - throw_type_error(scope, c"value is not an object"); - return Err(ConversionError::ExnPending); - } - - unsafe { AssertSameCompartment(scope.cx_mut().raw_cx(), val.to_object()) }; - - Ok(val.to_object()) - } -} - -impl<'s> ToJSVal<'s> for *mut JS::Symbol { - #[inline] - fn to_jsval_raw(&self, _scope: &'s Scope<'_>) -> Result { - Ok(SymbolValue(unsafe { &**self })) - } -} - -impl FromJSVal<'_, '_> for *mut JS::Symbol { - type Config = (); - #[inline] - fn from_jsval( - scope: &Scope<'_>, - val: HandleValue, - _option: (), - ) -> Result<*mut JS::Symbol, ConversionError> { - if !val.is_symbol() { - throw_type_error(scope, c"value is not a symbol"); - return Err(ConversionError::ExnPending); - } - - Ok(val.to_symbol()) - } -} - pub use mozjs::conversions::Utf8Chars; diff --git a/crates/js/src/date.rs b/crates/js/src/date.rs index 82716171..d29ca015 100644 --- a/crates/js/src/date.rs +++ b/crates/js/src/date.rs @@ -92,3 +92,5 @@ impl<'s> Stack<'s, Date> { } crate::gc::handle::deref_to_object!(Date); + +crate::gc::handle::from_jsval_via_cast!(Date, c"Value isn't a Date"); diff --git a/crates/js/src/error.rs b/crates/js/src/error.rs index 610a7b35..8e773baa 100644 --- a/crates/js/src/error.rs +++ b/crates/js/src/error.rs @@ -23,14 +23,11 @@ use std::fmt; use std::ptr; use crate::exception::is_pending; -use crate::function::EmptyArgs; use crate::gc::scope::Scope; use crate::Object; use mozjs::jsapi::{ - JSErrorFormatString, JSExnType, JSString, JS_ClearPendingException, JS_GetPendingException, - JS_IsExceptionPending, JS_ReportErrorNumberUTF8, StackFormat, + JSErrorFormatString, JSExnType, JSString, JS_ReportErrorNumberUTF8, StackFormat, }; -use mozjs::jsval::UndefinedValue; use mozjs::rooted; use mozjs::rust::wrappers2; #[cfg(debug_assertions)] @@ -94,21 +91,15 @@ impl ExnThrown { // guaranteed by the `Scope` parameter. unsafe { let raw = scope.cx_mut().raw_cx(); - if !JS_IsExceptionPending(raw) { + let Some(exc_val) = crate::exception::take_pending(scope) else { return CapturedError::default(); - } - - let mut exc_val = scope.root_value_mut(UndefinedValue()); - if !JS_GetPendingException(raw, exc_val.reborrow().into()) { - return CapturedError::default(); - } - JS_ClearPendingException(raw); + }; // For object exceptions, extract the attached stack and, for // `Error` objects, the engine's error report. let mut stack = None; if exc_val.is_object() { - let exc_obj = Object::from_value(scope, *exc_val).unwrap(); + let exc_obj = Object::from_value(scope, exc_val).unwrap(); // Try to extract the stack trace from the exception object. let maybe_stack = @@ -158,14 +149,14 @@ impl ExnThrown { // No error report (a primitive throw like `throw "boom"`, or a non-`Error` object without engine error data): stringify the value itself. // Any attached stack extracted above is kept. - let message = match crate::string::Str::from_value(scope, exc_val.handle()) { + let message = match crate::string::Str::from_value(scope, exc_val) { Ok(s) => s .to_utf8(scope) .unwrap_or_else(|_| "(non-Error exception)".into()), Err(ExnThrown) => { // ToString itself threw (a symbol, or a throwing // `toString`); discard that secondary exception. - JS_ClearPendingException(raw); + crate::exception::clear(scope); "(non-Error exception)".into() } }; @@ -465,39 +456,31 @@ pub fn throw_error(scope: &Scope<'_>, msg: &str) -> ExnThrown { // capture_stack_from_error // --------------------------------------------------------------------------- -/// Capture the current call stack from a temporary `Error` object and set it -/// as the `stack` property on `obj`. -/// -/// This creates `new Error()` to let SpiderMonkey capture the stack trace, -/// reads the resulting `stack` property, and copies it onto `obj`. +/// Capture the current call stack as a `SavedFrame` and store it in `obj`'s +/// [`ERROR_STACK_SLOT`](crate::class::ERROR_STACK_SLOT), for the `stack` accessor on the class's +/// prototype to format when it is read. /// /// Used by error-like classes (e.g. DOMException, or any class with -/// `js_proto = "Error"`) that need `[[ErrorData]]` behavior. -/// -/// Silently returns without setting `stack` if any step fails. +/// `js_proto = "Error"`) that need `[[ErrorData]]` behavior. `obj` must be an instance of such a +/// class. /// +/// Leaves the slot empty if no script is running, or if the capture fails. A failed capture's +/// exception is cleared. pub fn capture_stack_from_error(scope: &Scope<'_>, obj: &Object<'_>) { - use crate::class_spec::JSProtoKey; - - // Create `new Error()` to capture the current stack. - let error_ctor = match crate::class::get_class_object(scope, JSProtoKey::JSProto_Error) { - Ok(ctor) => ctor, - Err(_) => return, - }; - - let ctor_val = scope.root_value(unsafe { crate::value::from_object(error_ctor.get()) }); - - let error_obj = match crate::Function::construct(scope, ctor_val, EmptyArgs) { - Ok(obj) => obj, - Err(_) => return, - }; - - // Read the `stack` property from the Error object. - let stack_val = match error_obj.get_property(scope, c"stack") { - Ok(v) => v, - Err(_) => return, - }; - - // Set the stack as a non-enumerable own property on the target object. - let _ = obj.define_property(scope, c"stack", stack_val, 0); + // SpiderMonkey's `Error` keeps at most this many frames (`MAX_REPORTED_STACK_DEPTH`). + const MAX_FRAMES: u32 = 128; + match crate::stack::capture_saved_frame(scope, MAX_FRAMES) { + Ok(Some(frame)) => { + // SAFETY: `obj` is an instance of an error-data class, which has the slot. + unsafe { + crate::object::set_reserved_slot( + obj.as_raw(), + crate::class::ERROR_STACK_SLOT, + &crate::value::from_object(frame.as_raw()), + ) + } + } + Ok(None) => {} + Err(ExnThrown) => crate::exception::clear(scope), + } } diff --git a/crates/js/src/exception.rs b/crates/js/src/exception.rs index 927ee498..7fe32cdf 100644 --- a/crates/js/src/exception.rs +++ b/crates/js/src/exception.rs @@ -6,8 +6,6 @@ //! `JSContext`. For the higher-level error type that wraps these operations, //! see [`super::error::ExnThrown`]. -use std::hint::cold_path; - use crate::gc::scope::Scope; use mozjs::jsapi::ExceptionStackBehavior; use mozjs::jsval::UndefinedValue; @@ -40,29 +38,21 @@ pub fn is_throwing_oom(scope: &Scope<'_>) -> bool { unsafe { wrappers2::JS_IsThrowingOutOfMemory(scope.cx()) } } -/// Get the pending exception value. -/// -/// Returns `Err` if no exception is pending or retrieval fails. -pub fn get_pending<'r>(scope: &'r Scope<'_>) -> Result, &'static str> { +/// Get the pending exception value, or `None` if no exception is pending or it +/// can't be retrieved. +pub fn get_pending<'r>(scope: &'r Scope<'_>) -> Option> { let mut vp = scope.root_value_mut(UndefinedValue()); let ok = unsafe { wrappers2::JS_GetPendingException(scope.cx_mut(), vp.reborrow()) }; - if !ok { - cold_path(); - if !is_pending(scope) { - return Err("No exception pending"); - } - return Err("Failed to get pending exception"); - } - Ok(vp.handle()) + ok.then(|| vp.handle()) } -/// Get and clear the pending exception value. -/// -/// Returns `Err` if no exception is pending or retrieval fails. -pub fn take_pending<'r>(scope: &'r Scope<'_>) -> Result, &'static str> { +/// Get and clear the pending exception value, or `None` if no exception is +/// pending or it can't be retrieved. An exception that can't be retrieved stays +/// pending. +pub fn take_pending<'r>(scope: &'r Scope<'_>) -> Option> { let result = get_pending(scope)?; clear(scope); - Ok(result) + Some(result) } /// Get and clear the pending exception value, or return `undefined` if there is none. @@ -70,7 +60,7 @@ pub fn take_pending<'r>(scope: &'r Scope<'_>) -> Result, &'stati /// Can be used in contexts where JS execution failed, but an uncatchable exception /// (e.g., OOM) may be pending. pub fn take_pending_or_undefined<'r>(scope: &'r Scope<'_>) -> HandleValue<'r> { - take_pending(scope).unwrap_or_else(|_| HandleValue::undefined()) + take_pending(scope).unwrap_or(HandleValue::undefined()) } /// Set a pending exception on the context. diff --git a/crates/js/src/function.rs b/crates/js/src/function.rs index 9d1420ce..4f6543ce 100644 --- a/crates/js/src/function.rs +++ b/crates/js/src/function.rs @@ -188,8 +188,8 @@ impl<'s> Stack<'s, Function> { fval: HandleValue, args: &[impl ToJSVal<'a>], ) -> Result, ExnThrown> { - let mut args_root = ValueArrayRooter::new(scope, args)?; - let args = args_root.root(scope); + let mut args_root = ValueArrayRooter::new(args.len()); + let args = args_root.root(scope, args)?; let mut rval = scope.root_value_mut(UndefinedValue()); let ok = unsafe { wrappers2::JS_CallFunctionValue( @@ -211,8 +211,8 @@ impl<'s> Stack<'s, Function> { name: &CStr, args: &[impl ToJSVal<'a>], ) -> Result, ExnThrown> { - let mut args_root = ValueArrayRooter::new(scope, args)?; - let args = args_root.root(scope); + let mut args_root = ValueArrayRooter::new(args.len()); + let args = args_root.root(scope, args)?; let mut rval = scope.root_value_mut(UndefinedValue()); let ok = unsafe { wrappers2::JS_CallFunctionName( @@ -234,8 +234,8 @@ impl<'s> Stack<'s, Function> { fun: impl ToJSVal<'a>, args: &[impl ToJSVal<'a>], ) -> Result, ExnThrown> { - let mut args_root = ValueArrayRooter::new(scope, args)?; - let args = args_root.root(scope); + let mut args_root = ValueArrayRooter::new(args.len()); + let args = args_root.root(scope, args)?; let mut rval = scope.root_value_mut(UndefinedValue()); let ok = unsafe { wrappers2::Call( @@ -256,8 +256,8 @@ impl<'s> Stack<'s, Function> { fun: impl ToJSVal<'s>, args: &[impl ToJSVal<'s>], ) -> Result, ExnThrown> { - let mut args_root = ValueArrayRooter::new(scope, args)?; - let args = args_root.root(scope); + let mut args_root = ValueArrayRooter::new(args.len()); + let args = args_root.root(scope, args)?; let mut result = scope.root_object_mut(std::ptr::null_mut()); let ok = unsafe { wrappers2::Construct1( @@ -278,8 +278,8 @@ impl<'s> Stack<'s, Function> { new_target: HandleObject, args: &[impl ToJSVal<'s>], ) -> Result, ExnThrown> { - let mut args_root = ValueArrayRooter::new(scope, args)?; - let args = args_root.root(scope); + let mut args_root = ValueArrayRooter::new(args.len()); + let args = args_root.root(scope, args)?; let mut result = scope.root_object_mut(std::ptr::null_mut()); let ok = unsafe { wrappers2::Construct( @@ -332,7 +332,7 @@ impl<'s> Stack<'s, Function> { /// c"greet", /// 1, /// |_scope, _args, _payload| Ok(js::value::from_i32(42)), - /// js::value::undefined(), + /// (), /// ).unwrap(); /// # } /// ``` @@ -390,7 +390,7 @@ impl<'s> Stack<'s, Function> { ReservedSlot::Slot0, mozjs::jsval::PrivateValue(cb as *const std::ffi::c_void), ); - fun.set_reserved(ReservedSlot::Slot1, payload.to_jsval_throwing(scope)?.get()); + fun.set_reserved(ReservedSlot::Slot1, payload.to_jsval_raw_throwing(scope)?); } Ok(fun) @@ -401,6 +401,40 @@ crate::gc::handle::deref_to_object!(Function); crate::gc::handle::from_jsval_via_cast!(Function, c"Value isn't a Function"); +/// Marker type for any callable object: a function, a bound function, a +/// callable proxy, or any other object SpiderMonkey's `IsCallable` accepts. +/// +/// Used for WebIDL callback function types: the conversion throws a +/// `TypeError` for a value that is not callable. [`Function`] accepts only +/// `JSFunction` objects, which excludes bound functions and callable proxies. +pub struct Callable; + +impl JSType for Callable { + const JS_NAME: &'static str = "callable object"; + type Rooted<'s> = Stack<'s, Self>; + + fn js_class() -> *const JSClass { + Function::js_class() + } + + #[inline] + unsafe fn is_instance(obj: *mut mozjs::jsapi::JSObject) -> bool { + unsafe { mozjs::jsapi::JS::IsCallable(obj) } + } +} + +crate::gc::handle::deref_to_object!(Callable); + +crate::gc::handle::from_jsval_via_cast!(Callable, c"Value is not callable"); + +impl<'s> Stack<'s, Function> { + /// This function as a [`Callable`](crate::Callable). + pub fn as_callable(self) -> crate::Callable<'s> { + // SAFETY: every function is callable. + unsafe { Stack::from_handle_unchecked(self.handle()) } + } +} + pub enum ReservedSlot { Slot0 = 0, Slot1 = 1, @@ -417,40 +451,74 @@ impl From for usize { // Closure-based callbacks // --------------------------------------------------------------------------- -/// Safe wrapper around [`CallArgs`](mozjs::jsapi::CallArgs) for use in -/// closure-based callbacks. +/// The arguments of a [`Callback`] invocation: either a JS call's +/// [`CallArgs`](mozjs::jsapi::CallArgs), or rooted values passed from Rust by a +/// direct call (see [`from_values`](Self::from_values)). /// /// Provides indexed access to arguments, the `this` value, and the argument /// count. pub struct CallbackArgs<'a> { - args: &'a mozjs::jsapi::CallArgs, + source: ArgsSource<'a>, +} + +enum ArgsSource<'a> { + Call(&'a mozjs::jsapi::CallArgs), + Values(&'a [HandleValue<'a>]), } impl<'a> CallbackArgs<'a> { + /// Arguments for calling a [`Callback`] directly from Rust. `this` is `undefined`, and the + /// call is not a constructor call. + pub fn from_values(values: &'a [HandleValue<'a>]) -> Self { + Self { + source: ArgsSource::Values(values), + } + } + /// Number of arguments passed by the caller. #[inline] pub fn len(&self) -> u32 { - self.args.argc_ + match self.source { + ArgsSource::Call(args) => args.argc_, + ArgsSource::Values(values) => values.len() as u32, + } } /// Whether no arguments were passed. #[inline] pub fn is_empty(&self) -> bool { - self.args.argc_ == 0 + self.len() == 0 } - /// Get argument `i` as a raw [`Value`]. + /// Get argument `i`. /// /// Returns `undefined` if `i` is out of range. #[inline] pub fn get(&'a self, i: u32) -> HandleValue<'a> { - unsafe { HandleValue::from_raw(self.args.get(i)) } + match self.source { + ArgsSource::Call(args) => unsafe { HandleValue::from_raw(args.get(i)) }, + ArgsSource::Values(values) => values + .get(i as usize) + .copied() + .unwrap_or(HandleValue::undefined()), + } + } + + /// Argument `i` without rooting it, for an immediate type check. + #[inline] + fn raw(&self, i: u32) -> Value { + match self.source { + ArgsSource::Call(args) => args.get(i).get(), + ArgsSource::Values(values) => values + .get(i as usize) + .map_or_else(crate::value::undefined, |v| v.get()), + } } /// Get argument `i` as an `i32`, or `None` if it isn't an int32. #[inline] pub fn get_i32(&self, i: u32) -> Option { - let v = self.args.get(i).get(); + let v = self.raw(i); if v.is_int32() { Some(v.to_int32()) } else { @@ -461,7 +529,7 @@ impl<'a> CallbackArgs<'a> { /// Get argument `i` as an `f64`, or `None` if it isn't a number. #[inline] pub fn get_f64(&self, i: u32) -> Option { - let v = self.args.get(i).get(); + let v = self.raw(i); if v.is_double() { Some(v.to_double()) } else if v.is_int32() { @@ -474,7 +542,7 @@ impl<'a> CallbackArgs<'a> { /// Get argument `i` as a `bool`, or `None` if it isn't a boolean. #[inline] pub fn get_bool(&self, i: u32) -> Option { - let v = self.args.get(i).get(); + let v = self.raw(i); if v.is_boolean() { Some(v.to_boolean()) } else { @@ -485,19 +553,41 @@ impl<'a> CallbackArgs<'a> { /// Get the `this` value. #[inline] pub fn this(&self) -> Value { - self.args.thisv().get() + match self.source { + ArgsSource::Call(args) => args.thisv().get(), + ArgsSource::Values(_) => crate::value::undefined(), + } } /// Whether this is a constructor call (`new`). #[inline] pub fn is_constructing(&self) -> bool { - self.args.is_constructing() + match self.source { + ArgsSource::Call(args) => args.is_constructing(), + ArgsSource::Values(_) => false, + } } } /// Type-erased callback stored in a helper function's reserved slot. pub type Callback = fn(&Scope<'_>, CallbackArgs<'_>, HandleValue) -> Result; +/// Recover the class instance `T` a [`Callback`]'s `payload` was created with. +/// +/// # Panics +/// +/// Panics if `payload` is not an instance of `T`. The code that creates a +/// callback also sets its payload, so a mismatch is a bug in that code. +pub fn cast_payload<'r, T>(scope: &'r Scope<'_>, payload: HandleValue<'_>) -> T +where + T: crate::builtins::CastTarget<'r, Output = T>, +{ + Object::from_value(scope, *payload) + .expect("callback payload is an object") + .cast::() + .unwrap_or_else(|e| panic!("callback payload: {e}")) +} + /// The extern "C" trampoline that bridges JSNative to the stored callback. /// /// # Safety @@ -525,7 +615,9 @@ unsafe extern "C" fn callback_trampoline( std::mem::transmute((*mozjs::jsapi::GetFunctionNativeReserved(callee, 0)).to_private()); let payload = scope.root_value(*mozjs::jsapi::GetFunctionNativeReserved(callee, 1)); - let cb_args = CallbackArgs { args: &args }; + let cb_args = CallbackArgs { + source: ArgsSource::Call(&args), + }; // Call the function pointer. match cb(&scope, cb_args, payload) { @@ -535,7 +627,7 @@ unsafe extern "C" fn callback_trampoline( } Err(_) => { // If no exception is already pending, throw a generic one. - if !mozjs::jsapi::JS_IsExceptionPending(cx) { + if !crate::exception::is_pending(&scope) { report_error_ascii(&scope, c"Native callback returned an error"); } false diff --git a/crates/js/src/gc/handle.rs b/crates/js/src/gc/handle.rs index 077ea180..29396c88 100644 --- a/crates/js/src/gc/handle.rs +++ b/crates/js/src/gc/handle.rs @@ -106,11 +106,8 @@ impl<'s, T: JSType> Stack<'s, T> { } /// Get the JS value representation. - /// - /// # Safety - /// - /// The handle is rooted, so the object is guaranteed to be alive. pub fn as_value(self) -> crate::native::Value { + // SAFETY: the handle is rooted, so the object is alive. unsafe { crate::value::from_object(self.handle.get()) } } @@ -365,7 +362,7 @@ impl Heap { } pub fn eq_stack(&self, other: &Stack<'_, T>) -> bool { - unsafe { self.as_ptr() == other.as_raw() } + unsafe { self.as_ptr().eq(&other.as_raw()) } } } @@ -529,10 +526,16 @@ impl From for Heap { } } +/// Panics on a null (default-constructed) `Heap`, as [`Heap::get`] does. impl ToJSVal<'_> for Heap { #[inline] fn to_jsval_raw(&self, _scope: &Scope<'_>) -> Result { - Ok(ObjectValue(self.heap.get())) + let obj = self.heap.get(); + debug_assert!( + !obj.is_null(), + "converted a null (default-constructed) Heap" + ); + Ok(ObjectValue(obj)) } } @@ -568,18 +571,19 @@ impl RootedHeap { /// Returns `T::Rooted<'s>`, the stack-rooted type for `Heap`. pub fn get<'s>(&self, scope: &'s Scope<'_>) -> T::Rooted<'s> { let obj = self.boxed.get(); - let handle = scope.root_object(NonNull::new(obj).expect("BoxedHeap::get can't fail")); + let handle = scope.root_object(NonNull::new(obj).expect("RootedHeap holds an object")); T::Rooted::from(Stack { handle, _marker: PhantomData, }) } - /// Return a raw pointer to the references `JSObject` + /// The referenced `JSObject`'s current address, for identity comparison. /// /// # Safety /// - /// The returned pointer must either be used in rooted immeda + /// The returned pointer is not rooted. It must not be dereferenced or held across anything + /// that can GC. Use [`get`](Self::get) for any other use. pub unsafe fn as_ptr(&self) -> *mut JSObject { self.boxed.get() } diff --git a/crates/js/src/gc/mod.rs b/crates/js/src/gc/mod.rs index d8da64b4..6c15edb5 100644 --- a/crates/js/src/gc/mod.rs +++ b/crates/js/src/gc/mod.rs @@ -50,6 +50,28 @@ pub fn shutdown(cx: &JSContext) { pool::shutdown(); } +/// Register `tracer` to run whenever the current runtime's GC traces its roots, minor collections +/// included, until the runtime shuts down. Values it traces are roots of every collection, so +/// storing them needs no write barriers. Registering the same function again has no effect. +pub fn add_root_tracer(tracer: unsafe fn(*mut crate::native::JSTracer)) { + pool::add_root_tracer(tracer); +} + +/// Trace `value` as a root, updating it if the collection moves what it refers to. For use from a +/// tracer registered with [`add_root_tracer`]. +/// +/// # Safety +/// +/// `trc` must be the tracer the registered function was called with, and `value` must point to a +/// valid `Value`. +pub unsafe fn trace_value_root( + trc: *mut crate::native::JSTracer, + value: *mut crate::native::Value, +) { + // SAFETY: guaranteed by the caller. + unsafe { mozjs::glue::CallValueRootTracer(trc, value, c"root-value".as_ptr()) }; +} + /// Trigger a full, non-incremental garbage collection. pub fn gc(scope: &Scope<'_>, reason: GCReason) { unsafe { wrappers2::JS_GC(scope.cx_mut(), reason) } @@ -178,17 +200,15 @@ impl AssociatedMemory { self.bytes } - /// Attribute `bytes` to `object`, replacing the previous amount. - /// - /// # Safety - /// - /// `object` must be alive, and must be the object whose private data owns this field. - pub unsafe fn set(&mut self, object: *mut JSObject, bytes: usize) { - // SAFETY: the caller keeps `object` alive. + /// Attribute `bytes` to `object`, replacing the previous amount. `object` must be the object + /// whose private data owns this field. + pub fn set(&mut self, object: crate::Object<'_>, bytes: usize) { + let object = object.as_raw(); + // SAFETY: `object` is rooted, so this is its current address. unsafe { self.release(object) }; self.bytes = bytes; if bytes > 0 { - // SAFETY: the caller keeps `object` alive. + // SAFETY: as above. unsafe { JS::AddAssociatedMemory(object, bytes, MemoryUse::Embedding1) }; } } diff --git a/crates/js/src/gc/pool.rs b/crates/js/src/gc/pool.rs index 7f85c735..8055c3fa 100644 --- a/crates/js/src/gc/pool.rs +++ b/crates/js/src/gc/pool.rs @@ -1,29 +1,25 @@ // SPDX-License-Identifier: Apache-2.0-WITH-LLVM-exception -//! Page-based allocator for GC root handles. +//! Allocator for GC root handles. //! -//! The [`HandlePool`] is a page freelist that hands out fixed-size -//! [`Page`]s to individual scopes. Each scope owns its own pages and -//! performs bump allocation locally — no shared cursor, no aliasing -//! between scopes. -//! -//! This design guarantees that dropping one scope cannot invalidate -//! another scope's handles, which was an unsoundness issue with the -//! previous shared-cursor approach. +//! Every scope ([`RootScope`] or [`InnerScope`]) roots values through a [`ScopeAlloc`]. The +//! innermost live scope bump-allocates on the [`HandlePool`]'s shared root stack. A scope that +//! roots a value while a scope nested in it is alive gets [`PrivatePages`] instead, since the +//! nested scope's drop truncates the stack back to where it started. //! //! # Architecture //! -//! - **[`HandlePool`]**: Owns a freelist of reusable pages and maintains -//! an intrusive linked list of active [`ScopeAlloc`]s for GC tracing. -//! - **[`ScopeAlloc`]**: A per-scope bump allocator. Each scope -//! ([`RootScope`] or [`InnerScope`]) owns one. Allocates from a -//! current page and requests new pages from the pool when full. -//! - **[`Page`]**: A fixed-size array of 128 tagged `u64` slots. +//! - **[`HandlePool`]**: owns the root stack (chunks of [`Page`]s, whose slots never move) and +//! one [`Level`] per live scope, a freelist of pages for private allocations, and an intrusive +//! linked list of the live [`PrivatePages`] for GC tracing. +//! - **[`ScopeAlloc`]**: a scope's handle on its level, and its private pages if it has any. +//! Dropping it truncates the stack to the level's base once every scope created after it has +//! dropped too. +//! - **[`Page`]**: a fixed-size array of 128 tagged `u64` slots. //! -//! The pool is wrapped in a [`PoolRooter`] — a `#[repr(C)]` -//! `CustomAutoRooter` that sits on the `autoGCRooters` stack for the -//! entire lifetime of the [`Runtime`]. During GC, the rooter walks the -//! active allocator list and traces each scope's live slots. +//! The pool is wrapped in a [`PoolRooter`], a `#[repr(C)]` `CustomAutoRooter` that sits on the +//! `autoGCRooters` stack for the entire lifetime of the [`Runtime`]. During GC, it traces the +//! root stack up to its top and every live private allocation. //! //! [`RootScope`]: crate::gc::scope::RootScope //! [`InnerScope`]: crate::gc::scope::InnerScope @@ -80,18 +76,84 @@ impl Page { } // --------------------------------------------------------------------------- -// ScopeAlloc — per-scope bump allocator +// ScopeAlloc: a scope's root allocation // --------------------------------------------------------------------------- -/// A per-scope bump allocator that owns its pages independently. -/// -/// Each [`RootScope`](crate::gc::scope::RootScope) and -/// [`InnerScope`](crate::gc::scope::InnerScope) owns a `ScopeAlloc`. -/// Pages are obtained from the [`HandlePool`] freelist and returned on drop. -/// -/// `ScopeAlloc` is registered in the pool's intrusive linked list of active -/// allocators so that the GC tracer can discover all live slots. +/// A scope's root allocation: its level on the pool's root stack, and the private pages it +/// roots into while a scope nested in it is alive. pub struct ScopeAlloc { + /// The pool this allocator roots in. + pool: *const HandlePool, + + /// Index of this scope's [`Level`] in the pool. + level: usize, + + /// Pages for values rooted while this scope was not the innermost one. `None` until needed. + private: Option>, +} + +impl ScopeAlloc { + /// Create an allocator for a new scope, nested in every scope that is alive. + /// + /// # Safety + /// + /// `pool` must point to a valid `HandlePool` that outlives this `ScopeAlloc`. + pub unsafe fn new(pool: *const HandlePool) -> Self { + // SAFETY: guaranteed by the caller. + let level = unsafe { &*pool }.push_level(); + ScopeAlloc { + pool, + level, + private: None, + } + } + + /// Allocate a slot for a rooted value. + /// + /// Returns a stable pointer to the value's `u64` storage. The pointer + /// remains valid until this `ScopeAlloc` is dropped. + #[inline] + pub fn alloc(&mut self, tag: SlotTag, value: u64) -> *mut u64 { + // SAFETY: the pool outlives every allocator created from it. + let pool = unsafe { &*self.pool }; + if pool.is_innermost(self.level) { + return pool.stack_alloc(tag, value); + } + self.private_alloc(tag, value) + } + + /// Allocate a slot in this scope's private pages, for a scope that is not the innermost one. + #[cold] + #[inline(never)] + fn private_alloc(&mut self, tag: SlotTag, value: u64) -> *mut u64 { + self.private + // SAFETY: the pool outlives every allocator created from it. + .get_or_insert_with(|| unsafe { PrivatePages::new(self.pool) }) + .alloc(tag, value) + } + + /// Get the pool pointer that this allocator roots in. + pub fn pool(&self) -> *const HandlePool { + self.pool + } +} + +impl Drop for ScopeAlloc { + fn drop(&mut self) { + if let Some(private) = self.private.take() { + PrivatePages::recycle(private); + } + // SAFETY: the pool outlives every allocator created from it. + unsafe { &*self.pool }.pop_level(self.level); + } +} + +/// A scope's private root pages, for values it roots while a scope nested in it is alive. +/// +/// Pages are obtained from the [`HandlePool`] freelist and returned on drop. `PrivatePages` is +/// registered in the pool's intrusive linked list of active allocators so that the GC tracer can +/// discover its slots. +pub struct PrivatePages { /// The pool this allocator borrows pages from. pool: *const HandlePool, @@ -107,41 +169,89 @@ pub struct ScopeAlloc { /// Intrusive linked-list pointers for the pool's active-allocator list. /// These are raw pointers because the list is manually managed. - next: *mut ScopeAlloc, - prev: *mut ScopeAlloc, + next: *mut PrivatePages, + prev: *mut PrivatePages, + + /// Whether this allocator is in the pool's active list. False once + /// [`release`](Self::release) has run, and while the box waits in the + /// pool's allocator freelist. + active: bool, } -impl ScopeAlloc { - /// Create a new scope allocator, boxed for address stability, and register - /// it with the pool's active list. +impl PrivatePages { + /// Create private pages, boxed for address stability, and register them + /// with the pool's active list. /// - /// Returns a `Box` because the intrusive linked list requires + /// Returns a `Box` because the intrusive linked list requires /// a stable address. The box must not be moved out of its allocation. /// /// No page is allocated until the first `alloc` call (lazy initialization). + /// The box comes from the pool's allocator freelist when one is available. + /// Pass a box that is done with to [`recycle`](Self::recycle) to return it + /// there. /// /// # Safety /// - /// `pool` must point to a valid `HandlePool` that outlives this `ScopeAlloc`. + /// `pool` must point to a valid `HandlePool` that outlives this `PrivatePages`. pub unsafe fn new(pool: *const HandlePool) -> Box { + // SAFETY: guaranteed by the caller. let pool_ref = unsafe { &*pool }; - let mut alloc = Box::new(ScopeAlloc { - pool, - current_page: None, - cursor: 0, - full_pages: Vec::new(), - next: ptr::null_mut(), - prev: ptr::null_mut(), + let mut alloc = pool_ref.take_alloc().unwrap_or_else(|| { + Box::new(PrivatePages { + pool, + current_page: None, + cursor: 0, + full_pages: Vec::new(), + next: ptr::null_mut(), + prev: ptr::null_mut(), + active: false, + }) }); + debug_assert!(!alloc.active && alloc.current_page.is_none() && alloc.full_pages.is_empty()); + alloc.pool = pool; + alloc.cursor = 0; + alloc.active = true; // Register using the heap address (stable across moves of the Box pointer). pool_ref.register(&mut *alloc); alloc } + /// Release `alloc`'s slots and pages, and keep the box in the pool's + /// allocator freelist for a later [`new`](Self::new). + pub fn recycle(mut alloc: Box) { + alloc.release(); + // SAFETY: the pool outlives every allocator created from it. + let pool = unsafe { &*alloc.pool }; + pool.return_alloc(alloc); + } + + /// Unregister from the pool and return this allocator's pages. Does nothing + /// if already released. + fn release(&mut self) { + if !self.active { + return; + } + self.active = false; + // SAFETY: pool pointer is valid for the lifetime of the Runtime, + // which outlives all scopes. + let pool = unsafe { &*self.pool }; + + // Unregister from the active allocator list. + pool.unregister(self); + + // Return all pages to the pool freelist. + if let Some(page) = self.current_page.take() { + pool.return_page(page); + } + for page in self.full_pages.drain(..) { + pool.return_page(page); + } + } + /// Allocate a slot for a rooted value. /// /// Returns a stable pointer to the value's `u64` storage. The pointer - /// remains valid until this `ScopeAlloc` is dropped. + /// remains valid until this `PrivatePages` is dropped. pub fn alloc(&mut self, tag: SlotTag, value: u64) -> *mut u64 { // Ensure we have a page to allocate from. if self.current_page.is_none() { @@ -169,14 +279,6 @@ impl ScopeAlloc { ptr } - /// Get the pool pointer that this allocator borrows from. - /// - /// Used by `Scope::inner_scope()` to create a new `ScopeAlloc` from - /// the same pool. - pub fn pool(&self) -> *const HandlePool { - self.pool - } - /// Trace all live slots for GC. /// /// # Safety @@ -194,22 +296,9 @@ impl ScopeAlloc { } } -impl Drop for ScopeAlloc { +impl Drop for PrivatePages { fn drop(&mut self) { - // SAFETY: pool pointer is valid for the lifetime of the Runtime, - // which outlives all scopes. - let pool = unsafe { &*self.pool }; - - // Unregister from the active allocator list. - pool.unregister(self); - - // Return all pages to the pool freelist. - if let Some(page) = self.current_page.take() { - pool.return_page(page); - } - for page in self.full_pages.drain(..) { - pool.return_page(page); - } + self.release(); } } @@ -273,14 +362,21 @@ unsafe fn trace_page(trc: *mut JSTracer, page: &Page, count: usize) { } // --------------------------------------------------------------------------- -// HandlePool — page freelist + active allocator registry +// HandlePool: root stack, page freelist, and private allocation registry // --------------------------------------------------------------------------- -/// A page freelist and registry of active scope allocators. +/// A scope's entry on the root stack: the stack's height when the scope was created, and whether +/// the scope is still alive. +struct Level { + base: usize, + alive: bool, +} + +/// The root stack shared by all scopes, a freelist of pages for private allocations, and a +/// registry of the live [`PrivatePages`]. /// -/// The pool owns reusable pages and maintains an intrusive doubly-linked -/// list of active [`ScopeAlloc`]s. During GC, the tracer walks this list -/// to discover all live root slots. +/// During GC, the tracer traces the root stack up to its top and walks the registry to discover +/// the private slots. /// /// # Thread safety /// @@ -288,16 +384,37 @@ unsafe fn trace_page(trc: *mut JSTracer, page: &Page, count: usize) { /// single-threaded, matching SpiderMonkey's threading model. #[derive(Default)] pub struct HandlePool { - /// Freelist of reusable pages. Pages are returned here when scopes drop - /// and reused by new scopes, avoiding repeated heap allocation. + /// The root stack's pages. Slot `i` is slot `i % PAGE_SIZE` of page `i / PAGE_SIZE`. Pages are + /// added as the stack grows and kept when it shrinks, so slots never move. + #[allow(clippy::vec_box)] + stack: UnsafeCell>>, + + /// The number of slots in use on the root stack. + top: Cell, + + /// One entry per scope, innermost last. Entries of dropped scopes stay until every scope + /// created after them has dropped. + levels: UnsafeCell>, + + /// Freelist of reusable pages for private allocations. #[allow(clippy::vec_box)] freelist: UnsafeCell>>, - /// Head of the intrusive doubly-linked list of active `ScopeAlloc`s. - /// Null when no scopes are active. - active_head: Cell<*mut ScopeAlloc>, + /// Head of the intrusive doubly-linked list of live `PrivatePages`. + /// Null when there are none. + active_head: Cell<*mut PrivatePages>, + + /// Released private allocations kept for reuse, at most [`MAX_FREE_ALLOCS`]. + #[allow(clippy::vec_box)] + alloc_freelist: UnsafeCell>>, + + /// Tracers registered with [`crate::gc::add_root_tracer`], run whenever the pool is traced. + root_tracers: UnsafeCell>, } +/// The most released private allocations the pool keeps. +const MAX_FREE_ALLOCS: usize = 32; + /// This module's share of the crate's thread-local state. See [`crate::tls`]. pub(crate) struct PoolTls { handle_pool: RefCell>>, @@ -337,6 +454,19 @@ pub(crate) fn init_pool(cx: &mut JSContext) { }); } +/// Register `tracer` with the current thread's pool, unless it already is. +pub(crate) fn add_root_tracer(tracer: unsafe fn(*mut JSTracer)) { + // SAFETY: the pool outlives this call, and single-threaded access leaves no other reference to + // its tracer list. + let tracers = unsafe { &mut *(*current_pool()).root_tracers.get() }; + if !tracers + .iter() + .any(|®istered| ptr::fn_addr_eq(registered, tracer)) + { + tracers.push(tracer); + } +} + /// Get a raw pointer to the current thread's HandlePool. /// /// The returned pointer is valid for the lifetime of the Runtime (until @@ -364,6 +494,71 @@ impl HandlePool { Self::default() } + /// Add a level for a new scope, nested in every live scope, and return its index. + #[inline] + fn push_level(&self) -> usize { + // SAFETY: Single-threaded access, and no reference to `levels` outlives this call. + let levels = unsafe { &mut *self.levels.get() }; + levels.push(Level { + base: self.top.get(), + alive: true, + }); + levels.len() - 1 + } + + /// Whether `level` belongs to the innermost live scope. + #[inline] + fn is_innermost(&self, level: usize) -> bool { + // SAFETY: Single-threaded access, and no reference to `levels` outlives this call. + let levels = unsafe { &*self.levels.get() }; + level == levels.len() - 1 + } + + /// Mark `level`'s scope dropped. If no live scope was created after it, truncate the root stack + /// to the base of the oldest of the dropped scopes on top of the level stack, and remove their + /// levels. + #[inline] + fn pop_level(&self, level: usize) { + // SAFETY: Single-threaded access, and no reference to `levels` outlives this call. + let levels = unsafe { &mut *self.levels.get() }; + levels[level].alive = false; + while let Some(last) = levels.last() { + if last.alive { + break; + } + self.top.set(last.base); + levels.pop(); + } + } + + /// Push a slot for the innermost scope onto the root stack, and return its storage. + #[inline] + fn stack_alloc(&self, tag: SlotTag, value: u64) -> *mut u64 { + let index = self.top.get(); + // SAFETY: Single-threaded access, and no reference to `stack` outlives this call. + let stack = unsafe { &mut *self.stack.get() }; + let page_index = index / PAGE_SIZE; + if page_index == stack.len() { + Self::grow_stack(stack); + } + let page = &stack[page_index]; + let slot = index % PAGE_SIZE; + page.tags[slot].set(tag); + let ptr = page.values[slot].get(); + // SAFETY: the slot is above the stack's top, so no handle refers to it. + unsafe { *ptr = value }; + self.top.set(index + 1); + ptr + } + + /// Add a page to the root stack. + #[cold] + #[inline(never)] + #[allow(clippy::vec_box)] + fn grow_stack(stack: &mut Vec>) { + stack.push(Box::new(Page::new())); + } + /// Take a page from the freelist, or allocate a new one. fn take_page(&self) -> Box { // SAFETY: Single-threaded access. @@ -378,12 +573,29 @@ impl HandlePool { fl.push(page); } - /// Register a `ScopeAlloc` in the active allocator list. + /// Take a released allocator from the freelist, if there is one. + fn take_alloc(&self) -> Option> { + // SAFETY: Single-threaded access. + let fl = unsafe { &mut *self.alloc_freelist.get() }; + fl.pop() + } + + /// Keep a released allocator for reuse, or drop it if the freelist is full. + fn return_alloc(&self, alloc: Box) { + debug_assert!(!alloc.active); + // SAFETY: Single-threaded access. + let fl = unsafe { &mut *self.alloc_freelist.get() }; + if fl.len() < MAX_FREE_ALLOCS { + fl.push(alloc); + } + } + + /// Register a `PrivatePages` in the active allocator list. /// /// Pushes to the head of the doubly-linked list. - fn register(&self, alloc: *mut ScopeAlloc) { + fn register(&self, alloc: *mut PrivatePages) { let head = self.active_head.get(); - // SAFETY: alloc is a valid pointer to a ScopeAlloc being constructed. + // SAFETY: alloc is a valid pointer to a PrivatePages being constructed. unsafe { (*alloc).next = head; (*alloc).prev = ptr::null_mut(); @@ -394,9 +606,9 @@ impl HandlePool { self.active_head.set(alloc); } - /// Unregister a `ScopeAlloc` from the active allocator list. - fn unregister(&self, alloc: *const ScopeAlloc) { - // SAFETY: alloc is a valid pointer to a ScopeAlloc being dropped. + /// Unregister a `PrivatePages` from the active allocator list. + fn unregister(&self, alloc: *const PrivatePages) { + // SAFETY: alloc is a valid pointer to a PrivatePages being dropped. // The linked-list pointers are valid because they were set by register(). unsafe { let prev = (*alloc).prev; @@ -415,7 +627,7 @@ impl HandlePool { } unsafe impl CustomTrace for HandlePool { - /// Trace all live slots across all active scope allocators. + /// Trace the root stack up to its top, and the slots of all live private allocations. /// /// # Safety /// @@ -423,9 +635,26 @@ unsafe impl CustomTrace for HandlePool { /// `trc` must be a valid `JSTracer` pointer. #[allow(clippy::not_unsafe_ptr_arg_deref)] fn trace(&self, trc: *mut JSTracer) { + // SAFETY: Single-threaded access. Tracing runs while no allocation is in progress. + let stack = unsafe { &*self.stack.get() }; + let top = self.top.get(); + for (index, page) in stack.iter().enumerate() { + let start = index * PAGE_SIZE; + if start >= top { + break; + } + // SAFETY: `trc` is valid per this function's contract, and the slots below `top` hold + // values of their tags. + unsafe { trace_page(trc, page, (top - start).min(PAGE_SIZE)) }; + } + // SAFETY: Single-threaded access. Registration does not run during tracing. + for tracer in unsafe { &*self.root_tracers.get() } { + // SAFETY: `trc` is valid per this function's contract. + unsafe { tracer(trc) }; + } let mut current = self.active_head.get(); while !current.is_null() { - // SAFETY: The linked list contains only valid ScopeAlloc pointers + // SAFETY: The linked list contains only valid PrivatePages pointers // because register/unregister maintain the invariant. unsafe { (*current).trace(trc); diff --git a/crates/js/src/gc/scope.rs b/crates/js/src/gc/scope.rs index 6804f0da..2783cb76 100644 --- a/crates/js/src/gc/scope.rs +++ b/crates/js/src/gc/scope.rs @@ -9,19 +9,19 @@ //! //! # Design //! -//! Each scope owns its own [`ScopeAlloc`](mozjs::gc::pool::ScopeAlloc) — a -//! per-scope bump allocator that draws pages from the [`HandlePool`] -//! freelist. This ensures that different scopes have disjoint storage: -//! dropping one scope cannot invalidate another scope's handles. +//! Each scope roots through its own [`ScopeAlloc`](mozjs::gc::pool::ScopeAlloc). The innermost +//! live scope pushes its roots onto the [`HandlePool`]'s root stack, and the stack is truncated +//! when the scope drops. A scope that roots a value while a scope nested in it is alive puts it in +//! pages of its own instead, so dropping one scope cannot invalidate another scope's handles. //! //! The pool is wrapped in a `PoolRooter` that sits on the `autoGCRooters` //! stack for the entire lifetime of the [`Runtime`]. During GC, the -//! rooter walks all active scope allocators and traces their live slots. +//! rooter traces the root stack and every scope's own pages. //! //! # Type hierarchy //! //! - [`Scope<'cx>`] — the core scope type. Holds a `JSContext` pointer -//! and a per-scope bump allocator. All rooting methods and `cx()`/`cx_mut()` +//! and the scope's root allocation. All rooting methods and `cx()`/`cx_mut()` //! access are defined here. This is the parameter type for most //! [`mozjs::js`](mozjs::js) API functions. //! @@ -32,17 +32,16 @@ //! - [`EnteredRealm`]: a realm is active. `Deref`s to `&Scope<'cx>`, //! providing all rooting methods. //! -//! - [`InnerScope<'parent>`] — a nested scope with its own allocator. +//! - [`InnerScope<'parent>`]: a nested scope with its own root allocation. //! `Deref`s to `&Scope<'parent>`, so it can be used wherever `&Scope` //! is expected. Values rooted via the inner scope are freed when it //! drops, independently of the parent scope's roots. //! //! # Nested scopes //! -//! Call [`Scope::inner_scope`] to create a child scope. Because each -//! scope owns separate storage, rooting on the parent while an inner -//! scope is alive is safe — the parent's handles are not affected when -//! the inner scope drops. +//! Call [`Scope::inner_scope`] to create a child scope. Rooting on the parent while an inner +//! scope is alive is safe: those roots go to the parent's own pages, so the parent's handles are +//! not affected when the inner scope drops. //! //! # Example //! @@ -87,9 +86,8 @@ pub struct EnteredRealm(()); /// The core scope type for rooting GC values. /// -/// Each `Scope` owns its own [`ScopeAlloc`] — a per-scope bump allocator -/// backed by pages from the [`HandlePool`]. This ensures that different -/// scopes have disjoint storage and cannot corrupt each other's roots. +/// Each `Scope` roots through its own [`ScopeAlloc`], which keeps its roots apart from every +/// other scope's, so no scope's drop can corrupt another scope's roots. /// /// Both [`RootScope<'cx, EnteredRealm>`] and [`InnerScope`] deref to /// `&Scope`, so all API functions that take `scope: &Scope<'_>` work @@ -104,22 +102,17 @@ pub struct Scope<'cx> { /// mutability (same rationale as `RootScope`). raw_cx: UnsafeCell<*mut RawJSContext>, - /// Per-scope bump allocator, boxed for stable heap address. - /// - /// The `Box` guarantees that the `ScopeAlloc`'s address doesn't change - /// when the `Scope` is moved (e.g. during `RootScope::enter_realm`). - /// This is essential because the pool's intrusive linked list stores - /// raw pointers to active allocators. + /// This scope's root allocation. /// /// Wrapped in `UnsafeCell` because rooting methods take `&self` /// (to allow multiple live handles) but the allocator needs mutation. - alloc: UnsafeCell>, + alloc: UnsafeCell, _phantom: PhantomData<&'cx mut ()>, } impl<'cx> Scope<'cx> { - /// Create a new Scope with its own boxed allocator. + /// Create a new Scope, nested in every live scope. fn new(raw_cx: *mut RawJSContext) -> Self { let pool = super::pool::current_pool(); // SAFETY: The pool pointer is valid for the lifetime of the Runtime @@ -140,8 +133,7 @@ impl<'cx> Scope<'cx> { /// immediately for a single `alloc()` call and released. #[allow(clippy::mut_from_ref)] fn alloc_mut(&self) -> &mut ScopeAlloc { - // SAFETY: The Box provides a stable heap address. We deref through - // the UnsafeCell to get the Box, then deref the Box to get the alloc. + // SAFETY: the borrow is never stored, see above. unsafe { &mut *self.alloc.get() } } @@ -173,8 +165,8 @@ impl<'cx> Scope<'cx> { /// /// # Safety /// - /// The returned pointer must only be used for operations that cannot - /// trigger GC. + /// The pointer is valid while the context lives. As with [`Scope::cx_mut`], a use that can + /// trigger a GC must keep every GC value the caller holds rooted. pub unsafe fn raw_cx_no_gc(&self) -> *mut RawJSContext { *self.raw_cx.get() } @@ -262,15 +254,14 @@ impl<'cx> Scope<'cx> { /// Create a nested inner scope. /// - /// The inner scope has its own allocator backed by separate pages. Values - /// rooted in the inner scope are freed when it drops, without affecting + /// Values rooted in the inner scope are freed when it drops, without affecting /// the parent scope's roots. /// /// The returned [`InnerScope`] dereferences to [`Scope`] and can be used /// anywhere a `&Scope` is accepted. /// - /// Handles rooted in an inner scope cannot outlive it — their slots are - /// returned to the pool when the inner scope drops: + /// Handles rooted in an inner scope cannot outlive it, since their slots are + /// released when the inner scope drops: /// /// ```compile_fail,E0597 /// fn escape(scope: &js::gc::scope::Scope<'_>, v: js::native::Value) { @@ -281,7 +272,6 @@ impl<'cx> Scope<'cx> { /// let _ = h.get(); /// } /// ``` - // TODO: can we make InnerScope cheaper by sharing the allocator and just tracking a "high water mark" for roots at the time of creation, freeing anything above that on drop? pub fn inner_scope(&self) -> InnerScope<'_> { InnerScope { scope: Scope::new(unsafe { *self.raw_cx.get() }), @@ -475,22 +465,22 @@ impl<'cx, S> RootScope<'cx, S> { /// /// # Safety /// - /// The returned pointer must only be used for operations that cannot - /// trigger GC. + /// The pointer is valid while the context lives. As with [`Scope::cx_mut`], a use that can + /// trigger a GC must keep every GC value the caller holds rooted. pub unsafe fn raw_cx_no_gc(&self) -> *mut RawJSContext { *self.raw_cx.get() } } // --------------------------------------------------------------------------- -// InnerScope — nested scope with its own allocator +// InnerScope: nested scope // --------------------------------------------------------------------------- -/// A nested scope that owns its own allocator backed by separate pages. +/// A nested scope, whose roots are freed when it drops. /// /// Created by [`Scope::inner_scope`]. Dereferences to `&Scope`, so all -/// rooting methods and `cx()`/`cx_mut()` are available. Each inner scope -/// has disjoint storage from its parent, allowing concurrent use of both. +/// rooting methods and `cx()`/`cx_mut()` are available. Its roots are kept +/// apart from its parent's, so both can be used while it is alive. pub struct InnerScope<'parent> { scope: Scope<'parent>, } diff --git a/crates/js/src/iteration.rs b/crates/js/src/iteration.rs index 5c2d6245..1e13ca01 100644 --- a/crates/js/src/iteration.rs +++ b/crates/js/src/iteration.rs @@ -18,9 +18,9 @@ //! resolved trigger ([`Promise::shared_resolved_undefined`]). use crate::class::{get_or_init_shared_function, get_prototype_for}; -use crate::conversion::FromJSVal; +use crate::conversion::{FromJSVal, ToJSVal}; use crate::error::ExnThrown; -use crate::function::EmptyArgs; +use crate::function::{cast_payload, EmptyArgs}; use crate::gc::handle::Heap; use crate::gc::scope::Scope; use crate::macros::{jsclass, jsmethods}; @@ -110,8 +110,7 @@ impl AsyncIteratorRecord<'_> { self.data_mut().sync_next = Some(Heap::from(sync_next)); // `CreateAsyncFromSyncIterator`: the next method becomes a native // function that calls the sync `next` and awaits the yielded value. - let record_v = scope.root_value(self.as_value()); - let afs = Function::new_callback(scope, c"", 1, afs_next, record_v)?; + let afs = Function::new_callback(scope, c"", 1, afs_next, self)?; self.data_mut().next_method.set(*afs); } } @@ -168,9 +167,7 @@ impl<'s> AsyncIteratorRecord<'s> { let cb = get_or_init_shared_function( scope, return_result_check as *const () as usize, - |scope| { - Function::new_callback(scope, c"", 1, return_result_check, HandleValue::undefined()) - }, + |scope| Function::new_callback(scope, c"", 1, return_result_check, ()), )?; return_promise.then(scope, Some(*cb), None) } @@ -202,12 +199,7 @@ fn return_result_check( args: CallbackArgs<'_>, _payload: HandleValue<'_>, ) -> Result { - if !args.get(0).is_object() { - return Err(crate::error::throw_type_error( - scope, - c"iterator result is not an object", - )); - } + iter_result_object(scope, args.get(0))?; Ok(value::undefined()) } @@ -219,7 +211,7 @@ fn is_callable(scope: &Scope<'_>, v: HandleValue<'_>) -> bool { /// `GetMethod`-style check: undefined/null yields `None`; a non-callable /// throws a `TypeError`; a callable is returned. -fn ensure_is_method<'r>( +pub(crate) fn ensure_is_method<'r>( scope: &'r Scope<'_>, v: HandleValue<'r>, ) -> Result>, ExnThrown> { @@ -249,7 +241,7 @@ fn afs_next( } fn afs_next_sync<'s>(scope: &'s Scope<'_>, payload: HandleValue) -> Result, ExnThrown> { - let record = AsyncIteratorRecord::from_jsval(scope, payload, ()).unwrap(); + let record = cast_payload::(scope, payload); let sync_iter = record.data().iterator.get(scope); let sync_next = record .data() @@ -294,29 +286,13 @@ fn afs_next_sync<'s>(scope: &'s Scope<'_>, payload: HandleValue) -> Result, payload: HandleValue<'_>, ) -> Result { - let record = AsyncIteratorRecord::from_jsval(scope, payload, ()).unwrap(); + let record = cast_payload::(scope, payload); let error = args.get(0); let sync_iter = record.data().iterator.get(scope); // `IteratorClose`: call the iterator's `return` (a missing `return` is a no-op). @@ -380,7 +356,7 @@ fn afs_wrap_parked( _args: CallbackArgs<'_>, payload: HandleValue<'_>, ) -> Result { - let record = AsyncIteratorRecord::from_jsval(scope, payload, ()).unwrap(); + let record = cast_payload::(scope, payload); // Take the parked value: root it, then clear the slot so it is not kept // alive past this iteration. let value = record.data().pending_value.get(scope); @@ -422,31 +398,117 @@ fn afs_value_fulfilled_not_done( /// the value and suppress the own property; the define path is immune. pub fn create_iter_result<'r>( scope: &'r Scope<'_>, - value: HandleValue<'_>, + value: impl ToJSVal<'r>, done: bool, ) -> Result, ExnThrown> { let attrs = crate::class_spec::JSPROP_ENUMERATE as std::ffi::c_uint; let obj = Object::new_plain(scope)?; - obj.define_property(scope, c"value", value, attrs)?; - let done_val = scope.root_value(value::from_bool(done)); - obj.define_property(scope, c"done", done_val, attrs)?; + let value_id = crate::class::get_or_init_property_id(scope, c"value")?; + obj.define_value_by_id(scope, value_id, value, attrs)?; + let done_id = crate::class::get_or_init_property_id(scope, c"done")?; + obj.define_value_by_id(scope, done_id, done, attrs)?; Ok(obj) } +/// A WebIDL default iterator object's kind. +/// +/// +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub enum IterationKind { + /// "`key`" + Key, + /// "`value`" + Value, + /// "`key+value`" + #[default] + KeyValue, +} + +/// +/// The iterator result for a value pair (`key`, `value`) and a kind `kind`. +pub fn pair_iterator_result<'r>( + scope: &'r Scope<'_>, + key: impl ToJSVal<'r>, + value: impl ToJSVal<'r>, + kind: IterationKind, +) -> Result, ExnThrown> { + // Step 1: Let _result_ be a value determined by the value of _kind_: + let result = match kind { + // "`key`": + // Let _idlKey_ be _pair_'s `key`. + // Let _key_ be the result of `converting` _idlKey_ to a JavaScript value. + // _result_ is _key_. + IterationKind::Key => key.to_jsval_throwing(scope)?, + // "`value`": + // Let _idlValue_ be _pair_'s `value`. + // Let _value_ be the result of `converting` _idlValue_ to a JavaScript value. + // _result_ is _value_. + IterationKind::Value => value.to_jsval_throwing(scope)?, + // "`key+value`": + // Let _idlKey_ be _pair_'s `key`. + // Let _idlValue_ be _pair_'s `value`. + // Let _key_ be the result of `converting` _idlKey_ to a JavaScript value. + // Let _value_ be the result of `converting` _idlValue_ to a JavaScript value. + // Let _array_ be ! ArrayCreate(2). + // Perform ! CreateDataPropertyOrThrow(_array_, "`0`", _key_). + // Perform ! CreateDataPropertyOrThrow(_array_, "`1`", _value_). + // _result_ is _array_. + IterationKind::KeyValue => { + let key = key.to_jsval_throwing(scope)?; + let value = value.to_jsval_throwing(scope)?; + crate::Array::with_contents(scope, &[key, value])?.to_jsval_throwing(scope)? + } + }; + // Step 2: Return CreateIteratorResultObject(_result_, false). + create_iter_result(scope, result, false) +} + +/// +/// The `forEach` method steps of an interface with a pair iterator, from the step that converts +/// `callback` onwards. `target` is the `this` value, already checked to implement the interface. +/// +/// `pair_at(i)` returns the `i`th entry of `target`'s current list of value pairs to iterate over, +/// converted to JS values, or `None` if the list has fewer entries. It is called again after every +/// callback invocation, since the callback can change the list. +pub fn for_each_pair<'r>( + scope: &'r Scope<'_>, + target: impl ToJSVal<'r>, + callback: crate::Callable<'_>, + this_arg: HandleValue<'_>, + mut pair_at: impl FnMut(usize) -> Result, HandleValue<'r>)>, ExnThrown>, +) -> Result<(), ExnThrown> { + // Step 4: Let _idlCallback_ be _callback_, `converted` to a ``Function``. + // (Done by the caller's argument conversion.) + // Step 5: Let _idlObject_ be the IDL `interface type` value that represents a reference to + // _jsValue_. + let idl_object = target.to_jsval_throwing(scope)?; + // Step 6: Let _pairs_ be _idlObject_'s list of `value pairs to iterate over`. + // (`pair_at` reads the current list.) + // Step 7: Let _i_ be 0. + let mut i = 0; + // Step 8: While _i_ < _pairs_'s `size`: + // Step 8.1: Let _pair_ be _pairs_[_i_]. + while let Some((key, value)) = pair_at(i)? { + // Step 8.2: `Invoke` _idlCallback_ with « _pair_'s `value`, _pair_'s `key`, _idlObject_ » + // and with _thisArg_ as the `callback this value`. + Function::call(scope, this_arg, callback, &[value, key, idl_object])?; + // Step 8.3: Set _pairs_ to _idlObject_'s current list of `value pairs to iterate over`. + // (It might have changed.) + // (The next `pair_at` call reads the current list.) + // Step 8.4: Set _i_ to _i_ + 1. + i += 1; + } + Ok(()) +} + /// The iterator-result Object requirement shared by every result consumer: a /// non-object result is a `TypeError`. pub fn iter_result_object<'r>( scope: &'r Scope<'_>, result: HandleValue<'_>, ) -> Result, ExnThrown> { - if !result.is_object() { - Err(crate::error::throw_type_error( - scope, - c"iterator result is not an object", - )) - } else { - Ok(Object::from_value(scope, *result).unwrap()) - } + Object::from_value(scope, *result) + .map_err(|_| crate::error::throw_type_error(scope, c"iterator result is not an object")) } /// `IteratorComplete(iterResult)`: `Get(iterResult, "done")`, coerced to a diff --git a/crates/js/src/lib.rs b/crates/js/src/lib.rs index 8cd8e387..df373cf6 100644 --- a/crates/js/src/lib.rs +++ b/crates/js/src/lib.rs @@ -66,11 +66,11 @@ //! # Quick Start //! //! ```ignore -//! use mozjs::rust::{JSEngine, Runtime}; -//! use core_runtime::js::prelude::*; +//! use js::engine::{JSEngine, MozJSRuntime}; +//! use js::prelude::*; //! //! let engine = JSEngine::init().unwrap(); -//! let runtime = Runtime::new(engine.handle()); +//! let runtime = MozJSRuntime::new(engine.handle()); //! // ... enter a realm, evaluate scripts, etc. //! ``` //! @@ -291,6 +291,9 @@ pub type WeakMap<'s> = Stack<'s, collections::weak_map::WeakMap>; /// A scope-rooted handle to a JavaScript `Function` object. pub type Function<'s> = Stack<'s, function::Function>; +/// A scope-rooted handle to any callable JavaScript object. +pub type Callable<'s> = Stack<'s, function::Callable>; + /// A scope-rooted handle to a JavaScript string. /// /// Unlike `Object`, `Array`, and `Function`, JS strings are not JS objects — diff --git a/crates/js/src/module.rs b/crates/js/src/module.rs index b4ffbf36..1736ed5f 100644 --- a/crates/js/src/module.rs +++ b/crates/js/src/module.rs @@ -249,7 +249,7 @@ unsafe extern "C" fn load_rejected( pub fn load_requested_modules(scope: &Scope<'_>, module_record: Object) -> Result<(), ExnThrown> { // `LoadRequestedModules` takes no host-defined data. The callbacks receive // this value back and ignore it. - let host_defined = scope.root_value(UndefinedValue()); + let host_defined = HandleValue::undefined(); // A load hook may itself compile and load a module graph, so save and // restore any enclosing call's outcome around this one. let enclosing = load_outcome(|outcome| outcome.replace(None)); diff --git a/crates/js/src/object.rs b/crates/js/src/object.rs index 8991b059..28a2a0ec 100644 --- a/crates/js/src/object.rs +++ b/crates/js/src/object.rs @@ -364,6 +364,27 @@ impl<'s> Stack<'s, Object> { ExnThrown::check(ok) } + /// Define a property by key with a JS value and attribute flags. + #[inline] + pub fn define_value_by_id<'v>( + &self, + scope: &'v Scope<'_>, + id: HandleId, + value: impl ToJSVal<'v>, + attrs: c_uint, + ) -> Result<(), ExnThrown> { + let ok = unsafe { + wrappers2::JS_DefinePropertyById2( + scope.cx_mut(), + self.handle(), + id, + value.to_jsval_throwing(scope)?, + attrs, + ) + }; + ExnThrown::check(ok) + } + /// Define an element by index with attribute flags. /// /// Installs an own data property without consulting the prototype chain or invoking setters. @@ -613,16 +634,6 @@ impl<'s> Stack<'s, Object> { Self::from_handle(objp.handle()).ok_or(ExnThrown) } - /// Set an immutable prototype on this object. - pub fn set_immutable_prototype(&self, scope: &Scope<'_>) -> Result { - let mut succeeded = false; - let ok = unsafe { - wrappers2::JS_SetImmutablePrototype(scope.cx_mut(), self.handle(), &mut succeeded) - }; - ExnThrown::check(ok)?; - Ok(succeeded) - } - /// Assign all enumerable own properties from `src` to this object. pub fn assign(&self, scope: &Scope<'_>, src: Stack<'_, Object>) -> Result<(), ExnThrown> { let ok = unsafe { wrappers2::JS_AssignObject(scope.cx_mut(), self.handle(), src.handle()) }; @@ -686,6 +697,7 @@ impl<'s, 'v> FromJSVal<'s, 'v> for Stack<'s, Object> { /// # Safety /// /// - `obj` must be a valid, non-null JS object pointer. +#[inline] pub unsafe fn get_object_class(obj: *mut JSObject) -> *const JSClass { mozjs::rust::get_object_class(obj) } @@ -714,6 +726,31 @@ pub unsafe fn get_reserved_slot(obj: *mut JSObject, slot: u32) -> Value { val } +/// Get the value stored in a reserved slot of a native (non-proxy) JS object. +/// +/// Use this instead of [`get_reserved_slot`] for objects known to be native: it is inlined. +/// +/// # Safety +/// +/// - `obj` must be a valid, non-null pointer to a native JS object. +/// - `slot` must be less than the number of reserved slots for the object's class, and less than +/// 16. +#[inline] +pub unsafe fn get_native_reserved_slot(obj: *mut JSObject, slot: u32) -> Value { + use mozjs::jsapi::JS::shadow; + debug_assert!(slot < reserved_slot_count(obj)); + debug_assert!((slot as usize) < shadow::Object_MAX_FIXED_SLOTS); + // Native shape kinds have the low kind bit set. + debug_assert!( + (*(*(obj as *const shadow::Object)).shape).immutableFlags >> shadow::Shape_KIND_SHIFT & 1 + == 1 + ); + // The first 16 reserved slots of a native object are fixed slots, which directly follow the + // object header. + let fixed_slots = (obj as *const u8).add(size_of::()) as *const Value; + *fixed_slots.add(slot as usize) +} + /// Set a value in a reserved slot of a JS object. /// /// # Safety diff --git a/crates/js/src/promise.rs b/crates/js/src/promise.rs index 4868917e..3d48a3fb 100644 --- a/crates/js/src/promise.rs +++ b/crates/js/src/promise.rs @@ -117,10 +117,12 @@ impl<'s> Stack<'s, Promise> { std::ptr::from_ref(&KEY) as usize, |scope| { let p = Self::new_resolved_with_value(scope, crate::value::undefined())?; - Object::from_value(scope, p.as_value()).map_err(|_| ExnThrown) + Ok(*p) }, )?; - obj.cast::().map_err(|_| ExnThrown) + Ok(obj + .cast::() + .expect("the shared resolved promise is a Promise")) } /// Check whether an object is a `Promise`. @@ -330,6 +332,7 @@ impl<'s> Stack<'s, Promise> { let vector = mozjs::rust::RootedObjectVectorWrapper::new(scope.cx_mut()); for promise in promises { if !vector.append(promise.get()) { + crate::exception::report_out_of_memory(scope); return Err(ExnThrown); } } @@ -375,6 +378,8 @@ impl<'s> Stack<'s, Promise> { crate::gc::handle::deref_to_object!(Promise); +crate::gc::handle::from_jsval_via_cast!(Promise, c"Value isn't a Promise"); + /// The WebIDL [`Promise`](https://webidl.spec.whatwg.org/#idl-promise) type: /// a `Promise<'s>` that carries the type its value is checked against. /// @@ -419,9 +424,10 @@ impl<'s, T> PromiseOf<'s, T> { // `react` to the wrapper with the conversion to `T` as its fulfillment steps. Resolving // with a non-thenable settles `wrapped` immediately, so the check runs in the next // microtask; resolving with a thenable defers it to that thenable's resolution. - // TODO: check if we can use a singleton callback per type here. let on_fulfilled = - crate::Function::new_callback(scope, c"", 1, check, crate::value::undefined())?; + crate::class::get_or_init_shared_function(scope, check as usize, |scope| { + crate::Function::new_callback(scope, c"", 1, check, crate::value::undefined()) + })?; // A `None` rejection reaction forwards the rejection unchanged, which is what the spec's // `onRejectedSteps` amount to for a caller that supplies no rejection steps. let checked = wrapped.then(scope, Some(*on_fulfilled), None)?; @@ -669,23 +675,35 @@ pub fn cancel_pending_future(promise: Stack) -> bool { removed } -/// Drop every pending async-promise future owned by `owner` (or unowned, id 0). +/// Drop every pending async-promise future owned by `owner`, which must not be 0. /// /// Called when an event loop terminates with futures still in flight (e.g. WPT mode stops the loop /// once a test's completion callback fires, even though a cancelled/disturbed body read is still /// pending). Dropping each future cancels its in-flight host I/O and unregisters its /// `RootedHeap` from the engine's extra-roots tracer while the `JSContext` is still alive. /// Otherwise engine teardown's `finishRoots` would trace a now-freed box and crash. +/// +/// Unowned futures (id 0, spawned with no loop active) are left in place: any loop may be +/// waiting on one. [`cancel_all_pending_futures`] drops them. pub fn cancel_pending_futures_for(owner: u64) { + debug_assert_ne!(owner, 0, "unowned futures belong to no single loop"); let drop_owned = |queue: &RefCell>| { queue .borrow_mut() - .retain(|(future_owner, _, _)| *future_owner != owner && *future_owner != 0); + .retain(|(future_owner, _, _)| *future_owner != owner); }; pending_futures(drop_owned); active_futures(drop_owned); } +/// Drop every pending async-promise future, whatever its owner. For runtime teardown, which must +/// call it while the `JSContext` is still alive, for the reason [`cancel_pending_futures_for`] +/// gives. +pub fn cancel_all_pending_futures() { + pending_futures(|f| f.borrow_mut().clear()); + active_futures(|a| a.borrow_mut().clear()); +} + /// Whether any async-promise future owned by `owner` (or unowned) is pending (spawned but not yet /// settled). /// @@ -744,11 +762,7 @@ pub fn poll_pending_futures( /// Settle promises whose futures completed (from [`poll_pending_futures`]): resolve/reject each and /// drain the reactions they queue. Must be called with the owning event loop active so a reaction /// that touches the loop (a timer, releasing interest) reaches the right loop. -/// -/// # Safety -/// -/// `js_cx` must be a valid `JSContext` whose current realm is entered. -pub unsafe fn settle_completed_futures(scope: &Scope<'_>, completed: Vec) { +pub fn settle_completed_futures(scope: &Scope<'_>, completed: Vec) { if completed.is_empty() { return; } @@ -775,7 +789,7 @@ fn settle_promise(scope: &Scope<'_>, promise: Stack, outcome: PromiseOu } Err(_) => { // The resolve callback left a pending exception; reject with it. - if let Ok(error) = crate::exception::take_pending(scope) { + if let Some(error) = crate::exception::take_pending(scope) { let _ = promise.reject(scope, error); } } @@ -783,7 +797,7 @@ fn settle_promise(scope: &Scope<'_>, promise: Stack, outcome: PromiseOu } PromiseOutcome::Reject(message) => { let _ = crate::error::TypeError(message).throw(scope); - if let Ok(error) = crate::exception::take_pending(scope) { + if let Some(error) = crate::exception::take_pending(scope) { let _ = promise.reject(scope, error); } } diff --git a/crates/js/src/regexp.rs b/crates/js/src/regexp.rs index c6b5b51d..35f8d9cf 100644 --- a/crates/js/src/regexp.rs +++ b/crates/js/src/regexp.rs @@ -142,3 +142,5 @@ impl<'s> Stack<'s, RegExp> { } crate::gc::handle::deref_to_object!(RegExp); + +crate::gc::handle::from_jsval_via_cast!(RegExp, c"Value isn't a RegExp"); diff --git a/crates/js/src/stack.rs b/crates/js/src/stack.rs index 778ab224..dd6a9fef 100644 --- a/crates/js/src/stack.rs +++ b/crates/js/src/stack.rs @@ -40,6 +40,35 @@ pub unsafe fn capture_current_stack( ExnThrown::check(ok) } +/// Capture up to `max_frames` frames of the current JavaScript call stack as a `SavedFrame` +/// object. Returns `None` if no script is running. +pub fn capture_saved_frame<'s>( + scope: &'s Scope<'_>, + max_frames: u32, +) -> Result>, ExnThrown> { + let mut capture = std::mem::MaybeUninit::::uninit(); + // SAFETY: the glue function initializes `capture`, which holds no pointers into itself and + // so can be moved afterwards. + let mut capture = unsafe { + mozjs::jsapi::JS_StackCapture_MaxFrames(max_frames, capture.as_mut_ptr()); + capture.assume_init() + }; + // SAFETY: the pointer is only used to root `frame` in this frame. + let raw = unsafe { scope.cx_mut().raw_cx() }; + mozjs::rooted!(in(raw) let mut frame = std::ptr::null_mut::()); + // SAFETY: `capture` was initialized above. + unsafe { + capture_current_stack( + scope, + frame.handle_mut(), + &mut capture, + HandleObject::null(), + )? + }; + // SAFETY: `frame` is null or a live `SavedFrame`, which `from_raw` roots in `scope`. + Ok(unsafe { crate::Object::from_raw(scope, frame.get()) }) +} + /// Build a string representation of a stack trace. /// /// `principals` controls security-filtered access — pass `std::ptr::null_mut()` diff --git a/crates/js/src/string.rs b/crates/js/src/string.rs index fdb42fe5..3f6c2f14 100644 --- a/crates/js/src/string.rs +++ b/crates/js/src/string.rs @@ -30,14 +30,53 @@ use std::ffi::CStr; use std::os::raw::c_char; use std::ptr::NonNull; -use crate::conversion::Utf8Chars; +use super::error::ExnThrown; +use crate::conversion::{ConversionError, ToJSVal, Utf8Chars}; use crate::gc::scope::Scope; use mozjs::gc::Handle; use mozjs::jsapi::{JSLinearString, JSString}; use mozjs::jsval::StringValue; -use mozjs::rust::wrappers2::{self, JS_NewStringCopyUTF8N}; +use mozjs::rust::wrappers2; +use smallvec::SmallVec; -use super::error::ExnThrown; +/// Create an unrooted JS string holding `s`, in the smallest encoding that represents it. Returns +/// null with an exception pending if allocation fails. +/// +/// ASCII text is copied as Latin-1 directly. Other text that fits in Latin-1 is converted with +/// `encoding_rs`' vectorized routine before SpiderMonkey copies it, since SpiderMonkey's own +/// UTF-8 to Latin-1 conversion (`JS_NewStringCopyUTF8N`) converts one code point at a time. Text +/// that needs UTF-16 goes through `JS_NewStringCopyUTF8N`, which converts it directly into the +/// string's buffer. +pub(crate) fn new_js_string(scope: &Scope<'_>, s: &str) -> *mut JSString { + let bytes = s.as_bytes(); + let ascii = encoding_rs::Encoding::ascii_valid_up_to(bytes); + if ascii == bytes.len() { + // SAFETY: ASCII bytes are valid Latin-1 characters. + return unsafe { + wrappers2::JS_NewStringCopyN(scope.cx_mut(), bytes.as_ptr().cast(), bytes.len()) + }; + } + if encoding_rs::mem::is_str_latin1(&s[ascii..]) { + // The Latin-1 text has fewer characters than `s` has bytes. + let mut latin1: SmallVec<[u8; 256]> = SmallVec::from_elem(0, bytes.len()); + let written = encoding_rs::mem::convert_utf8_to_latin1_lossy(bytes, &mut latin1); + // SAFETY: `latin1[..written]` holds Latin-1 characters. + return unsafe { + wrappers2::JS_NewStringCopyN(scope.cx_mut(), latin1.as_ptr().cast(), written) + }; + } + let utf8 = Utf8Chars::from(s); + // SAFETY: `utf8` borrows `s`, which outlives the call. + unsafe { wrappers2::JS_NewStringCopyUTF8N(scope.cx_mut(), &*utf8 as *const _) } +} + +/// The characters of a JS string, in the representation the string stores them in. +pub enum StrChars<'a> { + /// One byte per character, U+0000 to U+00FF. + Latin1(&'a [u8]), + /// UTF-16 code units, which may include unpaired surrogates. + TwoByte(&'a [u16]), +} // --------------------------------------------------------------------------- // Str — scope-rooted JS string handle @@ -69,13 +108,20 @@ impl<'s> Str<'s> { /// this is the standard wrapper for their results. fn from_mozjs_rval(scope: &'s Scope<'_>, ptr: *mut JSString) -> Result { NonNull::new(ptr) - .map(|p| Str::from_handle(scope.root_string(p))) + .map(|p| Str::from_non_null(scope, p)) .ok_or(ExnThrown) } - /// Wrap a rooted string handle. - pub fn from_handle(handle: Handle<'s, *mut JSString>) -> Self { - Str { handle } + /// Root `ptr` in `scope`. + pub fn from_non_null(scope: &'s Scope<'_>, ptr: NonNull) -> Self { + Str { + handle: scope.root_string(ptr), + } + } + + /// Wrap a rooted string handle, or `None` if it is null. + pub fn from_handle(handle: Handle<'s, *mut JSString>) -> Option { + (!handle.get().is_null()).then_some(Str { handle }) } /// Get the underlying rooted handle. @@ -100,9 +146,17 @@ impl<'s> Str<'s> { /// Create a new JS string from a Rust `&str`. pub fn from_str(scope: &'s Scope<'_>, s: &str) -> Result { - let s = Utf8Chars::from(s); - let jsstr = unsafe { JS_NewStringCopyUTF8N(scope.cx_mut(), &*s as *const _) }; - Self::from_mozjs_rval(scope, jsstr) + Self::from_mozjs_rval(scope, new_js_string(scope, s)) + } + + /// Create a new JS string from Latin-1 characters, one byte each. + // TODO: explore whether we can use the non-copying `JS_NewStrin*` variants here and below. + pub fn from_latin1(scope: &'s Scope<'_>, chars: &[u8]) -> Result { + // SAFETY: every byte is a Latin-1 character. + let js_str = unsafe { + wrappers2::JS_NewStringCopyN(scope.cx_mut(), chars.as_ptr().cast(), chars.len()) + }; + Self::from_mozjs_rval(scope, js_str) } /// Create a new JS string from a null-terminated C string. @@ -125,17 +179,15 @@ impl<'s> Str<'s> { let ptr = unsafe { wrappers2::JS_GetEmptyString(scope.cx()) }; // SAFETY: The empty string is always present in a valid runtime. let nn = unsafe { NonNull::new_unchecked(ptr) }; - Str::from_handle(scope.root_string(nn)) + Str::from_non_null(scope, nn) } /// Convert a JS value to a string via the `ToString` abstract operation. /// /// Throws a `TypeError` for symbols — unlike `String(value)`, which /// special-cases them to their description. - pub fn from_value( - scope: &'s Scope<'_>, - val: mozjs::gc::HandleValue, - ) -> Result { + pub fn from_value(scope: &'s Scope<'_>, val: impl ToJSVal<'s>) -> Result { + let val = val.to_jsval_throwing(scope)?; let js_str = unsafe { mozjs::rust::ToString(scope.cx_mut(), val) }; Self::from_mozjs_rval(scope, js_str) } @@ -146,9 +198,8 @@ impl<'s> Str<'s> { /// Encode this string to UTF-8, returning an owned Rust [`String`]. pub fn to_utf8(&self, scope: &Scope<'_>) -> Result { - NonNull::new(self.as_raw()) - .map(|nn| crate::conversion::jsstr_to_string(scope, nn)) - .ok_or(ExnThrown) + let nn = NonNull::new(self.as_raw()).expect("a Str is never null"); + Ok(crate::conversion::jsstr_to_string(scope, nn)) } /// Get a single character (code unit) at the given index. @@ -263,6 +314,37 @@ impl<'s> Str<'s> { unsafe { mozjs::jsapi::JS_StringIsLinear(self.as_raw()) } } + /// Call `f` with this string's characters, flattening the string first if it is a rope. + /// + /// # Safety + /// + /// A garbage collection can move the characters, so `f` must not call into SpiderMonkey. + pub unsafe fn with_chars( + &self, + scope: &Scope<'_>, + f: impl FnOnce(StrChars<'_>) -> R, + ) -> Result { + let linear = self.ensure_linear(scope)?.as_ptr().cast::(); + let mut length = 0; + // SAFETY: `linear` is a live linear string, whose characters stay in place until the next + // call into SpiderMonkey, which `f` does not make. + unsafe { + if mozjs::jsapi::JS_DeprecatedStringHasLatin1Chars(linear) { + let chars = + wrappers2::JS_GetLatin1StringCharsAndLength(scope.cx(), linear, &mut length); + Ok(f(StrChars::Latin1(std::slice::from_raw_parts( + chars, length, + )))) + } else { + let chars = + wrappers2::JS_GetTwoByteStringCharsAndLength(scope.cx(), linear, &mut length); + Ok(f(StrChars::TwoByte(std::slice::from_raw_parts( + chars, length, + )))) + } + } + } + /// Ensure this string has a linear (flat) representation. /// /// Returns the linear string pointer, or an error if allocation fails. @@ -322,6 +404,29 @@ impl<'s> Str<'s> { } } +/// The value converted with `ToString`. +/// +/// Used for WebIDL `DOMString` types. +impl<'s, 'v> crate::conversion::FromJSVal<'s, 'v> for Str<'s> { + type Config = (); + fn from_jsval( + scope: &'s Scope<'s>, + val: mozjs::gc::HandleValue<'v>, + _: (), + ) -> Result { + // SAFETY: `val` is rooted. + let js_str = unsafe { mozjs::rust::ToString(scope.cx_mut(), val) }; + Str::from_mozjs_rval(scope, js_str).map_err(|_| ConversionError::ExnPending) + } +} + +impl ToJSVal<'_> for Str<'_> { + #[inline] + fn to_jsval_raw(&self, _scope: &Scope<'_>) -> Result { + Ok(self.as_value()) + } +} + impl<'s> std::fmt::Debug for Str<'s> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("JSString") diff --git a/crates/js/src/try_catch.rs b/crates/js/src/try_catch.rs index 984a9556..8f8f17f6 100644 --- a/crates/js/src/try_catch.rs +++ b/crates/js/src/try_catch.rs @@ -26,10 +26,9 @@ use crate::gc::scope::{InnerScope, Scope}; use mozjs::gc::HandleValue; use mozjs::jsapi::ExceptionStackBehavior; -use mozjs::jsval::UndefinedValue; -use mozjs::rust::wrappers2; use super::error::{CapturedError, ExnThrown}; +use crate::exception; /// A scoped exception handler. /// @@ -52,8 +51,7 @@ impl<'a> TryCatch<'a> { /// Records whether an exception is already pending. Operations on the /// returned scope's inner will be monitored for new exceptions. pub fn new(scope: &'a Scope<'_>) -> Self { - // SAFETY: Scope guarantees a valid context pointer. - let had_exception = unsafe { wrappers2::JS_IsExceptionPending(scope.cx()) }; + let had_exception = exception::is_pending(scope); TryCatch { inner: scope.inner_scope(), had_exception, @@ -68,27 +66,14 @@ impl<'a> TryCatch<'a> { /// Returns `true` if an exception was caught (i.e., an exception is pending /// that was not pending when this `TryCatch` was created). pub fn has_caught(&self) -> bool { - // SAFETY: Scope guarantees a valid context pointer. - let pending = unsafe { wrappers2::JS_IsExceptionPending(self.inner.cx()) }; - pending && !self.had_exception + exception::is_pending(&self.inner) && !self.had_exception } /// Get the pending exception value, if any. /// /// Returns `None` if no exception is pending. Does NOT clear the exception. pub fn exception(&self) -> Option> { - // SAFETY: Scope guarantees a valid context with an entered realm. - unsafe { - if !wrappers2::JS_IsExceptionPending(self.inner.cx()) { - return None; - } - let mut exc = self.inner.root_value_mut(UndefinedValue()); - if wrappers2::JS_GetPendingException(self.inner.cx_mut(), exc.reborrow()) { - Some(exc.handle()) - } else { - None - } - } + exception::get_pending(&self.inner) } /// Capture the pending exception as a [`CapturedError`], clearing it. @@ -102,8 +87,7 @@ impl<'a> TryCatch<'a> { /// Clear the pending exception without inspecting it. pub fn reset(&self) { - // SAFETY: Scope guarantees a valid context pointer. - unsafe { wrappers2::JS_ClearPendingException(self.inner.cx()) }; + exception::clear(&self.inner); } /// Re-set the given exception value as pending without capturing the stack. @@ -111,13 +95,6 @@ impl<'a> TryCatch<'a> { /// This is useful when you've inspected an exception and want to let it /// propagate. Pass the handle obtained from [`exception()`](Self::exception). pub fn rethrow(&self, exc: HandleValue<'_>) { - // SAFETY: Scope guarantees a valid context with an entered realm. - unsafe { - wrappers2::JS_SetPendingException( - self.inner.cx_mut(), - exc, - ExceptionStackBehavior::DoNotCapture, - ); - }; + exception::set_pending(&self.inner, exc, ExceptionStackBehavior::DoNotCapture); } } diff --git a/crates/js/src/typedarray.rs b/crates/js/src/typedarray.rs index 435671b8..9101e0d2 100644 --- a/crates/js/src/typedarray.rs +++ b/crates/js/src/typedarray.rs @@ -269,24 +269,30 @@ impl<'s> Stack<'s, ArrayBuffer> { /// /// This is the WHATWG abstract operation `TransferArrayBuffer`: the returned /// buffer holds the receiver's bytes and the receiver is left detached - /// (zero-length). The receiver must not already be detached. - /// - /// The contents are copied rather than moved. A zero-copy steal/adopt is - /// possible (`StealArrayBufferContents` + `NewArrayBufferWithContents`) but - /// the adopt half lives in an awkward-to-reach glue module; the copy is - /// correct and the streams paths transfer small chunks. + /// (zero-length). The receiver must not already be detached. The contents are + /// moved, not copied. pub fn transfer(self, scope: &'s Scope<'_>) -> Result { - // `CopyArrayBuffer` of a zero-length buffer can yield null (no pending - // exception), which would surface as a spurious `ExnThrown`; hand back a - // fresh empty buffer instead. The receiver is still detached, matching - // `TransferArrayBuffer`'s observable effect. - if self.byte_length() == 0 { + let length = self.byte_length(); + // A zero-length buffer may have no contents to steal. Hand back a fresh empty buffer + // instead. The receiver is still detached, matching `TransferArrayBuffer`'s observable + // effect. + if length == 0 { self.detach(scope)?; return Self::new(scope, 0); } - let copy = Self::copy_from(scope, self.handle())?; - self.detach(scope)?; - Ok(copy) + // SAFETY: `self` is a rooted `ArrayBuffer`. Stealing detaches it and returns its + // contents, or null with an exception pending. + let contents = + unsafe { wrappers2::StealArrayBufferContents(scope.cx_mut(), self.handle()) }; + if contents.is_null() { + return Err(ExnThrown); + } + // SAFETY: `contents` holds `length` bytes allocated by the JS allocator, as + // `StealArrayBufferContents` returns them. The call takes ownership of `contents`, + // including freeing it if creating the buffer fails. + let obj = + unsafe { wrappers2::NewArrayBufferWithContents(scope.cx_mut(), length, contents) }; + root_or_throw(scope, obj) } /// Clone the region `[byte_offset, byte_offset + length)` of this buffer into diff --git a/crates/js/src/value.rs b/crates/js/src/value.rs index 39e31a57..1114d4cd 100644 --- a/crates/js/src/value.rs +++ b/crates/js/src/value.rs @@ -169,46 +169,63 @@ pub unsafe fn from_string_raw(s: *mut JSString) -> JSVal { /// Rooted backing buffer for a [`HandleValueArray`]. /// -/// `Function::call*` and `Array::with_contents` take `&[HandleValue]` and -/// copy the values in here. The copies are traced for as long as the rooted -/// guard lives. +/// `Function::call*` and `Array::with_contents` take `&[impl ToJSVal]` and +/// convert the values in here. The buffer is rooted before the first +/// conversion, so a conversion that allocates cannot invalidate the values +/// converted before it. The values are traced for as long as the rooted guard +/// lives. /// /// ```ignore -/// let mut args_root = ValueArrayRooter::new(args); -/// let args = args_root.root(scope); +/// let mut args_root = ValueArrayRooter::new(args.len()); +/// let args = args_root.root(scope, args)?; /// some_jsapi_call(scope.cx_mut(), &args.handles()); /// ``` -pub(crate) struct ValueArrayRooter(mozjs::gc::CustomAutoRooter>); +pub(crate) struct ValueArrayRooter(mozjs::gc::CustomAutoRooter); -impl<'s> ValueArrayRooter { - pub(crate) fn new( - scope: &'s crate::gc::scope::Scope<'_>, - values: &[impl ToJSVal<'s>], - ) -> Result { - let mut raw_values: Vec = Vec::with_capacity(values.len()); - for v in values { - raw_values.push(v.to_jsval_raw_throwing(scope)?); +/// The values a [`ValueArrayRooter`] holds. Up to [`INLINE_VALUES`] live inline, on the stack. +pub(crate) struct ValueBuffer(smallvec::SmallVec<[JSVal; INLINE_VALUES]>); + +/// How many values a [`ValueBuffer`] holds without allocating. +const INLINE_VALUES: usize = 8; + +// SAFETY: every element is a `JSVal`, and `trace` is only invoked with an engine provided tracer. +unsafe impl mozjs::gc::CustomTrace for ValueBuffer { + fn trace(&self, trc: *mut mozjs::jsapi::JSTracer) { + for value in &self.0 { + value.trace(trc); } - Ok(Self(mozjs::gc::CustomAutoRooter::new(raw_values))) + } +} + +impl ValueArrayRooter { + pub(crate) fn new(capacity: usize) -> Self { + Self(mozjs::gc::CustomAutoRooter::new(ValueBuffer( + smallvec::SmallVec::with_capacity(capacity), + ))) } - pub(crate) fn root<'a>( + pub(crate) fn root<'a, 's>( &'a mut self, - scope: &crate::gc::scope::Scope<'_>, - ) -> RootedValueArray<'a> { + scope: &'s crate::gc::scope::Scope<'_>, + values: &[impl ToJSVal<'s>], + ) -> Result, ExnThrown> { // SAFETY: adding the rooter to the root stack performs no GC. - RootedValueArray(self.0.root(unsafe { scope.raw_cx_no_gc() })) + let mut rooted = RootedValueArray(self.0.root(unsafe { scope.raw_cx_no_gc() })); + for v in values { + rooted.0 .0.push(v.to_jsval_raw_throwing(scope)?); + } + Ok(rooted) } } /// A rooted view over a [`ValueArrayRooter`]'s values; see there. -pub(crate) struct RootedValueArray<'a>(mozjs::gc::CustomAutoRooterGuard<'a, Vec>); +pub(crate) struct RootedValueArray<'a>(mozjs::gc::CustomAutoRooterGuard<'a, ValueBuffer>); impl RootedValueArray<'_> { pub(crate) fn handles(&self) -> mozjs::jsapi::HandleValueArray { mozjs::jsapi::HandleValueArray { - length_: self.0.len(), - elements_: self.0.as_ptr(), + length_: self.0 .0.len(), + elements_: self.0 .0.as_ptr(), } } } diff --git a/crates/libstarling/src/lib.rs b/crates/libstarling/src/lib.rs index 779bd396..e337b79e 100644 --- a/crates/libstarling/src/lib.rs +++ b/crates/libstarling/src/lib.rs @@ -16,14 +16,6 @@ mod serve_common; #[cfg(target_arch = "wasm32")] pub mod serve_wasm; -/// Sleep for `duration` on the WASIp3 monotonic clock — the timer every event-loop driver on wasm32 -/// awaits, here rather than repeated at each of them. -#[cfg(target_arch = "wasm32")] -pub(crate) async fn wasm_sleep(duration: std::time::Duration) { - let nanos = duration.as_nanos().min(u64::MAX as u128) as u64; - wasip3::clocks::monotonic_clock::wait_for(nanos).await; -} - /// Register all built-in global initializers. /// /// This must be called before `Runtime::init()` to ensure built-in web @@ -34,6 +26,8 @@ pub fn register_builtins() { runtime::register_global_initializer(web_url::add_to_global); runtime::register_global_initializer(web_fetch::add_to_global); runtime::register_global_initializer(fetch_event::add_to_global); + // SAFETY: a global initializer runs with the new global's realm entered, and `global` is + // that global. runtime::register_global_initializer(|scope, global| unsafe { cpp_builtins::install(scope.cx_mut().raw_cx(), global.handle()); }); @@ -99,7 +93,7 @@ pub async fn run(config: config::RuntimeConfig) -> Result<(), String> { ); } - let (runtime, mut invocation) = match core_runtime::setup(config)? { + let (runtime, invocation) = match core_runtime::setup(config)? { Some(pair) => pair, None => return Ok(()), }; @@ -120,12 +114,12 @@ pub async fn run(config: config::RuntimeConfig) -> Result<(), String> { // SAFETY: `scope` (and the `Runtime` it borrows from) lives until the // explicit `drop(scope)` below. let raw_cx = unsafe { scope.raw_cx_no_gc() }; - let el = invocation.event_loop_mut(); + let el = invocation.event_loop(); // SAFETY: `raw_cx` is valid for the duration of the await — `scope` // keeps the `Runtime` alive and the realm entered. unsafe { - run_to_completion(raw_cx, el, wasm_sleep).await; + run_to_completion(raw_cx, el, platform::clock::sleep).await; } drop(scope); @@ -137,7 +131,7 @@ pub async fn run(config: config::RuntimeConfig) -> Result<(), String> { #[cfg(not(target_arch = "wasm32"))] fn drive_event_loop_native( runtime: std::rc::Rc, - mut invocation: invocation::InvocationState, + invocation: invocation::InvocationState, ) -> Result<(), String> { // Enable both the timer and IO drivers: timers back `setTimeout`, and IO backs // `fetch`'s async HTTP transport (which uses tokio TCP sockets). @@ -159,7 +153,7 @@ fn drive_event_loop_native( // Tasks running inside `run_to_completion` (timers, promise reactions, // etc.) need an active realm to call into JS. let scope = runtime.default_global(); - let el = invocation.event_loop_mut(); + let el = invocation.event_loop(); tokio_rt.block_on(async { // SAFETY: the scope (and its Runtime) must outlive this future. diff --git a/crates/libstarling/src/serve_common.rs b/crates/libstarling/src/serve_common.rs index dbbea28b..14b45dd5 100644 --- a/crates/libstarling/src/serve_common.rs +++ b/crates/libstarling/src/serve_common.rs @@ -9,12 +9,12 @@ use core_runtime::event_loop::{run_until, with_event_loop, EventLoop}; use core_runtime::invocation::InvocationState; use fetch_event::fetch_event::FetchEvent; -use js::error::ThrowException; +use js::conversion::ToJSVal; use web_globals::events::algorithms::ScriptStackState; use std::time::{Duration, Instant}; use web_fetch::request::Request; -use web_globals::dom_exception::DOMExceptionError; +use web_globals::dom_exception::DOMException; use web_globals::signals::abort_controller::{AbortController, AbortControllerImpl}; pub(crate) const NO_FETCH_LISTENER: &str = @@ -201,8 +201,9 @@ pub(crate) fn signal_request_abort( message: &str, ) { with_event_loop(event_loop, |_| { - let _ = DOMExceptionError::new(name, message).throw(scope); - let reason = js::exception::take_pending_or_undefined(scope); + let reason = DOMException::new(scope, message.into(), name.into()) + .and_then(|exception| exception.to_jsval_throwing(scope)) + .unwrap_or_else(|_| js::exception::take_pending_or_undefined(scope)); if abort_controller .abort(scope, reason, ScriptStackState::Empty) .is_err() @@ -240,7 +241,7 @@ fn header_list(headers: &http::HeaderMap) -> Vec<(String, String)> { /// the transport layer, and sending a `500` response for a `network error`. pub(crate) async fn dispatch_fetch<'s, S, F>( scope: &'s js::gc::scope::Scope<'_>, - invocation: &mut InvocationState, + invocation: &InvocationState, method: String, url: String, headers: http::HeaderMap, diff --git a/crates/libstarling/src/serve_native.rs b/crates/libstarling/src/serve_native.rs index cac244fe..73a4d7ba 100644 --- a/crates/libstarling/src/serve_native.rs +++ b/crates/libstarling/src/serve_native.rs @@ -29,6 +29,7 @@ use futures_util::stream::{FuturesUnordered, StreamExt}; use hyper::body::{Body, Frame, Incoming}; use js::gc::scope::{EnteredRealm, RootScope}; use platform::http::OutgoingBody; +use std::cell::Cell; use std::future::Future; use std::pin::Pin; use std::rc::Rc; @@ -110,7 +111,7 @@ pub fn serve_with_shutdown( // its side effects) one extra time, in a global that goes on to serve nothing, so that mode // only sets up the runtime itself. let (runtime, startup) = if config.serve_isolated { - (Runtime::init(&config), None) + (Runtime::init(&config)?, None) } else { let (runtime, invocation, evaluation) = core_runtime::setup_for_serve(config.clone())?; (runtime, Some((invocation, evaluation))) @@ -177,7 +178,7 @@ async fn run_server( unsafe { run_until_evaluated( raw_cx, - invocation.state_mut().event_loop(), + invocation.state().event_loop(), tokio::time::sleep, evaluation, ) @@ -195,7 +196,7 @@ async fn run_server( // for each request, there is no single realm a background loop could be stepped in. let background_loop = background .as_mut() - .map(|(invocation, _)| invocation.state_mut().event_loop()); + .map(|(invocation, _)| invocation.state().event_loop()); // We clone rather than move the caller's handle. `server_realm` points into this runtime, and // a local holding the last reference would free the runtime on the way out ahead of @@ -351,19 +352,19 @@ impl RequestLoop { )) } - fn state_mut(&mut self) -> &mut InvocationState { - self.0.state_mut() + fn state(&self) -> &InvocationState { + self.0.state() } - fn event_loop(&mut self) -> &EventLoop { - self.0.state_mut().event_loop() + fn event_loop(&self) -> &EventLoop { + self.0.state().event_loop() } } impl Drop for RequestLoop { fn drop(&mut self) { // `cancel_pending_futures` is idempotent, so it's safe to always run this. - self.0.state_mut().event_loop().cancel_pending_futures(); + self.0.state().event_loop().cancel_pending_futures(); } } @@ -513,12 +514,12 @@ async fn serve_request<'a>( // request's own work, so its end-to-end deadline starts here. let clock = context.timeouts.start_clock(); - let mut invocation = RequestLoop::new(&context.runtime); + let invocation = RequestLoop::new(&context.runtime); let realm = context.enter_request_realm(); if let Some(script) = context.per_request_script() { if let Err(status) = - evaluate_into_request_global(context, &mut invocation, script, &clock).await + evaluate_into_request_global(context, &invocation, script, &clock).await { return Ok(status_response(status)); } @@ -541,7 +542,7 @@ async fn serve_request<'a>( let scope = unsafe { context.runtime.scope() }; let dispatch = crate::serve_common::dispatch_fetch( &scope, - invocation.state_mut(), + invocation.state(), head.method.to_string(), request_url(&head), std::mem::take(&mut head.headers), @@ -603,7 +604,7 @@ async fn serve_request<'a>( /// place before the event is dispatched. `Err` is the status to respond with instead. async fn evaluate_into_request_global( context: &ServeContext, - invocation: &mut RequestLoop, + invocation: &RequestLoop, script: &ContentScript, clock: &RequestClock, ) -> Result<(), u16> { @@ -662,7 +663,7 @@ async fn evaluate_into_request_global( async fn finish_request( _realm: Option>, context: &ServeContext, - mut invocation: RequestLoop, + invocation: RequestLoop, abort_controller: Option>, producer: Option, mut incoming_pump: BodyPump, @@ -1211,25 +1212,27 @@ async fn send_response_body( // as long as the body needed it. If the loop finishes first, nothing can produce // another chunk, and waiting on the drain would hang the connection. That happens // once a guest reaches a state in which no more async work is pending, but the body - // `ReadableStream` hasn't been fully served. + // `ReadableStream` hasn't been fully served. The drain then goes on with the channel's + // existing contents, potentially including a chunk it is still handing to hyper. OutgoingBody::Stream(mut receiver) => { - let forwarded = futures_lite::future::or( - async { - Some(forward_stream(&mut receiver, &mut chunks, &mut remaining).await) - }, - async { + let loop_done = Cell::new(false); + let mut forwarding = std::pin::pin!(forward_stream( + &mut receiver, + &mut chunks, + &mut remaining, + &loop_done + )); + let forwarded = + futures_lite::future::or(async { Some((&mut forwarding).await) }, async { unsafe { run_to_completion(raw_cx, event_loop, tokio::time::sleep).await }; None - }, - ) - .await; + }) + .await; match forwarded { Some(result) => result, - // The loop finished first. Anything already in the channel is all there will - // ever be, so take what is ready. If that includes the end of the stream, the - // body completed just before the loop did, and the response is complete. None => { - finish_orphaned_stream(&mut receiver, &mut chunks, &mut remaining).await + loop_done.set(true); + forwarding.await } } } @@ -1284,7 +1287,7 @@ async fn forward_body( } }, OutgoingBody::Stream(mut receiver) => { - forward_stream(&mut receiver, chunks, remaining).await + forward_stream(&mut receiver, chunks, remaining, &Cell::new(false)).await } // hyper does not report write progress: a chunk it accepted may still be in its write // buffer. It does stop accepting chunks while that buffer is above its watermark, so @@ -1304,50 +1307,53 @@ async fn forward_body( } /// Forward the chunks a handler's JS produces, as its pump enqueues them, until the stream ends. +/// Once `loop_done` is set, the stream ends with an error when the channel has nothing ready. async fn forward_stream( receiver: &mut platform::http::OutgoingBodyReceiver, chunks: &mut futures_channel::mpsc::Sender>, remaining: &mut platform::http::Remaining, + loop_done: &Cell, ) -> Result<(), String> { - while let Some(chunk) = receiver.next().await { - match chunk { - Ok(chunk) => { - if !send_chunk(chunks, bytes::Bytes::from(chunk), remaining).await? { - return Ok(()); - } + let error = loop { + // Once the event loop is done, an empty channel that is still open can never receive + // another chunk. + let next = futures_lite::future::poll_fn(|cx| match receiver.poll_next_unpin(cx) { + Poll::Ready(next) => Poll::Ready(Some(next)), + Poll::Pending if loop_done.get() => Poll::Ready(None), + Poll::Pending => Poll::Pending, + }) + .await; + let mut chunk = match next { + Some(Some(Ok(chunk))) => chunk, + Some(Some(Err(e))) => break e, + Some(None) => return Ok(()), + None => { + return Err( + "the event loop finished while the response body was still open, so \ + the body can never complete" + .to_string(), + ) } - Err(e) => return Err(format!("response body errored mid-stream: {e}")), + }; + let error = platform::http::coalesce_queued_chunks(receiver, &mut chunk); + if !send_chunk(chunks, bytes::Bytes::from(chunk), remaining).await? { + return Ok(()); } - } - Ok(()) + if let Some(e) = error { + break e; + } + }; + Err(fail_after_sent_chunks(chunks, &error).await) } -/// Take what a stream body left behind once its event loop finished: everything already enqueued, -/// and the end of the stream if the body turned out to be complete. -async fn finish_orphaned_stream( - receiver: &mut platform::http::OutgoingBodyReceiver, +/// Wait until hyper has taken the last chunk sent to `chunks`, so it is written out before the +/// body fails with `error`, and return the failure's message. +async fn fail_after_sent_chunks( chunks: &mut futures_channel::mpsc::Sender>, - remaining: &mut platform::http::Remaining, -) -> Result<(), String> { - // `poll_once` takes only what is ready: a pending read means the channel is still open with - // nothing in it, so the body was abandoned. - while let Some(next) = futures_lite::future::poll_once(receiver.next()).await { - match next { - // The channel closed: the body was complete after all. - None => return Ok(()), - Some(Ok(chunk)) => { - if !send_chunk(chunks, bytes::Bytes::from(chunk), remaining).await? { - return Ok(()); - } - } - Some(Err(e)) => return Err(format!("response body errored mid-stream: {e}")), - } - } - Err( - "the event loop finished while the response body was still open, so the body can never \ - complete" - .to_string(), - ) + error: &platform::http::Error, +) -> String { + let _ = futures_lite::future::poll_fn(|cx| chunks.poll_ready(cx)).await; + format!("response body errored mid-stream: {error}") } /// Hand one chunk to hyper, waiting for room, trimmed so the total sent does not exceed the diff --git a/crates/libstarling/src/serve_wasm.rs b/crates/libstarling/src/serve_wasm.rs index 7c2385ec..934e7ef1 100644 --- a/crates/libstarling/src/serve_wasm.rs +++ b/crates/libstarling/src/serve_wasm.rs @@ -33,6 +33,11 @@ js::instance_local! { static RUNTIME: RefCell, *mut js::native::RawJSContext, ServeTimeouts)>> = const { RefCell::new(None) }; + /// Why [`runtime`] failed to start the runtime. Every later request fails with the same error + /// instead of repeating the whole bootstrap, since a bootstrap per request is far heavier + /// than serving from a running instance. + static STARTUP_FAILURE: RefCell> = const { RefCell::new(None) }; + /// The content script's startup event loop, holding its top-level async work. [`runtime`] /// creates it undriven, since it must stay synchronous so concurrent first requests can't /// race into two runtimes. The first request drives it to completion via [`ensure_started`] @@ -79,10 +84,22 @@ fn install_runtime(pair: (Rc, *mut js::native::RawJSContext, ServeTimeo /// Get the runtime and its context, creating them on first use: run the content script /// (registering `fetch` handlers) and enter the global realm persistently. Synchronous (no /// `await`), so concurrent first requests can't race into two runtimes. +/// +/// A failed startup is final: this returns its error on every later call without retrying. fn runtime() -> Result<(Rc, *mut js::native::RawJSContext, ServeTimeouts), String> { if let Some(pair) = RUNTIME.with(|cell| cell.borrow().clone()) { return Ok(pair); } + if let Some(failure) = STARTUP_FAILURE.with(|cell| cell.borrow().clone()) { + return Err(failure); + } + start_runtime().inspect_err(|failure| { + STARTUP_FAILURE.with(|cell| *cell.borrow_mut() = Some(failure.clone())); + }) +} + +/// Create the runtime for [`runtime`]. +fn start_runtime() -> Result<(Rc, *mut js::native::RawJSContext, ServeTimeouts), String> { // `wasmtime serve` passes the guest no arguments, so the HTTP entry point is configured // through `STARLINGMONKEY_CONFIG` instead (an empty one yields the defaults: `./index.js`). let config = RuntimeConfig::from_env().map_err(|e| e.to_string())?; @@ -100,6 +117,8 @@ fn runtime() -> Result<(Rc, *mut js::native::RawJSContext, ServeTimeout // single one, harmless for a long-running server). The raw context stays valid as long as the // runtime (held in this thread-local) lives. let scope = runtime.default_global(); + // SAFETY: the context lives as long as `runtime`, which `install_runtime` keeps for the + // process lifetime. The drivers `raw_cx` is handed to root what they hold. let raw_cx = unsafe { scope.raw_cx_no_gc() }; std::mem::forget(scope); let pair = (runtime, raw_cx, timeouts); @@ -124,14 +143,14 @@ pub fn mark_resumed_from_snapshot() { /// state. pub async fn pre_initialize() -> Result<(), String> { let (_runtime, raw_cx, _) = runtime()?; - let Startup::Pending(mut invocation, evaluation) = + let Startup::Pending(invocation, evaluation) = STARTUP.with(|cell| std::mem::replace(&mut *cell.borrow_mut(), Startup::Driving)) else { // Nothing to evaluate: `runtime` was already stood up, so this is a second call. mark_resumed_from_snapshot(); return Ok(()); }; - drive_startup(raw_cx, invocation.state_mut().event_loop(), &evaluation).await; + drive_startup(raw_cx, invocation.state().event_loop(), &evaluation).await; // SAFETY: `runtime` entered the default global's realm for the process lifetime. let scope = unsafe { js::gc::scope::RootScope::from_current_realm(raw_cx) }; @@ -142,7 +161,7 @@ pub async fn pre_initialize() -> Result<(), String> { // If any external async tasks are active, that means component model resources are held, making // a snapshot impossible. if invocation - .state_mut() + .state() .event_loop() .has_active_external_async_tasks() { @@ -213,24 +232,16 @@ async fn ensure_started(raw_cx: *mut js::native::RawJSContext) { } let action = STARTUP.with(|cell| { let mut state = cell.borrow_mut(); - match &*state { - Startup::Pending(..) => { - let Startup::Pending(invocation, evaluation) = - std::mem::replace(&mut *state, Startup::Driving) - else { - unreachable!("matched Pending above") - }; + match std::mem::replace(&mut *state, Startup::Done) { + Startup::Pending(invocation, evaluation) => { + *state = Startup::Driving; Action::Drive(invocation, evaluation) } - Startup::Evaluated(..) => { - let Startup::Evaluated(invocation) = - std::mem::replace(&mut *state, Startup::Done) - else { - unreachable!("matched Evaluated above") - }; - Action::Keep(invocation) + Startup::Evaluated(invocation) => Action::Keep(invocation), + Startup::Driving => { + *state = Startup::Driving; + Action::Wait } - Startup::Driving => Action::Wait, Startup::Done => Action::Ready, } }); @@ -247,7 +258,7 @@ async fn ensure_started(raw_cx: *mut js::native::RawJSContext) { pending: Some((invocation, evaluation)), }; let (invocation, evaluation) = driving.pending.as_mut().expect("just set"); - drive_startup(raw_cx, invocation.state_mut().event_loop(), evaluation).await; + drive_startup(raw_cx, invocation.state().event_loop(), evaluation).await; report_missing_fetch_listener(raw_cx, evaluation); let (invocation, _) = driving.pending.take().expect("just driven"); STARTUP.with(|cell| *cell.borrow_mut() = Startup::Done); @@ -281,7 +292,7 @@ async fn drive_startup( // SAFETY: `raw_cx` is valid for the duration of this await, since the runtime outlives the // request. unsafe { - run_until_evaluated(raw_cx, event_loop, crate::wasm_sleep, evaluation).await; + run_until_evaluated(raw_cx, event_loop, platform::clock::sleep, evaluation).await; } } @@ -310,10 +321,7 @@ fn report_missing_fetch_listener( /// in flight. The task lives as long as the script requires, from one poll for a script that /// left nothing behind to the instance's lifetime for one whose `fetch` never responds. // TODO: Should reconsider this, it might make more sense to abort async work that's not added to waitUntil. -fn keep_startup_loop_running( - raw_cx: *mut js::native::RawJSContext, - mut invocation: OwnedInvocation, -) { +fn keep_startup_loop_running(raw_cx: *mut js::native::RawJSContext, invocation: OwnedInvocation) { wasip3::wit_bindgen::spawn_local(async move { // SAFETY: `runtime` entered the default global's realm for the process lifetime, and // `raw_cx` outlives this task, since the runtime is held in a process-lifetime @@ -321,8 +329,8 @@ fn keep_startup_loop_running( unsafe { core_runtime::event_loop::run_to_completion( raw_cx, - invocation.state_mut().event_loop(), - crate::wasm_sleep, + invocation.state().event_loop(), + platform::clock::sleep, ) .await; } @@ -377,7 +385,7 @@ async fn dispatch_request( let body = has_body.then_some(body); let head_request = method.eq_ignore_ascii_case("HEAD"); - let mut invocation = OwnedInvocation::new(runtime.clone(), InvocationState::new()); + let invocation = OwnedInvocation::new(runtime.clone(), InvocationState::new()); // The realm this request runs in is the process-wide global, which stays entered for the // process lifetime (`runtime` leaks the entering scope). There is no per-request global: one @@ -387,13 +395,13 @@ async fn dispatch_request( let request_realm = unsafe { js::gc::scope::RootScope::from_current_realm(raw_cx) }; let mut parts = crate::serve_common::dispatch_fetch( &request_realm, - invocation.state_mut(), + invocation.state(), method, url, headers, body, content_length, - crate::wasm_sleep, + platform::clock::sleep, &clock, ) .await; @@ -438,8 +446,8 @@ async fn dispatch_request( unsafe { let outcome = drive_body_send( raw_cx, - invocation.state_mut().event_loop(), - crate::wasm_sleep, + invocation.state().event_loop(), + platform::clock::sleep, &clock, body_done, || abandon_body.abandon(), @@ -467,7 +475,7 @@ async fn dispatch_request( let scope = js::gc::scope::RootScope::from_current_realm(raw_cx); crate::serve_common::signal_body_outcome( &scope, - invocation.state_mut().event_loop(), + invocation.state().event_loop(), abort_controller.as_ref(), outcome, ); @@ -478,8 +486,8 @@ async fn dispatch_request( // `waitUntil` window must not depend on which. crate::serve_common::drain_lifetime_work( raw_cx, - invocation.state_mut().event_loop(), - crate::wasm_sleep, + invocation.state().event_loop(), + platform::clock::sleep, &clock, ) .await; @@ -521,6 +529,7 @@ where let sending = async { let mut body_done = std::pin::pin!(body_done); let ended = futures_lite::future::or(async { Some(body_done.as_mut().await) }, async { + // SAFETY: this function's caller keeps `raw_cx` valid for the call. unsafe { core_runtime::event_loop::run_until(raw_cx, event_loop, &sleep, |_| false).await }; diff --git a/crates/libstarling/tests/serve.rs b/crates/libstarling/tests/serve.rs index 9cd3a70e..d0fe3e2e 100644 --- a/crates/libstarling/tests/serve.rs +++ b/crates/libstarling/tests/serve.rs @@ -2480,6 +2480,55 @@ fn a_response_stream_that_errors_mid_flight_leaves_the_body_unterminated() { handle.stop(); } +/// Chunks queued faster than the transport writes them are written together, in batches of up to +/// 64 KiB: 100 distinct 1 KiB chunks enqueued at once arrive intact and in order. +#[test] +fn serve_streams_many_queued_chunks_in_order() { + let handler = r#"addEventListener('fetch', (event) => { + event.respondWith(new Response(new ReadableStream({ + start(c) { + for (let i = 0; i < 100; i++) { + c.enqueue(new TextEncoder().encode(String(i).padStart(3, '0').repeat(341) + '|')); + } + c.close(); + }, + }))); + })"#; + let handle = start_serve(handler, 18471); + let body = request(18471, "GET", "/", ""); + let expected: String = (0..100) + .map(|i| format!("{:03}", i).repeat(341) + "|") + .collect(); + assert_eq!(dechunk(&body), expected); + handle.stop(); +} + +/// Chunks queued ahead of an invalid chunk are written before the body errors, and the body then +/// ends unterminated. +#[test] +fn queued_chunks_before_an_invalid_chunk_are_written_before_the_error() { + let handler = r#"addEventListener('fetch', (event) => { + const encoder = new TextEncoder(); + event.respondWith(new Response(new ReadableStream({ + start(c) { + c.enqueue(encoder.encode('first-')); + c.enqueue(encoder.encode('second-')); + c.enqueue('a string, not a Uint8Array'); + c.close(); + }, + }))); + })"#; + let handle = start_serve(handler, 18472); + let response = raw_request( + 18472, + b"GET / HTTP/1.1\r\nHost: x\r\nConnection: close\r\n\r\n", + ); + let (_, body) = response.split_once("\r\n\r\n").expect("a complete head"); + assert!(body.contains("first-second-"), "got: {response}"); + assert!(!body.ends_with("0\r\n\r\n"), "got: {response}"); + handle.stop(); +} + /// The same, for a stream that enqueues something that is not a `Uint8Array`: the body errors /// before any byte is written, so the head is already committed and the connection ends /// unterminated. WPT diff --git a/crates/libstarling/tests/serve_isolation.rs b/crates/libstarling/tests/serve_isolation.rs index bffaba50..43450b4f 100644 --- a/crates/libstarling/tests/serve_isolation.rs +++ b/crates/libstarling/tests/serve_isolation.rs @@ -17,7 +17,7 @@ use web_globals::events::algorithms::ScriptStackState; fn test_runtime() -> std::rc::Rc { clear_global_initializers(); libstarling::register_builtins(); - Runtime::init(&core_runtime::config::RuntimeConfig::default()) + Runtime::init(&core_runtime::config::RuntimeConfig::default()).expect("runtime init") } fn eval(scope: &Scope<'_>, src: &str) -> Result { diff --git a/crates/platform/Cargo.toml b/crates/platform/Cargo.toml index 868cc197..7444791f 100644 --- a/crates/platform/Cargo.toml +++ b/crates/platform/Cargo.toml @@ -17,6 +17,7 @@ futures-lite.workspace = true url.workspace = true [target.'cfg(not(target_arch = "wasm32"))'.dependencies] +libc = "0.2" reqwest.workspace = true [target.'cfg(target_arch = "wasm32")'.dependencies] diff --git a/crates/platform/src/clock.rs b/crates/platform/src/clock.rs index c3029289..dc9cf012 100644 --- a/crates/platform/src/clock.rs +++ b/crates/platform/src/clock.rs @@ -27,13 +27,48 @@ fn host_monotonic_ns() -> u64 { wasip3::clocks::monotonic_clock::now() } +/// WASI has no cheaper clock, so the coarse reading is the precise one. +#[cfg(target_arch = "wasm32")] +fn host_coarse_monotonic_ns() -> u64 { + host_monotonic_ns() +} + +/// The precise monotonic clock, and a coarse one on the same timeline that is cheaper to read. +/// Linux updates `CLOCK_MONOTONIC_COARSE` every scheduler tick. macOS updates +/// `CLOCK_UPTIME_RAW_APPROX` on context switches. +#[cfg(target_os = "linux")] +const CLOCKS: (libc::clockid_t, libc::clockid_t) = + (libc::CLOCK_MONOTONIC, libc::CLOCK_MONOTONIC_COARSE); +#[cfg(target_os = "macos")] +const CLOCKS: (libc::clockid_t, libc::clockid_t) = + (libc::CLOCK_UPTIME_RAW, libc::CLOCK_UPTIME_RAW_APPROX); +#[cfg(all( + not(target_arch = "wasm32"), + not(target_os = "linux"), + not(target_os = "macos") +))] +const CLOCKS: (libc::clockid_t, libc::clockid_t) = (libc::CLOCK_MONOTONIC, libc::CLOCK_MONOTONIC); + +#[cfg(not(target_arch = "wasm32"))] +fn read_clock(clock: libc::clockid_t) -> u64 { + let mut ts = libc::timespec { + tv_sec: 0, + tv_nsec: 0, + }; + // SAFETY: `ts` is a valid `timespec` to write to, and `clock` is a supported clock id. + let result = unsafe { libc::clock_gettime(clock, &mut ts) }; + debug_assert_eq!(result, 0, "clock_gettime failed"); + ts.tv_sec as u64 * 1_000_000_000 + ts.tv_nsec as u64 +} + #[cfg(not(target_arch = "wasm32"))] fn host_monotonic_ns() -> u64 { - /// The origin of the readings below. Native processes are not snapshotted, so any fixed point - /// in the process will do, and the first reading is the earliest one available. - static ORIGIN: std::sync::LazyLock = - std::sync::LazyLock::new(std::time::Instant::now); - ORIGIN.elapsed().as_nanos() as u64 + read_clock(CLOCKS.0) +} + +#[cfg(not(target_arch = "wasm32"))] +fn host_coarse_monotonic_ns() -> u64 { + read_clock(CLOCKS.1) } /// The current monotonic clock reading, in nanoseconds, continuous across a snapshot. @@ -43,6 +78,12 @@ pub fn monotonic_ns() -> u64 { OFFSET_NS.load(Ordering::Relaxed) + host_monotonic_ns() } +/// A [`monotonic_ns`] reading from a clock that is cheaper to read but less precise, both +/// depending on the platform. It can lag a [`monotonic_ns`] reading taken just before it. +pub fn coarse_monotonic_ns() -> u64 { + OFFSET_NS.load(Ordering::Relaxed) + host_coarse_monotonic_ns() +} + /// Record where the clock stood as a snapshot was made. /// /// Must be the last thing an instance being snapshotted does, since a reading taken afterwards @@ -62,6 +103,13 @@ pub fn resume_from_snapshot() -> u64 { nanoseconds } +/// Wait for `duration` on the host's monotonic clock. +#[cfg(target_arch = "wasm32")] +pub async fn sleep(duration: std::time::Duration) { + let nanos = duration.as_nanos().min(u64::MAX as u128) as u64; + wasip3::clocks::monotonic_clock::wait_for(nanos).await; +} + /// A monotonic clock reading, for deadlines that outlive a Wizer snapshot. /// /// `std::time::Instant` reads the host clock directly, so a deadline taken before a snapshot sits diff --git a/crates/platform/src/http.rs b/crates/platform/src/http.rs index daf8f1fa..8750fd06 100644 --- a/crates/platform/src/http.rs +++ b/crates/platform/src/http.rs @@ -129,6 +129,37 @@ pub enum OutgoingBody { /// backpressure rather than buffering the whole body. pub(crate) const BODY_CHANNEL_CAPACITY: usize = 8; +/// The size up to which [`coalesce_queued_chunks`] appends queued chunks. +const COALESCE_LIMIT: usize = 64 * 1024; + +/// Append chunks already queued in `receiver` to `chunk` without waiting, so the transport can write +/// them in one call. +/// +/// Appending stops at the first of: +/// - `chunk` reaching 64 KiB. The last appended chunk can take it past that. +/// - an empty queue or a closed channel. The caller's next read from `receiver` reports the close. +/// - a queued `Err`, which is returned. The body failed after the bytes now in `chunk`. +pub fn coalesce_queued_chunks( + receiver: &mut OutgoingBodyReceiver, + chunk: &mut Vec, +) -> Option { + while chunk.len() < COALESCE_LIMIT { + let next = match receiver.try_recv() { + Ok(Ok(next)) => next, + Ok(Err(error)) => return Some(error), + Err(_) => break, + }; + let len = chunk.len() + next.len(); + if len > chunk.capacity() { + // Reserve up to the limit on the first reallocation, so later appends within the limit + // don't copy `chunk` again. + chunk.reserve_exact(len.max(COALESCE_LIMIT) - chunk.len()); + } + chunk.extend_from_slice(&next); + } + None +} + /// Wait for channel capacity, then enqueue. `futures_channel`'s `Sender` has no /// inherent async `send`, so drive `poll_ready` directly (avoids pulling in /// `futures-util`'s `SinkExt`). Returns `false` if the receiver was dropped. @@ -155,10 +186,24 @@ impl BodySender { send_on(&mut self.0, Ok(chunk)).await } + /// Append a body chunk if the channel has capacity for it right away. Returns the chunk if it + /// has no capacity, or if the receiver was dropped. + pub fn try_send_chunk(&mut self, chunk: Vec) -> Result<(), Vec> { + self.0 + .try_send(Ok(chunk)) + .map_err(|error| error.into_inner().expect("the chunk sent above is `Ok`")) + } + /// Abort the body with an error (so the send fails), awaiting capacity. pub async fn send_error(&mut self, message: String) -> bool { send_on(&mut self.0, Err(Error(message))).await } + + /// Abort the body with an error without waiting for capacity. If the channel is full, the + /// error is not queued and the body ends after the chunks already queued. + pub fn fail(mut self, message: String) { + let _ = self.0.try_send(Err(Error(message))); + } } /// Create a streaming request body and its write end. Send chunks through the diff --git a/crates/platform/src/http_wasm.rs b/crates/platform/src/http_wasm.rs index 09efc16d..4bb35562 100644 --- a/crates/platform/src/http_wasm.rs +++ b/crates/platform/src/http_wasm.rs @@ -139,10 +139,7 @@ fn spawn_body_writer( None => write.await, Some(limit) => { let expired = async { - wasip3::clocks::monotonic_clock::wait_for( - limit.as_nanos().min(u64::MAX as u128) as u64, - ) - .await; + crate::clock::sleep(limit).await; BodySendOutcome::TimedOut }; futures_lite::future::or(write, expired).await @@ -211,7 +208,8 @@ async fn write_stream_body( }) .await; match chunk { - Some(Some(Ok(chunk))) => { + Some(Some(Ok(mut chunk))) => { + let error = crate::http::coalesce_queued_chunks(receiver, &mut chunk); // Content past the declared length would leave the host framing a message it has // no room for, so the body ends here instead. let Some(chunk) = remaining.take(chunk) else { @@ -220,6 +218,9 @@ async fn write_stream_body( if !body_tx.write_all(chunk).await.is_empty() { return BodySendOutcome::Failed(HOST_STOPPED_READING.to_string()); } + if let Some(e) = error { + return BodySendOutcome::Failed(e.to_string()); + } } Some(Some(Err(e))) => return BodySendOutcome::Failed(e.to_string()), Some(None) => return outcome_at_end(remaining), @@ -345,8 +346,16 @@ pub async fn send(request: Request) -> Result { )); } } - let fields = - Fields::from_list(&header_entries).map_err(|e| Error(format!("invalid headers: {e:?}")))?; + // Appended one at a time rather than with the all-or-nothing `Fields::from_list`, so a header + // the host refuses fails the request with an error naming it. + let fields = Fields::new(); + for (name, value) in &header_entries { + fields.append(name, value).map_err(|e| { + Error(format!( + "the host refused the request header `{name}`: {e:?}" + )) + })?; + } let (body, trailers_tx) = body_contents(request.body); let (wasi_request, transmitted) = WasiRequest::new(fields, body.contents, body.trailers, None); diff --git a/crates/serve-test-support/tests/serve_wasm_e2e.rs b/crates/serve-test-support/tests/serve_wasm_e2e.rs index 08556fb3..a0c9f16f 100644 --- a/crates/serve-test-support/tests/serve_wasm_e2e.rs +++ b/crates/serve-test-support/tests/serve_wasm_e2e.rs @@ -175,6 +175,62 @@ fn a_runtime_that_will_not_start_keeps_its_reasons_off_the_wire() { ); } +/// A runtime whose startup failed responds to every later request with a 500 without running the +/// startup again: a reused instance evaluates the content script once. +#[test] +fn a_failed_startup_is_not_retried_by_later_requests() { + let Some(server) = Serve::new(18461) + .reusing_one_instance() + .script("console.error('startup-ran'); throw new Error('startup failed');") + .ready(Ready::AnyResponse) + .start() + else { + return; + }; + + let (first, second) = met_in_one_instance(&server, || { + let first = server.full_request("GET", "/", ""); + (first, server.full_request("GET", "/", "")) + }); + assert!(first.starts_with("HTTP/1.1 500"), "{first}"); + assert!(second.starts_with("HTTP/1.1 500"), "{second}"); + + let log = server.log(); + let (instances, _) = server.instances_since(0); + let distinct: std::collections::HashSet<_> = instances.iter().collect(); + assert_eq!( + log.matches("startup-ran").count(), + distinct.len(), + "each instance must run the failing startup once, not once per request: {log}" + ); +} + +/// A `fetch` whose request has a header the host refuses rejects with a `TypeError` naming +/// that header. `http2-settings` is allowed by Fetch but refused by `wasi:http`. +#[test] +fn a_fetch_with_a_header_the_host_refuses_rejects_naming_it() { + let upstream = common::start_upstream("UPSTREAM-BODY"); + let handler = format!( + r#"addEventListener('fetch', (event) => {{ + if (new URL(event.request.url).pathname === '/ready') {{ + event.respondWith(new Response('ready')); + return; + }} + event.respondWith( + fetch('http://127.0.0.1:{upstream}/', {{ headers: {{ 'http2-settings': 'AAMAAABk' }} }}) + .then(() => new Response('sent'), (e) => new Response(e.name + ': ' + e.message)) + ); + }});"# + ); + let Some(server) = Serve::new(18462).script(&handler).start() else { + return; + }; + + let body = server.get("/"); + assert!(body.starts_with("TypeError: "), "{body}"); + assert!(body.contains("http2-settings"), "{body}"); +} + /// An unusual status is the handler's to choose: it reaches the client as itself, and the host /// must not act on it behind the handler's back — a `421` invites a client to retry elsewhere, not /// the server to re-dispatch (`serve_delivers_an_unusual_status_without_retrying_it`). That the diff --git a/crates/starling-macro/src/lib.rs b/crates/starling-macro/src/lib.rs index 76b78a94..4ca09c17 100644 --- a/crates/starling-macro/src/lib.rs +++ b/crates/starling-macro/src/lib.rs @@ -393,6 +393,7 @@ fn process_class_def(attr: TokenStream, item: TokenStream, config: ClassConfig) let has_error_data_const = if opts.js_proto.as_deref() == Some("Error") { quote! { const HAS_ERROR_DATA: bool = true; + const RESERVED_SLOTS: u32 = ::js::class::ERROR_STACK_SLOT + 1; } } else { quote! {} @@ -407,27 +408,39 @@ fn process_class_def(attr: TokenStream, item: TokenStream, config: ClassConfig) quote! {} }; - // Generate the `install_unforgeable` ClassDef method. It chains to the - // parent interface (for inherited unforgeable accessors) and delegates own - // accessors to the `__UnforgeableRegistrar` provided by `#[jsmethods]`. + // Generate the `define_unforgeable` and `has_unforgeable` ClassDef methods. + // They chain to the parent interface (for inherited unforgeable accessors) + // and delegate own accessors to the `__UnforgeableRegistrar` provided by + // `#[jsmethods]`. let install_unforgeable_method = { - let parent_call = if let Some(ref inner_parent_name) = inner_parent { - quote! { - <#inner_parent_name as ::js::class::ClassDef>::install_unforgeable(scope, obj)?; - } + let (parent_define, parent_has) = if let Some(ref inner_parent_name) = inner_parent { + ( + quote! { + <#inner_parent_name as ::js::class::ClassDef>::define_unforgeable(scope, obj)?; + }, + quote! { + <#inner_parent_name as ::js::class::ClassDef>::has_unforgeable() || + }, + ) } else { - quote! {} + (quote! {}, quote! {}) }; quote! { - fn install_unforgeable( + fn define_unforgeable( scope: &::js::gc::scope::Scope<'_>, obj: ::js::Object<'_>, ) -> ::std::result::Result<(), ::js::error::ExnThrown> { - #parent_call + #parent_define use ::js::class::__UnforgeableRegistrar; let reg = ::js::class::__UnforgeableReg::::new(); (®).install(scope, obj) } + + fn has_unforgeable() -> bool { + use ::js::class::__UnforgeableRegistrar; + let reg = ::js::class::__UnforgeableReg::::new(); + #parent_has (®).has() + } } }; @@ -679,8 +692,9 @@ fn process_class_def(attr: TokenStream, item: TokenStream, config: ClassConfig) /// In debug builds, panics if the data is already mutably borrowed /// (a reentrant access to the same object). See /// `js::class::Stack::data`. + #[inline] pub fn data(&self) -> ::js::class::Ref<'_, #inner_name> { - self.0.data().unwrap() + self.0.data() } /// Mutably borrow the private Rust data (guard dereferencing to @@ -689,8 +703,9 @@ fn process_class_def(attr: TokenStream, item: TokenStream, config: ClassConfig) /// In debug builds, panics if the data is already borrowed (a /// reentrant access to the same object). See /// `js::class::Stack::data_mut`. + #[inline] pub fn data_mut(&self) -> ::js::class::RefMut<'_, #inner_name> { - self.0.data_mut().unwrap() + self.0.data_mut() } } @@ -812,8 +827,6 @@ enum MethodKind { Setter { js_name: String, }, - /// Post-construction initialization hook. - PostInit, } /// How the return value of a method should be handled. @@ -1111,9 +1124,6 @@ fn process_methods(_attr: TokenStream, item: TokenStream, config: ClassConfig) - } else if attr.path().is_ident("destructor") { kind = Some(MethodKind::Destructor); false - } else if attr.path().is_ident("post_init") { - kind = Some(MethodKind::PostInit); - false } else { true // keep other attrs } @@ -1192,8 +1202,6 @@ fn process_methods(_attr: TokenStream, item: TokenStream, config: ClassConfig) - // Setup-style constructor: detected when #[constructor] has &self/&mut self. // The constructor body runs on the stack newtype after allocation + boxing. let mut setup_ctor_info: Option = None; // index into `methods` - // New-style post_init: runs on the stack newtype with auto-extracted params. - let mut new_post_init_info: Option = None; // index into `methods` // Collect property accessors indexed by JS name for pairing struct PropertyEntry { @@ -1243,9 +1251,6 @@ fn process_methods(_attr: TokenStream, item: TokenStream, config: ClassConfig) - destructor_takes_object = method.fn_item.sig.inputs.len() > 1; destructor_fn_name = Some(method.fn_item.sig.ident.clone()); } - MethodKind::PostInit => { - new_post_init_info = Some(i); - } MethodKind::Method { js_name } => { let (native_fn, builder_call) = gen_method_native( method, @@ -1330,19 +1335,16 @@ fn process_methods(_attr: TokenStream, item: TokenStream, config: ClassConfig) - } // ================================================================ - // Remove setup-style ctors, new-style post_init, and stack newtype methods from impl FooImpl. + // Remove setup-style ctors and stack newtype methods from impl FooImpl. // ================================================================ // Setup-style ctor bodies run on the stack newtype (as `__ctor_setup_*`), // so they are dropped from the FooImpl block; old-style ctors stay there. - let mut remove_fn_names: Vec = ctor_indices + let remove_fn_names: Vec = ctor_indices .iter() .map(|i| &methods[*i]) .filter(|m| m.has_self || m.has_mut_self) .map(|m| m.fn_item.sig.ident.clone()) .collect(); - if let Some(i) = new_post_init_info { - remove_fn_names.push(methods[i].fn_item.sig.ident.clone()); - } // Collect names of methods that go on the newtype. let newtype_method_names: Vec = methods @@ -1351,7 +1353,7 @@ fn process_methods(_attr: TokenStream, item: TokenStream, config: ClassConfig) - .map(|m| m.fn_item.sig.ident.clone()) .collect(); - // Remove setup ctors, post_init, and stack newtype methods from the FooImpl impl block. + // Remove setup ctors and stack newtype methods from the FooImpl impl block. let mut newtype_items: Vec = Vec::new(); { let remove_names: Vec<&Ident> = remove_fn_names.iter().collect(); @@ -1361,7 +1363,7 @@ fn process_methods(_attr: TokenStream, item: TokenStream, config: ClassConfig) - if let ImplItem::Fn(ref fn_item) = item { let ident = &fn_item.sig.ident; if remove_names.iter().any(|n| *ident == **n) { - continue; // setup ctors / post_init — dropped entirely + continue; // setup ctors are dropped entirely } if newtype_method_names.contains(ident) || helper_fn_names.contains(ident) { // Registered methods and unannotated helpers alike move to @@ -1499,66 +1501,20 @@ fn process_methods(_attr: TokenStream, item: TokenStream, config: ClassConfig) - quote! {} }; - // Generate the PostInitRegistrar impl. - // - // Three cases: - // 1. Setup-style ctor (with or without explicit post_init) - // 2. Old-style ctor with new-style post_init (&self on newtype) - // 3. No post_init at all (no-op, handled by autoref fallback) - let post_init_impl = if setup_ctor_info.is_some() || new_post_init_info.is_some() { - // Root the object and create the stack newtype for both ctor setup and post_init. - let cast = quote! { - let __typed = obj.cast::<#type_name>().unwrap(); - }; - - // Generate the setup-style constructor call (if present). - let setup_call = if let Some(idx) = setup_ctor_info { - let info = &methods[idx]; - let setup_fn_name = setup_fn_ident(&info.fn_item.sig.ident); - let arg_extractions = - gen_arg_extractions(&info.params, quote!(args), true, quote!(scope), info.nargs); - let arg_names: Vec<_> = info.params.iter().map(|(name, _)| quote!(#name)).collect(); - let call = if info.has_cx { - quote! { #type_name::#setup_fn_name(&__typed, scope, #(#arg_names),*) } - } else { - quote! { #type_name::#setup_fn_name(&__typed, #(#arg_names),*) } - }; - quote! { - #(#arg_extractions)* - match #call { - Ok(()) => {}, - Err(__e) => { - unsafe { ::js::error::ThrowException::throw(__e, scope); } - return Err(::js::error::ExnThrown); - } - } - } - } else { - quote! {} - }; - - // Generate the post_init call (if present). - let post_init_call = if let Some(idx) = new_post_init_info { - let info = &methods[idx]; - let pi_fn_name = format_ident!("__post_init"); - let call = if info.has_cx { - quote! { #type_name::#pi_fn_name(&__typed, scope) } - } else { - quote! { #type_name::#pi_fn_name(&__typed) } - }; - quote! { - match #call { - Ok(()) => {}, - Err(__e) => { - unsafe { ::js::error::ThrowException::throw(__e, scope); } - return Err(::js::error::ExnThrown); - } - } - } + // Generate the PostInitRegistrar impl for a setup-style constructor: extract the + // arguments and run the constructor body on the rooted newtype. Classes without one + // fall back to the no-op registrar through autoref specialization. + let post_init_impl = if let Some(idx) = setup_ctor_info { + let info = &methods[idx]; + let setup_fn_name = setup_fn_ident(&info.fn_item.sig.ident); + let arg_extractions = + gen_arg_extractions(&info.params, quote!(args), true, quote!(scope), info.nargs); + let arg_names: Vec<_> = info.params.iter().map(|(name, _)| quote!(#name)).collect(); + let call = if info.has_cx { + quote! { #type_name::#setup_fn_name(&__typed, scope, #(#arg_names),*) } } else { - quote! {} + quote! { #type_name::#setup_fn_name(&__typed, #(#arg_names),*) } }; - quote! { #[allow(clippy::not_unsafe_ptr_arg_deref)] impl ::js::class::__PostInitRegistrar<#inner_name> for ::js::class::__PostInitReg<#inner_name> { @@ -1568,10 +1524,9 @@ fn process_methods(_attr: TokenStream, item: TokenStream, config: ClassConfig) - obj: ::js::Object<'_>, args: &::js::native::CallArgs, ) -> ::std::result::Result<(), ::js::error::ExnThrown> { - #cast - #setup_call - #post_init_call - Ok(()) + let __typed = obj.cast::<#type_name>().unwrap(); + #(#arg_extractions)* + #call.map_err(|__e| ::js::error::ThrowException::throw(__e, scope)) } } } @@ -1593,48 +1548,12 @@ fn process_methods(_attr: TokenStream, item: TokenStream, config: ClassConfig) - } }; - // Generate `impl<'s> Foo<'s>` containing constructors, add_to_global(), and - // supporting methods such as setup-style constructor post_init methods. - - // The `#[post_init]` body, moved onto the newtype as `__post_init`. - let moved_post_init_fn = new_post_init_info - .map(|pi_idx| { - let pi = &methods[pi_idx]; - let mut pi_fn = pi.fn_item.clone(); - pi_fn.sig.ident = format_ident!("__post_init"); - pi_fn.vis = Visibility::Inherited; // private - pi_fn.attrs.retain(|a| !a.path().is_ident("post_init")); - quote! { #pi_fn } - }) - .unwrap_or_else(|| quote! {}); - - // The `__post_init` call setup-style factories make after running setup. - let factory_post_init_call = if let Some(pi_idx) = new_post_init_info { - let pi_info = &methods[pi_idx]; - let pi_fn_ident = format_ident!("__post_init"); - if pi_info.has_cx { - quote! { #type_name::#pi_fn_ident(&__typed, scope).map_err(|e| { - ::js::error::ThrowException::throw(e, scope) - })?; } - } else { - quote! { #type_name::#pi_fn_ident(&__typed).map_err(|e| { - ::js::error::ThrowException::throw(e, scope) - })?; } - } - } else { - quote! {} - }; - + // Generate `impl<'s> Foo<'s>` containing constructors and add_to_global(). let mut ctor_items: Vec = Vec::new(); for i in &ctor_indices { let method = &methods[*i]; if method.has_self || method.has_mut_self { - ctor_items.push(gen_setup_factory( - method, - &type_name, - &inner_name, - &factory_post_init_call, - )); + ctor_items.push(gen_setup_factory(method, &type_name, &inner_name)); } else if !method.is_raw { // A raw (`&CallArgs`) constructor is callable only from a JSNative // wrapper, so it gets no Rust-side factory. @@ -1651,7 +1570,6 @@ fn process_methods(_attr: TokenStream, item: TokenStream, config: ClassConfig) - impl<'s> #type_name<'s> { #(#ctor_items)* #add_to_global_fn - #moved_post_init_fn } }; @@ -1710,7 +1628,6 @@ fn process_methods(_attr: TokenStream, item: TokenStream, config: ClassConfig) - let __typed = ::js::class::create_instance_with::<#inner_name>(scope, |_| { #inner_name::#fn_name(#inner_arg, #cx_arg #(#param_names),*) })?; - <#inner_name as ::js::class::ClassDef>::install_unforgeable(scope, __typed.as_object())?; Ok(__typed) } } @@ -1722,7 +1639,7 @@ fn process_methods(_attr: TokenStream, item: TokenStream, config: ClassConfig) - // Generate the UnforgeableRegistrar impl when the interface has // `#[getter(unforgeable)]` accessors (e.g. Event.isTrusted). These are - // installed per-instance by `ClassDef::install_unforgeable`. + // defined on the class's template object by `ClassDef::define_unforgeable`. let unforgeable_impl = if unforgeable_getters.is_empty() { quote! {} } else { @@ -1745,6 +1662,9 @@ fn process_methods(_attr: TokenStream, item: TokenStream, config: ClassConfig) - impl ::js::class::__UnforgeableRegistrar<#inner_name> for ::js::class::__UnforgeableReg<#inner_name> { + fn has(&self) -> bool { + true + } fn install( &self, scope: &::js::gc::scope::Scope<'_>, @@ -1932,13 +1852,10 @@ fn parse_method_info( /// on `FooImpl`. Methods on the newtype receive `self` as the rooted stack /// newtype, giving access to both the JS object and the private data. fn is_method_on_newtype(method: &MethodInfo) -> bool { - // Constructors, destructors, post-init, and static methods stay on FooImpl. + // Constructors, destructors, and static methods stay on FooImpl. if matches!( method.kind, - MethodKind::Constructor - | MethodKind::Destructor - | MethodKind::PostInit - | MethodKind::StaticMethod { .. } + MethodKind::Constructor | MethodKind::Destructor | MethodKind::StaticMethod { .. } ) { return false; } @@ -2850,12 +2767,11 @@ fn setup_fn_ident(ident: &Ident) -> Ident { /// A Rust-side instantiation function on the stack newtype for a setup-style /// constructor (`&self` receiver): allocate a default-initialized JS object, /// run the moved constructor body (`__ctor_setup_*`, emitted alongside) on the -/// rooted newtype, then `__post_init` if the class declares one. +/// rooted newtype. fn gen_setup_factory( info: &MethodInfo, type_name: &Ident, inner_name: &Ident, - post_init_call: &proc_macro2::TokenStream, ) -> proc_macro2::TokenStream { let factory_name = &info.fn_item.sig.ident; let setup_ident = setup_fn_ident(factory_name); @@ -2893,9 +2809,7 @@ fn gen_setup_factory( let mut setup_fn = info.fn_item.clone(); setup_fn.sig.ident = setup_ident; setup_fn.vis = Visibility::Inherited; // private - setup_fn - .attrs - .retain(|a| !a.path().is_ident("constructor") && !a.path().is_ident("post_init")); + setup_fn.attrs.retain(|a| !a.path().is_ident("constructor")); quote! { /// Construct a new instance and return the stack newtype. @@ -2907,10 +2821,6 @@ fn gen_setup_factory( #inner_name::default() })?; #setup_call - #post_init_call - // Install [LegacyUnforgeable] accessors, as the JS - // constructor path does. - <#inner_name as ::js::class::ClassDef>::install_unforgeable(scope, __typed.as_object())?; #[cfg(debug_assertions)] if let Some(__data) = ::js::class::get_private::<#inner_name>(__typed.0.as_raw()) { ::js::class::ClassDef::debug_assert_fully_initialized(__data); @@ -2925,8 +2835,7 @@ fn gen_setup_factory( /// A Rust-side instantiation function on the stack newtype for an old-style /// constructor (no receiver, returns `Self` or `Result`): build the -/// data, allocate the JS object around it, and install `[LegacyUnforgeable]` -/// accessors as the JS constructor path would. A fallible constructor's error +/// data and allocate the JS object around it. A fallible constructor's error /// is thrown as a JS exception via `ThrowException`, as in the JS constructor /// path. fn gen_instance_factory( @@ -2957,8 +2866,6 @@ fn gen_instance_factory( { unsafe { let __typed = #create; - // Install [LegacyUnforgeable] accessors. - <#inner_name as ::js::class::ClassDef>::install_unforgeable(scope, __typed.as_object())?; #[cfg(debug_assertions)] if let Some(__data) = ::js::class::get_private::<#inner_name>(__typed.0.as_raw()) { ::js::class::ClassDef::debug_assert_fully_initialized(__data); @@ -3098,14 +3005,14 @@ fn emit_native_fn( }, ReturnStyle::Void => quote! { #call; - let __set_ok = ::js::class::set_return(&scope, &__args, &::js::value::undefined()); - ::js::exception::check_fn_return(&scope, __set_ok, &#name_str) + __args.rval().set(::js::value::undefined()); + ::js::exception::check_fn_return(&scope, true, &#name_str) }, ReturnStyle::ResultVoid => quote! { match #call { Ok(()) => { - let __set_ok = ::js::class::set_return(&scope, &__args, &::js::value::undefined()); - ::js::exception::check_fn_return(&scope, __set_ok, &#name_str) + __args.rval().set(::js::value::undefined()); + ::js::exception::check_fn_return(&scope, true, &#name_str) } Err(__e) => { ::js::error::ThrowException::throw(__e, &scope); @@ -3140,7 +3047,7 @@ fn emit_native_fn( #this_in_closure #(#extractions)* #rest_setup - #call + #call.map_err(|__e| ::js::error::ThrowException::throw(__e, &scope)) })(); match __result { Ok(__v) => { @@ -3188,12 +3095,8 @@ fn emit_native_fn( Ok(o) => o, Err(_) => return ::js::exception::check_fn_return(&scope, false, &#name_str), }; - // Install [LegacyUnforgeable] accessors and assert full - // initialization, so an instance minted by a JS method call matches - // one built by the constructor or the Rust-side factory. - if <#type_name as ::js::class::ClassDef>::install_unforgeable(&scope, __typed.as_object()).is_err() { - return ::js::exception::check_fn_return(&scope, false, &#name_str); - } + // Assert full initialization, so an instance created by a JS method call + // matches one built by the constructor or the Rust-side factory. #[cfg(debug_assertions)] if let Some(__data) = unsafe { ::js::class::get_private::<#type_name>(__typed.as_raw()) } { ::js::class::ClassDef::debug_assert_fully_initialized(__data); @@ -3480,7 +3383,10 @@ fn gen_accessor_native( __args.rval().set(unsafe { ::js::value::from_object(__p.as_raw()) }); ::js::exception::check_fn_return(&scope, true, &#name_str) } - Err(_) => match ::js::Promise::new_rejected_with_pending_error(&scope) { + Err(__e) => match { + ::js::error::ThrowException::throw(__e, &scope); + ::js::Promise::new_rejected_with_pending_error(&scope) + } { Ok(__rejected) => { __args.rval().set(unsafe { ::js::value::from_object(__rejected.as_raw()) }); ::js::exception::check_fn_return(&scope, true, &#name_str) @@ -4447,15 +4353,27 @@ struct ModuleConstExport { /// assert_eq!(my_math::add(1.0, my_math::PI), 4.14159); /// ``` /// -/// // Call from JS: +/// Call from JS: +/// /// ```js /// console.log(add(1, PI)); // prints 4.14159 /// ``` -/// ``` +/// +/// The attribute takes no options. #[proc_macro_attribute] pub fn jsglobals(attr: TokenStream, item: TokenStream) -> TokenStream { - let opts = parse_macro_input!(attr as AttrOpts); - let _ = opts; // No options used currently + if !attr.is_empty() { + return syn::Error::new( + proc_macro2::TokenStream::from(attr) + .into_iter() + .next() + .unwrap() + .span(), + "#[jsglobals] takes no options", + ) + .to_compile_error() + .into(); + } let input = parse_macro_input!(item as syn::ItemMod); let mod_name = &input.ident; @@ -4536,7 +4454,6 @@ pub fn jsglobals(attr: TokenStream, item: TokenStream) -> TokenStream { .collect(); let output = quote! { - #[allow(unused_imports)] #mod_vis mod #mod_name { #(#original_items)* @@ -4707,7 +4624,6 @@ fn process_namespace(opts: AttrOpts, input: syn::ItemMod, config: NamespaceConfi let js_ns_name_cstr = proc_macro2::Literal::byte_string(js_ns_name_bytes.as_bytes()); let output = quote! { - #[allow(unused_imports)] #mod_vis mod #mod_name { #(#original_items)* @@ -5189,7 +5105,12 @@ fn gen_dict_member_extraction(member: &DictMember) -> proc_macro2::TokenStream { quote! { #binding = if let Some(ref __obj) = __obj { - let __prop = __obj.get_property(scope, #js_name_cstr_lit) + let __id = ::js::class::get_or_init_property_id( + scope, + #js_name_cstr_lit, + ) + .map_err(|_| ::js::conversion::ConversionError::ExnPending)?; + let __prop = __obj.get_property_by_id(scope, __id) .map_err(|_| ::js::conversion::ConversionError::ExnPending)?; if __prop.get().is_undefined() { #absent diff --git a/starling/examples/class_inheritance.rs b/starling/examples/class_inheritance.rs index a5752d94..f202a22b 100644 --- a/starling/examples/class_inheritance.rs +++ b/starling/examples/class_inheritance.rs @@ -9,6 +9,7 @@ //! - Constructor with parent initialization use js::compile::evaluate_with_filename; +use js::conversion::FromJSVal; use js::gc::scope::Scope; use js::prelude::HandleValue; use libstarling::config::RuntimeConfig; @@ -108,7 +109,7 @@ impl Puppy { } fn main() { - let rt = Runtime::init(&RuntimeConfig::default()); + let rt = Runtime::init(&RuntimeConfig::default()).expect("runtime init"); let scope = rt.default_global(); let global = scope.global(); @@ -277,8 +278,7 @@ r.join(", ") /// Helper: extract a Rust String from a JS string value. fn val_to_string(scope: &Scope<'_>, val: &HandleValue) -> String { assert!(val.is_string(), "Expected string value"); - let s = js::JSString::from_value(scope, *val).expect("null string"); - s.to_utf8(scope).expect("utf8 conversion failed") + String::from_jsval(scope, *val, ()).expect("ToString succeeds") } #[test] diff --git a/starling/examples/class_methods.rs b/starling/examples/class_methods.rs index 351577b7..3435124a 100644 --- a/starling/examples/class_methods.rs +++ b/starling/examples/class_methods.rs @@ -46,7 +46,7 @@ impl MyClass { } fn main() { - let rt = Runtime::init(&RuntimeConfig::default()); + let rt = Runtime::init(&RuntimeConfig::default()).expect("runtime init"); let scope = rt.default_global(); let global = scope.global(); diff --git a/starling/examples/globals.rs b/starling/examples/globals.rs index 1fb127f8..95e655ea 100644 --- a/starling/examples/globals.rs +++ b/starling/examples/globals.rs @@ -63,7 +63,7 @@ mod my_globals { // ============================================================================ fn main() { - let rt = Runtime::init(&RuntimeConfig::default()); + let rt = Runtime::init(&RuntimeConfig::default()).expect("runtime init"); let scope = rt.default_global(); let global = scope.global(); diff --git a/starling/examples/heap_refs.rs b/starling/examples/heap_refs.rs index bd912ec0..f956dfde 100644 --- a/starling/examples/heap_refs.rs +++ b/starling/examples/heap_refs.rs @@ -115,7 +115,7 @@ impl Container { // ============================================================================ fn main() { - let rt = Runtime::init(&RuntimeConfig::default()); + let rt = Runtime::init(&RuntimeConfig::default()).expect("runtime init"); let scope = rt.default_global(); let global = scope.global(); diff --git a/starling/examples/native_module.rs b/starling/examples/native_module.rs index 6d06f8bd..d2006a06 100644 --- a/starling/examples/native_module.rs +++ b/starling/examples/native_module.rs @@ -43,7 +43,7 @@ mod math_utils { } fn main() { - let rt = Runtime::init(&RuntimeConfig::default()); + let rt = Runtime::init(&RuntimeConfig::default()).expect("runtime init"); let scope = rt.default_global(); // Register native module using the generated convenience function diff --git a/starling/examples/showcase.rs b/starling/examples/showcase.rs index 3461c1a0..d04e2c25 100644 --- a/starling/examples/showcase.rs +++ b/starling/examples/showcase.rs @@ -297,7 +297,7 @@ mod app_globals { // ============================================================================ fn main() { - let rt = Runtime::init(&RuntimeConfig::default()); + let rt = Runtime::init(&RuntimeConfig::default()).expect("runtime init"); let scope = rt.default_global(); let global = scope.global(); diff --git a/starling/src/lib.rs b/starling/src/lib.rs index 788e3376..6c2683f6 100644 --- a/starling/src/lib.rs +++ b/starling/src/lib.rs @@ -20,19 +20,16 @@ /// not be called more than once". #[unsafe(no_mangle)] pub extern "C" fn __wrap___wasm_call_ctors() { - static mut RAN: bool = false; - // SAFETY: wasm32 is single-threaded, and the constructors this guards run - // before any code that could call it reentrantly. - unsafe { - if RAN { - return; - } - RAN = true; - unsafe extern "C" { - fn __real___wasm_call_ctors(); - } - __real___wasm_call_ctors(); + static RAN: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false); + if RAN.swap(true, std::sync::atomic::Ordering::Relaxed) { + return; + } + unsafe extern "C" { + fn __real___wasm_call_ctors(); } + // SAFETY: `__real___wasm_call_ctors` is the linker's constructor entry point, which takes no + // arguments. The guard above runs it at most once. + unsafe { __real___wasm_call_ctors() }; } /// The wasm component exports both `wasi:cli/run` and `wasi:http/handler`, so one build serves diff --git a/starling/src/main.rs b/starling/src/main.rs index 178ed825..673b42c4 100644 --- a/starling/src/main.rs +++ b/starling/src/main.rs @@ -16,13 +16,15 @@ use libstarling::config::RuntimeConfig; fn main() { let config = match RuntimeConfig::from_args(std::env::args()) { Ok(config) => config, - Err(e) => { - let _ = e.print(); - exit(0); - } + // `--help` and `--version` print to stdout and exit with 0. A usage error prints to + // stderr and exits with 2. + Err(e) => e.exit(), }; - let _ = libstarling::run(config).map_err(|e| println!("{e}")); + if let Err(e) = libstarling::run(config) { + eprintln!("{e}"); + exit(1); + } } #[cfg(target_arch = "wasm32")] diff --git a/starling/tests/exit_status.rs b/starling/tests/exit_status.rs index b1903674..48008b57 100644 --- a/starling/tests/exit_status.rs +++ b/starling/tests/exit_status.rs @@ -6,11 +6,8 @@ use std::process::Command; -/// An error before the event loop runs — here an unreadable `-i` initializer -/// script — must exit with code 1. The Runtime is still alive when -/// `process::exit` fires the atexit handler, so a shutdown that insisted on -/// dropping the engine would trip mozjs's outstanding-handle assert and turn -/// the exit into a SIGABRT. +/// An error before the event loop runs, here an unreadable `-i` initializer +/// script, exits with code 1 rather than aborting. #[test] fn initializer_error_exits_with_code_one() { let out = Command::new(env!("CARGO_BIN_EXE_starlingmonkey")) @@ -94,3 +91,52 @@ fn initializer_event_loop_semantics() { String::from_utf8_lossy(&out.stderr) ); } + +/// A content script that throws reports the error on stderr and exits with 1. +#[test] +fn throwing_script_exits_with_code_one() { + let out = Command::new(env!("CARGO_BIN_EXE_starlingmonkey")) + .args(["-e", "throw new Error('boom')"]) + .output() + .expect("failed to run starling"); + let stderr = String::from_utf8_lossy(&out.stderr); + assert_eq!(out.status.code(), Some(1), "stderr: {stderr}"); + assert!(stderr.contains("boom"), "stderr: {stderr}"); + assert!( + !String::from_utf8_lossy(&out.stdout).contains("boom"), + "the error must not go to stdout" + ); +} + +/// `--help` exits with 0 and an unknown flag with clap's usage-error status 2. +#[test] +fn argument_errors_use_clap_exit_statuses() { + let help = Command::new(env!("CARGO_BIN_EXE_starlingmonkey")) + .arg("--help") + .output() + .expect("failed to run starling"); + assert_eq!(help.status.code(), Some(0)); + let bad = Command::new(env!("CARGO_BIN_EXE_starlingmonkey")) + .arg("--no-such-flag") + .output() + .expect("failed to run starling"); + assert_eq!(bad.status.code(), Some(2)); +} + +/// A syntax error in an imported file module is reported with its own message +/// and location, not replaced by a generic load failure. +#[test] +fn imported_module_syntax_error_keeps_its_location() { + let dir = tempfile::tempdir().unwrap(); + let main = dir.path().join("main.mjs"); + std::fs::write(&main, "import './bad.mjs';").unwrap(); + std::fs::write(dir.path().join("bad.mjs"), "export const x = ;").unwrap(); + let out = Command::new(env!("CARGO_BIN_EXE_starlingmonkey")) + .arg(&main) + .output() + .expect("failed to run starling"); + let stderr = String::from_utf8_lossy(&out.stderr); + assert_eq!(out.status.code(), Some(1), "stderr: {stderr}"); + assert!(stderr.contains("expected expression"), "stderr: {stderr}"); + assert!(stderr.contains("bad.mjs:1:"), "stderr: {stderr}"); +} diff --git a/tests/wpt-harness/expectations/fetch/api/request/request-error.any.js.json b/tests/wpt-harness/expectations/fetch/api/request/request-error.any.js.json index 72387c40..62f0a68c 100644 --- a/tests/wpt-harness/expectations/fetch/api/request/request-error.any.js.json +++ b/tests/wpt-harness/expectations/fetch/api/request/request-error.any.js.json @@ -41,8 +41,7 @@ "permissive_status": "FAIL" }, "Bad referrerPolicy init parameter value": { - "status": "PASS", - "permissive_status": "FAIL" + "status": "PASS" }, "Bad mode init parameter value": { "status": "PASS"