mirror of
https://github.com/lexmount/moli.git
synced 2026-10-30 16:07:24 +00:00
perf(webidl): install template methods from static metadata
Batch consecutive statically named template methods without callback data through constant metadata and a shared installer. Resolve native callback mappings during installation, preserving aliases, ordering, descriptors and the original JavaScript callback path. Keep generic callback data, nested entries, return values and exception identity covered by the binding regression tests. This step passes focused WebIDL clippy and 98 binding/derive tests. Validation on the unchanged final source tree: cargo fmt --all; cargo clippy --workspace --all-targets --all-features -- -D warnings; cargo nextest run --no-fail-fast (20,505 passed, 18 existing skips).
This commit is contained in:
@@ -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<proc_macro2::TokenStream>,
|
||||
}
|
||||
|
||||
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<proc_macro2::TokenStream>,
|
||||
}
|
||||
|
||||
fn flush_template_methods(
|
||||
output: &mut Vec<proc_macro2::TokenStream>,
|
||||
methods: &mut Vec<proc_macro2::TokenStream>,
|
||||
) {
|
||||
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<Option<proc_macro2::TokenStream>, 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<FunctionTemplateFieldExpansions, Error> {
|
||||
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,
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<T: CallbackMarker> {
|
||||
#[webapi(method, callback = Self::call, data = 3u32)]
|
||||
call: (),
|
||||
#[webapi(method, callback = Self::call)]
|
||||
no_data: (),
|
||||
marker: std::marker::PhantomData<T>,
|
||||
}
|
||||
|
||||
impl<T: CallbackMarker> GenericCallbacks<T> {
|
||||
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::<v8::Function>::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::<T>::call, data = self.data)]
|
||||
call: (),
|
||||
data: v8::Local<'s, v8::Value>,
|
||||
marker: std::marker::PhantomData<T>,
|
||||
}
|
||||
|
||||
#[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::<GenericCallbacks<Seven>>(scope, global);
|
||||
let data = v8::Integer::new_from_unsigned(scope, 5).into();
|
||||
let object = GenericCallbackObject::<Seven>::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::<MethodOrder>(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)
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user