Skip to content
Open
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
1 change: 1 addition & 0 deletions src/rust/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ _Snapshot — the set drifts as crates come and go; `bazel query //src/rust/...`
- `src/rust/jsg-compile-tests` holds snippets that must not compile (or, prefixed `ok-`, must). Add one for every new lifetime or `Send`/`Sync` rule; the `//~ ERROR` annotations must not depend on how many errors rustc reports.
- **Parameter ordering**: `&Lock` / `&mut Lock` must always be the first parameter in any function that takes a lock (matching the C++ convention where `jsg::Lock&` is always first). This applies to free functions, trait methods, and associated functions (excluding `&self`/`&mut self` receivers and a JSG method's `this: jsg::Rc<Self>`, which come before `lock`).
- **Method naming**: do not use `get_` prefixes on methods — e.g. `buf.backing_store()` not `buf.get_backing_store()`. Static constructors belong on the marker struct (`impl ArrayBuffer { fn new(...) }`) not on `impl Local<'_, ArrayBuffer>`.
- **`v8` module conventions**: handles in, handles out; `FromJS`/`ToJS` are the only conversions between Rust and JavaScript values. A `v8` method returns a Rust value only where V8 returns a C scalar, from a typed handle (`value`, `length`, `is_*`, `value_of`) or from a coercion of any value (`int32_value`, `uint32_value`, `integer_value`, which return `Result`), or copies memory out (`to_vec`, `to_string`), and takes raw Rust data only where V8 takes C data (constructor input, indices, times, `&str` property keys). So `Function::call` returns a `Local<Value>`; for a Rust value write `let result = f.call(lock, None, &[])?;` then `f64::from_js(lock, result)?`. Anything that can run JavaScript or needs a context returns `Result`; `Option` means only that something is absent or that V8 refused with nothing pending (the `String` constructors past `MAX_LENGTH`, except `new_from_utf8_literal`, which panics); never an infallible signature over a C++ shim that can throw. Names: `as_*` borrows for free, `to_*` costs something, `value`/`*_value` read a scalar, named as in V8. Full text in `src/rust/jsg/README.md` §Conventions of the `v8` module.
- **FFI naming**: instance methods on an existing handle use a `local_<type>_<method>` prefix (e.g. `local_array_buffer_byte_length`). Static constructors that create a new value do **not** use the `local_` prefix — name them `<type>_<method>` (e.g. `array_buffer_new_with_mode`, `array_buffer_maybe_new`, `backing_store_new_resizable`).
- **FFI groups**: `v8.rs` `mod ffi`, `ffi.h`, and `ffi.c++` all use matching comment groups (e.g. `// Local<T>`, `// Local<Array>`, `// Local<TypedArray>`, `// Local<ArrayBuffer>`, `// Local<ArrayBufferView>`, `// Local<SharedArrayBuffer>`, `// BackingStore`, `// Unwrappers`, `// TracedGlobal<T>`, `// FunctionCallbackInfo`). When adding new FFI functions, place them in the correct group in **all three files**. Do not scatter related functions across groups.
- **Feature flags**: `Lock::feature_flags()` returns a capnp `compatibility_flags::Reader` for the current worker. Use `lock.feature_flags().get_node_js_compat()`. Flags are parsed once and stored in the `Realm` at construction; C++ passes canonical capnp bytes to `realm_create()`. Schema: `src/workerd/io/compatibility-date.capnp`, generated Rust bindings: `compatibility_date_capnp` crate.
Expand Down
2 changes: 1 addition & 1 deletion src/rust/api/inspector.rs
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ impl InspectorConnection {
let message = message.to_js(lock)?;
// If the callback throws, its exception propagates from `dispatch()`, and the
// rest of the batch is dropped.
callback.call::<v8::Local<v8::Value>>(lock, None, &[message])?;
callback.call(lock, None, &[message])?;
}
}
}
Expand Down
4 changes: 3 additions & 1 deletion src/rust/jsg-bench/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
use std::cell::Cell;
use std::hint::black_box;

use jsg::FromJS;
use jsg::ToJS;
use jsg::v8;
use jsg_macros::jsg_constructor;
Expand Down Expand Up @@ -266,7 +267,8 @@ impl Target {
.try_into()
.map_err(|_| jsg::Error::new_type_error("expected a function"))?;
let n = n.to_js(lock)?;
a.call::<f64>(lock, None, &[n])
let result = a.call(lock, None, &[n])?;
f64::from_js(lock, result)
}

