Skip to content
File

Blob: src/rust/jsg-macros/lib.rs

rust505 lines
1// Copyright (c) 2026 Cloudflare, Inc.
2// Licensed under the Apache 2.0 license found in the LICENSE file or at:
3// https://opensource.org/licenses/Apache-2.0
4 
5//! Procedural macros for the JSG Rust bindings.
6//!
7//! # Macros
8//!
9//! | Macro | Apply to | Purpose |
10//! |--------------------------|------------------|----------------------------------------------------------------|
11//! | `#[jsg_resource]` | struct / impl | Expose a Rust type to JavaScript as a GC resource |
12//! | `#[jsg_method]` | fn inside impl | Register a method (instance or static) on a resource |
13//! | `#[jsg_constructor]` | fn inside impl | Register `new MyResource(…)` JavaScript constructor |
14//! | `#[jsg_static_constant]` | const inside impl | Expose a numeric constant on both constructor and prototype |
15//! | `#[jsg_property]` | fn inside impl | Register a resource accessor property (getter/setter) |
16//! | `#[jsg_inspect_property]` | fn inside impl | Register a debug-inspect-only symbol property |
17//! | `#[jsg_struct]` | struct | Expose a Rust struct as a plain JavaScript object |
18//! | `#[jsg_oneof]` | enum | Accept one of several JavaScript types (`kj::OneOf`) |
19//!
20//! See [`jsg/README.md`](../jsg/README.md) for full usage documentation.
21 
22mod resource;
23mod trace;
24mod utils;
25 
26use proc_macro::TokenStream;
27use quote::quote;
28use resource::generate_resource_impl;
29use resource::generate_resource_struct;
30use syn::Data;
31use syn::DeriveInput;
32use syn::Fields;
33use syn::FnArg;
34use syn::ItemFn;
35use syn::ItemImpl;
36use syn::parse_macro_input;
37use utils::error;
38use utils::extract_name_attribute;
39use utils::extract_named_fields;
40use utils::is_lock_ref;
41use utils::is_result_type;
42 
43// =============================================================================
44// #[jsg_struct]
45// =============================================================================
46 
47/// Generates `jsg::Struct`, `jsg::Type`, `jsg::ToJS`, and `jsg::FromJS`
48/// implementations for a plain data struct.
49///
50/// Only `pub` fields are projected into the JavaScript object.
51/// Use `#[jsg_struct(name = "MyName")]` to override `Type::class_name()`
52/// metadata (used in diagnostics/type reporting), not to define a JS class.
53#[proc_macro_attribute]
54pub fn jsg_struct(attr: TokenStream, item: TokenStream) -> TokenStream {
55 let input = parse_macro_input!(item as DeriveInput);
56 let name = &input.ident;
57 let class_name = extract_name_attribute(attr).unwrap_or_else(|| name.to_string());
58 
59 let named_fields = match extract_named_fields(&input, "jsg_struct") {
60 Ok(fields) => fields,
61 Err(err) => return err,
62 };
63 
64 let mut field_assignments = Vec::new();
65 let mut field_extractions = Vec::new();
66 let mut field_names = Vec::new();
67 
68 for field in &named_fields {
69 // Only public fields are projected into JavaScript objects.
70 if !matches!(field.vis, syn::Visibility::Public(_)) {
71 continue;
72 }
73 // Named fields always have an ident; guard is purely defensive.
74 let Some(field_name) = field.ident.as_ref() else {
75 continue;
76 };
77 let field_name_str = field_name.to_string();
78 let field_type = &field.ty;
79 
80 field_assignments.push(quote! {
81 let #field_name = jsg::v8::ToLocalValue::to_local(&this.#field_name, lock);
82 obj.set(lock, #field_name_str, #field_name);
83 });
84 field_extractions.push(quote! {
85 let #field_name = {
86 let prop = obj.get(lock, #field_name_str)
87 .ok_or_else(|| jsg::Error::new_type_error(
88 format!("Missing property '{}'", #field_name_str)
89 ))?;
90 <#field_type as jsg::FromJS>::from_js(lock, prop)?
91 };
92 });
93 field_names.push(field_name);
94 }
95 
96 quote! {
97 #input
98 
99 impl jsg::Type for #name {
100 fn class_name() -> &'static str { #class_name }
101 
102 fn is_exact(value: &jsg::v8::Local<jsg::v8::Value>) -> bool {
103 value.is_object()
104 }
105 }
106 
107 impl jsg::ToJS for #name {
108 fn to_js<'a, 'b>(self, lock: &'a mut jsg::Lock) -> jsg::v8::Local<'b, jsg::v8::Value>
109 where
110 'b: 'a,
111 {
112 // TODO(soon): Use a precached ObjectTemplate instance to create the object,
113 // similar to how C++ JSG optimizes object creation. This would avoid recreating
114 // the object shape on every wrap() call and improve performance.
115 {
116 let this = self;
117 let mut obj = lock.new_object();
118 #(#field_assignments)*
119 obj.into()
120 }
121 }
122 }
123 
124 impl jsg::FromJS for #name {
125 type ResultType = Self;
126 
127 fn from_js(lock: &mut jsg::Lock, value: jsg::v8::Local<jsg::v8::Value>) -> Result<Self::ResultType, jsg::Error> {
128 if !value.is_object() {
129 return Err(jsg::Error::new_type_error(
130 format!("Expected object but got {}", value.type_of())
131 ));
132 }
133 let obj: jsg::v8::Local<'_, jsg::v8::Object> = value.into();
134 #(#field_extractions)*
135 Ok(Self { #(#field_names),* })
136 }
137 }
138 
139 impl jsg::Struct for #name {}
140 
141 #[automatically_derived]
142 impl jsg::Traced for #name {}
143 }
144 .into()
145}
146 
147// =============================================================================
148// #[jsg_method]
149// =============================================================================
150 
151/// Generates a V8 `FunctionCallback` for a JSG resource method.
152///
153/// Parameters are extracted from JavaScript arguments via `jsg::FromJS`.
154/// Return values are converted via `jsg::ToJS`.
155/// `Result<T, E>` return types automatically throw exceptions on `Err`.
156///
157/// The first typed parameter may be `&mut Lock` (or `&mut jsg::Lock`) to receive
158/// the isolate lock directly; it is not counted as a JavaScript argument.
159///
160/// Use `#[jsg_method(name = "jsName")]` to override the default `camelCase`
161/// conversion of the Rust function name.
162#[proc_macro_attribute]
163pub fn jsg_method(_attr: TokenStream, item: TokenStream) -> TokenStream {
164 let input_fn = parse_macro_input!(item as ItemFn);
165 let fn_name = &input_fn.sig.ident;
166 let fn_vis = &input_fn.vis;
167 let fn_sig = &input_fn.sig;
168 let fn_block = &input_fn.block;
169 let callback_name = syn::Ident::new(&format!("{fn_name}_callback"), fn_name.span());
170 
171 // Methods with a receiver (&self, &mut self) become instance methods on the prototype.
172 // Methods without a receiver become static methods on the constructor.
173 let has_self = fn_sig
174 .inputs
175 .iter()
176 .any(|arg| matches!(arg, FnArg::Receiver(_)));
177 
178 let params: Vec<_> = fn_sig
179 .inputs
180 .iter()
181 .filter_map(|arg| match arg {
182 FnArg::Typed(pat_type) => Some(&pat_type.ty),
183 FnArg::Receiver(_) => None,
184 })
185 .collect();
186 
187 // Check if the first typed parameter is `&mut Lock` — if so, pass `&mut lock`
188 // directly instead of extracting it from JS args (like C++ jsg::Lock&).
189 let has_lock_param = params.first().is_some_and(|ty| is_lock_ref(ty));
190 let js_arg_offset = usize::from(has_lock_param);
191 
192 let (unwraps, arg_exprs): (Vec<_>, Vec<_>) = params
193 .iter()
194 .enumerate()
195 .map(|(i, ty)| {
196 // First param is &mut Lock — pass the callback's lock directly.
197 if i == 0 && has_lock_param {
198 return (quote! {}, quote! { &mut lock });
199 }
200 
201 let js_index = i - js_arg_offset;
202 let arg = syn::Ident::new(&format!("arg{js_index}"), fn_name.span());
203 let unwrap = quote! {
204 let #arg = match <#ty as jsg::FromJS>::from_js(&mut lock, args.get(#js_index)) {
205 Ok(v) => v,
206 Err(err) => {
207 lock.throw_exception(&err);
208 return;
209 }
210 };
211 };
212 // For reference types (like &str), FromJS returns an owned type (String),
213 // so we need to borrow it when passing to the function.
214 let is_ref = matches!(ty.as_ref(), syn::Type::Reference(_));
215 let arg_expr = if is_ref {
216 quote! { &#arg }
217 } else {
218 quote! { #arg }
219 };
220 (unwrap, arg_expr)
221 })
222 .unzip();
223 
224 // Check if return type is Result<T, E>
225 let is_result = matches!(&fn_sig.output, syn::ReturnType::Type(_, ty) if is_result_type(ty));
226 
227 let result_handling = if is_result {
228 quote! {
229 match result {
230 Ok(value) => args.set_return_value(jsg::ToJS::to_js(value, &mut lock)),
231 Err(err) => lock.throw_exception(&err.into()),
232 }
233 }
234 } else {
235 quote! {
236 args.set_return_value(jsg::ToJS::to_js(result, &mut lock));
237 }
238 };
239 
240 let invocation = if has_self {
241 quote! {
242 let this = args.this();
243 // SAFETY: `v8::Signature` (passed to `FunctionTemplate::New` in
244 // `create_resource_template`) enforces that V8 only dispatches this
245 // callback when `this` is an instance of the resource's own
246 // `FunctionTemplate`. If the caller destructures the method and calls
247 // it with a wrong receiver (e.g. `const {abort} = ac; abort()`), V8
248 // throws a `TypeError: Illegal invocation` *before* reaching this
249 // code. Given that guarantee, `from_js` / `resolve_resource` perform
250 // a belt-and-suspenders `TypeId` check; the `.expect` panics
251 // (aborting the isolate) rather than triggering UB on a mismatch.
252 // The `&mut` is sound because V8 is single-threaded and no other
253 // Rust code can alias the resource during the callback.
254 let self_: &mut Self = unsafe {
255 let wrappable = jsg::v8::WrappableRc::from_js(lock.isolate(), this)
256 .expect("receiver is not a Rust-wrapped resource");
257 &mut *wrappable.resolve_resource::<Self>()
258 .expect("type mismatch in resource callback")
259 .as_ptr()
260 };
261 let result = self_.#fn_name(#(#arg_exprs),*);
262 }
263 } else {
264 quote! {
265 let result = Self::#fn_name(#(#arg_exprs),*);
266 }
267 };
268 
269 quote! {
270 #fn_vis #fn_sig { #fn_block }
271 
272 #[automatically_derived]
273 #[expect(clippy::undocumented_unsafe_blocks)]
274 extern "C" fn #callback_name(args: *mut jsg::v8::ffi::FunctionCallbackInfo) {
275 let mut lock = unsafe { jsg::Lock::from_args(args) };
276 jsg::catch_panic(&mut lock, || {
277 let mut lock = unsafe { jsg::Lock::from_args(args) };
278 let mut args = unsafe { jsg::v8::FunctionCallbackInfo::from_ffi(args) };
279 #(#unwraps)*
280 #invocation
281 #result_handling
282 });
283 }
284 }
285 .into()
286}
287 
288// =============================================================================
289// #[jsg_resource]
290// =============================================================================
291 
292/// Generates JSG boilerplate for a resource type or its impl block.
293///
294/// **On a struct** — emits `jsg::Type`, `jsg::ToJS`, `jsg::FromJS`,
295/// `jsg::Traced`, and `jsg::GarbageCollected`.
296///
297/// The generated `Traced::trace` body simply delegates to `Traced::trace()` on
298/// every named field.
299///
300/// **On an impl block** — emits `jsg::Resource::members()` registering every
301/// `#[jsg_method]`, `#[jsg_property]`, `#[jsg_inspect_property]`,
302/// `#[jsg_constructor]`, and `#[jsg_static_constant]` item.
303///
304/// Use `#[jsg_resource(name = "JSName")]` on the struct to override the default
305/// JavaScript class name.
306#[proc_macro_attribute]
307pub fn jsg_resource(attr: TokenStream, item: TokenStream) -> TokenStream {
308 if let Ok(impl_block) = syn::parse::<ItemImpl>(item.clone()) {
309 return generate_resource_impl(&impl_block);
310 }
311 let input = parse_macro_input!(item as DeriveInput);
312 generate_resource_struct(attr, &input)
313}
314 
315// =============================================================================
316// #[jsg_static_constant] (marker only — processed by #[jsg_resource])
317// =============================================================================
318 
319/// Marks a `const` item inside a `#[jsg_resource]` impl block as a static
320/// constant exposed to JavaScript on both the constructor and its prototype.
321///
322/// The constant name is used as-is (no camelCase conversion), matching the
323/// convention that constants are `UPPER_SNAKE_CASE` in both Rust and JavaScript.
324/// Only numeric types (`i8`..`i64`, `u8`..`u64`, `f32`, `f64`) are supported.
325///
326/// ```ignore
327/// #[jsg_resource]
328/// impl WebSocket {
329/// #[jsg_static_constant]
330/// pub const CONNECTING: i32 = 0;
331/// }
332/// // JS: WebSocket.CONNECTING === 0 / instance.CONNECTING === 0
333/// ```
334#[proc_macro_attribute]
335pub fn jsg_static_constant(_attr: TokenStream, item: TokenStream) -> TokenStream {
336 // Marker only — registration is handled by #[jsg_resource] on the impl block.
337 item
338}
339 
340// =============================================================================
341// #[jsg_constructor] (marker only — processed by #[jsg_resource])
342// =============================================================================
343 
344/// Marks a static method as the JavaScript constructor for a `#[jsg_resource]`.
345///
346/// The method must have no `self` receiver and must return `Self`.
347/// An optional first parameter of `&mut Lock` (or `&mut jsg::Lock`) receives
348/// the isolate lock and is not exposed as a JavaScript argument.
349///
350/// Only one `#[jsg_constructor]` is allowed per impl block. Without it,
351/// `new MyResource()` throws `Illegal constructor`, matching C++ JSG behaviour.
352///
353/// ```ignore
354/// #[jsg_resource]
355/// impl Greeting {
356/// #[jsg_constructor]
357/// fn constructor(message: String) -> Self {
358/// Self { message }
359/// }
360/// }
361/// // JS: let g = new Greeting("hello");
362/// ```
363#[proc_macro_attribute]
364pub fn jsg_constructor(_attr: TokenStream, item: TokenStream) -> TokenStream {
365 // Marker only — registration is handled by #[jsg_resource] on the impl block.
366 item
367}
368 
369/// Registers a method as a JavaScript property getter or setter on a
370/// `#[jsg_resource]` type.
371///
372/// Supported arguments:
373/// - `prototype` or `instance` (optional, defaults to `prototype`)
374/// - `name = "..."` for an explicit JS property name
375/// - `readonly` to require no matching setter
376///
377/// Methods must start with `get_` (getter) or `set_` (setter). If `name` is
378/// omitted, the prefix is stripped and the remainder is converted
379/// `snake_case` -> `camelCase`.
380#[proc_macro_attribute]
381pub fn jsg_property(_attr: TokenStream, item: TokenStream) -> TokenStream {
382 // Reuse jsg_method callback generation; registration as Member::Property is
383 // handled by #[jsg_resource] on the enclosing impl block.
384 jsg_method(TokenStream::new(), item)
385}
386 
387/// Registers a method as a debug-inspect-only property on a `#[jsg_resource]`
388/// type. This maps to `jsg::PropertyKind::Inspect`.
389///
390/// Optional argument: `name = "..."` to set the symbol description.
391///
392/// Inspect properties are always read-only; setters are rejected at compile time.
393#[proc_macro_attribute]
394pub fn jsg_inspect_property(_attr: TokenStream, item: TokenStream) -> TokenStream {
395 // Reuse jsg_method callback generation; registration as Member::Property is
396 // handled by #[jsg_resource] on the enclosing impl block.
397 jsg_method(TokenStream::new(), item)
398}
399 
400// =============================================================================
401// #[jsg_oneof]
402// =============================================================================
403 
404/// Generates `jsg::Type` and `jsg::FromJS` for a union enum, equivalent to
405/// `kj::OneOf<…>` in C++ JSG.
406///
407/// Each variant must be a single-field tuple variant whose inner type implements
408/// `jsg::Type` and `jsg::FromJS`. The macro tries each variant in declaration
409/// order using exact-type matching and returns the first that succeeds.
410///
411/// ```ignore
412/// #[jsg_oneof]
413/// #[derive(Debug, Clone)]
414/// enum StringOrNumber {
415/// String(String),
416/// Number(jsg::Number),
417/// }
418/// ```
419#[proc_macro_attribute]
420pub fn jsg_oneof(_attr: TokenStream, item: TokenStream) -> TokenStream {
421 let input = parse_macro_input!(item as DeriveInput);
422 let name = &input.ident;
423 
424 let Data::Enum(data) = &input.data else {
425 return error(&input, "#[jsg_oneof] can only be applied to enums");
426 };
427 
428 let mut variants = Vec::new();
429 for variant in &data.variants {
430 let variant_name = &variant.ident;
431 let Fields::Unnamed(fields) = &variant.fields else {
432 return error(
433 variant,
434 "#[jsg_oneof] variants must be tuple variants (e.g., `Variant(Type)`)",
435 );
436 };
437 if fields.unnamed.len() != 1 {
438 return error(variant, "#[jsg_oneof] variants must have exactly one field");
439 }
440 let inner_type = &fields.unnamed[0].ty;
441 variants.push((variant_name, inner_type));
442 }
443 
444 if variants.is_empty() {
445 return error(&input, "#[jsg_oneof] requires at least one variant");
446 }
447 
448 let type_checks: Vec<_> = variants
449 .iter()
450 .map(|(variant_name, inner_type)| {
451 quote! {
452 if let Some(result) = <#inner_type as jsg::FromJS>::try_from_js_exact(lock, &value) {
453 return result.map(Self::#variant_name);
454 }
455 }
456 })
457 .collect();
458 
459 let type_names: Vec<_> = variants
460 .iter()
461 .map(|(_, inner_type)| quote! { <#inner_type as jsg::Type>::class_name() })
462 .collect();
463 
464 let is_exact_checks: Vec<_> = variants
465 .iter()
466 .map(|(_, inner_type)| quote! { <#inner_type as jsg::Type>::is_exact(value) })
467 .collect();
468 
469 let error_msg = quote! {
470 let expected: Vec<&str> = vec![#(#type_names),*];
471 let msg = format!(
472 "Expected one of [{}] but got {}",
473 expected.join(", "),
474 value.type_of()
475 );
476 Err(jsg::Error::new_type_error(msg))
477 };
478 
479 quote! {
480 #input
481 
482 #[automatically_derived]
483 impl jsg::Type for #name {
484 fn class_name() -> &'static str {
485 stringify!(#name)
486 }
487 
488 fn is_exact(value: &jsg::v8::Local<jsg::v8::Value>) -> bool {
489 #(#is_exact_checks)||*
490 }
491 }
492 
493 #[automatically_derived]
494 impl jsg::FromJS for #name {
495 type ResultType = Self;
496 
497 fn from_js(lock: &mut jsg::Lock, value: jsg::v8::Local<jsg::v8::Value>) -> Result<Self::ResultType, jsg::Error> {
498 #(#type_checks)*
499 #error_msg
500 }
501 }
502 }
503 .into()
504}