From 1b5869e15a715f8753a6ecefa879ae542a99ac82 Mon Sep 17 00:00:00 2001 From: David Wendt Date: Sat, 23 Oct 2021 18:11:54 -0400 Subject: [PATCH] avm2: Preserve ability to `callmethod` on slot IDs that have not yet been bound, by binding them at that time. --- core/src/avm2/activation.rs | 7 +- core/src/avm2/class.rs | 11 +++ core/src/avm2/object.rs | 53 ++++++++++++-- core/src/avm2/object/class_object.rs | 103 +++++++++++++++++++++++++++ 4 files changed, 165 insertions(+), 9 deletions(-) diff --git a/core/src/avm2/activation.rs b/core/src/avm2/activation.rs index fe8ab36eb..e4ccbf099 100644 --- a/core/src/avm2/activation.rs +++ b/core/src/avm2/activation.rs @@ -1126,11 +1126,8 @@ impl<'a, 'gc, 'gc_context> Activation<'a, 'gc, 'gc_context> { ) -> Result, Error> { let args = self.context.avm2.pop_args(arg_count); let receiver = self.context.avm2.pop().coerce_to_object(self)?; - let function: Result, 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); diff --git a/core/src/avm2/class.rs b/core/src/avm2/class.rs index 77665a9f0..8407c015c 100644 --- a/core/src/avm2/class.rs +++ b/core/src/avm2/class.rs @@ -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[..] diff --git a/core/src/avm2/object.rs b/core/src/avm2/object.rs index 21e623cb4..6a46cf448 100644 --- a/core/src/avm2/object.rs +++ b/core/src/avm2/object.rs @@ -487,11 +487,56 @@ pub trait TObject<'gc>: 'gc + Collect + Debug + Into> + Clone + Copy base.init_slot(id, value, mc) } - /// Retrieve a method by its index. - fn get_method(self, id: u32) -> Option> { - 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, 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 diff --git a/core/src/avm2/object/class_object.rs b/core/src/avm2/object/class_object.rs index e8cb6b06a..c69d27f61 100644 --- a/core/src/avm2/object/class_object.rs +++ b/core/src/avm2/object/class_object.rs @@ -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>, 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, 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, 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 }