Files
moli/moli-webapi-declare/src/constructor.rs
T

189 lines
6.9 KiB
Rust

//! WebIDL argument conversion precedes the single Get(NewTarget, "prototype").
//! V8 allocates API constructor receivers before entering their callbacks. A
//! private NewTarget lets the native callback perform that Get at the specified
//! point, without guessing from V8's already selected Object.prototype fallback.
use moli_v8_util::{
callable_relevant_context, get_private_object, get_private_value, global_constructor_prototype,
new_null_prototype_object, private_key, throw_type_error, v8str,
};
const REFLECT_CONSTRUCT: &str = "moli.webidl.reflectConstruct";
const INVOCATION: &str = "moli.webidl.constructorInvocation";
const NEW_TARGET: &str = "moli.webidl.originalNewTarget";
const SELECTED_PROTOTYPE: &str = "moli.webidl.selectedConstructorPrototype";
/// Capture before author script runs, including before lazy interface access.
pub fn capture_web_api_constructor_intrinsics<'s>(scope: &mut v8::PinScope<'s, '_>) -> Option<()> {
let global = scope.get_current_context().global(scope);
if get_private_value(scope, global, REFLECT_CONSTRUCT).is_some() {
return Some(());
}
let reflect = global.get(scope, v8str(scope, "Reflect").into())?;
let reflect = v8::Local::<v8::Object>::try_from(reflect).ok()?;
let construct = reflect.get(scope, v8str(scope, "construct").into())?;
let construct = v8::Local::<v8::Function>::try_from(construct).ok()?;
let key = private_key(scope, REFLECT_CONSTRUCT)?;
global
.set_private(scope, key, construct.into())?
.then_some(())
}
/// Keep the native function as the intrinsic constructor; publish this entry
/// for JavaScript construction. Native callback data and instance templates are
/// preserved by invoking the original function with a private NewTarget.
pub fn web_api_constructor_with_deferred_prototype<'s>(
scope: &mut v8::PinScope<'s, '_>,
constructor: v8::Local<'s, v8::Function>,
) -> Option<v8::Local<'s, v8::Proxy>> {
let global = scope.get_current_context().global(scope);
let construct = get_private_value(scope, global, REFLECT_CONSTRUCT)?;
let trap = v8::Function::builder(construct_trap)
.data(construct)
.length(3)
.build(scope)?;
let handler = new_null_prototype_object(scope);
handler
.define_own_property(
scope,
v8str(scope, "construct").into(),
trap.into(),
v8::PropertyAttribute::NONE,
)?
.then_some(())?;
v8::Proxy::new(scope, constructor.into(), handler)
}
fn construct_trap<'s>(
scope: &mut v8::PinScope<'s, '_>,
args: v8::FunctionCallbackArguments<'s>,
mut rv: v8::ReturnValue<'s>,
) {
let construct =
v8::Local::<v8::Function>::try_from(args.data()).expect("captured Reflect.construct");
let Some(hidden_new_target) = v8::Function::new(scope, private_constructor) else {
return;
};
let prototype = new_null_prototype_object(scope);
if !hidden_new_target
.define_own_property(
scope,
v8str(scope, "prototype").into(),
prototype.into(),
v8::PropertyAttribute::READ_ONLY
| v8::PropertyAttribute::DONT_ENUM
| v8::PropertyAttribute::DONT_DELETE,
)
.unwrap_or(false)
{
return;
}
let invocation = new_null_prototype_object(scope);
let Some(new_target_key) = private_key(scope, NEW_TARGET) else {
return;
};
let Some(invocation_key) = private_key(scope, INVOCATION) else {
return;
};
if !invocation
.set_private(scope, new_target_key, args.get(2))
.unwrap_or(false)
|| !hidden_new_target
.set_private(scope, invocation_key, invocation.into())
.unwrap_or(false)
{
return;
}
if let Some(result) = construct.call(
scope,
v8::undefined(scope).into(),
&[args.get(0), args.get(1), hidden_new_target.into()],
) {
rv.set(result);
}
}
fn private_constructor(
_scope: &mut v8::PinScope<'_, '_>,
_args: v8::FunctionCallbackArguments<'_>,
_rv: v8::ReturnValue<'_>,
) {
}
pub(crate) fn restore_constructor_new_target<'s>(
scope: &mut v8::PinScope<'s, '_>,
args: v8::FunctionCallbackArguments<'s>,
) -> Option<(v8::FunctionCallbackArguments<'s>, bool)> {
if !args.is_construct_call() || args.new_target().is_proxy() {
return Some((args, false));
}
let Ok(new_target) = v8::Local::<v8::Object>::try_from(args.new_target()) else {
return Some((args, false));
};
let Some(invocation) = get_private_object(scope, new_target, INVOCATION) else {
return Some((args, false));
};
let original = get_private_value(scope, invocation, NEW_TARGET)?;
let key = private_key(scope, INVOCATION)?;
args.this()
.set_private(scope, key, invocation.into())?
.then_some((args.with_new_target(original), true))
}
pub(crate) fn clear_constructor_invocation(
scope: &mut v8::PinScope<'_, '_>,
receiver: v8::Local<'_, v8::Object>,
) {
if let Some(key) = private_key(scope, INVOCATION) {
let _ = receiver.delete_private(scope, key);
}
}
/// Call after overload resolution and all WebIDL argument conversion, before
/// constructor initialization. Internal construction through the cached native
/// function retains its V8 receiver and does not inspect author properties.
pub fn initialize_web_api_constructor_receiver<'s>(
scope: &mut v8::PinScope<'s, '_>,
receiver: v8::Local<'s, v8::Object>,
default_interface_name: &str,
) -> bool {
let Some(invocation) = get_private_object(scope, receiver, INVOCATION) else {
return true;
};
if get_private_value(scope, invocation, SELECTED_PROTOTYPE).is_some() {
return true;
}
let Some(new_target) = get_private_object(scope, invocation, NEW_TARGET) else {
return false;
};
let Some(prototype) = new_target.get(scope, v8str(scope, "prototype").into()) else {
return false;
};
let prototype = if let Ok(prototype) = v8::Local::<v8::Object>::try_from(prototype) {
prototype
} else {
let Some(context) = callable_relevant_context(scope, new_target.into()) else {
return false;
};
let prototype = {
let scope = &mut v8::ContextScope::new(scope, context);
let Some(prototype) = global_constructor_prototype(scope, default_interface_name)
else {
throw_type_error(scope, "WebIDL constructor prototype is unavailable");
return false;
};
v8::Global::new(scope, prototype)
};
v8::Local::new(scope, &prototype)
};
let Some(key) = private_key(scope, SELECTED_PROTOTYPE) else {
return false;
};
receiver
.set_prototype(scope, prototype.into())
.unwrap_or(false)
&& invocation
.set_private(scope, key, prototype.into())
.unwrap_or(false)
}