#[jsg_method]
Expand Down
2 changes: 1 addition & 1 deletion src/rust/jsg-compile-tests/ok-slice-then-js.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,6 @@ pub fn t<'s>(
f: v8::Local<'s, v8::Function>,
) -> Result<u8, jsg::Error> {
let first = bytes.as_slice(lock)[0];
f.call::<v8::Local<'s, v8::Value>>(lock, None, &[])?;
f.call(lock, None, &[])?;
Ok(first)
}
2 changes: 1 addition & 1 deletion src/rust/jsg-compile-tests/slice-across-js-call.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,6 @@ pub fn t<'s>(
f: v8::Local<'s, v8::Function>,
) {
let slice = bytes.as_slice(lock);
let _ = f.call::<v8::Local<'s, v8::Value>>(lock, None, &[]); //~ ERROR E0502
let _ = f.call(lock, None, &[]); //~ ERROR E0502
let _ = slice[0];
}
2 changes: 1 addition & 1 deletion src/rust/jsg-test/tests/coercion_safety.rs
Original file line number Diff line number Diff line change
Expand Up @@ -790,7 +790,7 @@ impl ConvertingResource {
) -> Result<(), jsg::Error> {
let callback = v8::Local::<v8::Function>::try_from(callback)
.map_err(|_| jsg::Error::new_type_error("not a function"))?;
let called = callback.call::<v8::Local<v8::Value>>(lock, None, &[]);
let called = callback.call(lock, None, &[]);
let conversion = self.conversion.get().unwrap();
let converted = conversion.run(lock, value);
self.saw_pending
Expand Down
35 changes: 20 additions & 15 deletions src/rust/jsg-test/tests/function.rs
Original file line number Diff line number Diff line change
Expand Up @@ -81,8 +81,8 @@ fn call_zero_args() {
harness.run_in_context(|lock, ctx| {
let value = ctx.eval_raw(lock, "(() => 42)").unwrap();
let func = v8::Local::<v8::Function>::try_from(value).unwrap();
let result = func.call::<u32>(lock, None::<v8::Local<v8::Value>>, &[])?;
assert_eq!(result, 42);
let result = func.call(lock, None, &[])?;
assert_eq!(u32::from_js(lock, result)?, 42);
Ok(())
});
}
Expand All @@ -97,8 +97,8 @@ fn call_with_args() {

let a = 10_i32.to_js(lock)?;
let b = 32_i32.to_js(lock)?;
let result = func.call::<i32>(lock, None::<v8::Local<v8::Value>>, &[a, b])?;
assert_eq!(result, -22);
let result = func.call(lock, None, &[a, b])?;
assert_eq!(i32::from_js(lock, result)?, -22);
Ok(())
});
}
Expand All @@ -112,7 +112,8 @@ fn call_returns_string() {
let func = v8::Local::<v8::Function>::try_from(value).unwrap();

let arg = String::from("hello").to_js(lock)?;
let result = func.call::<String>(lock, None::<v8::Local<v8::Value>>, &[arg])?;
let result = func.call(lock, None, &[arg])?;
let result = String::from_js(lock, result)?;
assert_eq!(result, "HELLO");
Ok(())
});
Expand All @@ -130,8 +131,8 @@ fn call_with_receiver() {
.unwrap();
let func = v8::Local::<v8::Function>::try_from(func_val).unwrap();

let result = func.call::<u32>(lock, Some(obj), &[])?;
assert_eq!(result, 100);
let result = func.call(lock, Some(obj), &[])?;
assert_eq!(u32::from_js(lock, result)?, 100);
Ok(())
});
}
Expand All @@ -150,7 +151,8 @@ fn call_with_object_receiver_via_into() {
let func = v8::Local::<v8::Function>::try_from(func_val).unwrap();

// Pass Local<Object> as receiver — the Into<Local<Value>> bound handles conversion.
let result = func.call::<String>(lock, Some(obj.into()), &[])?;
let result = func.call(lock, Some(obj.into()), &[])?;
let result = String::from_js(lock, result)?;
assert_eq!(result, "hello world");
Ok(())
});
Expand All @@ -168,9 +170,7 @@ fn call_leaves_thrown_exception_pending() {
let func = v8::Local::<v8::Function>::try_from(func).unwrap();

let caught = lock.try_catch(|lock| {
let err = func
.call::<v8::Local<v8::Value>>(lock, None, &[])
.unwrap_err();
let err = func.call(lock, None, &[]).unwrap_err();
assert!(err.is_pending());
Err::<(), _>(err)
})?;
Expand All @@ -187,7 +187,10 @@ fn try_catch_catches_thrown_non_error() {
let func = ctx.eval_raw(lock, "(() => { throw 42; })").unwrap();
let func = v8::Local::<v8::Function>::try_from(func).unwrap();

let caught = lock.try_catch(|lock| func.call::<u32>(lock, None, &[]))?;
let caught = lock.try_catch(|lock| {
let result = func.call(lock, None, &[])?;
u32::from_js(lock, result)
})?;
let thrown = u32::from_js(lock, caught.unwrap_err())?;
assert_eq!(thrown, 42);
Ok(())
Expand All @@ -214,8 +217,7 @@ fn try_catch_does_not_run_thrown_value_accessors() {
let func = ctx.eval_raw(lock, "(() => { throw thrown; })").unwrap();
let func = v8::Local::<v8::Function>::try_from(func).unwrap();

let caught =
lock.try_catch(|lock| func.call::<v8::Local<v8::Value>>(lock, None, &[]))?;
let caught = lock.try_catch(|lock| func.call(lock, None, &[]))?;
assert_eq!(caught.unwrap_err(), thrown);
let reads = ctx.eval::<u32>(lock, "reads").unwrap();
assert_eq!(reads, 0, "{thrown:?} ran {reads} accessors");
Expand All @@ -232,7 +234,10 @@ fn try_catch_returns_result_on_success() {
let func = ctx.eval_raw(lock, "(() => 7)").unwrap();
let func = v8::Local::<v8::Function>::try_from(func).unwrap();

let result = lock.try_catch(|lock| func.call::<u32>(lock, None, &[]))?;
let result = lock.try_catch(|lock| {
let result = func.call(lock, None, &[])?;
u32::from_js(lock, result)
})?;
assert_eq!(result.unwrap(), 7);
Ok(())
});
Expand Down
10 changes: 3 additions & 7 deletions src/rust/jsg-test/tests/function_new.rs
Original file line number Diff line number Diff line change
Expand Up @@ -204,9 +204,7 @@ impl TerminatingCreator {
) -> Result<(), jsg::Error> {
lock.terminate_execution();
let spin: v8::Local<v8::Function> = spin.try_into().unwrap();
let err = spin
.call::<v8::Local<v8::Value>>(lock, None, &[])
.unwrap_err();
let err = spin.call(lock, None, &[]).unwrap_err();
assert!(err.is_pending());
let err = v8::Function::new(lock, (), |lock, (), _args| Ok(lock.undefined())).unwrap_err();
self.saw_pending.set(err.is_pending());
Expand Down Expand Up @@ -255,9 +253,7 @@ impl ThrowingCreator {
throws: v8::Local<'s, v8::Value>,
) -> Result<(), jsg::Error> {
let throws: v8::Local<v8::Function> = throws.try_into().unwrap();
let err = throws
.call::<v8::Local<v8::Value>>(lock, None, &[])
.unwrap_err();
let err = throws.call(lock, None, &[]).unwrap_err();
assert!(err.is_pending());
let err = v8::Function::new(lock, (), |lock, (), _args| Ok(lock.undefined())).unwrap_err();
self.saw_pending.set(err.is_pending());
Expand Down Expand Up @@ -376,7 +372,7 @@ fn reentrant_call_sees_its_own_data() {
if depth > 0 {
let again: v8::Local<v8::Function> = args.get(1).try_into().unwrap();
let next = (depth - 1).to_js(lock)?;
again.call::<v8::Local<v8::Value>>(lock, None, &[next])?;
again.call(lock, None, &[next])?;
}
log.borrow_mut().push(format!("-{depth}"));
log.borrow().join(" ").to_js(lock)
Expand Down
2 changes: 1 addition & 1 deletion src/rust/jsg-test/tests/isolates.rs
Original file line number Diff line number Diff line change
Expand Up @@ -343,7 +343,7 @@ fn function_call_with_argument_of_another_isolate_panics_in_debug_builds() {
.try_into()
.unwrap();
assert_isolate_check_panics("calling B's function with A's object", || {
let _ = func.call::<u32>(lock_b, None, &[object]);
let _ = func.call(lock_b, None, &[object]);
});
Ok(())
});
Expand Down
4 changes: 3 additions & 1 deletion src/rust/jsg-test/tests/local_cast.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
//! Tests for `impl_local_cast!` conversions added for Float/BigInt typed arrays,
//! and upcasts to Object (Function -> Object, Array -> Object, `TypedArray` -> Object).

use jsg::FromJS;
use jsg::ToJsTypedArray;
use jsg::v8;

Expand Down Expand Up @@ -358,7 +359,8 @@ fn function_object_roundtrip() {
assert_eq!(name, "namedFn");
// Roundtrip back and verify it's still callable
let back: v8::Local<v8::Function> = obj.try_into().unwrap();
assert_eq!(back.call::<u32>(lock, None, &[])?, 42);
let result = back.call(lock, None, &[])?;
assert_eq!(u32::from_js(lock, result)?, 42);
Ok(())
});
}
Expand Down
104 changes: 104 additions & 0 deletions src/rust/jsg-test/tests/lock_errors.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
// 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 errors a `Lock` creates and throws reach JavaScript as errors of their type, with their
//! message.

use jsg_macros::jsg_method;
use jsg_macros::jsg_resource;

#[jsg_resource]
struct Thrower;

#[jsg_resource]
impl Thrower {
#[jsg_method]
fn throw_error(&self, lock: &mut jsg::Lock, message: String) {
lock.throw_error(&message);
}

#[jsg_method]
fn throw_invalid_string_length(&self, lock: &mut jsg::Lock) {
lock.throw_exception(&jsg::Error::invalid_string_length());
}

#[jsg_method]
fn range_error<'s>(
&self,
lock: &mut jsg::Lock<'s>,
message: String,
) -> jsg::v8::Local<'s, jsg::v8::Object> {
lock.range_error(&message)
}
}

