Skip to content

Commit 91275d4

Browse files
authored
implement jsg::NonCoercible in Rust (#5730)
* implement jsg::NonCoercible in Rust * add resource callback tests * simplify implementation and improve error message * rename into_ffi() to as_ffi() * avoid unnecessary string copies in resources * address pr review
1 parent c4f43c1 commit 91275d4

File tree

16 files changed

+1431
-240
lines changed

16 files changed

+1431
-240
lines changed

src/rust/api/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ mod tests {
4848
#[test]
4949
fn test_wrap_resource_equality() {
5050
let harness = Harness::new();
51-
harness.run_in_context(|isolate| unsafe {
51+
harness.run_in_context(|isolate, _ctx| unsafe {
5252
let mut lock = jsg::Lock::from_isolate_ptr(isolate);
5353
let dns_util = jsg::Ref::new(DnsUtil {
5454
_state: ResourceState::default(),

src/rust/jsg-macros/README.md

Lines changed: 0 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -20,24 +20,6 @@ pub struct MyRecord {
2020
}
2121
```
2222

23-
## `#[jsg_method]`
24-
25-
Generates FFI callback functions for JSG resource methods. The `name` parameter is optional and defaults to converting the method name from `snake_case` to `camelCase`.
26-
27-
```rust
28-
impl DnsUtil {
29-
#[jsg_method(name = "parseCaaRecord")]
30-
pub fn parse_caa_record(&self, record: &str) -> Result<CaaRecord, DnsParserError> {
31-
// implementation
32-
}
33-
34-
#[jsg_method]
35-
pub fn parse_naptr_record(&self, record: &str) -> Result<NaptrRecord, DnsParserError> {
36-
// Exposed as "parseNaptrRecord" in JavaScript
37-
}
38-
}
39-
```
40-
4123
## `#[jsg_resource]`
4224

4325
Generates boilerplate for JSG resources. Applied to both struct definitions and impl blocks. Automatically implements `jsg::Type::class_name()` using the struct name, or a custom name if provided via the `name` parameter.

src/rust/jsg-macros/lib.rs

Lines changed: 84 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ pub fn jsg_struct(attr: TokenStream, item: TokenStream) -> TokenStream {
7575
.expect("Named fields must have identifiers")
7676
.to_string();
7777
Some(quote! {
78-
let #field_name = jsg::v8::ToLocalValue::to_local(&self.#field_name, lock);
78+
let #field_name = jsg::v8::ToLocalValue::to_local(&this.#field_name, lock);
7979
obj.set(lock, #field_name_str, #field_name);
8080
})
8181
} else {
@@ -87,11 +87,13 @@ pub fn jsg_struct(attr: TokenStream, item: TokenStream) -> TokenStream {
8787
#input
8888

8989
impl jsg::Type for #name {
90+
type This = Self;
91+
9092
fn class_name() -> &'static str {
9193
#class_name
9294
}
9395

94-
fn wrap<'a, 'b>(&self, lock: &'a mut jsg::Lock) -> jsg::v8::Local<'b, jsg::v8::Value>
96+
fn wrap<'a, 'b>(this: Self::This, lock: &'a mut jsg::Lock) -> jsg::v8::Local<'b, jsg::v8::Value>
9597
where
9698
'b: 'a,
9799
{
@@ -104,6 +106,15 @@ pub fn jsg_struct(attr: TokenStream, item: TokenStream) -> TokenStream {
104106
obj.into()
105107
}
106108
}
109+
110+
fn is_exact(value: &jsg::v8::Local<jsg::v8::Value>) -> bool {
111+
value.is_object()
112+
}
113+
114+
fn unwrap(_isolate: jsg::v8::IsolatePtr, _value: jsg::v8::Local<jsg::v8::Value>) -> Self {
115+
// TODO(soon): Implement proper unwrapping for struct types
116+
unimplemented!("Struct unwrap is not yet supported")
117+
}
107118
}
108119

109120
impl jsg::Struct for #name {
@@ -119,16 +130,47 @@ pub fn jsg_struct(attr: TokenStream, item: TokenStream) -> TokenStream {
119130
/// Creates a `{method_name}_callback` extern "C" function that bridges JavaScript and Rust.
120131
/// If no name is provided, automatically converts `snake_case` to `camelCase`.
121132
///
133+
/// # Supported Parameter Types
134+
///
135+
/// | Type | `T` | `NonCoercible<T>` |
136+
/// |------------|---------------|-------------------|
137+
/// | `&str` | ✅ Supported | ❌ |
138+
/// | `T: Type` | ❌ | ✅ Supported |
139+
///
140+
/// Any type implementing [`jsg::Type`] can be used with `NonCoercible<T>`.
141+
/// Built-in types include `String`, `bool`, and `f64`.
142+
///
143+
/// # `NonCoercible<T>` Parameters
144+
///
145+
/// Use `NonCoercible<T>` when you want to accept a value only if it's already the expected
146+
/// type, without JavaScript's automatic type coercion:
147+
///
148+
/// ```ignore
149+
/// #[jsg_method]
150+
/// pub fn strict_string(&self, param: NonCoercible<String>) -> Result<String, Error> {
151+
/// // Only accepts actual strings - passing null/undefined/numbers will throw
152+
/// // Access via Deref: *param, or via AsRef: param.as_ref()
153+
/// Ok(param.as_ref().clone())
154+
/// }
155+
///
156+
/// #[jsg_method]
157+
/// pub fn strict_bool(&self, param: NonCoercible<bool>) -> Result<bool, Error> {
158+
/// // Only accepts actual booleans
159+
/// Ok(*param)
160+
/// }
161+
/// ```
162+
///
122163
/// # Example
123-
/// ```rust
164+
///
165+
/// ```ignore
124166
/// // With explicit name
125-
/// #[jsg::method(name = "parseRecord")]
167+
/// #[jsg_method(name = "parseRecord")]
126168
/// pub fn parse_record(&self, data: &str) -> Result<Record, Error> {
127169
/// // implementation
128170
/// }
129171
///
130172
/// // Without name - automatically becomes "parseRecord"
131-
/// #[jsg::method]
173+
/// #[jsg_method]
132174
/// pub fn parse_record(&self, data: &str) -> Result<Record, Error> {
133175
/// // implementation
134176
/// }
@@ -210,11 +252,34 @@ fn is_str_reference(ty: &Type) -> bool {
210252
}
211253
}
212254

255+
/// Returns true if the type is `NonCoercible<T>`.
256+
fn is_non_coercible(ty: &Type) -> bool {
257+
let Type::Path(type_path) = ty else {
258+
return false;
259+
};
260+
type_path
261+
.path
262+
.segments
263+
.last()
264+
.is_some_and(|seg| seg.ident == "NonCoercible")
265+
}
266+
213267
fn generate_unwrap_code(
214268
arg_name: &syn::Ident,
215269
ty: &Type,
216270
index: usize,
217271
) -> quote::__private::TokenStream {
272+
// Check for NonCoercible<T> types
273+
if is_non_coercible(ty) {
274+
return quote! {
275+
let Some(#arg_name) = (unsafe {
276+
<#ty>::unwrap(&mut lock, args.get(#index))
277+
}) else {
278+
return;
279+
};
280+
};
281+
}
282+
218283
if is_str_reference(ty) {
219284
quote! {
220285
let #arg_name = unsafe {
@@ -223,7 +288,7 @@ fn generate_unwrap_code(
223288
}
224289
} else {
225290
quote! {
226-
compile_error!("Unsupported parameter type for jsg::method. Currently only &str is supported.");
291+
compile_error!("Unsupported parameter type for jsg::method. Currently only &str and NonCoercible<T> are supported.");
227292
}
228293
}
229294
}
@@ -294,16 +359,27 @@ pub fn jsg_resource(attr: TokenStream, item: TokenStream) -> TokenStream {
294359

295360
#[automatically_derived]
296361
impl jsg::Type for #name {
362+
type This = jsg::Ref<Self>;
363+
297364
fn class_name() -> &'static str {
298365
#class_name
299366
}
300367

301-
fn wrap<'a, 'b>(&self, lock: &'a mut jsg::Lock) -> jsg::v8::Local<'b, jsg::v8::Value>
368+
fn wrap<'a, 'b>(_this: Self::This, _lock: &'a mut jsg::Lock) -> jsg::v8::Local<'b, jsg::v8::Value>
302369
where
303370
'b: 'a,
304371
{
305372
todo!("Implement wrap for jsg::Resource")
306373
}
374+
375+
fn is_exact(value: &jsg::v8::Local<jsg::v8::Value>) -> bool {
376+
value.is_object()
377+
}
378+
379+
fn unwrap(_isolate: jsg::v8::IsolatePtr, _value: jsg::v8::Local<jsg::v8::Value>) -> Self {
380+
// TODO(soon): Implement proper unwrapping for resource types
381+
unimplemented!("Resource unwrap is not yet supported")
382+
}
307383
}
308384

309385
#[automatically_derived]
@@ -353,7 +429,7 @@ fn generate_resource_impl(impl_block: &ItemImpl) -> TokenStream {
353429

354430
method_registrations.push(quote! {
355431
jsg::Member::Method {
356-
name: #js_name,
432+
name: #js_name.to_owned(),
357433
callback: Self::#callback_name,
358434
}
359435
});

src/rust/jsg-test/ffi.c++

Lines changed: 75 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
#include "ffi.h"
22

33
#include <workerd/jsg/setup.h>
4+
#include <workerd/rust/jsg-test/lib.rs.h>
5+
#include <workerd/rust/jsg/ffi-inl.h>
46
#include <workerd/rust/jsg/lib.rs.h>
57
#include <workerd/rust/jsg/v8.rs.h>
68

@@ -44,11 +46,81 @@ kj::Own<TestHarness> create_test_harness() {
4446
[](::workerd::jsg::V8StackScope& stackScope) { return kj::heap<TestHarness>(stackScope); });
4547
}
4648

47-
void TestHarness::run_in_context(::rust::Fn<void(Isolate*)> callback) const {
49+
EvalContext::EvalContext(v8::Isolate* isolate, v8::Local<v8::Context> context)
50+
: v8Isolate(isolate),
51+
v8Context(isolate, context) {}
52+
53+
void EvalContext::set_global(::rust::Str name, ::workerd::rust::jsg::Local value) const {
54+
auto ctx = v8Context.Get(v8Isolate);
55+
auto key = ::workerd::jsg::check(v8::String::NewFromUtf8(
56+
v8Isolate, name.data(), v8::NewStringType::kNormal, static_cast<int>(name.size())));
57+
auto v8Value = ::workerd::rust::jsg::local_from_ffi<v8::Value>(kj::mv(value));
58+
::workerd::jsg::check(ctx->Global()->Set(ctx, key, v8Value));
59+
}
60+
61+
EvalResult EvalContext::eval(::rust::Str code) const {
62+
EvalResult result;
63+
result.success = false;
64+
65+
v8::Local<v8::Context> ctx = v8Context.Get(v8Isolate);
66+
67+
v8::Local<v8::String> source = ::workerd::jsg::check(v8::String::NewFromUtf8(
68+
v8Isolate, code.data(), v8::NewStringType::kNormal, static_cast<int>(code.size())));
69+
70+
v8::Local<v8::Script> script;
71+
if (!v8::Script::Compile(ctx, source).ToLocal(&script)) {
72+
result.success = false;
73+
result.result_type = "CompileError";
74+
result.result_value = "Failed to compile script";
75+
return result;
76+
}
77+
78+
v8::TryCatch catcher(v8Isolate);
79+
80+
v8::Local<v8::Value> value;
81+
if (script->Run(ctx).ToLocal(&value)) {
82+
v8::String::Utf8Value type(v8Isolate, value->TypeOf(v8Isolate));
83+
v8::String::Utf8Value valueStr(v8Isolate, value);
84+
85+
result.success = true;
86+
result.result_type = *type;
87+
result.result_value = *valueStr;
88+
} else if (catcher.HasCaught()) {
89+
v8::String::Utf8Value message(v8Isolate, catcher.Exception());
90+
91+
result.success = false;
92+
result.result_type = "throws";
93+
result.result_value = *message ? *message : "Unknown error";
94+
} else {
95+
result.success = false;
96+
result.result_type = "error";
97+
result.result_value = "Returned empty handle but didn't throw exception";
98+
}
99+
100+
return result;
101+
}
102+
103+
void TestHarness::run_in_context(::rust::Fn<void(Isolate*, EvalContext&)> callback) const {
104+
isolate->runInLockScope([&](TestIsolate::Lock& lock) {
105+
auto context = lock.newContext<TestContext>();
106+
v8::Local<v8::Context> v8Context = context.getHandle(lock.v8Isolate);
107+
v8::Context::Scope contextScope(v8Context);
108+
109+
EvalContext evalContext(lock.v8Isolate, v8Context);
110+
callback(lock.v8Isolate, evalContext);
111+
});
112+
}
113+
114+
void TestHarness::set_global(::rust::Str name, ::workerd::rust::jsg::Local value) const {
48115
isolate->runInLockScope([&](TestIsolate::Lock& lock) {
49116
auto context = lock.newContext<TestContext>();
50-
v8::Context::Scope contextScope(context.getHandle(isolate->getIsolate()));
51-
callback(isolate->getIsolate());
117+
v8::Local<v8::Context> v8Context = context.getHandle(lock.v8Isolate);
118+
v8::Context::Scope contextScope(v8Context);
119+
120+
v8::Local<v8::String> key = ::workerd::jsg::check(v8::String::NewFromUtf8(
121+
lock.v8Isolate, name.data(), v8::NewStringType::kNormal, static_cast<int>(name.size())));
122+
v8::Local<v8::Value> v8Value = ::workerd::rust::jsg::local_from_ffi<v8::Value>(kj::mv(value));
123+
::workerd::jsg::check(v8Context->Global()->Set(v8Context, key, v8Value));
52124
});
53125
}
54126

src/rust/jsg-test/ffi.h

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
#pragma once
22

33
#include <workerd/jsg/jsg.h>
4+
#include <workerd/rust/jsg/ffi.h>
45
#include <workerd/rust/jsg/v8.rs.h>
56

67
#include <kj-rs/kj-rs.h>
@@ -17,14 +18,31 @@ namespace rust::jsg_test {
1718

1819
using Isolate = v8::Isolate;
1920

21+
struct EvalResult;
22+
23+
class EvalContext {
24+
public:
25+
EvalContext(v8::Isolate* isolate, v8::Local<v8::Context> context);
26+
27+
EvalResult eval(::rust::Str code) const;
28+
void set_global(::rust::Str name, ::workerd::rust::jsg::Local value) const;
29+
30+
v8::Isolate* v8Isolate;
31+
v8::Global<v8::Context> v8Context;
32+
};
33+
2034
// Testing harness that provides a simple V8 isolate for Rust JSG testing
2135
class TestHarness {
2236
public:
2337
// Use create_test_harness() instead - it ensures proper V8 stack scope
2438
TestHarness(::workerd::jsg::V8StackScope& stackScope);
2539

2640
// Runs a callback within a proper V8 context and stack scope
27-
void run_in_context(::rust::Fn<void(Isolate*)> callback) const;
41+
// The callback receives both the isolate and a context that can be used with eval
42+
void run_in_context(::rust::Fn<void(Isolate*, EvalContext&)> callback) const;
43+
44+
// Sets a global variable on the context
45+
void set_global(::rust::Str name, ::workerd::rust::jsg::Local value) const;
2846

2947
private:
3048
mutable kj::Own<TestIsolate> isolate;

0 commit comments

Comments
 (0)