avm2: Preserve ability to `callmethod` on slot IDs that have not yet been bound, by binding them at that time.

This commit is contained in:
David Wendt 2021-10-23 18:11:54 -04:00 committed by Mike Welsh
parent d0d19bcf38
commit 1b5869e15a
4 changed files with 165 additions and 9 deletions

View File

@ -1126,11 +1126,8 @@ impl<'a, 'gc, 'gc_context> Activation<'a, 'gc, 'gc_context> {
) -> Result<FrameControl<'gc>, Error> {
let args = self.context.avm2.pop_args(arg_count);
let receiver = self.context.avm2.pop().coerce_to_object(self)?;
let function: Result<Object<'gc>, Error> = receiver
.get_method(index.0)
.ok_or_else(|| format!("Object method {} does not exist", index.0).into());
let superclass_object = receiver.instance_of();
let value = function?.call(Some(receiver), &args, self, superclass_object)?;
let value = receiver.call_method(index.0, &args, self)?;
self.context.avm2.push(value);

View File

@ -765,6 +765,17 @@ impl<'gc> Class<'gc> {
false
}
/// Determines if this class provides a given trait on its instances.
pub fn has_instance_trait_by_id(&self, id: u32) -> bool {
for trait_entry in self.instance_traits.iter() {
if id == trait_entry.slot_id() {
return true;
}
}
false
}
/// Return instance traits provided by this class.
pub fn instance_traits(&self) -> &[Trait<'gc>] {
&self.instance_traits[..]

View File

@ -487,11 +487,56 @@ pub trait TObject<'gc>: 'gc + Collect + Debug + Into<Object<'gc>> + Clone + Copy
base.init_slot(id, value, mc)
}
/// Retrieve a method by its index.
fn get_method(self, id: u32) -> Option<Object<'gc>> {
let base = self.base();
/// Call a method by its index.
///
/// This directly corresponds with the AVM2 operation `callmethod`.
#[allow(unused_mut)] //Not unused.
fn call_method(
mut self,
id: u32,
arguments: &[Value<'gc>],
activation: &mut Activation<'_, 'gc, '_>,
) -> Result<Value<'gc>, Error> {
if self.base().get_method(id).is_none() {
if let Some(class) = self.instance_of() {
if let Some((bound_method, name, is_final)) =
class.bound_instance_method_by_id(activation, self.into(), id)?
{
self.install_method(
activation.context.gc_context,
name.clone(),
id,
bound_method,
is_final,
);
}
}
base.get_method(id)
if let Some(class) = self.as_class_object() {
if let Some((bound_method, name, is_final)) =
class.bound_class_method_by_id(activation, id)?
{
self.install_method(
activation.context.gc_context,
name.clone(),
id,
bound_method,
is_final,
);
}
}
}
if let Some(method_object) = self.base().get_method(id) {
return method_object.call(
Some(self.into()),
arguments,
activation,
self.instance_of(),
);
}
Err(format!("Cannot call unknown method id {}", id).into())
}
/// Resolve a multiname into a single QName, if any of the namespaces

View File

@ -365,6 +365,27 @@ impl<'gc> ClassObject<'gc> {
Ok(None)
}
/// Retrieve the class object that a particular QName trait is defined in.
///
/// Must be called on a class object; will error out if called on
/// anything else.
///
/// This function returns `None` for non-trait properties, such as actually
/// defined prototype methods for ES3-style classes.
pub fn find_class_for_trait_by_id(self, id: u32) -> Result<Option<ClassObject<'gc>>, Error> {
let class_definition = self.inner_class_definition();
if class_definition.read().has_instance_trait_by_id(id) {
return Ok(Some(self));
}
if let Some(base) = self.superclass_object() {
return base.find_class_for_trait_by_id(id);
}
Ok(None)
}
/// Determine if this class has a given type in its superclass chain.
///
/// The given object `test_class` should be either a superclass or
@ -741,6 +762,51 @@ impl<'gc> ClassObject<'gc> {
}
}
/// Retrieve a bound instance method by slot ID.
///
/// This returns the bound method object itself, as well as it's name,
/// and if it's a final method. You will need the additional properties in
/// order to install the method into your object.
///
/// You should only call this method once per reciever/name pair, and cache
/// the result. Otherwise, code that relies on bound methods having stable
/// object identitities (e.g. `EventDispatcher.removeEventListener`) will
/// fail.
pub fn bound_instance_method_by_id(
self,
activation: &mut Activation<'_, 'gc, '_>,
receiver: Object<'gc>,
id: u32,
) -> Result<Option<(Object<'gc>, QName<'gc>, bool)>, Error> {
if let Some(superclass) = self.find_class_for_trait_by_id(id)? {
let superclassdef = superclass.inner_class_definition();
let traits = superclassdef.read().lookup_instance_traits_by_slot(id)?;
if let Some((_superclass, method_trait)) = traits
.and_then(|t| match t.kind() {
TraitKind::Method { .. } => Some(t),
_ => None,
})
.map(|t| (superclass, t))
{
let name = method_trait.name().clone();
let method = method_trait.as_method().unwrap();
let is_final = method_trait.is_final();
let scope = self.instance_scope();
Ok(Some((
FunctionObject::from_method(activation, method, scope, Some(receiver)),
name,
is_final,
)))
} else {
Ok(None)
}
} else {
Ok(None)
}
}
/// Retrieve a class method by name.
///
/// This does not return a defining class as class methods are not
@ -789,6 +855,43 @@ impl<'gc> ClassObject<'gc> {
}
}
/// Retrieve a bound class method by id.
///
/// This returns the bound method object itself, as well as it's name,
/// and if it's a final method. You will need the additional properties in
/// order to install the method into your object.
///
/// You should only call this method once per reciever/name pair, and cache
/// the result. Otherwise, code that relies on bound methods having stable
/// object identitities (e.g. `EventDispatcher.removeEventListener`) will
/// fail.
pub fn bound_class_method_by_id(
self,
activation: &mut Activation<'_, 'gc, '_>,
id: u32,
) -> Result<Option<(Object<'gc>, QName<'gc>, bool)>, Error> {
let classdef = self.inner_class_definition();
let traits = classdef.read().lookup_class_traits_by_slot(id)?;
if let Some(method_trait) = traits.and_then(|t| match t.kind() {
TraitKind::Method { .. } => Some(t),
_ => None,
}) {
let method = method_trait.as_method().unwrap();
let name = method_trait.name().clone();
let is_final = method_trait.is_final();
let scope = self.class_scope();
Ok(Some((
FunctionObject::from_method(activation, method, scope, Some(self.into())),
name,
is_final,
)))
} else {
Ok(None)
}
}
pub fn inner_class_definition(self) -> GcCell<'gc, Class<'gc>> {
self.0.read().class
}