Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions src/rust/jsg-test/tests/local_value.rs
Original file line number Diff line number Diff line change
Expand Up @@ -144,3 +144,24 @@ fn stored_value_round_trips_through_traced_global() {
Ok(())
});
}

#[test]
fn to_detail_string_describes_a_value_without_running_javascript() {
crate::Harness::new().run_in_context(|lock, ctx| {
let cases = [
("new TypeError('bad')", "TypeError: bad"),
("'text'", "text"),
("42", "42"),
("undefined", "undefined"),
(
"({ toString() { throw new Error('ran'); } })",
"[object Object]",
),
];
for (code, expected) in cases {
let value = ctx.eval_raw(lock, code).unwrap();
assert_eq!(value.to_detail_string(lock)?, expected, "{code}");
}
Ok(())
});
}
36 changes: 36 additions & 0 deletions src/rust/jsg-test/tests/promise.rs
Original file line number Diff line number Diff line change
Expand Up @@ -354,6 +354,8 @@ impl jsg::Traced for DropCounter {}

static SETTLED_DATA_DROPS: AtomicUsize = AtomicUsize::new(0);

/// As in C++ JSG, reaction data is dropped once a reaction has run, without waiting for the
/// garbage collector, while the promise stays reachable.
#[test]
fn then_data_is_dropped_once_the_promise_settles() {
let harness = crate::Harness::new();
Expand All @@ -375,6 +377,7 @@ fn then_data_is_dropped_once_the_promise_settles() {
let value = lock.undefined();
resolver.resolve(lock, value)?;
crate::Harness::run_microtasks(lock);
assert_eq!(SETTLED_DATA_DROPS.load(Ordering::SeqCst), 1);
Ok(())
});
harness.run_in_context(|lock, _ctx| {
Expand All @@ -384,6 +387,39 @@ fn then_data_is_dropped_once_the_promise_settles() {
});
}

static PASSED_THROUGH_DROPS: AtomicUsize = AtomicUsize::new(0);

/// The data of `then` is dropped when a rejection passes through, that of `catch` when a
/// fulfillment does, and that of a callback that fails once it has returned.
#[test]
fn reaction_data_is_dropped_whichever_way_the_promise_settles() {
crate::Harness::new().run_in_context(|lock, ctx| {
let reason = lock.undefined();
let rejected = v8::Promise::rejected(lock, reason)?;
let passed = rejected.then(lock, DropCounter(&PASSED_THROUGH_DROPS), |lock, _, _| {
Ok(lock.undefined())
})?;
passed.mark_as_handled();
let value = lock.undefined();
let fulfilled = v8::Promise::resolved(lock, value)?;
fulfilled.catch(lock, DropCounter(&PASSED_THROUGH_DROPS), |lock, _, _| {
Ok(lock.undefined())
})?;
let value = lock.undefined();
let fulfilled = v8::Promise::resolved(lock, value)?;
let failed = fulfilled.then(lock, DropCounter(&PASSED_THROUGH_DROPS), |_, _, _| {
Err(jsg::Error::new_type_error("failed"))
})?;
failed.mark_as_handled();
// All three stay reachable, so only their reactions can have dropped the data.
ctx.set_global(lock, "passed", passed);
ctx.set_global(lock, "failed", failed);
crate::Harness::run_microtasks(lock);
assert_eq!(PASSED_THROUGH_DROPS.load(Ordering::SeqCst), 3);
Ok(())
});
}

static SHARED_LEAF_DROPS: AtomicUsize = AtomicUsize::new(0);

#[jsg_resource]
Expand Down
17 changes: 5 additions & 12 deletions src/rust/jsg/ffi.c++
Original file line number Diff line number Diff line change
Expand Up @@ -302,6 +302,11 @@ bool local_is_shared_array_buffer(const Local& val) {
return ::rust::String(*utf8, utf8.length());
}

MaybeLocal local_to_detail_string(Isolate* isolate, const Local& val) {
return maybe_local_to_ffi(
local_as_ref_from_ffi<v8::Value>(val)->ToDetailString(isolate->GetCurrentContext()));
}

// Utf8Value
Utf8Value utf8_value_new(Isolate* isolate, Local value) {
auto* v = new v8::String::Utf8Value(isolate, local_from_ffi<v8::Value>(kj::mv(value)));
Expand Down Expand Up @@ -534,18 +539,6 @@ bool exceptionPending(Isolate* isolate) {
}
} // namespace