fn caught(lock: &mut jsg::Lock<'_>, ctx: &crate::EvalContext<'_>, call: &str, check: &str) -> bool {
ctx.eval::<bool>(
lock,
&format!(
"{{ let caught;
try {{ {call}; }} catch (e) {{ caught = e; }}
{check} }}"
),
)
.unwrap()
}

#[test]
fn throw_error_throws_an_error_with_the_message() {
crate::Harness::new().run_in_context(|lock, ctx| {
ctx.set_global(lock, "thrower", jsg::Rc::new(Thrower));
assert!(caught(
lock,
ctx,
"thrower.throwError('boom')",
"caught.constructor === Error && caught.message === 'boom'"
));
// A message with a multi-byte character reaches JavaScript whole.
assert!(caught(
lock,
ctx,
"thrower.throwError('héllo')",
"caught.message === 'héllo'"
));
Ok(())
});
}

#[test]
fn invalid_string_length_is_v8s_range_error() {
crate::Harness::new().run_in_context(|lock, ctx| {
ctx.set_global(lock, "thrower", jsg::Rc::new(Thrower));
assert!(caught(
lock,
ctx,
"thrower.throwInvalidStringLength()",
"caught instanceof RangeError && caught.message === 'Invalid string length'"
));
// The same error V8 throws for a string it cannot create.
assert!(caught(
lock,
ctx,
"'x'.repeat(2 ** 30)",
"caught instanceof RangeError && caught.message === 'Invalid string length'"
));
Ok(())
});
}

