Skip to content

Commit fa13c8a

Browse files
committed
support non-result return values in jsg rust
1 parent 91275d4 commit fa13c8a

File tree

4 files changed

+204
-3
lines changed

4 files changed

+204
-3
lines changed

src/rust/jsg-macros/README.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,31 @@ 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+
Methods can return `Result<T, E>` (errors become JavaScript exceptions) or return values directly:
28+
29+
```rust
30+
impl DnsUtil {
31+
#[jsg_method(name = "parseCaaRecord")]
32+
pub fn parse_caa_record(&self, record: &str) -> Result<CaaRecord, DnsParserError> {
33+
// Errors are thrown as JavaScript exceptions
34+
}
35+
36+
#[jsg_method]
37+
pub fn get_name(&self) -> String {
38+
self.name.clone()
39+
}
40+
41+
#[jsg_method]
42+
pub fn reset(&self) {
43+
// Void methods return undefined in JavaScript
44+
}
45+
}
46+
```
47+
2348
## `#[jsg_resource]`
2449

2550
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: 78 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,30 @@ pub fn jsg_struct(attr: TokenStream, item: TokenStream) -> TokenStream {
160160
/// }
161161
/// ```
162162
///
163+
/// # Return Types
164+
///
165+
/// Methods can return either `Result<T, E>` or directly return `T`:
166+
///
167+
/// ```ignore
168+
/// // Returning Result - errors are thrown as JavaScript exceptions
169+
/// #[jsg_method]
170+
/// pub fn parse_record(&self, data: &str) -> Result<Record, Error> {
171+
/// // implementation
172+
/// }
173+
///
174+
/// // Returning a value directly - no error handling needed
175+
/// #[jsg_method]
176+
/// pub fn get_name(&self) -> String {
177+
/// self.name.clone()
178+
/// }
179+
///
180+
/// // Returning nothing (unit type)
181+
/// #[jsg_method]
182+
/// pub fn reset(&self) {
183+
/// // implementation
184+
/// }
185+
/// ```
186+
///
163187
/// # Example
164188
///
165189
/// ```ignore
@@ -223,6 +247,9 @@ pub fn jsg_method(_attr: TokenStream, item: TokenStream) -> TokenStream {
223247
})
224248
.collect();
225249

250+
// Determine how to handle the return value based on return type
251+
let return_handling = generate_return_handling(&fn_sig.output, fn_name, &arg_refs);
252+
226253
let expanded = quote! {
227254
#fn_vis #fn_sig {
228255
#fn_block
@@ -235,14 +262,63 @@ pub fn jsg_method(_attr: TokenStream, item: TokenStream) -> TokenStream {
235262
#(#unwrap_statements)*
236263
let this = args.this();
237264
let self_ = jsg::unwrap_resource::<Self>(&mut lock, this);
238-
let result = self_.#fn_name(#(#arg_refs),*);
239-
unsafe { jsg::handle_result(&mut lock, &mut args, result) };
265+
#return_handling
240266
}
241267
};
242268

243269
TokenStream::from(expanded)
244270
}
245271