MaybeLocal local_promise_then(Isolate* isolate, const Local& promise, const Local& on_fulfilled) {
if (exceptionPending(isolate)) return maybe_local_to_ffi(v8::MaybeLocal<v8::Value>());
return maybe_local_to_ffi(local_as_ref_from_ffi<v8::Promise>(promise)->Then(
isolate->GetCurrentContext(), local_as_ref_from_ffi<v8::Function>(on_fulfilled)));
}

MaybeLocal local_promise_catch(Isolate* isolate, const Local& promise, const Local& on_rejected) {
if (exceptionPending(isolate)) return maybe_local_to_ffi(v8::MaybeLocal<v8::Value>());
return maybe_local_to_ffi(local_as_ref_from_ffi<v8::Promise>(promise)->Catch(
isolate->GetCurrentContext(), local_as_ref_from_ffi<v8::Function>(on_rejected)));
}

MaybeLocal local_promise_then_or_catch(
Isolate* isolate, const Local& promise, const Local& on_fulfilled, const Local& on_rejected) {
if (exceptionPending(isolate)) return maybe_local_to_ffi(v8::MaybeLocal<v8::Value>());
Expand Down
8 changes: 3 additions & 5 deletions src/rust/jsg/ffi.h
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,7 @@ bool local_is_name(const Local& val);
bool local_is_big_int(const Local& val);
bool local_is_date(const Local& val);
::rust::String local_type_of(Isolate* isolate, const Local& val);
MaybeLocal local_to_detail_string(Isolate* isolate, const Local& val);

// Local<String>
Local local_string_empty(Isolate* isolate);
Expand Down Expand Up @@ -248,18 +249,15 @@ PromiseState local_promise_state(const Local& promise);
// 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
// termination), as local_function_call() leaves it. While one is pending this, 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<PromiseResolver>
// An empty result, or false, means an exception is pending, as for local_promise_then().
// An empty result, or false, means an exception is pending, as for local_promise_then_or_catch().
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
Expand Down
101 changes: 61 additions & 40 deletions src/rust/jsg/promise.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
reason = "settles and reacts to promises through the V8 FFI"
)]

use std::cell::RefCell;

use crate::Error;
use crate::FromJS;
use crate::Lock;
Expand Down Expand Up @@ -171,12 +173,27 @@ impl Promise {
}
}

/// The data of a promise's reactions, which the first of them to run drops once it returns, as
/// C++ JSG drops a `ThenCatchPair`: a promise settles once, so the other can no longer run. Until
/// then, the garbage collector traces it through the reactions.
struct ReactionData<T>(RefCell<Option<T>>);

impl<T: Traced> Traced for ReactionData<T> {
fn holds_gc_references() -> bool {
T::holds_gc_references()
}

fn trace(&self, visitor: &mut crate::GcVisitor) {
self.0.trace(visitor);
}
}

/// The callback of a reaction that [`Local::then`], [`Local::catch`] or
/// [`Local::then_or_catch`] registers: calls `F` with the reaction's data and its argument, the
/// fulfillment value or rejection reason.
/// fulfillment value or rejection reason, then drops the data.
fn react<'a, T, F>(
lock: &mut Lock<'a>,
data: &T,
data: &ReactionData<T>,
args: &mut FunctionCallbackInfo<'a>,
) -> Result<Local<'a, Value>, Error>
where
Expand All @@ -187,18 +204,39 @@ where
{
// SAFETY: `then`, `catch` and `then_or_catch` were handed a value of `F`.
let f = unsafe { crate::scope::unit_value::<F>() };
f(lock, data, args.get(0))
// The data stays in the cell, where the garbage collector traces it, while `f` runs.
let result = match data.0.borrow().as_ref() {
Some(data) => f(lock, data, args.get(0)),
None => Err(Error::new_internal_error(
"a promise reaction ran after another reaction to the promise",
)),
};
// Dropped after the borrow ends, so that what its drop does cannot find the cell borrowed.
let data = data.0.borrow_mut().take();
drop(data);
result
}

/// The callback of a function created with data `T` that calls the reaction `F`.
/// The callback of a function created with reaction data `T` that calls the reaction `F`.
fn reaction<T, F>() -> crate::RawFunctionCallback
where
T: Traced + 'static,
F: UnitType
+ 'static
+ for<'b> Fn(&mut Lock<'b>, &T, Local<'b, Value>) -> Result<Local<'b, Value>, Error>,
{
Function::callback::<T, _>(react::<T, F>)
Function::callback::<ReactionData<T>, _>(react::<T, F>)
}

