diff --git a/moli-webapi-declare-derive/src/expand.rs b/moli-webapi-declare-derive/src/expand.rs index cb4e7bb468..c32a6dfa97 100644 --- a/moli-webapi-declare-derive/src/expand.rs +++ b/moli-webapi-declare-derive/src/expand.rs @@ -10,6 +10,27 @@ use crate::attrs::{ parse_function_template_attrs, parse_object_attrs, }; +/// Static metadata stores mappings so V8's own callback adapter can be resolved +/// at installation time. Associated functions can reference Self and generics. +#[derive(Default)] +struct CallbackMappings { + helpers: Vec, +} + +impl CallbackMappings { + fn map(&mut self, callback: proc_macro2::TokenStream) -> proc_macro2::TokenStream { + let name = format_ident!("__webapi_callback_mapping_{}", self.helpers.len()); + self.helpers.push(quote! { + fn #name() -> ::moli_webapi_declare::v8::FunctionCallback { + ::moli_webapi_declare::v8::MapFnTo::< + ::moli_webapi_declare::v8::FunctionCallback, + >::map_fn_to(#callback) + } + }); + quote!(Self::#name) + } +} + struct DeclaredField { field: Field, attrs: FieldAttrs, @@ -153,7 +174,10 @@ pub(crate) fn expand_webapi_function_template( None } }); - let template_fields = expand_function_template_fields(&fields, &template_name)?; + let mut callback_mappings = CallbackMappings::default(); + let template_fields = + expand_function_template_fields(&fields, &template_name, &mut callback_mappings)?; + let callback_mappings = callback_mappings.helpers; let FunctionTemplateFieldExpansions { template_methods, prototype_methods, @@ -161,6 +185,8 @@ pub(crate) fn expand_webapi_function_template( Ok(quote! { impl #impl_generics #struct_name #ty_generics #where_clause { + #(#callback_mappings)* + fn __webapi_mark_declaration_fields_used(&self) { #(#declaration_field_reads)* } @@ -469,14 +495,83 @@ struct FunctionTemplateFieldExpansions { prototype_methods: Vec, } +fn flush_template_methods( + output: &mut Vec, + methods: &mut Vec, +) { + if !methods.is_empty() { + let methods = std::mem::take(methods); + output.push(quote! { + ::moli_webapi_declare::__private::install_function_template_methods( + scope, prototype, const { &[#(#methods),*] }, + ); + }); + } +} + +fn static_template_method( + field: &Field, + attrs: &FieldAttrs, + callback_mappings: &mut CallbackMappings, +) -> Result, Error> { + if !matches!(attrs.kind, FieldKind::Method) + || attrs.data.is_some() + || attrs.symbol.is_some() + || attrs.function_name.is_some() + { + return Ok(None); + } + let Some(name) = webapi_field_name_literal(attrs.name.as_ref()) else { + return Ok(None); + }; + let callback = attrs.callback.as_ref().ok_or_else(|| { + Error::new( + field.span(), + "method field requires #[webapi(callback = path)]", + ) + })?; + let callback = callback_mappings.map(expand_callback(callback, attrs, false)); + let length = attrs.length.unwrap_or(0); + let enumerable = attrs.enumerable; + let readonly = attrs.readonly; + let dont_delete = attrs.dont_delete; + let receiver_access_check = attrs + .receiver_access_check + .unwrap_or_default() + .checks_getter(); + Ok(Some(quote! { + ::moli_webapi_declare::__private::FunctionTemplateMethod { + name: #name, + callback: #callback, + length: #length, + enumerable: #enumerable, + readonly: #readonly, + dont_delete: #dont_delete, + receiver_access_check: #receiver_access_check, + } + })) +} + fn expand_function_template_fields( fields: &[DeclaredField], template_name: &proc_macro2::TokenStream, + callback_mappings: &mut CallbackMappings, ) -> Result { let mut template_methods = Vec::new(); let mut prototype_methods = Vec::new(); let mut method_bindings = HashMap::new(); + // Aliases reference the actual templates bound by earlier declarations. + let can_batch = !fields + .iter() + .any(|field| matches!(field.attrs.kind, FieldKind::Alias(_))); + let mut methods = Vec::new(); for (index, DeclaredField { field, attrs }) in fields.iter().enumerate() { + if can_batch && let Some(method) = static_template_method(field, attrs, callback_mappings)? + { + methods.push(method); + continue; + } + flush_template_methods(&mut prototype_methods, &mut methods); if matches!(attrs.kind, FieldKind::Constant) { template_methods.push(expand_function_template_constant_field( field, @@ -550,6 +645,7 @@ fn expand_function_template_fields( )); } } + flush_template_methods(&mut prototype_methods, &mut methods); Ok(FunctionTemplateFieldExpansions { template_methods, prototype_methods, diff --git a/moli-webapi-declare/src/__private.rs b/moli-webapi-declare/src/__private.rs index 7380985ca8..aeb82e4c17 100644 --- a/moli-webapi-declare/src/__private.rs +++ b/moli-webapi-declare/src/__private.rs @@ -6,9 +6,8 @@ pub use moli_v8_util::{global_constructor_prototype, v8_string, v8str}; /// Complete description of one function installed by /// `WebApiFunctionTemplate`. /// -/// The derive maps each Rust callback to V8's raw callback type at the call -/// site. Keeping that raw pointer in this short-lived value lets the shared -/// installers avoid monomorphization without adding a callback trampoline or +/// The derive maps each Rust callback using V8's native adapter at installation +/// time. The shared installers need neither a call-time trampoline nor extra /// persistent callback data. #[doc(hidden)] #[derive(Clone, Copy)] @@ -120,3 +119,52 @@ pub fn build_object_function<'s>( None => builder.build(scope), } } + +/// Static metadata for a string-named template method without callback data. +/// The callback mapping runs only during installation, not JavaScript calls. +#[doc(hidden)] +pub struct FunctionTemplateMethod { + pub name: &'static str, + pub callback: fn() -> v8::FunctionCallback, + pub length: i32, + pub enumerable: bool, + pub readonly: bool, + pub dont_delete: bool, + pub receiver_access_check: bool, +} + +/// Installs a run of methods from metadata instead of repeating call-site code. +#[doc(hidden)] +#[inline(never)] +pub fn install_function_template_methods<'s>( + scope: &v8::PinScope<'s, '_, ()>, + target: v8::Local<'s, v8::ObjectTemplate>, + methods: &[FunctionTemplateMethod], +) { + for method in methods { + let name = v8str(scope, method.name); + let mut attributes = v8::PropertyAttribute::NONE; + if !method.enumerable { + attributes = attributes | v8::PropertyAttribute::DONT_ENUM; + } + if method.readonly { + attributes = attributes | v8::PropertyAttribute::READ_ONLY; + } + if method.dont_delete { + attributes = attributes | v8::PropertyAttribute::DONT_DELETE; + } + install_function_template_method( + scope, + target, + name.into(), + FunctionTemplateMember { + callback: (method.callback)(), + length: method.length, + data: None, + class_name: Some(name), + receiver_access_check: method.receiver_access_check, + }, + attributes, + ); + } +} diff --git a/moli-webapi-declare/tests/declare.rs b/moli-webapi-declare/tests/declare.rs index 240aeadbe4..23a0f607fd 100644 --- a/moli-webapi-declare/tests/declare.rs +++ b/moli-webapi-declare/tests/declare.rs @@ -3146,3 +3146,134 @@ fn null_prototype_declarations_ignore_object_prototype_pollution() { .is_true() ); } +trait CallbackMarker { + const VALUE: u32; +} + +struct Seven; +impl CallbackMarker for Seven { + const VALUE: u32 = 7; +} + +#[derive(WebApiFunctionTemplate)] +#[webapi(name = "GenericCallbacks")] +struct GenericCallbacks { + #[webapi(method, callback = Self::call, data = 3u32)] + call: (), + #[webapi(method, callback = Self::call)] + no_data: (), + marker: std::marker::PhantomData, +} + +impl GenericCallbacks { + fn call<'s>( + scope: &mut v8::PinScope<'s, '_>, + args: v8::FunctionCallbackArguments<'s>, + mut rv: v8::ReturnValue<'s>, + ) { + let value = if let Ok(callback) = v8::Local::::try_from(args.get(0)) { + let Some(value) = callback.call(scope, args.this().into(), &[args.data()]) else { + return; + }; + value.uint32_value(scope).unwrap() + } else { + args.data().uint32_value(scope).unwrap() + }; + rv.set_uint32(T::VALUE + value); + } +} + +#[derive(WebApiObject)] +#[webapi(plain)] +struct GenericCallbackObject<'s, T: CallbackMarker> { + #[webapi(method, callback = GenericCallbacks::::call, data = self.data)] + call: (), + data: v8::Local<'s, v8::Value>, + marker: std::marker::PhantomData, +} + +#[test] +fn generic_callbacks_keep_data_and_return_values_across_nested_v8_entries() { + ensure_v8(); + let mut isolate = v8::Isolate::new(Default::default()); + let scope = pin!(v8::HandleScope::new(&mut isolate)); + let scope = &mut scope.init(); + let context = v8::Context::new(scope, Default::default()); + let scope = &mut v8::ContextScope::new(scope, context); + let global = context.global(scope); + install_template::>(scope, global); + let data = v8::Integer::new_from_unsigned(scope, 5).into(); + let object = GenericCallbackObject::::new(data, std::marker::PhantomData) + .bind(scope) + .unwrap(); + global + .set( + scope, + v8::String::new(scope, "object").unwrap().into(), + object.into(), + ) + .unwrap(); + assert!( + run_script( + scope, + r#" + (() => { + const template = GenericCallbacks.prototype; + if (template.call() !== 10 || object.call() !== 12 || template.noData() !== 7) return false; + if (template.call(n => object.call(m => n + m)) !== 22) return false; + const error = {}; + try { object.call(() => { throw error; }); return false; } + catch (caught) { if (caught !== error) return false; } + return template.call() === 10 && object.call() === 12; + })() + "# + ) + .boolean_value(scope) + ); +} +#[derive(WebApiFunctionTemplate)] +#[webapi(name = "MethodOrder", enumerable)] +struct MethodOrder { + #[webapi(method, callback = sample_now_callback, readonly)] + first: (), + #[webapi(method, callback = sample_now_callback, length = 2, dont_delete)] + second: (), + #[webapi(accessor_property, getter = sample_now_callback)] + middle: (), + #[webapi(method, callback = sample_data_callback, data = "payload")] + data: (), + #[webapi(method, callback = sample_now_callback)] + last: (), +} + +#[test] +fn template_method_runs_preserve_order_descriptors_and_callback_data() { + ensure_v8(); + let mut isolate = v8::Isolate::new(Default::default()); + let scope = pin!(v8::HandleScope::new(&mut isolate)); + let scope = &mut scope.init(); + let context = v8::Context::new(scope, Default::default()); + let scope = &mut v8::ContextScope::new(scope, context); + let global = context.global(scope); + install_template::(scope, global); + assert!( + run_script( + scope, + r#" + (() => { + const p = MethodOrder.prototype; + const names = Object.keys(p); + if (names.join(',') !== 'first,second,middle,data,last') return false; + const first = Object.getOwnPropertyDescriptor(p, 'first'); + const second = Object.getOwnPropertyDescriptor(p, 'second'); + return !first.writable && first.configurable && first.enumerable + && second.writable && !second.configurable && second.enumerable + && p.first.name === 'first' && p.first.length === 0 + && p.second.name === 'second' && p.second.length === 2 + && p.middle === 7 && p.data() === 'payload' && p.last() === 7; + })() + "# + ) + .boolean_value(scope) + ); +}