avm2: Allow resolving interfaces before ClassObjects are available
The Adobe Animate compiler can emit a 'newclass' opcode for a concrete class before the 'newclass' opcodes for the interfaces it implements. As a result, we cannot rely on looking up an interface `ClassObject` when resolving a class's interfaces. We now store a map of exported classes in `Domain`, and use this to lookup interfaces before their `ClassObject`s have been created. Additionally, `link_interfaces` was failing to consider superinterfaces, which meant that methods from superinterfaces were not being copied into the vtable. I've fixed this along with the other changes.
This commit is contained in:
parent
52e395ecca
commit
c258423dc3
|
@ -10,6 +10,8 @@ use crate::avm2::Multiname;
|
|||
use crate::avm2::QName;
|
||||
use gc_arena::{Collect, GcCell, MutationContext};
|
||||
|
||||
use super::class::Class;
|
||||
|
||||
/// Represents a set of scripts and movies that share traits across different
|
||||
/// script-global scopes.
|
||||
#[derive(Copy, Clone, Collect)]
|
||||
|
@ -22,6 +24,10 @@ struct DomainData<'gc> {
|
|||
/// A list of all exported definitions and the script that exported them.
|
||||
defs: PropertyMap<'gc, Script<'gc>>,
|
||||
|
||||
/// A map of all Clasess defined in this domain. Used by ClassObject
|
||||
/// to perform early interface resolution.
|
||||
classes: PropertyMap<'gc, GcCell<'gc, Class<'gc>>>,
|
||||
|
||||
/// The parent domain.
|
||||
parent: Option<Domain<'gc>>,
|
||||
|
||||
|
@ -48,6 +54,7 @@ impl<'gc> Domain<'gc> {
|
|||
mc,
|
||||
DomainData {
|
||||
defs: PropertyMap::new(),
|
||||
classes: PropertyMap::new(),
|
||||
parent: None,
|
||||
domain_memory: None,
|
||||
},
|
||||
|
@ -67,6 +74,7 @@ impl<'gc> Domain<'gc> {
|
|||
activation.context.gc_context,
|
||||
DomainData {
|
||||
defs: PropertyMap::new(),
|
||||
classes: PropertyMap::new(),
|
||||
parent: Some(parent),
|
||||
domain_memory: None,
|
||||
},
|
||||
|
@ -121,6 +129,22 @@ impl<'gc> Domain<'gc> {
|
|||
Ok(None)
|
||||
}
|
||||
|
||||
pub fn get_class(
|
||||
self,
|
||||
multiname: &Multiname<'gc>,
|
||||
) -> Result<Option<GcCell<'gc, Class<'gc>>>, Error<'gc>> {
|
||||
let read = self.0.read();
|
||||
if let Some(class) = read.classes.get_for_multiname(multiname).copied() {
|
||||
return Ok(Some(class));
|
||||
}
|
||||
|
||||
if let Some(parent) = read.parent {
|
||||
return parent.get_class(multiname);
|
||||
}
|
||||
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
/// Resolve a Multiname and return the script that provided it.
|
||||
///
|
||||
/// If a name does not exist or cannot be resolved, an error will be thrown.
|
||||
|
@ -179,6 +203,16 @@ impl<'gc> Domain<'gc> {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
pub fn export_class(
|
||||
&self,
|
||||
name: QName<'gc>,
|
||||
class: GcCell<'gc, Class<'gc>>,
|
||||
mc: MutationContext<'gc, '_>,
|
||||
) -> Result<(), Error<'gc>> {
|
||||
self.0.write(mc).classes.insert(name, class);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn domain_memory(&self) -> ByteArrayObject<'gc> {
|
||||
self.0
|
||||
.read()
|
||||
|
|
|
@ -16,6 +16,8 @@ use gc_arena::{Collect, GcCell, MutationContext};
|
|||
use std::sync::Arc;
|
||||
use swf::TagCode;
|
||||
|
||||
use super::traits::Trait;
|
||||
|
||||
mod array;
|
||||
mod boolean;
|
||||
mod class;
|
||||
|
@ -291,6 +293,8 @@ fn class<'gc>(
|
|||
activation.avm2().classes().class,
|
||||
);
|
||||
domain.export_definition(class_name, script, activation.context.gc_context)?;
|
||||
domain.export_class(class_name, class_def, activation.context.gc_context)?;
|
||||
script.install_trait_late(Trait::from_class(class_def), activation);
|
||||
|
||||
Ok(class_object)
|
||||
}
|
||||
|
|
|
@ -84,7 +84,7 @@ pub struct ClassObjectData<'gc> {
|
|||
applications: FnvHashMap<Option<ClassObject<'gc>>, ClassObject<'gc>>,
|
||||
|
||||
/// Interfaces implemented by this class.
|
||||
interfaces: Vec<ClassObject<'gc>>,
|
||||
interfaces: Vec<GcCell<'gc, Class<'gc>>>,
|
||||
|
||||
/// VTable used for instances of this class.
|
||||
instance_vtable: VTable<'gc>,
|
||||
|
@ -307,29 +307,7 @@ impl<'gc> ClassObject<'gc> {
|
|||
let interface_names = class.read().interfaces().to_vec();
|
||||
let mut interfaces = Vec::with_capacity(interface_names.len());
|
||||
for interface_name in interface_names {
|
||||
let interface = scope.resolve(&interface_name, activation)?;
|
||||
|
||||
if interface.is_none() {
|
||||
return Err(format!("Could not resolve interface {interface_name:?}").into());
|
||||
}
|
||||
|
||||
let iface_class = interface
|
||||
.unwrap()
|
||||
.as_object()
|
||||
.and_then(|o| o.as_class_object())
|
||||
.ok_or_else(|| Error::from("Object is not an interface"))?;
|
||||
if !iface_class.inner_class_definition().read().is_interface() {
|
||||
return Err(format!(
|
||||
"Class {:?} is not an interface and cannot be implemented by classes",
|
||||
iface_class
|
||||
.inner_class_definition()
|
||||
.read()
|
||||
.name()
|
||||
.local_name()
|
||||
)
|
||||
.into());
|
||||
}
|
||||
interfaces.push(iface_class);
|
||||
interfaces.push(self.early_resolve_interface(scope, &interface_name)?);
|
||||
}
|
||||
|
||||
if !interfaces.is_empty() {
|
||||
|
@ -343,10 +321,9 @@ impl<'gc> ClassObject<'gc> {
|
|||
let mut class = Some(self);
|
||||
|
||||
while let Some(cls) = class {
|
||||
for interface in cls.interfaces() {
|
||||
let iface_static_class = interface.inner_class_definition();
|
||||
let iface_read = iface_static_class.read();
|
||||
|
||||
let mut interfaces = cls.interfaces();
|
||||
while let Some(interface) = interfaces.pop() {
|
||||
let iface_read = interface.read();
|
||||
for interface_trait in iface_read.instance_traits() {
|
||||
if !interface_trait.name().namespace().is_public() {
|
||||
let public_name = QName::new(
|
||||
|
@ -360,6 +337,12 @@ impl<'gc> ClassObject<'gc> {
|
|||
);
|
||||
}
|
||||
}
|
||||
let super_interfaces: Result<Vec<_>, _> = iface_read
|
||||
.interfaces()
|
||||
.iter()
|
||||
.map(|super_iface| self.early_resolve_interface(scope, super_iface))
|
||||
.collect();
|
||||
interfaces.extend(super_interfaces?);
|
||||
}
|
||||
|
||||
class = cls.superclass_object();
|
||||
|
@ -368,6 +351,30 @@ impl<'gc> ClassObject<'gc> {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
// Looks up an interface by name, without using `ScopeChain.resolve`
|
||||
// This lets us look up an interface before its `ClassObject` has been constructed,
|
||||
// which is needed to resolve interfaces when constructing a (different) `ClassObject`.
|
||||
fn early_resolve_interface(
|
||||
&self,
|
||||
scope: ScopeChain<'gc>,
|
||||
interface_name: &Multiname<'gc>,
|
||||
) -> Result<GcCell<'gc, Class<'gc>>, Error<'gc>> {
|
||||
let interface_class = scope.domain().get_class(interface_name)?;
|
||||
|
||||
let Some(interface_class) = interface_class else {
|
||||
return Err(format!("Could not resolve interface {interface_name:?}").into());
|
||||
};
|
||||
|
||||
if !interface_class.read().is_interface() {
|
||||
return Err(format!(
|
||||
"Class {:?} is not an interface and cannot be implemented by classes",
|
||||
interface_class.read().name().local_name()
|
||||
)
|
||||
.into());
|
||||
}
|
||||
Ok(interface_class)
|
||||
}
|
||||
|
||||
/// Manually set the type of this `Class`.
|
||||
///
|
||||
/// This is intended to support initialization of early types such as
|
||||
|
@ -434,7 +441,7 @@ impl<'gc> ClassObject<'gc> {
|
|||
}
|
||||
|
||||
for interface in class.interfaces() {
|
||||
if Object::ptr_eq(interface, test_class) {
|
||||
if GcCell::ptr_eq(interface, test_class.inner_class_definition()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
@ -701,7 +708,7 @@ impl<'gc> ClassObject<'gc> {
|
|||
self.0.read().prototype.unwrap()
|
||||
}
|
||||
|
||||
pub fn interfaces(self) -> Vec<ClassObject<'gc>> {
|
||||
pub fn interfaces(self) -> Vec<GcCell<'gc, Class<'gc>>> {
|
||||
self.0.read().interfaces.clone()
|
||||
}
|
||||
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
//! Whole script representation
|
||||
|
||||
use super::traits::TraitKind;
|
||||
use crate::avm2::activation::Activation;
|
||||
use crate::avm2::class::Class;
|
||||
use crate::avm2::domain::Domain;
|
||||
|
@ -466,12 +467,31 @@ impl<'gc> Script<'gc> {
|
|||
*self,
|
||||
activation.context.gc_context,
|
||||
)?;
|
||||
if let TraitKind::Class { class, .. } = newtrait.kind() {
|
||||
write.domain.export_class(
|
||||
newtrait.name(),
|
||||
*class,
|
||||
activation.context.gc_context,
|
||||
)?;
|
||||
}
|
||||
|
||||
write.traits.push(newtrait);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Install a trait on this `Script` object
|
||||
/// This should only ever be called on the `global` script, during Rust-side initialization.
|
||||
pub fn install_trait_late(
|
||||
&self,
|
||||
loaded_trait: Trait<'gc>,
|
||||
activation: &mut Activation<'_, 'gc>,
|
||||
) {
|
||||
let mut write = self.0.write(activation.context.gc_context);
|
||||
write.traits.push(loaded_trait);
|
||||
}
|
||||
|
||||
/// Return the entrypoint for the script and the scope it should run in.
|
||||
pub fn init(self) -> (Method<'gc>, Object<'gc>, Domain<'gc>) {
|
||||
let read = self.0.read();
|
||||
|
|
|
@ -0,0 +1,54 @@
|
|||
package {
|
||||
public class Test {
|
||||
public static function test() {
|
||||
trace("Calling directly");
|
||||
var concrete = new Concrete();
|
||||
concrete.base_interface();
|
||||
concrete.parent_one();
|
||||
concrete.parent_two();
|
||||
concrete.grandparent_one();
|
||||
concrete.grandparent_two();
|
||||
|
||||
trace();
|
||||
trace("Calling through string lookup");
|
||||
concrete["base_interface"]();
|
||||
concrete["parent_one"]();
|
||||
concrete["parent_two"]();
|
||||
concrete["grandparent_one"]();
|
||||
concrete["grandparent_two"]();
|
||||
|
||||
trace();
|
||||
trace("Calling through interface");
|
||||
var launder: BaseInterface = concrete;
|
||||
launder.base_interface();
|
||||
launder.parent_one();
|
||||
launder.parent_two();
|
||||
launder.grandparent_one();
|
||||
launder.grandparent_two();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class Concrete implements BaseInterface {
|
||||
public function base_interface() { trace("BaseInterface method") }
|
||||
public function parent_one() { trace("ParentOne method"); }
|
||||
public function parent_two() { trace("ParentTwo method"); }
|
||||
public function grandparent_one() { trace("GrandParentOne method"); }
|
||||
public function grandparent_two() { trace("GrandParentTwo method"); }
|
||||
}
|
||||
|
||||
interface BaseInterface extends ParentOne, ParentTwo {
|
||||
function base_interface();
|
||||
}
|
||||
interface ParentOne extends GrandParentOne {
|
||||
function parent_one();
|
||||
}
|
||||
interface ParentTwo extends GrandParentOne, GrandParentTwo {
|
||||
function parent_two();
|
||||
}
|
||||
interface GrandParentOne {
|
||||
function grandparent_one();
|
||||
}
|
||||
interface GrandParentTwo {
|
||||
function grandparent_two();
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
Calling directly
|
||||
BaseInterface method
|
||||
ParentOne method
|
||||
ParentTwo method
|
||||
GrandParentOne method
|
||||
GrandParentTwo method
|
||||
|
||||
Calling through string lookup
|
||||
BaseInterface method
|
||||
ParentOne method
|
||||
ParentTwo method
|
||||
GrandParentOne method
|
||||
GrandParentTwo method
|
||||
|
||||
Calling through interface
|
||||
BaseInterface method
|
||||
ParentOne method
|
||||
ParentTwo method
|
||||
GrandParentOne method
|
||||
GrandParentTwo method
|
Binary file not shown.
Binary file not shown.
|
@ -0,0 +1 @@
|
|||
num_frames = 1
|
Loading…
Reference in New Issue