Repository navigation
Capture and enter async context frames in Rust JSG - #7980
Draft
bartlomieju wants to merge 9 commits into
Draft
bartlomieju wants to merge 9 commits into
bartlomieju wants to merge 9 commits into
Conversation
Porting C++ APIs such as node:module's createRequire() to Rust needs the counterpart of C++ JSG's wrapReturningFunction(). Add v8::Function::new(lock, data, f): f captures nothing and receives data, which the function owns through a Wrappable, so the GC traces it and collects cycles through it. No JS-visible change; nothing uses it yet.
Functions that C++ JSG wraps also have an opaque wrapper as their data, so fci_get_data_wrappable() checks the Wrappable's type. Add a jsg-test helper that wraps a function through Lock::wrapReturningFunction() and a test that its data is rejected.
…nction-new # Conflicts: # src/rust/jsg/README.md # src/rust/jsg/v8.rs
Function::new reported a failure to create the function, which happens only while execution is terminating, as a new internal error. It now checks for the termination first and returns the pending error that Local<Function>::call returns, so callers handle both alike and pass the termination on with `?`.
A spike toward async Rust APIs such as MemoryCache.read(). Add untyped promise handles, like C++ JSG's JsPromise: - v8::PromiseResolver::new(), resolve(), reject() and reject_with_error(); a resolver never converts to a JS value. - v8::Promise::resolved(), rejected() and rejected_with_error(). - Local<Promise>::state(), result(), mark_as_handled() and then(), whose reactions are v8::Function::new functions sharing GC-traced data, so reactions capture nothing and cycles through them are collected. - FromJS for Local<Promise>, which turns any value into a promise as C++ JSG's promise arguments do; ToJS comes from the casts. - Lock::exception(), the exception throw_exception() would throw, with internal errors redacted. As with v8::Function::new, an operation fails, with an internal jsg::Error, only when execution is terminating. The test harness gains run_microtasks(), since isolates use an explicit microtask policy. The tests include two consumers: a resource that settles a promise from a later call, and a read-through cache shaped like MemoryCache.read(key, fallback). No JS-visible change; nothing outside tests uses it yet.
then() gave each of its two reactions a Wrappable of its own over one std::rc::Rc of the data, so a jsg::Rc in the data had two parents and failed C++'s single-owner assertion when the second reaction was created. Function::new_sharing_data now creates any number of functions over one Wrappable, attaching its opaque wrapper once; Function::new uses it too, and function_new takes the wrapper as the data. Reactions follow V8's API: then(on_fulfilled), catch(on_rejected), and then_or_catch(on_fulfilled, on_rejected), sharing one data. The first two pass the other outcome through natively, so passing a rejection on no longer creates a rejected promise. A failed operation now returns a pending error, as Function::call does, rather than an internal error that claimed a termination. The shims also enter V8 for nothing while an exception or termination is pending: Promise::Resolver::New, Then, Catch and ObjectTemplate::NewInstance open a PrepareForExecutionScope, which clears the pending exception, so a terminated script ran on. Lock::exception returns a pending error as is, so rejecting with one propagates it. The read-through cache test keeps its state in a resource that the cache and each in-flight fill hold a jsg::Rc to, instead of sharing TracedGlobals through std::rc::Rc. New tests cover a jsg::Rc in reaction data, teardown with a pending promise, a reaction that calls back into its resource, and every operation while terminating. The promise shims add no C++ JSG: exception_create_internal shares isolate_throw_internal_error's makeInternalError() call, and the test harness runs microtasks through V8. local_promise_result no longer fails. promise.rs is now a crate module, re-exported from jsg::v8.
Lock::await_io, and any Rust code that resumes JavaScript after a KJ event, must run in the async context frame that started the work, as IoContext::awaitIo does in C++. Add jsg::AsyncContextFrame, which captures the current frame and later runs a closure in it, restoring the previous frame however the closure ends, and TracedAsyncContextFrame for resource fields and Function::new data. Both use V8's continuation-preserved embedder data directly; nothing outside tests uses them yet.
…ync-context # Conflicts: # src/rust/AGENTS.md # src/rust/jsg-test/ffi.c++ # src/rust/jsg-test/ffi.h # src/rust/jsg-test/lib.rs # src/rust/jsg-test/tests/function_new.rs # src/rust/jsg-test/tests/promise.rs # src/rust/jsg/README.md # src/rust/jsg/ffi.c++ # src/rust/jsg/lib.rs # src/rust/jsg/promise.rs # src/rust/jsg/v8.rs
C++ AsyncContextFrame::current() treats embedder data that is not a frame wrapper as the root frame, while Rust treats anything but undefined as a frame. They agree only because nothing else sets the embedder data. Rust cannot recognize the wrapper without C++ JSG, so enforce its side instead: the frame handles now use a private marker type that only the embedder-data getter produces, so Rust can make current only undefined or a value that was current before. Document the invariant at the C++ setter, and test that Rust's is_root() agrees with C++ in every state Rust can leave the isolate in.
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #7980 +/- ##
==========================================
- Coverage 41.96% 41.94% -0.02%
==========================================
Files 1064 1065 +1
Lines 302088 302224 +136
Branches 29079 29079
==========================================
+ Hits 126771 126772 +1
- Misses 158284 158420 +136
+ Partials 17033 17032 -1 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Lock::await_io, and any Rust code that resumes JavaScript after a KJ event, must run in the async context frame that started the work, asIoContext::awaitIodoes in C++. Addjsg::AsyncContextFrame, which captures the current frame and later runs a closure in it, restoring the previous frame however the closure ends, andTracedAsyncContextFramefor resource fields andFunction::newdata. Both use V8's continuation-preserved embedder data directly; nothing outside tests uses them yet.C++
AsyncContextFrame::currenttreats embedder data that is not a frame wrapper as the root frame, while Rust, which cannot recognize the wrapper without C++ JSG, treats anything butundefinedas a frame. They agree because nothing else is ever set: C++ sets only a frame's wrapper orundefined, and Rust can only setundefinedor a value it read from the embedder data, which a private handle type enforces. A test checks that Rust'sis_root()agrees with C++ in every state Rust can leave the isolate in.There is no production consumer yet. The real consumer,
await_io, also needs the IoContext bindings (#7963) and bridging between KJ and JavaScript promises. Like #7956, this is a building block, tested throughsrc/rust/jsg-test, whose test-only shims create real C++jsg::AsyncContextFrames the wayAsyncLocalStorage.run()does.Open questions:
AsyncContextFrame/TracedAsyncContextFramefollow C++ andGlobal/TracedGlobal; the reverse conversion isTracedAsyncContextFrame::to_async_context_frame(lock).AsyncLocalStoragevalues (StorageKey,get) andAsyncContextFrame::wrapare left for later.