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:
Aaron Hill 2023-02-20 23:49:07 -06:00
parent 52e395ecca
commit c258423dc3
9 changed files with 170 additions and 30 deletions

View File

@ -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()

View File

@ -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)
}

View File

@ -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()
}

View File

@ -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();

View File

@ -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();
}

View File

@ -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.

View File

@ -0,0 +1 @@
num_frames = 1