diff --git a/src/rust/AGENTS.md b/src/rust/AGENTS.md index acbfb96200a..89a879e55d3 100644 --- a/src/rust/AGENTS.md +++ b/src/rust/AGENTS.md @@ -14,7 +14,7 @@ _Snapshot — the set drifts as crates come and go; `bazel query //src/rust/...` | Crate | Purpose | | ----------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `jsg/` | Rust JSG bindings: `Lock`, `Rc`, `Resource`, `Struct`, `Type`, `Realm`, `FeatureFlags`, module registration; V8 handle types including typed arrays, `ArrayBuffer`, `ArrayBufferView`, `SharedArrayBuffer`, `BackingStore`; JS functions from Rust closures (`v8::Function::new`) | +| `jsg/` | Rust JSG bindings: `Lock`, `Rc`, `Resource`, `Struct`, `Type`, `Realm`, `FeatureFlags`, module registration; V8 handle types including typed arrays, `ArrayBuffer`, `ArrayBufferView`, `SharedArrayBuffer`, `BackingStore`; JS functions from Rust closures (`v8::Function::new`); async context frames (`AsyncContextFrame`, `TracedAsyncContextFrame`) | | `jsg-macros/` | Proc macros: `#[jsg_struct]`, `#[jsg_method]`, `#[jsg_resource]`, `#[jsg_oneof]`, `#[jsg_static_constant]`, `#[jsg_constructor]`, `#[jsg_property]`, `#[jsg_inspect_property]`, `#[jsg_wildcard_property]` | | `jsg-test/` | Test harness (`Harness`) for JSG Rust bindings | | `api/` | Rust-implemented Node.js APIs; registers modules via `register_nodejs_modules()` | diff --git a/src/rust/jsg-compile-tests/async-context-enter-holds-the-lock.rs b/src/rust/jsg-compile-tests/async-context-enter-holds-the-lock.rs new file mode 100644 index 00000000000..41fa2d22a2a --- /dev/null +++ b/src/rust/jsg-compile-tests/async-context-enter-holds-the-lock.rs @@ -0,0 +1,11 @@ +// Copyright (c) 2026 Cloudflare, Inc. +// Licensed under the Apache 2.0 license found in the LICENSE file or at: +// https://opensource.org/licenses/Apache-2.0 + +// `enter` borrows the caller's lock for as long as the frame is current, so the closure can only +// use the lock it is handed, not the caller's. +pub fn t(lock: &mut jsg::Lock<'_>, frame: &jsg::AsyncContextFrame) { + frame.enter(lock, |_inner| lock.new_object()); //~ ERROR E0501 + + //~| ERROR E0500 +} diff --git a/src/rust/jsg-compile-tests/async-context-enter-lock-cannot-escape.rs b/src/rust/jsg-compile-tests/async-context-enter-lock-cannot-escape.rs new file mode 100644 index 00000000000..7429cd9e070 --- /dev/null +++ b/src/rust/jsg-compile-tests/async-context-enter-lock-cannot-escape.rs @@ -0,0 +1,11 @@ +// Copyright (c) 2026 Cloudflare, Inc. +// Licensed under the Apache 2.0 license found in the LICENSE file or at: +// https://opensource.org/licenses/Apache-2.0 + +// The lock that `enter` hands its closure is only borrowed for the closure, while the frame is +// current, so it cannot be kept for use after the previous frame is restored. +pub fn t<'s>(lock: &mut jsg::Lock<'s>, frame: &jsg::AsyncContextFrame) { + let mut escaped = None; + frame.enter(lock, |lock| escaped = Some(lock)); //~ ERROR E0521 + let _ = escaped.map(|lock: &mut jsg::Lock<'s>| lock.new_object()); +} diff --git a/src/rust/jsg-compile-tests/async-context-frame-is-not-traced.rs b/src/rust/jsg-compile-tests/async-context-frame-is-not-traced.rs new file mode 100644 index 00000000000..6e56a8cefe1 --- /dev/null +++ b/src/rust/jsg-compile-tests/async-context-frame-is-not-traced.rs @@ -0,0 +1,18 @@ +// Copyright (c) 2026 Cloudflare, Inc. +// Licensed under the Apache 2.0 license found in the LICENSE file or at: +// https://opensource.org/licenses/Apache-2.0 + +// An `AsyncContextFrame` holds a strong `Global`, so a cycle through it is never collected. It is +// not `Traced`, so it cannot be a field of a resource that GC traces; a resource holds a +// `TracedAsyncContextFrame`. +use std::cell::Cell; + +use jsg_macros::jsg_resource; + +#[jsg_resource] //~ ERROR E0277 +struct Holder { + frame: Cell>, +} + +#[jsg_resource] +impl Holder {} diff --git a/src/rust/jsg-compile-tests/ok-async-context-frame.rs b/src/rust/jsg-compile-tests/ok-async-context-frame.rs new file mode 100644 index 00000000000..b5774d64cb7 --- /dev/null +++ b/src/rust/jsg-compile-tests/ok-async-context-frame.rs @@ -0,0 +1,34 @@ +// Copyright (c) 2026 Cloudflare, Inc. +// Licensed under the Apache 2.0 license found in the LICENSE file or at: +// https://opensource.org/licenses/Apache-2.0 + +// An `AsyncContextFrame` is `'static`, `Send` and `Sync`, so a future passed to async I/O can hold +// it, and a `TracedAsyncContextFrame` is `Send` and can be a resource field. `enter` opens no +// scope, so a handle created in the frame stays valid after it. +use std::cell::Cell; + +use jsg::v8; +use jsg_macros::jsg_resource; + +fn assert_static_send_and_sync() {} +fn assert_static_send() {} + +#[jsg_resource] +struct Holder { + frame: Cell>, +} + +#[jsg_resource] +impl Holder {} + +pub fn t<'s>(lock: &mut jsg::Lock<'s>) -> v8::Local<'s, v8::Object> { + assert_static_send_and_sync::(); + assert_static_send::(); + let frame = jsg::AsyncContextFrame::current(lock); + let object = frame.enter(lock, |lock| lock.new_object()); + let holder = Holder { + frame: Cell::new(Some(frame.into())), + }; + drop(holder); + object +} diff --git a/src/rust/jsg-compile-tests/traced-async-context-frame-is-not-sync.rs b/src/rust/jsg-compile-tests/traced-async-context-frame-is-not-sync.rs new file mode 100644 index 00000000000..912dac0d822 --- /dev/null +++ b/src/rust/jsg-compile-tests/traced-async-context-frame-is-not-sync.rs @@ -0,0 +1,17 @@ +// Copyright (c) 2026 Cloudflare, Inc. +// Licensed under the Apache 2.0 license found in the LICENSE file or at: +// https://opensource.org/licenses/Apache-2.0 + +// A `TracedAsyncContextFrame` holds a `TracedGlobal`, which can move to another thread but not be +// shared with one. +// +// This compiles only if the type is not Sync; see `traced-global-is-not-sync.rs`. +trait AmbiguousIfSync { + fn some_item() {} +} +impl AmbiguousIfSync<()> for T {} +impl AmbiguousIfSync for T {} + +pub fn t() { + let _ = >::some_item; +} diff --git a/src/rust/jsg-test/ffi.c++ b/src/rust/jsg-test/ffi.c++ index ed11c724a3e..e0f80d13262 100644 --- a/src/rust/jsg-test/ffi.c++ +++ b/src/rust/jsg-test/ffi.c++ @@ -5,6 +5,7 @@ #include "ffi.h" #include +#include #include #include #include @@ -254,6 +255,39 @@ void hold_across_gc(Isolate* isolate, request_gc(isolate, GcType::Full); } +namespace { +// The storage key of the async context frames the tests create, like the key of one +// AsyncLocalStorage. +const kj::Arc<::workerd::jsg::AsyncContextFrame::StorageKey>& testStorageKey() { + static const auto key = kj::arc<::workerd::jsg::AsyncContextFrame::StorageKey>(); + return key; +} +} // namespace + +void run_in_async_context(Isolate* isolate, + ::workerd::rust::jsg::Local value, + size_t data, + ::rust::Fn callback) { + auto& js = ::workerd::jsg::Lock::from(isolate); + ::workerd::jsg::AsyncContextFrame::StorageScope scope(js, testStorageKey().addRef(), + js.v8Ref(::workerd::rust::jsg::local_from_ffi(kj::mv(value)))); + callback(data); +} + +::workerd::rust::jsg::MaybeLocal async_context_value(Isolate* isolate) { + KJ_IF_SOME(frame, ::workerd::jsg::AsyncContextFrame::current(isolate)) { + KJ_IF_SOME(value, frame.get(*testStorageKey())) { + return ::workerd::rust::jsg::maybe_local_to_ffi( + v8::MaybeLocal(value.getHandle(isolate))); + } + } + return ::workerd::rust::jsg::maybe_local_to_ffi(v8::MaybeLocal()); +} + +bool cpp_sees_root_frame(Isolate* isolate) { + return ::workerd::jsg::AsyncContextFrame::current(isolate) == kj::none; +} + } // namespace rust::jsg_test } // namespace workerd diff --git a/src/rust/jsg-test/ffi.h b/src/rust/jsg-test/ffi.h index d54b0ef5d03..edacdb68dca 100644 --- a/src/rust/jsg-test/ffi.h +++ b/src/rust/jsg-test/ffi.h @@ -111,6 +111,20 @@ void hold_across_gc(Isolate* isolate, ::rust::Fn<::workerd::rust::jsg::MaybeLocal(Isolate*)> create, ::rust::Fn check); +// Calls `callback` in a new C++ jsg::AsyncContextFrame that stores `value` under the tests' +// storage key, as AsyncLocalStorage.run() does, then makes the previous frame current again. +void run_in_async_context(Isolate* isolate, + ::workerd::rust::jsg::Local value, + size_t data, + ::rust::Fn callback); + +// The value the current async context frame stores under the tests' storage key, or empty in the +// root frame or a frame that stores none. +::workerd::rust::jsg::MaybeLocal async_context_value(Isolate* isolate); + +// Returns whether C++ jsg::AsyncContextFrame::current() finds no frame, i.e. sees the root frame. +bool cpp_sees_root_frame(Isolate* isolate); + } // namespace rust::jsg_test } // namespace workerd diff --git a/src/rust/jsg-test/lib.rs b/src/rust/jsg-test/lib.rs index b4086b79944..c9bc2a53e2c 100644 --- a/src/rust/jsg-test/lib.rs +++ b/src/rust/jsg-test/lib.rs @@ -137,6 +137,22 @@ mod ffi { create: unsafe fn(*mut Isolate) -> MaybeLocal, check: unsafe fn(*mut Isolate, Local), ); + + /// Calls `callback` in a new C++ `jsg::AsyncContextFrame` that stores `value` under the + /// tests' storage key, as `AsyncLocalStorage.run()` does. + pub unsafe fn run_in_async_context( + isolate: *mut Isolate, + value: Local, + data: usize, /* callback */ + callback: unsafe fn(usize /* callback */), + ); + + /// The value the current async context frame stores under the tests' storage key, or + /// empty if it stores none. + pub unsafe fn async_context_value(isolate: *mut Isolate) -> MaybeLocal; + + /// Whether C++ `jsg::AsyncContextFrame::current()` finds no frame. + pub unsafe fn cpp_sees_root_frame(isolate: *mut Isolate) -> bool; } } @@ -498,6 +514,71 @@ impl Harness { // SAFETY: the isolate is valid and locked with a context entered (guaranteed by Lock). unsafe { ffi::hold_across_gc(lock.isolate().as_ffi(), create, check) }; } + + /// Runs `f` in a new C++ `jsg::AsyncContextFrame` that stores `value` under the tests' + /// storage key, as `AsyncLocalStorage.run()` does, and makes the previous frame current + /// again afterwards. [`Harness::async_context_value`] reads the value back. + /// + /// # Panics + /// + /// Panics if `f` panics, once the previous frame is current again. + pub fn run_in_async_context<'s, F: FnOnce(&mut jsg::Lock<'s>) -> R, R>( + lock: &mut jsg::Lock<'s>, + value: v8::Local<'s, v8::Value>, + f: F, + ) -> R { + struct Call<'a, 's, F, R> { + lock: &'a mut jsg::Lock<'s>, + f: Option, + result: Option>>, + } + + fn trampoline<'s, F: FnOnce(&mut jsg::Lock<'s>) -> R, R>(data: usize) { + // SAFETY: data was cast from &raw mut Call in run_in_async_context below. + let call = unsafe { &mut *(data as *mut Call<'_, 's, F, R>) }; + let Some(f) = call.f.take() else { + return; + }; + let lock = &mut *call.lock; + call.result = Some(std::panic::catch_unwind(std::panic::AssertUnwindSafe( + || f(lock), + ))); + } + + let isolate = lock.isolate().as_ffi(); + // SAFETY: `value` is valid in the lock's scope. + let value = unsafe { value.into_ffi() }; + let mut call = Call:: { + lock, + f: Some(f), + result: None, + }; + // SAFETY: the lock keeps the isolate locked with a context entered and a scope open, and + // C++ opens no scope around the callback, so the lock stays valid in it; `call` outlives + // the call. + unsafe { + ffi::run_in_async_context(isolate, value, &raw mut call as usize, trampoline::); + } + match call.result.expect("the callback ran") { + Ok(result) => result, + Err(payload) => std::panic::resume_unwind(payload), + } + } + + /// The value that the current async context frame stores under the tests' storage key (see + /// [`Harness::run_in_async_context`]), or `None` in the root frame. + pub fn async_context_value<'s>(lock: &mut jsg::Lock<'s>) -> Option> { + // SAFETY: the lock keeps the isolate locked with a scope open, which the value belongs + // to. + unsafe { v8::Local::from_ffi_maybe(ffi::async_context_value(lock.isolate().as_ffi())) } + } + + /// Whether C++ `jsg::AsyncContextFrame::current()` sees the root frame, to compare with + /// Rust's [`jsg::AsyncContextFrame::is_root`]. + pub fn cpp_sees_root_frame(lock: &mut jsg::Lock<'_>) -> bool { + // SAFETY: the lock keeps the isolate locked. + unsafe { ffi::cpp_sees_root_frame(lock.isolate().as_ffi()) } + } } /// Whether `a` and `b` belong to the same `v8::IsolateGroup`. diff --git a/src/rust/jsg-test/tests/async_context.rs b/src/rust/jsg-test/tests/async_context.rs new file mode 100644 index 00000000000..00376adb0b7 --- /dev/null +++ b/src/rust/jsg-test/tests/async_context.rs @@ -0,0 +1,530 @@ +// Copyright (c) 2026 Cloudflare, Inc. +// Licensed under the Apache 2.0 license found in the LICENSE file or at: +// https://opensource.org/licenses/Apache-2.0 + +//! `jsg::AsyncContextFrame` and `jsg::TracedAsyncContextFrame`: capturing the current async +//! context frame and running code in it later. +//! +//! The frames are C++ `jsg::AsyncContextFrame`s that `Harness::run_in_async_context` creates, +//! as `AsyncLocalStorage.run()` does, each storing a value under one test storage key. Rust +//! reads the current frame's value with `Harness::async_context_value`, and JavaScript with +//! `probe.value()`. + +use std::cell::Cell; +use std::panic::AssertUnwindSafe; +use std::sync::atomic::AtomicUsize; +use std::sync::atomic::Ordering; + +use jsg::AsyncContextFrame; +use jsg::FromJS; +use jsg::ToJS; +use jsg::TracedAsyncContextFrame; +use jsg::v8; +use jsg_macros::jsg_method; +use jsg_macros::jsg_resource; + +use crate::Harness; + +/// The value the current frame stores, or `None` in the root frame. +fn current(lock: &mut jsg::Lock) -> Option { + Harness::async_context_value(lock).map(|value| String::from_js(lock, value).unwrap()) +} + +/// Captures the frame of a new `run_in_async_context` that stores `value`. +fn frame_storing(lock: &mut jsg::Lock, value: &str) -> AsyncContextFrame { + let value = value.to_js(lock); + Harness::run_in_async_context(lock, value, AsyncContextFrame::current) +} + +/// Lets JavaScript read the current frame, and holds a frame for later calls. +#[jsg_resource] +struct Probe { + frame: Cell>, + /// Whether the frame current before `run_in_frame` entered its frame was current again + /// after it. + restored: Cell, + /// Whether the callback `run_in_frame` called failed with a pending error. + saw_pending: Cell, +} + +impl Probe { + /// A strong handle to the captured frame. The traced frame goes back into its field before + /// anything can run JavaScript, which the caller may do with the handle it gets. + fn frame(&self, lock: &mut jsg::Lock) -> AsyncContextFrame { + let traced = self.frame.take().unwrap(); + let frame = traced.to_async_context_frame(lock); + self.frame.set(Some(traced)); + frame + } +} + +#[jsg_resource] +impl Probe { + /// The value the current frame stores, or `undefined` in the root frame. + #[jsg_method] + fn value<'s>(&self, lock: &mut jsg::Lock<'s>) -> v8::Local<'s, v8::Value> { + Harness::async_context_value(lock).unwrap_or_else(|| lock.undefined()) + } + + /// Captures the frame of a new `run_in_async_context` that stores `value`. Nothing but + /// this resource holds the frame afterwards. + #[jsg_method] + fn capture_in<'s>(&self, lock: &mut jsg::Lock<'s>, value: v8::Local<'s, v8::Value>) { + let frame = Harness::run_in_async_context(lock, value, TracedAsyncContextFrame::current); + self.frame.set(Some(frame)); + } + + /// The value the captured frame stores, read in that frame. + #[jsg_method] + fn value_in_frame<'s>(&self, lock: &mut jsg::Lock<'s>) -> v8::Local<'s, v8::Value> { + self.frame(lock).enter(lock, |lock| self.value(lock)) + } + + /// Calls `callback` in the captured frame. + #[jsg_method] + fn run_in_frame<'s>( + &self, + lock: &mut jsg::Lock<'s>, + callback: v8::Local<'s, v8::Value>, + ) -> Result<(), jsg::Error> { + let callback = v8::Local::::try_from(callback).unwrap(); + let frame = self.frame(lock); + // Compared as handles, since converting them could run JavaScript, which must not run + // while execution is terminating. + let before = Harness::async_context_value(lock); + let result = frame.enter(lock, |lock| { + callback.call::>(lock, None, &[]) + }); + self.restored + .set(Harness::async_context_value(lock) == before); + self.saw_pending + .set(result.as_ref().is_err_and(jsg::Error::is_pending)); + result.map(|_| ()) + } + + /// Requests termination, which takes effect when JavaScript next checks for interrupts. + #[jsg_method] + fn terminate(&self, lock: &mut jsg::Lock) { + lock.terminate_execution(); + } +} + +fn install_probe(lock: &mut jsg::Lock, ctx: &crate::EvalContext<'_>) -> jsg::Rc { + let probe = jsg::Rc::new(Probe { + frame: Cell::new(None), + restored: Cell::new(false), + saw_pending: Cell::new(false), + }); + ctx.set_global(lock, "probe", probe.clone()); + probe +} + +#[test] +fn captured_frame_is_entered_later() { + Harness::new().run_in_context(|lock, ctx| { + install_probe(lock, ctx); + let frame = frame_storing(lock, "a"); + assert!(!frame.is_root()); + assert_eq!(current(lock), None); + let seen = frame.enter(lock, |lock| { + assert_eq!(current(lock).as_deref(), Some("a")); + ctx.eval::(lock, "probe.value()").unwrap() + }); + assert_eq!(seen, "a"); + assert_eq!(current(lock), None); + Ok(()) + }); +} + +#[test] +fn frame_captured_in_root_is_root() { + Harness::new().run_in_context(|lock, _ctx| { + let frame = AsyncContextFrame::current(lock); + assert!(frame.is_root()); + assert!(AsyncContextFrame::root().is_root()); + assert!(TracedAsyncContextFrame::current(lock).is_root()); + frame.enter(lock, |lock| assert_eq!(current(lock), None)); + Ok(()) + }); +} + +/// Rust treats any embedder data but `undefined` as a frame, while C++ recognizes a frame by its +/// wrapper. Since Rust sets only `undefined` and values it read, the two agree on whether the +/// current frame is the root in every state Rust can leave the isolate in. +#[test] +fn rust_and_cpp_agree_on_root_frame() { + /// Asserts that Rust and C++ agree on whether the current frame is the root, and returns it. + fn is_root(lock: &mut jsg::Lock<'_>) -> bool { + let rust = AsyncContextFrame::current(lock).is_root(); + assert_eq!(rust, Harness::cpp_sees_root_frame(lock)); + rust + } + + Harness::new().run_in_context(|lock, _ctx| { + assert!(is_root(lock)); + let frame = frame_storing(lock, "a"); + frame.enter(lock, |lock| assert!(!is_root(lock))); + assert!(is_root(lock)); + + let value = "outer".to_js(lock); + Harness::run_in_async_context(lock, value, |lock| { + assert!(!is_root(lock)); + AsyncContextFrame::root().enter(lock, |lock| assert!(is_root(lock))); + TracedAsyncContextFrame::root().enter(lock, |lock| assert!(is_root(lock))); + frame.enter(lock, |lock| assert!(!is_root(lock))); + assert!(!is_root(lock)); + }); + assert!(is_root(lock)); + Ok(()) + }); +} + +#[test] +fn entering_root_frame_leaves_frame_for_its_duration() { + Harness::new().run_in_context(|lock, _ctx| { + let value = "outer".to_js(lock); + Harness::run_in_async_context(lock, value, |lock| { + AsyncContextFrame::root().enter(lock, |lock| assert_eq!(current(lock), None)); + TracedAsyncContextFrame::root().enter(lock, |lock| assert_eq!(current(lock), None)); + assert_eq!(current(lock).as_deref(), Some("outer")); + }); + Ok(()) + }); +} + +#[test] +fn nested_enters_restore_each_previous_frame() { + Harness::new().run_in_context(|lock, _ctx| { + let a = frame_storing(lock, "a"); + let b: TracedAsyncContextFrame = frame_storing(lock, "b").into(); + a.enter(lock, |lock| { + b.enter(lock, |lock| { + assert_eq!(current(lock).as_deref(), Some("b")); + a.enter(lock, |lock| assert_eq!(current(lock).as_deref(), Some("a"))); + assert_eq!(current(lock).as_deref(), Some("b")); + }); + assert_eq!(current(lock).as_deref(), Some("a")); + }); + assert_eq!(current(lock), None); + Ok(()) + }); +} + +#[test] +fn clone_enters_same_frame() { + Harness::new().run_in_context(|lock, _ctx| { + let a = frame_storing(lock, "a"); + let clone = a.clone(lock); + drop(a); + clone.enter(lock, |lock| assert_eq!(current(lock).as_deref(), Some("a"))); + let traced = TracedAsyncContextFrame::from(clone); + let traced_clone = traced.clone(lock); + drop(traced); + traced_clone.enter(lock, |lock| assert_eq!(current(lock).as_deref(), Some("a"))); + Ok(()) + }); +} + +#[test] +fn enter_returns_closure_result_and_restores_after_error() { + Harness::new().run_in_context(|lock, _ctx| { + let a = frame_storing(lock, "a"); + let value = "outer".to_js(lock); + Harness::run_in_async_context(lock, value, |lock| { + let result: Result<(), jsg::Error> = + a.enter(lock, |_lock| Err(jsg::Error::new_type_error("failed"))); + assert_eq!(result.unwrap_err().message, "failed"); + assert_eq!(current(lock).as_deref(), Some("outer")); + let handle = a.enter(lock, |lock| "from a".to_js(lock)); + assert_eq!(String::from_js(lock, handle).unwrap(), "from a"); + }); + Ok(()) + }); +} + +#[test] +fn enter_restores_after_panic() { + Harness::new().run_in_context(|lock, _ctx| { + let a = frame_storing(lock, "a"); + let value = "outer".to_js(lock); + Harness::run_in_async_context(lock, value, |lock| { + let result = std::panic::catch_unwind(AssertUnwindSafe(|| { + a.enter(lock, |_lock| panic!("panic in frame")); + })); + assert!(result.is_err()); + assert_eq!(current(lock).as_deref(), Some("outer")); + }); + Ok(()) + }); +} + +/// A callback that terminates execution fails, and the frame current before `enter` is +/// current again while the termination still propagates. +#[test] +fn enter_restores_while_terminating() { + Harness::new().run_in_context(|lock, ctx| { + let probe = install_probe(lock, ctx); + let value = "a".to_js(lock); + probe.capture_in(lock, value); + let value = "outer".to_js(lock); + let result = Harness::run_in_async_context(lock, value, |lock| { + let result = ctx.eval_raw( + lock, + "probe.runInFrame(() => { probe.terminate(); while (true) {} }); 'not terminated'", + ); + assert_eq!(current(lock).as_deref(), Some("outer")); + result.is_err() + }); + assert!(result, "the script was not terminated"); + assert!(probe.saw_pending.get()); + assert!(probe.restored.get()); + assert_eq!(current(lock), None); + Ok(()) + }); +} + +/// V8 carries the current frame into the promise reactions registered in it, by JavaScript or +/// by Rust, which run in that frame after `enter` has returned. +#[test] +fn promise_reactions_registered_in_entered_frame_run_in_it() { + Harness::new().run_in_context(|lock, ctx| { + install_probe(lock, ctx); + let a = frame_storing(lock, "a"); + let resolver = v8::PromiseResolver::new(lock)?; + let promise = resolver.promise(); + ctx.set_global(lock, "pending", v8::Local::::from(promise)); + let in_root = promise.then(lock, (), |lock, (), _value| { + Ok(Harness::async_context_value(lock).unwrap_or_else(|| lock.undefined())) + })?; + let in_a = a.enter(lock, |lock| { + ctx.eval_raw( + lock, + "globalThis.seen = []; pending.then(() => seen.push(probe.value()))", + ) + .unwrap(); + promise.then(lock, (), |lock, (), _value| { + Ok(Harness::async_context_value(lock).unwrap_or_else(|| lock.undefined())) + }) + })?; + let undefined = lock.undefined(); + resolver.resolve(lock, undefined)?; + Harness::run_microtasks(lock); + + assert_eq!(ctx.eval::(lock, "seen.join()").unwrap(), "a"); + let result = in_a.result(lock).unwrap(); + assert_eq!(String::from_js(lock, result)?, "a"); + assert!(in_root.result(lock).unwrap().is_undefined()); + assert_eq!(current(lock), None); + Ok(()) + }); +} + +/// A frame captured under one lock is entered under a later one, as code that resumes after +/// async I/O does. +#[test] +fn frame_is_entered_in_later_lock() { + let harness = Harness::new(); + let mut kept = None; + harness.run_in_context(|lock, _ctx| { + kept = Some(frame_storing(lock, "a")); + Ok(()) + }); + let frame = kept.unwrap(); + harness.run_in_context(|lock, _ctx| { + // Only the `AsyncContextFrame` holds the frame's wrapper now. A minor GC would drop a + // wrapper that nothing else reaches, and the C++ frame with it. + Harness::request_minor_gc(lock); + Harness::request_gc(lock); + assert_eq!(current(lock), None); + frame.enter(lock, |lock| assert_eq!(current(lock).as_deref(), Some("a"))); + Ok(()) + }); +} + +static STORED_DROPS: AtomicUsize = AtomicUsize::new(0); + +/// A value to store in a frame, which counts its drops. +#[jsg_resource] +struct Stored { + /// The probe whose frame stores this value, which closes a cycle through the frame. + owner: Cell>>, +} + +impl Drop for Stored { + fn drop(&mut self) { + STORED_DROPS.fetch_add(1, Ordering::SeqCst); + } +} + +#[jsg_resource] +impl Stored {} + +/// A frame held only by a resource keeps itself and the values it stores alive across GC, and +/// GC collects a cycle through it: the probe holds the frame, which stores a value that holds +/// the probe. +/// +/// Each step runs under its own lock, so that no handle of an earlier step's scope keeps +/// anything alive. +#[test] +fn frame_in_resource_is_traced() { + STORED_DROPS.store(0, Ordering::SeqCst); + let harness = Harness::new(); + let mut kept = None; + harness.run_in_context(|lock, _ctx| { + let probe = jsg::Rc::new(Probe { + frame: Cell::new(None), + restored: Cell::new(false), + saw_pending: Cell::new(false), + }); + // The probe's wrapper is reachable, but the probe has no strong reference from Rust, so + // GC traces its frame through the wrapper. + let wrapper = probe.clone().to_js(lock); + let stored = jsg::Rc::new(Stored { + owner: Cell::new(Some(probe.clone())), + }) + .to_js(lock); + probe.capture_in(lock, stored); + kept = Some((wrapper.to_global(lock), probe.downgrade())); + Ok(()) + }); + let (wrapper, probe) = kept.unwrap(); + + // Each step runs two GCs, so that the result does not depend on whether a visit during the + // first one is what turns a strong handle into a traced one. + harness.run_in_context(|lock, _ctx| { + Harness::request_gc(lock); + Harness::request_gc(lock); + assert_eq!( + STORED_DROPS.load(Ordering::SeqCst), + 0, + "the stored value was collected while the frame was held" + ); + let probe = probe.upgrade().unwrap(); + let stored = probe.value_in_frame(lock); + assert!(jsg::Rc::::from_js(lock, stored).is_ok()); + Ok(()) + }); + + harness.run_in_context(|lock, _ctx| { + drop(wrapper); + Harness::request_gc(lock); + Harness::request_gc(lock); + assert_eq!( + STORED_DROPS.load(Ordering::SeqCst), + 1, + "the cycle through the frame was not collected" + ); + Ok(()) + }); +} + +static MARKER_DROPS: AtomicUsize = AtomicUsize::new(0); + +/// A value to store in a frame, which counts its drops. +#[jsg_resource] +struct Marker; + +impl Drop for Marker { + fn drop(&mut self) { + MARKER_DROPS.fetch_add(1, Ordering::SeqCst); + } +} + +#[jsg_resource] +impl Marker {} + +/// A frame in the data of a `v8::Function::new` function, which enters it to call its argument, +/// like C++ JSG's `AsyncContextFrame::wrap()`, survives GC while the function is reachable, and +/// is released with it. +/// +/// Each step runs under its own lock, and sets no globals, so that only the function keeps the +/// frame alive across the GCs between them. +#[test] +fn frame_in_function_data_lives_as_long_as_function() { + MARKER_DROPS.store(0, Ordering::SeqCst); + let harness = Harness::new(); + let mut kept = None; + harness.run_in_context(|lock, _ctx| { + let marker = jsg::Rc::new(Marker).to_js(lock); + let frame = Harness::run_in_async_context(lock, marker, TracedAsyncContextFrame::current); + let run_in_frame = v8::Function::new(lock, frame, |lock, frame, args| { + let callback = v8::Local::::try_from(args.get(0)).unwrap(); + frame.enter(lock, |lock| { + callback.call::>(lock, None, &[]) + }) + })?; + kept = Some(run_in_frame.to_global(lock)); + Ok(()) + }); + let run_in_frame = kept.unwrap(); + + harness.run_in_context(|lock, _ctx| { + Harness::request_gc(lock); + Harness::request_gc(lock); + assert_eq!(MARKER_DROPS.load(Ordering::SeqCst), 0); + let read_value = v8::Function::new(lock, (), |lock, (), _args| { + Ok(Harness::async_context_value(lock).unwrap_or_else(|| lock.undefined())) + })?; + let value = run_in_frame.as_local(lock).call::>( + lock, + None, + &[read_value.into()], + )?; + assert!(jsg::Rc::::from_js(lock, value).is_ok()); + assert_eq!(current(lock), None); + Ok(()) + }); + + harness.run_in_context(|lock, _ctx| { + drop(run_in_frame); + Harness::request_gc(lock); + Harness::request_gc(lock); + assert_eq!( + MARKER_DROPS.load(Ordering::SeqCst), + 1, + "the frame outlived the function that held it" + ); + Ok(()) + }); +} + +/// A frame converted from a resource's traced frame holds the frame strongly on its own. +#[test] +fn traced_frame_converts_to_async_context_frame() { + let harness = Harness::new(); + let mut kept = None; + harness.run_in_context(|lock, _ctx| { + let traced: TracedAsyncContextFrame = frame_storing(lock, "a").into(); + kept = Some(traced.to_async_context_frame(lock)); + assert!( + TracedAsyncContextFrame::root() + .to_async_context_frame(lock) + .is_root() + ); + Ok(()) + }); + let frame = kept.unwrap(); + harness.run_in_context(|lock, _ctx| { + Harness::request_minor_gc(lock); + Harness::request_gc(lock); + frame.enter(lock, |lock| assert_eq!(current(lock).as_deref(), Some("a"))); + Ok(()) + }); +} + +/// Frames may outlive their isolate: one held by a resource at teardown, and one held by Rust +/// and dropped after the isolate is disposed. +#[test] +fn frames_dropped_at_or_after_teardown() { + let harness = Harness::new(); + let mut kept = None; + harness.run_in_context(|lock, ctx| { + let probe = install_probe(lock, ctx); + let value = "held by the probe".to_js(lock); + probe.capture_in(lock, value); + kept = Some(frame_storing(lock, "held by Rust")); + Ok(()) + }); + drop(harness); + drop(kept.unwrap()); +} diff --git a/src/rust/jsg-test/tests/mod.rs b/src/rust/jsg-test/tests/mod.rs index 5810164dcbe..bd2403b7564 100644 --- a/src/rust/jsg-test/tests/mod.rs +++ b/src/rust/jsg-test/tests/mod.rs @@ -4,6 +4,7 @@ mod argument_errors; mod arrays; +mod async_context; mod buffer_types; mod coercion_safety; mod collections_gc; diff --git a/src/rust/jsg/README.md b/src/rust/jsg/README.md index aaab436649f..71a11bf1321 100644 --- a/src/rust/jsg/README.md +++ b/src/rust/jsg/README.md @@ -256,6 +256,8 @@ does for `&self` methods. A method that takes `this` has no Fast API callback. | `Cell>>` | Yes — when `Some` | | | `jsg::Weak` | No | Doesn't keep target alive | | `jsg::v8::Global` | Compile error — no `Traced` impl | Use `TracedGlobal` in resource fields | +| `jsg::TracedAsyncContextFrame` | Yes — like `TracedGlobal` | Nothing to trace for the root frame | +| `jsg::AsyncContextFrame` | Compile error — no `Traced` impl | Use `TracedAsyncContextFrame` in resource fields | | `std::rc::Rc` | Depends on `T` | Only for plain data; never around a `TracedGlobal` or `jsg::Rc` (Safety rules, rule 2) | | Any `T: Traced` | Depends on `T` | `#[jsg_resource]` calls `Traced::trace` on every field | @@ -509,6 +511,45 @@ or a JavaScript exception is pending, `Function::new` creates nothing and return propagating. As with the functions C++ JSG wraps, the function's `name` is empty, its `length` is 0, and it can be called with `new`. +### Async context + +`jsg::AsyncContextFrame::current(lock)` captures the async context frame that is current now, +which carries the values of `AsyncLocalStorage`, and `frame.enter(lock, |lock| ...)` runs a +closure with it current, as C++ JSG's `AsyncContextFrame::currentRef()` and +`AsyncContextFrame::Scope` do. `enter` makes the previous frame current again however the closure +ends, including with an error, while execution is terminating, or by panicking. It opens no handle +scope, so handles created in the closure stay valid after it. A microtask checkpoint run inside the +closure leaves the root frame current after it, as under C++ JSG's `AsyncContextFrame::Scope`. + +V8 carries the current frame into promise reactions, those that `Local::then` registers +included, so they need nothing more. Code that resumes JavaScript from outside a JavaScript +continuation does: the continuation of async I/O, or a callback that runs later, captures the frame +when the work starts and enters it when it resumes. + +The current frame is the isolate's continuation-preserved embedder data: `undefined` for the root +frame (`AsyncContextFrame::root()`, `is_root()`), otherwise the opaque wrapper of a C++ +`jsg::AsyncContextFrame`. A captured frame holds that wrapper, which owns the C++ frame, so the +frame and its values live as long as an `AsyncContextFrame` holding it, or as long as the resource +that traces a `TracedAsyncContextFrame` holding it. + +As with `Global` and `TracedGlobal`, there are two types: + +- `AsyncContextFrame` holds a `Global`, for a frame that Rust holds outside a resource, such as in + a future passed to async I/O. It is `Send` and `Sync`, and has no `Traced` impl. +- `TracedAsyncContextFrame` holds a `TracedGlobal`, for a resource field or `Function::new` data, + so that GC collects a cycle through the frame's values. It is `Send`, not `Sync`, and must have + exactly one owner that GC traces. `TracedAsyncContextFrame::from` converts an + `AsyncContextFrame`, and `to_async_context_frame(lock)` goes the other way, for example to pass + a resource's frame to async I/O. Like a `TracedGlobal` taken out of its resource, a + `TracedAsyncContextFrame` taken out of its field is traced by nothing: enter, clone or convert + it before anything can run JavaScript or allocate on the V8 heap, and put it back. + +```rust +let frame = jsg::AsyncContextFrame::current(lock); +// ... later, under another lock: +frame.enter(lock, |lock| callback.call::>(lock, None, &[]))?; +``` + ## Errors from JavaScript When a function that `Local::call` calls throws, the exception stays pending in V8, as with C++ JSG's `JsExceptionThrown`, and `call` returns an error for which `Error::is_pending()` is true. The same goes for these other operations that run JavaScript: converting a value to a string or a number (its `toString`, `valueOf` or `Symbol.toPrimitive`, or a Proxy trap), `Object::get` and `Object::has` (a getter or Proxy trap), and `Array::iterate` and so converting to a `Vec` (an element's getter). `Error`'s own `FromJS`, which reads `name` and `message`, passes such an error on too. `Lock::throw_exception` throws nothing for that error, so a callback that returns it propagates the original exception to its JavaScript caller. V8 keeps reporting the exception where it was thrown, and DevTools' "pause on uncaught exceptions" still sees whether JavaScript further up catches it. A termination is handled the same way. `Object::set`, `Array::get` and `Array::set` don't yet work this way: they still turn what JavaScript throws into a panic. diff --git a/src/rust/jsg/async_context.rs b/src/rust/jsg/async_context.rs new file mode 100644 index 00000000000..06ad4ec5070 --- /dev/null +++ b/src/rust/jsg/async_context.rs @@ -0,0 +1,229 @@ +// Copyright (c) 2026 Cloudflare, Inc. +// Licensed under the Apache 2.0 license found in the LICENSE file or at: +// https://opensource.org/licenses/Apache-2.0 + +//! Async context frames: capture the frame that is current now, and later run code in it, as +//! C++ JSG's `AsyncContextFrame::currentRef()` and `AsyncContextFrame::Scope` do. +//! +//! The current frame is the isolate's continuation-preserved embedder data: `undefined` for the +//! root frame, otherwise the opaque JavaScript wrapper of a C++ `AsyncContextFrame` (see +//! `src/workerd/jsg/async-context.h`). C++ treats any value that is not such a wrapper as the +//! root frame, while Rust, which cannot recognize the wrapper without C++ JSG, treats any value +//! but `undefined` as a frame. The two agree because the embedder data never holds anything +//! else: C++ sets only a frame's wrapper or `undefined`, and this module sets only `undefined` +//! or a value it read from the embedder data, which its private handle type enforces (see +//! [`FrameWrapper`]). V8 carries the value into promise reactions, so a reaction runs +//! in the frame that was current when it was registered, whether JavaScript or Rust registered +//! it. Code that resumes JavaScript from outside a JavaScript continuation, such as the +//! completion of async I/O or a callback that runs later, enters the frame it captured by hand. +//! +//! A captured frame holds the wrapper, and the wrapper owns its C++ frame, so the frame and the +//! values stored in it stay alive for as long as an [`AsyncContextFrame`] does, and for as long +//! as the resource that traces a [`TracedAsyncContextFrame`] does. + +#![allow( + unsafe_code, + reason = "reads and sets the isolate's continuation-preserved embedder data through the FFI" +)] + +use crate::Lock; +use crate::Traced; +use crate::v8::GcVisitor; +use crate::v8::Global; +use crate::v8::Local; +use crate::v8::TracedGlobal; +use crate::v8::ffi; + +/// Marker for a handle to a value read from the isolate's continuation-preserved embedder data, +/// which is a C++ frame's wrapper (the root frame's `undefined` is never held). +/// +/// It is private, and only [`current_value`] creates handles of it, so the only values that +/// [`set_current`] can make current are `undefined` and ones that were current before. Rust +/// therefore never puts a value in the embedder data that C++ would not recognize as a frame. +struct FrameWrapper; + +/// An async context frame that Rust holds outside a resource, for example in a future passed +/// to async I/O, to run the code that resumes after it in the frame that started it. +/// +/// It holds a strong [`Global`], so it has no [`Traced`] impl and cannot be a resource field; +/// a resource holds a [`TracedAsyncContextFrame`] instead. Like a `Global`, it is `Send` and +/// `Sync` and may be dropped on any thread. Unless it is the root frame, it panics when entered +/// or cloned in another isolate. +pub struct AsyncContextFrame { + /// The frame's wrapper, or `None` for the root frame. + value: Option>, +} + +impl AsyncContextFrame { + /// Captures the frame that is current now. + pub fn current(lock: &mut Lock<'_>) -> Self { + Self { + value: current_value(lock).map(|value| value.to_global(lock)), + } + } + + /// The root frame, which holds no stored values. + #[must_use] + pub const fn root() -> Self { + Self { value: None } + } + + /// Returns whether this is the root frame, captured while the isolate's + /// continuation-preserved embedder data was `undefined`. + #[must_use] + pub const fn is_root(&self) -> bool { + self.value.is_none() + } + + /// Creates another handle to the same frame. + #[must_use] + pub fn clone(&self, lock: &mut Lock<'_>) -> Self { + Self { + value: self.value.as_ref().map(|value| value.clone(lock)), + } + } + + /// Runs `f` with this frame current, then makes the frame that was current before current + /// again, also when `f` returns an error, execution is terminating, or `f` panics. + /// + /// `enter` opens no handle scope, so the handles `f` creates stay valid after it. A microtask + /// checkpoint that `f` runs leaves the root frame current after it, as V8 runs each microtask + /// in its own frame, the same as under C++ JSG's `AsyncContextFrame::Scope`. + pub fn enter<'s, R>(&self, lock: &mut Lock<'s>, f: impl FnOnce(&mut Lock<'s>) -> R) -> R { + let frame = self.value.as_ref().map(|value| value.as_local(lock)); + enter_value(lock, frame, f) + } +} + +/// An async context frame held by a resource field, which the garbage collector traces through +/// the resource, so that a cycle through the frame's stored values can be collected. +/// +/// It holds a [`TracedGlobal`]: like any traced state, it must have exactly one owner that the +/// garbage collector traces, so never put it behind an `std::rc::Rc`. It is `Send`, not `Sync`. +/// A frame that Rust holds outside a resource is an [`AsyncContextFrame`]: +/// [`TracedAsyncContextFrame::from`] turns one into one of these, and +/// [`TracedAsyncContextFrame::to_async_context_frame`] makes one of these into one, for example +/// to pass a resource's frame to async I/O. +/// +/// Once the resource has no strong reference from Rust, the garbage collector keeps the frame +/// alive only by tracing it through the resource, like any `TracedGlobal`. A frame taken out of +/// its field, for example with `Cell::take`, is traced by nothing, so enter it, clone it or +/// convert it before anything can run JavaScript or allocate on the V8 heap, and put it back. +pub struct TracedAsyncContextFrame { + /// The frame's wrapper, or `None` for the root frame. + value: Option>, +} + +impl TracedAsyncContextFrame { + /// Captures the frame that is current now. + pub fn current(lock: &mut Lock<'_>) -> Self { + AsyncContextFrame::current(lock).into() + } + + /// The root frame, which holds no stored values. + #[must_use] + pub const fn root() -> Self { + Self { value: None } + } + + /// Returns whether this is the root frame; see [`AsyncContextFrame::is_root`]. + #[must_use] + pub const fn is_root(&self) -> bool { + self.value.is_none() + } + + /// Creates another handle to the same frame. It is a separate owner, for another resource, + /// and holds the frame strongly until the garbage collector first traces it. + #[must_use] + pub fn clone(&self, lock: &mut Lock<'_>) -> Self { + Self { + value: self.value.as_ref().map(|value| value.clone(lock)), + } + } + + /// Creates an [`AsyncContextFrame`] for the same frame, which holds it strongly wherever it + /// goes. + #[must_use] + pub fn to_async_context_frame(&self, lock: &mut Lock<'_>) -> AsyncContextFrame { + AsyncContextFrame { + value: self + .value + .as_ref() + .map(|value| value.as_local(lock).to_global(lock)), + } + } + + /// Runs `f` with this frame current; see [`AsyncContextFrame::enter`]. + pub fn enter<'s, R>(&self, lock: &mut Lock<'s>, f: impl FnOnce(&mut Lock<'s>) -> R) -> R { + let frame = self.value.as_ref().map(|value| value.as_local(lock)); + enter_value(lock, frame, f) + } +} + +impl From for TracedAsyncContextFrame { + fn from(frame: AsyncContextFrame) -> Self { + Self { + value: frame.value.map(TracedGlobal::from), + } + } +} + +impl Traced for TracedAsyncContextFrame { + fn trace(&self, visitor: &mut GcVisitor) { + if let Some(value) = &self.value { + visitor.visit_traced_global(value); + } + } +} + +/// The current frame's wrapper, or `None` in the root frame. +fn current_value<'s>(lock: &mut Lock<'s>) -> Option> { + // SAFETY: the lock keeps the isolate locked with a scope open, which the handle V8 returns + // belongs to. `FrameWrapper` is only a marker, and the value is the embedder data itself. + let value = unsafe { + Local::from_ffi(ffi::isolate_get_continuation_preserved_embedder_data( + lock.isolate().as_ffi(), + )) + }; + (!value.is_undefined()).then_some(value) +} + +/// Makes `frame` (`None` for the root frame) current. +fn set_current(lock: &mut Lock<'_>, frame: Option>) { + let value = match frame { + // SAFETY: the handle is valid in the lock's scope, and passed straight back to V8. + Some(frame) => unsafe { frame.into_ffi() }, + // SAFETY: the handle is valid in the lock's scope, and passed straight back to V8. + None => unsafe { lock.undefined().into_ffi() }, + }; + // SAFETY: the lock keeps the isolate locked, and `value` is a valid handle of its isolate. + unsafe { + ffi::isolate_set_continuation_preserved_embedder_data(lock.isolate().as_ffi(), value); + } +} + +/// Runs `f` with `frame` current, and makes the previous frame current again on every exit. +fn enter_value<'s, R>( + lock: &mut Lock<'s>, + frame: Option>, + f: impl FnOnce(&mut Lock<'s>) -> R, +) -> R { + /// Makes `previous` current again when dropped, also while a panic unwinds. + struct Restore<'a, 's> { + lock: &'a mut Lock<'s>, + previous: Option>, + } + + impl Drop for Restore<'_, '_> { + fn drop(&mut self) { + // Setting the embedder data runs no JavaScript and throws nothing, so this is safe + // while execution is terminating. + set_current(self.lock, self.previous); + } + } + + let previous = current_value(lock); + set_current(lock, frame); + let restore = Restore { lock, previous }; + f(&mut *restore.lock) +} diff --git a/src/rust/jsg/ffi.c++ b/src/rust/jsg/ffi.c++ index 61c11785273..1b143395607 100644 --- a/src/rust/jsg/ffi.c++ +++ b/src/rust/jsg/ffi.c++ @@ -1603,6 +1603,14 @@ bool isolate_has_pending_exception(Isolate* isolate) { return isolate->HasPendingException(); } +Local isolate_get_continuation_preserved_embedder_data(Isolate* isolate) { + return to_ffi(isolate->GetContinuationPreservedEmbedderData().As()); +} + +void isolate_set_continuation_preserved_embedder_data(Isolate* isolate, Local value) { + isolate->SetContinuationPreservedEmbedderData(local_from_ffi(kj::mv(value))); +} + void ignore_leak(size_t ptr) { #if KJ_HAS_COMPILER_FEATURE(address_sanitizer) || defined(__SANITIZE_ADDRESS__) __lsan_ignore_object(reinterpret_cast(ptr)); diff --git a/src/rust/jsg/ffi.h b/src/rust/jsg/ffi.h index 68d31d27c27..d36df98d8c3 100644 --- a/src/rust/jsg/ffi.h +++ b/src/rust/jsg/ffi.h @@ -267,6 +267,31 @@ Local local_promise_resolver_promise(const Local& resolver); bool local_promise_resolver_resolve(Isolate* isolate, const Local& resolver, const Local& value); bool local_promise_resolver_reject(Isolate* isolate, const Local& resolver, const Local& reason); +// Local +PromiseState local_promise_state(const Local& promise); +// The fulfillment value or rejection reason. `promise` must not be pending: V8 aborts the process +// if it is. +Local local_promise_result(const Local& promise); +void local_promise_mark_as_handled(const Local& promise); +// `promise.then(on_fulfilled)`, `promise.catch(on_rejected)` and +// `promise.then(on_fulfilled, on_rejected)`, without looking up `then` or the species +// constructor. An empty result means an exception is pending (one that a promise hook threw, or a +// termination), as local_function_call() leaves it. While one is pending these, and the +// PromiseResolver shims, call nothing in V8, which would clear it. +MaybeLocal local_promise_then(Isolate* isolate, const Local& promise, const Local& on_fulfilled); +MaybeLocal local_promise_catch(Isolate* isolate, const Local& promise, const Local& on_rejected); +MaybeLocal local_promise_then_or_catch( + Isolate* isolate, const Local& promise, const Local& on_fulfilled, const Local& on_rejected); + +// Local +// An empty result, or false, means an exception is pending, as for local_promise_then(). +MaybeLocal promise_resolver_new(Isolate* isolate); +Local local_promise_resolver_promise(const Local& resolver); +// Resolving with a thenable reads its `then` property, which can run JavaScript; an exception it +// throws rejects the promise rather than failing the call. +bool local_promise_resolver_resolve(Isolate* isolate, const Local& resolver, const Local& value); +bool local_promise_resolver_reject(Isolate* isolate, const Local& resolver, const Local& reason); + // Local Local local_new_array(Isolate* isolate, size_t length); uint32_t local_array_length(const Local& array); @@ -487,6 +512,11 @@ void isolate_throw_internal_error(Isolate* isolate, ::rust::Str internalMessage) void isolate_terminate_execution(Isolate* isolate); bool isolate_is_execution_terminating(Isolate* isolate); bool isolate_has_pending_exception(Isolate* isolate); +// The isolate's continuation-preserved embedder data, which holds the current async context +// frame: `undefined` for the root frame, otherwise the frame's opaque JS wrapper (see +// workerd/jsg/async-context.h). V8 carries it into promise reactions. Neither runs JavaScript. +Local isolate_get_continuation_preserved_embedder_data(Isolate* isolate); +void isolate_set_continuation_preserved_embedder_data(Isolate* isolate, Local value); bool isolate_is_locked(Isolate* isolate); bool isolate_in_context(Isolate* isolate); void ignore_leak(size_t ptr); diff --git a/src/rust/jsg/lib.rs b/src/rust/jsg/lib.rs index e958d81b0e8..e015f9755d6 100644 --- a/src/rust/jsg/lib.rs +++ b/src/rust/jsg/lib.rs @@ -8,6 +8,7 @@ use std::ops::Deref; use std::pin::Pin; use std::sync::Arc; +mod async_context; pub mod fast_api; pub mod feature_flags; mod isolate_state; @@ -21,6 +22,8 @@ mod scope; pub mod v8; mod wrappable; +pub use async_context::AsyncContextFrame; +pub use async_context::TracedAsyncContextFrame; pub use feature_flags::FeatureFlags; use isolate_state::IsolateState; pub use lenient::Lenient; diff --git a/src/rust/jsg/v8.rs b/src/rust/jsg/v8.rs index 71300215a88..2529f003c4a 100644 --- a/src/rust/jsg/v8.rs +++ b/src/rust/jsg/v8.rs @@ -655,6 +655,13 @@ pub mod ffi { pub unsafe fn isolate_terminate_execution(isolate: *mut Isolate); pub unsafe fn isolate_is_execution_terminating(isolate: *mut Isolate) -> bool; pub unsafe fn isolate_has_pending_exception(isolate: *mut Isolate) -> bool; + pub unsafe fn isolate_get_continuation_preserved_embedder_data( + isolate: *mut Isolate, + ) -> Local; + pub unsafe fn isolate_set_continuation_preserved_embedder_data( + isolate: *mut Isolate, + value: Local, + ); pub unsafe fn isolate_is_locked(isolate: *mut Isolate) -> bool; pub unsafe fn isolate_in_context(isolate: *mut Isolate) -> bool; diff --git a/src/workerd/jsg/async-context.c++ b/src/workerd/jsg/async-context.c++ index dd5a1a61d22..a8dae39cc5e 100644 --- a/src/workerd/jsg/async-context.c++ +++ b/src/workerd/jsg/async-context.c++ @@ -12,6 +12,9 @@ namespace workerd::jsg { namespace { +// Sets only a frame's wrapper or undefined. Rust JSG (src/rust/jsg/async_context.rs) relies on +// this: it treats any embedder data but undefined as a frame, which agrees with current() only +// while nothing else is ever set. inline void maybeSetV8ContinuationContext( v8::Isolate* isolate, kj::Maybe maybeFrame) { v8::Local value;