/// The reaction [`Local::then`] registers for a rejection: passes it through.
fn pass_rejection<'a, T>(
lock: &mut Lock<'a>,
_data: &T,
reason: Local<'a, Value>,
) -> Result<Local<'a, Value>, Error> {
// SAFETY: `lock` guarantees the isolate is locked; `reason` is a handle in its scope.
unsafe { ffi::isolate_throw_exception(lock.isolate().as_ffi(), reason.into_ffi()) };
Err(Error::pending())
}

impl<'s> Local<'s, Promise> {
Expand Down Expand Up @@ -237,12 +275,13 @@ impl<'s> Local<'s, Promise> {
/// no JavaScript runs until the promise settles.
///
/// As with [`Function::new`], the callback must capture nothing, which a compile-time
/// assertion checks, and whatever it needs comes from `data`. The garbage collector traces
/// `data` until the promise settles and drops it once the callback can no longer run, so it
/// may hold `TracedGlobal`s and `jsg::Rc`s, including ones that lead back to the promise. It
/// must be their only owner that the garbage collector traces: to share a resource's state,
/// hold a `jsg::Rc` or `jsg::Weak` to the resource rather than an `std::rc::Rc` of the
/// state.
/// assertion checks, and whatever it needs comes from `data`. As in C++ JSG, `data` is
/// dropped as soon as the promise settles: after the callback returns, or when a rejection
/// passes through. If the promise never settles, the garbage collector drops it once the
/// promise is unreachable. Until then it traces `data`, so it may hold `TracedGlobal`s and
/// `jsg::Rc`s, including ones that lead back to the promise. It must be their only owner that
/// the garbage collector traces: to share a resource's state, hold a `jsg::Rc` or
/// `jsg::Weak` to the resource rather than an `std::rc::Rc` of the state.
///
/// # Errors
///
Expand All @@ -254,28 +293,19 @@ impl<'s> Local<'s, Promise> {
+ 'static
+ for<'a> Fn(&mut Lock<'a>, &T, Local<'a, Value>) -> Result<Local<'a, Value>, Error>,
{
self.debug_assert_isolate(lock);
let _ = on_fulfilled;
let [on_fulfilled] = Function::new_sharing_data(lock, data, [reaction::<T, F>()])?;
// SAFETY: lock guarantees the isolate is locked and a HandleScope is active; both are
// valid handles, and a non-empty result is a promise in the lock's scope.
unsafe {
Local::from_ffi_maybe(ffi::local_promise_then(
lock.isolate().as_ffi(),
self.raw(),
on_fulfilled.raw(),
))
}
.ok_or_else(Error::pending)
// As C++ JSG does, a rejection passes through a reaction of its own, so that `data` is
// dropped then too.
self.then_or_catch(lock, data, on_fulfilled, pass_rejection::<T>)
}

/// Registers `on_rejected` to run when the promise is rejected, like
/// `promise.catch(onRejected)`, and returns the promise for its result. A fulfillment passes
/// through to the returned promise unchanged.
///
/// Like [`Local::then`] otherwise, with the rejection reason as the callback's argument.
/// Unlike `promise.catch()`, this does not call `promise.then`. Registering `on_rejected`
/// marks the promise as handled, as in JavaScript.
/// Like [`Local::then`] otherwise, with the rejection reason as the callback's argument, and
/// `data` dropped when a fulfillment passes through. Unlike `promise.catch()`, this does not
/// call `promise.then`. Registering `on_rejected` marks the promise as handled, as in
/// JavaScript.
///
/// # Errors
///
Expand All @@ -287,19 +317,9 @@ impl<'s> Local<'s, Promise> {
+ 'static
+ for<'a> Fn(&mut Lock<'a>, &T, Local<'a, Value>) -> Result<Local<'a, Value>, Error>,
{
self.debug_assert_isolate(lock);
let _ = on_rejected;
let [on_rejected] = Function::new_sharing_data(lock, data, [reaction::<T, R>()])?;
// SAFETY: lock guarantees the isolate is locked and a HandleScope is active; both are
// valid handles, and a non-empty result is a promise in the lock's scope.
unsafe {
Local::from_ffi_maybe(ffi::local_promise_catch(
lock.isolate().as_ffi(),
self.raw(),
on_rejected.raw(),
))
}
.ok_or_else(Error::pending)
// As C++ JSG does, a fulfillment passes through a reaction of its own, so that `data` is
// dropped then too.
self.then_or_catch(lock, data, |_, _, value| Ok(value), on_rejected)
}

/// Registers `on_fulfilled` and `on_rejected` to run when the promise settles, like
Expand Down Expand Up @@ -330,6 +350,7 @@ impl<'s> Local<'s, Promise> {
{
self.debug_assert_isolate(lock);
let _ = (on_fulfilled, on_rejected);
let data = ReactionData(RefCell::new(Some(data)));
let [on_fulfilled, on_rejected] =
Function::new_sharing_data(lock, data, [reaction::<T, F>(), reaction::<T, R>()])?;
// SAFETY: lock guarantees the isolate is locked and a HandleScope is active; all three
Expand Down
36 changes: 26 additions & 10 deletions src/rust/jsg/v8.rs
Original file line number Diff line number Diff line change
Expand Up @@ -313,6 +313,7 @@ pub mod ffi {
pub unsafe fn local_is_big_int(value: &Local) -> bool;
pub unsafe fn local_is_date(value: &Local) -> bool;
pub unsafe fn local_type_of(isolate: *mut Isolate, value: &Local) -> String;
pub unsafe fn local_to_detail_string(isolate: *mut Isolate, value: &Local) -> MaybeLocal;

// Local<String>
pub unsafe fn local_string_length(value: &Local) -> i32;
Expand Down Expand Up @@ -428,16 +429,6 @@ pub mod ffi {
pub unsafe fn local_promise_state(promise: &Local) -> PromiseState;
pub unsafe fn local_promise_result(promise: &Local) -> Local;
pub unsafe fn local_promise_mark_as_handled(promise: &Local);
pub unsafe fn local_promise_then(
isolate: *mut Isolate,
promise: &Local,
on_fulfilled: &Local,
) -> MaybeLocal;
pub unsafe fn local_promise_catch(
isolate: *mut Isolate,
promise: &Local,
on_rejected: &Local,
) -> MaybeLocal;
pub unsafe fn local_promise_then_or_catch(
isolate: *mut Isolate,
promise: &Local,
Expand Down Expand Up @@ -1608,6 +1599,31 @@ impl<'s, T> Local<'s, T> {
// HandleScope.
unsafe { ffi::local_type_of(lock.isolate().as_ffi(), self.raw()) }
}

/// Describes the value for a log or a trace, as C++ `kj::str()` of a `v8::Local` does: an
/// error as `"TypeError: message"`, a string as itself, and a plain object as
/// `"[object Object]"`. Unlike `String(value)`, it runs no JavaScript, such as a `toString()`
/// method.
///
/// Corresponds to `v8::Value::ToDetailString()`.
///
/// # Errors
///
/// If V8 fails, which only happens if execution is terminating, the error is one for which
/// [`Error::is_pending`] is true.
pub fn to_detail_string(&self, lock: &mut Lock<'s>) -> crate::Result<std::string::String> {
let detail: Local<'s, String> = call_into_js(lock, |lock| {
// SAFETY: `lock` proves the isolate is locked, with a context entered and a
// HandleScope for `'s` open; a non-empty result is a string V8 created in that scope.
Ok(unsafe {
Local::from_ffi_maybe(ffi::local_to_detail_string(
lock.isolate().as_ffi(),
self.raw(),
))
})
})?;
Ok(detail.to_string(lock))
}
}

impl PartialEq for Local<'_, Value> {
Expand Down
1 change: 1 addition & 0 deletions src/rust/memory-cache/ffi/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ wd_rust_crate(
"@capnp-cpp//src/kj:kj-async-io",
],
cxx_bridge_src = "lib.rs",
cxx_bridge_visibility = ["//visibility:public"],
visibility = ["//visibility:public"],
deps = [
"//src/rust/memory-cache",
Expand Down
8 changes: 7 additions & 1 deletion src/rust/memory-cache/ffi/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -139,10 +139,16 @@ struct Namespace {
inner: memory_cache::Namespace,
}

struct Binding {
pub struct Binding {
inner: memory_cache::Binding,
}

impl Binding {
pub fn into_inner(self) -> memory_cache::Binding {
self.inner
}
}

struct ReadDecision {
inner: memory_cache::ReadDecision,
}
Expand Down
Loading
Loading