Skip to content

Commit 97e306b

Browse files
authored
add support for jsg_constructor (#6353)
1 parent dce7afd commit 97e306b

8 files changed

Lines changed: 396 additions & 6 deletions

File tree

src/rust/jsg-macros/README.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,25 @@ impl WebSocket {
125125

126126
Per Web IDL, constants are `{writable: false, enumerable: true, configurable: false}`.
127127

128+
## `#[jsg_constructor]`
129+
130+
Marks a static method as the JavaScript constructor for a `#[jsg_resource]`. When JavaScript calls `new MyClass(args)`, V8 invokes this method, creates a `jsg::Rc<Self>`, and attaches it to the `this` object.
131+
132+
```rust
133+
#[jsg_resource]
134+
impl MyResource {
135+
#[jsg_constructor]
136+
fn constructor(name: String) -> Self {
137+
Self { name }
138+
}
139+
}
140+
// JS: let r = new MyResource("hello");
141+
```
142+
143+
The method must be static (no `self` receiver) and must return `Self`. Only one `#[jsg_constructor]` is allowed per impl block. The first parameter may be `&mut Lock` if the constructor needs isolate access — it is not exposed as a JS argument.
144+
145+
If no `#[jsg_constructor]` is present, `new MyClass()` throws an `Illegal constructor` error.
146+
128147
## `#[jsg_oneof]`
129148

130149
Generates `jsg::Type` and `jsg::FromJS` implementations for union types. Use this to accept parameters that can be one of several JavaScript types.

src/rust/jsg-macros/lib.rs

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -510,6 +510,10 @@ fn generate_resource_impl(impl_block: &ItemImpl) -> TokenStream {
510510
})
511511
.collect();
512512

513+
let constructor_registration = generate_constructor_registration(impl_block, self_ty);
514+
515+
let constructor_vec: Vec<_> = constructor_registration.into_iter().collect();
516+
513517
quote! {
514518
#impl_block
515519

@@ -520,6 +524,7 @@ fn generate_resource_impl(impl_block: &ItemImpl) -> TokenStream {
520524
Self: Sized,
521525
{
522526
vec![
527+
#(#constructor_vec,)*
523528
#(#method_registrations,)*
524529
#(#constant_registrations,)*
525530
]
@@ -529,6 +534,129 @@ fn generate_resource_impl(impl_block: &ItemImpl) -> TokenStream {
529534
.into()
530535
}
531536

537+
/// Scans an impl block for a `#[jsg_constructor]` attribute and generates the
538+
/// constructor callback registration. Returns `None` if no constructor is defined.
539+
fn generate_constructor_registration(
540+
impl_block: &ItemImpl,
541+
self_ty: &syn::Type,
542+
) -> Option<quote::__private::TokenStream> {
543+
let constructors: Vec<_> = impl_block
544+
.items
545+
.iter()
546+
.filter_map(|item| match item {
547+
syn::ImplItem::Fn(m) if m.attrs.iter().any(|a| is_attr(a, "jsg_constructor")) => {
548+
Some(m)
549+
}
550+
_ => None,
551+
})
552+
.collect();
553+
554+
if constructors.len() > 1 {
555+
return Some(quote! {
556+
compile_error!("only one #[jsg_constructor] is allowed per impl block");
557+
});
558+
}
559+
560+
constructors
561+
.into_iter()
562+
.map(|method| {
563+
let rust_method_name = &method.sig.ident;
564+
let callback_name = syn::Ident::new(
565+
&format!("{rust_method_name}_constructor_callback"),
566+
rust_method_name.span(),
567+
);
568+
569+
// Constructor must NOT have a self receiver.
570+
let has_self = method
571+
.sig
572+
.inputs
573+
.iter()
574+
.any(|arg| matches!(arg, FnArg::Receiver(_)));
575+
if has_self {
576+
return quote! {
577+
compile_error!("#[jsg_constructor] must be a static method (no self receiver)");
578+
};
579+
}
580+
581+
// Constructor must return Self.
582+
let returns_self = matches!(&method.sig.output,
583+
syn::ReturnType::Type(_, ty) if matches!(&**ty,
584+
syn::Type::Path(p) if p.path.is_ident("Self")
585+
)
586+
);
587+
if !returns_self {
588+
return quote! {
589+
compile_error!("#[jsg_constructor] must return Self");
590+
};
591+
}
592+
593+
// Extract parameters (same pattern as jsg_method).
594+
let params: Vec<_> = method
595+
.sig
596+
.inputs
597+
.iter()
598+
.filter_map(|arg| {
599+
if let FnArg::Typed(pat_type) = arg {
600+
Some((*pat_type.ty).clone())
601+
} else {
602+
None
603+
}
604+
})
605+
.collect();
606+
607+
let has_lock_param = params.first().is_some_and(is_lock_ref);
608+
let js_arg_offset = usize::from(has_lock_param);
609+
610+
let (unwraps, arg_exprs): (Vec<_>, Vec<_>) = params
611+
.iter()
612+
.enumerate()
613+
.skip(js_arg_offset)
614+
.map(|(i, ty)| {
615+
let js_index = i - js_arg_offset;
616+
let var = syn::Ident::new(&format!("arg{js_index}"), method.sig.ident.span());
617+
let unwrap = quote! {
618+
let #var = match <#ty as jsg::FromJS>::from_js(&mut lock, args.get(#js_index)) {
619+
Ok(v) => v,
620+
Err(e) => {
621+
lock.throw_exception(&e);
622+
return;
623+
}
624+
};
625+
};
626+
(unwrap, quote! { #var })
627+
})
628+
.unzip();
629+
630+
let lock_arg = if has_lock_param {
631+
quote! { &mut lock, }
632+
} else {
633+
quote! {}
634+
};
635+
636+
quote! {
637+
jsg::Member::Constructor {
638+
callback: {
639+
unsafe extern "C" fn #callback_name(
640+
info: *mut jsg::v8::ffi::FunctionCallbackInfo,
641+
) {
642+
// SAFETY: info is a valid V8 FunctionCallbackInfo from the constructor call.
643+
let mut args = unsafe { jsg::v8::FunctionCallbackInfo::from_ffi(info) };
644+
let mut lock = unsafe { jsg::Lock::from_args(info) };
645+
646+
#(#unwraps)*
647+
648+
let resource = #self_ty::#rust_method_name(#lock_arg #(#arg_exprs),*);
649+
let rc = jsg::Rc::new(resource);
650+
rc.attach_to_this(&mut args);
651+
}
652+
#callback_name
653+
},
654+
}
655+
}
656+
})
657+
.next()
658+
}
659+
532660
/// Extracts named fields from a struct, returning an empty list for unit structs.
533661
/// Returns `Err` with a compile error for tuple structs or non-struct data.
534662
fn extract_named_fields(
@@ -634,6 +762,30 @@ pub fn jsg_static_constant(_attr: TokenStream, item: TokenStream) -> TokenStream
634762
item
635763
}
636764

765+
/// Marks a static method as the JavaScript constructor for a `#[jsg_resource]`.
766+
///
767+
/// The method must be a static function (no `self` receiver) that returns `Self`.
768+
/// When JavaScript calls `new MyResource(args)`, V8 invokes this method,
769+
/// wraps the returned resource, and attaches it to the `this` object.
770+
///
771+
/// ```ignore
772+
/// #[jsg_resource]
773+
/// impl MyResource {
774+
/// #[jsg_constructor]
775+
/// fn constructor(name: String) -> Self {
776+
/// Self { name }
777+
/// }
778+
/// }
779+
/// // JS: let obj = new MyResource("hello");
780+
/// ```
781+
///
782+
/// Only one `#[jsg_constructor]` is allowed per impl block.
783+
#[proc_macro_attribute]
784+
pub fn jsg_constructor(_attr: TokenStream, item: TokenStream) -> TokenStream {
785+
// Marker attribute — the actual registration is handled by #[jsg_resource] on the impl block.
786+
item
787+
}
788+
637789
/// Returns true if the type is `&mut Lock` or `&mut jsg::Lock`.
638790
///
639791
/// When a method's first typed parameter matches this pattern, the macro passes the

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

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ use std::rc::Rc;
1515
use jsg::ExceptionType;
1616
use jsg::Number;
1717
use jsg::ToJS;
18+
use jsg_macros::jsg_constructor;
1819
use jsg_macros::jsg_method;
1920
use jsg_macros::jsg_resource;
2021
use jsg_macros::jsg_static_constant;
@@ -413,3 +414,146 @@ fn static_constant_coexists_with_methods() {
413414
Ok(())
414415
});
415416
}
417+
418+
// =============================================================================
419+
// Constructor tests
420+
// =============================================================================
421+
422+
#[jsg_resource]
423+
struct Greeting {
424+
message: String,
425+
}
426+
427+
#[jsg_resource]
428+
impl Greeting {
429+
#[jsg_constructor]
430+
fn constructor(message: String) -> Self {
431+
Self { message }
432+
}
433+
434+
#[jsg_method]
435+
fn get_message(&self) -> String {
436+
self.message.clone()
437+
}
438+
}
439+
440+
/// Resources without `#[jsg_constructor]` should throw when called with `new`.
441+
#[test]
442+
fn resource_without_constructor_throws() {
443+
let harness = crate::Harness::new();
444+
harness.run_in_context(|lock, ctx| {
445+
let constructor = jsg::resource::function_template_of::<EchoResource>(lock);
446+
ctx.set_global("EchoResource", constructor.into());
447+
448+
let result: Result<Number, _> = ctx.eval(lock, "new EchoResource('hi')");
449+
assert!(result.is_err(), "should throw illegal constructor");
450+
Ok(())
451+
});
452+
}
453+
454+
/// A `#[jsg_constructor]` method is callable from JavaScript via `new`.
455+
#[test]
456+
fn constructor_creates_instance() {
457+
let harness = crate::Harness::new();
458+
harness.run_in_context(|lock, ctx| {
459+
let constructor = jsg::resource::function_template_of::<Greeting>(lock);
460+
ctx.set_global("Greeting", constructor.into());
461+
462+
let result: String = ctx
463+
.eval(lock, "new Greeting('hello').getMessage()")
464+
.unwrap();
465+
assert_eq!(result, "hello");
466+
Ok(())
467+
});
468+
}
469+
470+
/// Constructor arguments are converted from JS types via `FromJS`.
471+
#[test]
472+
fn constructor_converts_arguments() {
473+
let harness = crate::Harness::new();
474+
harness.run_in_context(|lock, ctx| {
475+
let constructor = jsg::resource::function_template_of::<Greeting>(lock);
476+
ctx.set_global("Greeting", constructor.into());
477+
478+
// Number is coerced to string by V8
479+
let result: String = ctx
480+
.eval(lock, "new Greeting(String(42)).getMessage()")
481+
.unwrap();
482+
assert_eq!(result, "42");
483+
Ok(())
484+
});
485+
}
486+
487+
/// Multiple `new` calls create distinct JS objects.
488+
#[test]
489+
fn constructor_creates_distinct_objects() {
490+
let harness = crate::Harness::new();
491+
harness.run_in_context(|lock, ctx| {
492+
let constructor = jsg::resource::function_template_of::<Greeting>(lock);
493+
ctx.set_global("Greeting", constructor.into());
494+
495+
let result: String = ctx
496+
.eval(
497+
lock,
498+
"let a = new Greeting('one'); let b = new Greeting('two'); \
499+
a.getMessage() + ',' + b.getMessage()",
500+
)
501+
.unwrap();
502+
assert_eq!(result, "one,two");
503+
Ok(())
504+
});
505+
}
506+
507+
/// `instanceof` works correctly for constructor-created instances.
508+
#[test]
509+
fn constructor_instanceof_works() {
510+
let harness = crate::Harness::new();
511+
harness.run_in_context(|lock, ctx| {
512+
let constructor = jsg::resource::function_template_of::<Greeting>(lock);
513+
ctx.set_global("Greeting", constructor.into());
514+
515+
let result: String = ctx
516+
.eval(
517+
lock,
518+
"let g = new Greeting('test'); \
519+
String(g instanceof Greeting)",
520+
)
521+
.unwrap();
522+
assert_eq!(result, "true");
523+
Ok(())
524+
});
525+
}
526+
527+
// Constructor with Lock parameter
528+
529+
#[jsg_resource]
530+
struct Counter {
531+
value: Number,
532+
}
533+
534+
#[jsg_resource]
535+
impl Counter {
536+
#[jsg_constructor]
537+
fn constructor(_lock: &mut jsg::Lock, value: Number) -> Self {
538+
Self { value }
539+
}
540+
541+
#[jsg_method]
542+
fn get_value(&self) -> Number {
543+
self.value
544+
}
545+
}
546+
547+
/// `#[jsg_constructor]` with a `Lock` parameter works.
548+
#[test]
549+
fn constructor_with_lock_parameter() {
550+
let harness = crate::Harness::new();
551+
harness.run_in_context(|lock, ctx| {
552+
let constructor = jsg::resource::function_template_of::<Counter>(lock);
553+
ctx.set_global("Counter", constructor.into());
554+
555+
let result: Number = ctx.eval(lock, "new Counter(99).getValue()").unwrap();
556+
assert!((result.value() - 99.0).abs() < f64::EPSILON);
557+
Ok(())
558+
});
559+
}

0 commit comments

Comments
 (0)