#[test]
fn range_error_is_a_range_error_object() {
crate::Harness::new().run_in_context(|lock, ctx| {
ctx.set_global(lock, "thrower", jsg::Rc::new(Thrower));
assert!(
ctx.eval::<bool>(
lock,
"const e = thrower.rangeError('out of range');
e instanceof RangeError && e.message === 'out of range'"
)
.unwrap()
);
Ok(())
});
}
1 change: 1 addition & 0 deletions src/rust/jsg-test/tests/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ mod jsg_struct;
mod local_cast;
mod local_handle;
mod local_value;
mod lock_errors;
mod name;
mod nested_type;
mod non_coercible;
Expand Down
9 changes: 3 additions & 6 deletions src/rust/jsg-test/tests/promise.rs
Original file line number Diff line number Diff line change
Expand Up @@ -529,7 +529,7 @@ impl Watcher {
watch.state.seen.borrow_mut().push(seen);
// No borrow is held across the call, whose JavaScript calls back into the watcher.
let callback = watch.callback.as_local(lock);
callback.call::<v8::Local<v8::Value>>(lock, None, &[value])
callback.call(lock, None, &[value])
})
}

Expand Down Expand Up @@ -588,9 +588,7 @@ impl TerminatingPromises {
let promise = resolver.promise();
lock.terminate_execution();
let spin = v8::Local::<v8::Function>::try_from(spin).unwrap();
let err = spin
.call::<v8::Local<v8::Value>>(lock, None, &[])
.unwrap_err();
let err = spin.call(lock, None, &[]).unwrap_err();
assert!(err.is_pending());

let mut pending = Vec::new();
Expand Down Expand Up @@ -918,8 +916,7 @@ impl ReadThroughCache {

let key_js = key.as_str().to_js(lock)?;
// No RefCell borrow is held here: the fallback is JavaScript and may call `read` again.
let called =
lock.try_catch(|lock| fallback.call::<v8::Local<v8::Value>>(lock, None, &[key_js]));
let called = lock.try_catch(|lock| fallback.call(lock, None, &[key_js]));
match called {
// Resolving with a promise follows it, and with any other value fulfills.
Ok(Ok(produced)) => resolver.resolve(lock, produced)?,
Expand Down
Loading
Loading