272+
/// Checks if a return type is `Result<T, E>`.
273+
fn is_result_type(ty: &Type) -> bool {
274+
let Type::Path(type_path) = ty else {
275+
return false;
276+
};
277+
type_path
278+
.path
279+
.segments
280+
.last()
281+
.is_some_and(|seg| seg.ident == "Result")
282+
}
283+
284+
/// Generates the code for handling the method's return value.
285+
fn generate_return_handling(
286+
output: &syn::ReturnType,
287+
fn_name: &syn::Ident,
288+
arg_refs: &[quote::__private::TokenStream],
289+
) -> quote::__private::TokenStream {
290+
match output {
291+
syn::ReturnType::Default => {
292+
// No return value (unit type) - just call the method
293+
quote! {
294+
self_.#fn_name(#(#arg_refs),*);
295+
}
296+
}
297+
syn::ReturnType::Type(_, ty) => {
298+
// Check for explicit unit tuple
299+
if matches!(&**ty, Type::Tuple(tuple) if tuple.elems.is_empty()) {
300+
return quote! {
301+
self_.#fn_name(#(#arg_refs),*);
302+
};
303+
}
304+
305+
if is_result_type(ty) {
306+
// Result<T, E> - use handle_result for error handling
307+
quote! {
308+
let result = self_.#fn_name(#(#arg_refs),*);
309+
unsafe { jsg::handle_result(&mut lock, &mut args, result) };
310+
}
311+
} else {
312+
// Non-Result type - wrap directly
313+
quote! {
314+
let result = self_.#fn_name(#(#arg_refs),*);
315+
unsafe { jsg::handle_value(&mut lock, &mut args, result) };
316+
}
317+
}
318+
}
319+
}
320+
}
321+
246322
fn is_str_reference(ty: &Type) -> bool {
247323
match ty {
248324
Type::Reference(type_ref) => {

src/rust/jsg-test/tests/resource_callback.rs

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@
55
//! V8's internal field embedder data type tags are used correctly when getting
66
//! aligned pointers from internal fields.
77
8+
use std::cell::Cell;
9+
use std::rc::Rc;
10+
811
use jsg::Lock;
912
use jsg::ResourceState;
1013
use jsg::ResourceTemplate;
@@ -28,6 +31,36 @@ impl EchoResource {
2831
}
2932
}
3033

34+
#[jsg_resource]
35+
struct DirectReturnResource {
36+
_state: ResourceState,
37+
name: String,
38+
counter: Rc<Cell<u32>>,
39+
}
40+
41+
#[jsg_resource]
42+
impl DirectReturnResource {
43+
#[jsg_method]
44+
pub fn get_name(&self) -> String {
45+
self.name.clone()
46+
}
47+
48+
#[jsg_method]
49+
pub fn is_valid(&self) -> bool {
50+
!self.name.is_empty()
51+
}
52+
53+
#[jsg_method]
54+
pub fn get_counter(&self) -> f64 {
55+
f64::from(self.counter.get())
56+
}
57+
58+
#[jsg_method]
59+
pub fn increment_counter(&self) {
60+
self.counter.set(self.counter.get() + 1);
61+
}
62+
}
63+
3164
/// Validates that resource methods can be called from JavaScript.
3265
/// This test ensures the embedder data type tag is correctly used when
3366
/// unwrapping resource pointers from V8 internal fields.
@@ -91,3 +124,58 @@ fn resource_method_can_be_called_multiple_times() {
91124
);
92125
});
93126
}
127+
128+
/// Validates that methods can return values directly without Result wrapper.
129+
#[test]
130+
fn resource_method_returns_non_result_values() {
131+
let harness = crate::Harness::new();
132+
harness.run_in_context(|isolate, ctx| unsafe {
133+
let mut lock = Lock::from_isolate_ptr(isolate);
134+
let counter = Rc::new(Cell::new(42));
135+
let resource = jsg::Ref::new(DirectReturnResource {
136+
_state: ResourceState::default(),
137+
name: "TestResource".to_owned(),
138+
counter: counter.clone(),
139+
});
140+
let mut template = DirectReturnResourceTemplate::new(&mut lock);
141+
let wrapped = jsg::wrap_resource(&mut lock, resource, &mut template);
142+
ctx.set_global_safe("resource", wrapped.into_ffi());
143+
144+
assert_eq!(
145+
ctx.eval_safe("resource.getName()"),
146+
EvalResult {
147+
success: true,
148+
result_type: "string".to_owned(),
149+
result_value: "TestResource".to_owned(),
150+
}
151+
);
152+
153+
assert_eq!(
154+
ctx.eval_safe("resource.isValid()"),
155+
EvalResult {
156+
success: true,
157+
result_type: "boolean".to_owned(),
158+
result_value: "true".to_owned(),
159+
}
160+
);
161+
162+
assert_eq!(
163+
ctx.eval_safe("resource.getCounter()"),
164+
EvalResult {
165+
success: true,
166+
result_type: "number".to_owned(),
167+
result_value: "42".to_owned(),
168+
}
169+
);
170+
171+
assert_eq!(
172+
ctx.eval_safe("resource.incrementCounter()"),
173+
EvalResult {
174+
success: true,
175+
result_type: "undefined".to_owned(),
176+
result_value: "undefined".to_owned(),
177+
}
178+
);
179+
assert_eq!(counter.get(), 43);
180+
});
181+
}

src/rust/jsg/lib.rs

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -612,6 +612,18 @@ unsafe fn realm_create(isolate: *mut v8::ffi::Isolate) -> Box<Realm> {
612612
unsafe { Box::new(Realm::from_isolate(v8::IsolatePtr::from_ffi(isolate))) }
613613
}
614614

615+
/// Handles a non-Result value by setting the return value directly.
616+
///
617+
/// # Safety
618+
/// The caller must ensure V8 operations are performed within the correct isolate/context.
619+
pub unsafe fn handle_value<T: Type<This = T>>(
620+
lock: &mut Lock,
621+
args: &mut v8::FunctionCallbackInfo,
622+
value: T,
623+
) {
624+
args.set_return_value(T::wrap(value, lock));
625+
}
626+
615627
/// Handles a result by setting the return value or throwing an error.
616628
///
617629
/// # Safety
@@ -622,7 +634,7 @@ pub unsafe fn handle_result<T: Type<This = T>, E: std::fmt::Display>(
622634
result: Result<T, E>,
623635
) {
624636
match result {
625-
Ok(result) => args.set_return_value(T::wrap(result, lock)),
637+
Ok(value) => unsafe { handle_value(lock, args, value) },
626638
Err(err) => {
627639
// TODO(soon): Make sure to use jsg::Error trait here and dynamically call proper method to throw the error.
628640
let description = err.to_string();

0 commit comments

Comments
 (0)