avm2: Implement describeType

This includes all of the XML elements described in 'describeType' docs.
Unfortunately, the order of elements produced by Flash depends on
the iteration order of internal hashtables. As a result, the test
manually stringifies an XML object, sorting the stringified children,
to produce consistent output between Flash and Ruffle.
This commit is contained in:
Aaron Hill 2023-03-02 12:25:40 -06:00
parent 5944bae33b
commit a07ff36726
14 changed files with 477 additions and 45 deletions

View File

@ -57,6 +57,15 @@ pub fn create_class<'gc>(activation: &mut Activation<'_, 'gc>) -> GcCell<'gc, Cl
let mut write = class_class.write(gc_context);
// 'length' is a weird undocumented constant in Class.
// We need to define it, since it shows up in 'describeType'
const CLASS_CONSTANTS: &[(&str, i32)] = &[("length", 1)];
write.define_constant_int_class_traits(
activation.avm2().public_namespace,
CLASS_CONSTANTS,
activation,
);
const PUBLIC_INSTANCE_PROPERTIES: &[(
&str,
Option<NativeMethodImpl>,

View File

@ -1,9 +1,10 @@
//! `flash.utils` namespace
use crate::avm2::method::Method;
use crate::avm2::object::TObject;
use crate::avm2::QName;
use crate::avm2::property::Property;
use crate::avm2::{Activation, Error, Object, Value};
use crate::avm2_stub_method;
use crate::avm2::{ClassObject, QName};
use crate::string::AvmString;
use crate::string::WString;
use instant::Instant;
@ -268,15 +269,54 @@ pub fn describe_type<'gc>(
_this: Option<Object<'gc>>,
args: &[Value<'gc>],
) -> Result<Value<'gc>, Error<'gc>> {
// This method is very incomplete, and should be fully implemented
// once we have a better way of constructing XML from the Rust side
avm2_stub_method!(activation, "flash.utils", "describeType");
let value = args[0].coerce_to_object(activation)?;
let class_obj = value.as_class_object().or_else(|| value.instance_of());
let Some(class_obj) = class_obj else {
return Ok(activation
.avm2()
.classes()
.xml
.construct(activation, &[])?
.into())
};
let mut xml_string = String::new();
let qualified_name =
get_qualified_class_name(activation, None, &[args[0]])?.coerce_to_string(activation)?;
xml_string += &format!("<type name=\"{qualified_name}\"></type>");
let is_static = value.as_class_object().is_some();
let class = class_obj.inner_class_definition();
let class = class.read();
let qualified_name = class
.name()
.to_qualified_name(activation.context.gc_context);
// If we're describing a Class object, then the "superclass" the the Class class
let superclass = if is_static {
Some(activation.avm2().classes().class)
} else {
class_obj.superclass_object()
};
let base_attr = if let Some(superclass) = superclass {
format!(
" base=\"{}\"",
superclass
.inner_class_definition()
.read()
.name()
.to_qualified_name(activation.context.gc_context)
)
} else {
String::new()
};
let is_dynamic = is_static || !class.is_sealed();
let is_final = is_static || class.is_final();
write!(xml_string, "<type name=\"{qualified_name}\"{base_attr} isDynamic=\"{is_dynamic}\" isFinal=\"{is_final}\" isStatic=\"{is_static}\">").unwrap();
xml_string += &describe_internal_body(activation, class_obj, is_static)?;
xml_string += "</type>";
let xml_avm_string = AvmString::new_utf8(activation.context.gc_context, xml_string);
Ok(activation
@ -286,3 +326,174 @@ pub fn describe_type<'gc>(
.construct(activation, &[xml_avm_string.into()])?
.into())
}
fn describe_internal_body<'gc>(
activation: &mut Activation<'_, 'gc>,
class_obj: ClassObject<'gc>,
is_static: bool,
) -> Result<String, Error<'gc>> {
let mut xml_string = String::new();
let class = class_obj.inner_class_definition();
let class = class.read();
let qualified_name = class
.name()
.to_qualified_name(activation.context.gc_context);
// If we're describing a Class object, then the "superclass" the the Class class
let superclass = if is_static {
Some(activation.avm2().classes().class)
} else {
class_obj.superclass_object()
};
let mut current_super_obj = superclass;
while let Some(super_obj) = current_super_obj {
let super_name = super_obj
.inner_class_definition()
.read()
.name()
.to_qualified_name(activation.context.gc_context);
write!(xml_string, "<extendsClass type=\"{super_name}\"/>").unwrap();
current_super_obj = super_obj.superclass_object();
}
// When we're describing a Class object, we use the class vtable (which hides instance properties)
let vtable = if is_static {
class_obj.class_vtable()
} else {
class_obj.instance_vtable()
};
for interface in class_obj.interfaces() {
let interface_name = interface
.read()
.name()
.to_qualified_name(activation.context.gc_context);
write!(
xml_string,
"<implementsInterface type=\"{interface_name}\"/>"
)
.unwrap();
}
// FIXME - avmplus iterates over their own hashtable, so the order in the final XML
// is different
for (prop_name, ns, prop) in vtable.resolved_traits().iter() {
// All non-public properties (including properties in the AS3 namespace) are hidden
if !ns.is_public() {
continue;
}
match prop {
Property::ConstSlot { slot_id } | Property::Slot { slot_id } => {
let prop_class_name = vtable
.slot_class_name(*slot_id, activation.context.gc_context)?
.to_qualified_name_or_star(activation.context.gc_context);
let elem_name = match prop {
Property::ConstSlot { .. } => "constant",
Property::Slot { .. } => "variable",
_ => unreachable!(),
};
write!(
xml_string,
"<{elem_name} name=\"{prop_name}\" type=\"{prop_class_name}\"/>"
)
.unwrap();
}
Property::Method { disp_id } => {
let method = vtable
.get_full_method(*disp_id)
.unwrap_or_else(|| panic!("Missing method for id {disp_id:?}"));
let return_type_name = method
.method
.return_type()
.to_qualified_name_or_star(activation.context.gc_context);
let declared_by = method
.class
.inner_class_definition()
.read()
.name()
.to_qualified_name(activation.context.gc_context);
write!(xml_string, "<method name=\"{prop_name}\" declaredBy=\"{declared_by}\" returnType=\"{return_type_name}\">").unwrap();
write_params(&mut xml_string, &method.method, activation);
xml_string += "</method>";
}
Property::Virtual { get, set } => {
let access = match (get, set) {
(Some(_), Some(_)) => "readwrite",
(Some(_), None) => "readonly",
(None, Some(_)) => "writeonly",
(None, None) => unreachable!(),
};
// For getters, obtain the type by looking at the getter return type.
// For setters, obtain the type by looking at the setter's first parameter.
let (method_type, defining_class) = if let Some(get) = get {
let getter = vtable
.get_full_method(*get)
.unwrap_or_else(|| panic!("Missing 'get' method for id {get:?}"));
(getter.method.return_type(), getter.class)
} else if let Some(set) = set {
let setter = vtable
.get_full_method(*set)
.unwrap_or_else(|| panic!("Missing 'set' method for id {set:?}"));
(
setter.method.signature()[0].param_type_name.clone(),
setter.class,
)
} else {
unreachable!();
};
let accessor_type =
method_type.to_qualified_name_or_star(activation.context.gc_context);
let declared_by = defining_class
.inner_class_definition()
.read()
.name()
.to_qualified_name(activation.context.gc_context);
write!(xml_string, "<accessor name=\"{prop_name}\" access=\"{access}\" type=\"{accessor_type}\" declaredBy=\"{declared_by}\"/>").unwrap();
}
}
}
let constructor = class_obj.constructor();
// Flash only shows a <constructor> element if it has at least one parameter
if !is_static && !constructor.signature().is_empty() {
xml_string += "<constructor>";
write_params(&mut xml_string, &constructor, activation);
xml_string += "</constructor>";
}
// If we're describing a Class object, add a <factory> element describing the instance.
if is_static {
write!(xml_string, "<factory type=\"{qualified_name}\">").unwrap();
xml_string += &describe_internal_body(activation, class_obj, false)?;
xml_string += "</factory>";
}
Ok(xml_string)
}
fn write_params<'gc>(
xml_string: &mut String,
method: &Method<'gc>,
activation: &mut Activation<'_, 'gc>,
) {
for (i, param) in method.signature().iter().enumerate() {
let index = i + 1;
let param_type_name = param
.param_type_name
.to_qualified_name_or_star(activation.context.gc_context);
let optional = param.default_value.is_some();
write!(
xml_string,
"<parameter index=\"{index}\" type=\"{param_type_name}\" optional=\"{optional}\"/>"
)
.unwrap();
}
}

View File

@ -3,7 +3,7 @@
use crate::avm2::activation::Activation;
use crate::avm2::class::{Class, ClassAttributes};
use crate::avm2::globals::number::{print_with_precision, print_with_radix};
use crate::avm2::method::{Method, NativeMethodImpl};
use crate::avm2::method::{Method, NativeMethodImpl, ParamConfig};
use crate::avm2::object::{primitive_allocator, FunctionObject, Object, TObject};
use crate::avm2::value::Value;
use crate::avm2::Multiname;
@ -269,7 +269,17 @@ pub fn create_class<'gc>(activation: &mut Activation<'_, 'gc>) -> GcCell<'gc, Cl
let class = Class::new(
QName::new(activation.avm2().public_namespace, "int"),
Some(Multiname::new(activation.avm2().public_namespace, "Object")),
Method::from_builtin(instance_init, "<int instance initializer>", mc),
Method::from_builtin_and_params(
instance_init,
"<int instance initializer>",
vec![ParamConfig {
param_name: AvmString::new_utf8(activation.context.gc_context, "value"),
param_type_name: Multiname::any(activation.context.gc_context),
default_value: Some(Value::Integer(0)),
}],
false,
mc,
),
Method::from_builtin(class_init, "<int class initializer>", mc),
mc,
);
@ -283,7 +293,13 @@ pub fn create_class<'gc>(activation: &mut Activation<'_, 'gc>) -> GcCell<'gc, Cl
mc,
));
const CLASS_CONSTANTS: &[(&str, i32)] = &[("MAX_VALUE", i32::MAX), ("MIN_VALUE", i32::MIN)];
// 'length' is a weird undocumented constant in int.
// We need to define it, since it shows up in 'describeType'
const CLASS_CONSTANTS: &[(&str, i32)] = &[
("MAX_VALUE", i32::MAX),
("MIN_VALUE", i32::MIN),
("length", 1),
];
write.define_constant_int_class_traits(
activation.avm2().public_namespace,
CLASS_CONSTANTS,

View File

@ -269,6 +269,9 @@ pub struct NativeMethod<'gc> {
/// The parameter signature of the method.
pub signature: Vec<ParamConfig<'gc>>,
/// The return type of this method.
pub return_type: Multiname<'gc>,
/// Whether or not this method accepts parameters beyond those
/// mentioned in the parameter list.
pub is_variadic: bool,
@ -318,6 +321,8 @@ impl<'gc> Method<'gc> {
method,
name,
signature,
// FIXME - take in the real return type. This is needed for 'describeType'
return_type: Multiname::any(mc),
is_variadic,
},
))
@ -335,6 +340,8 @@ impl<'gc> Method<'gc> {
method,
name,
signature: Vec::new(),
// FIXME - take in the real return type. This is needed for 'describeType'
return_type: Multiname::any(mc),
is_variadic: true,
},
))
@ -352,6 +359,13 @@ impl<'gc> Method<'gc> {
}
}
pub fn return_type(&self) -> Multiname<'gc> {
match self {
Method::Native(nm) => nm.return_type.clone(),
Method::Bytecode(bm) => bm.return_type.clone(),
}
}
pub fn signature(&self) -> &[ParamConfig<'gc>] {
match self {
Method::Native(nm) => &nm.signature,

View File

@ -375,7 +375,9 @@ impl<'gc> Multiname<'gc> {
uri.push_str(&ns);
if let Some(name) = self.name {
uri.push_str(WStr::from_units(b"::"));
if !uri.is_empty() {
uri.push_str(WStr::from_units(b"::"));
}
uri.push_str(&name);
} else {
uri.push_str(WStr::from_units(b"::*"));
@ -397,6 +399,16 @@ impl<'gc> Multiname<'gc> {
AvmString::new(mc, uri)
}
/// Like `to_qualified_name`, but returns `*` if `self.is_any()` is true.
/// This is used by `describeType`
pub fn to_qualified_name_or_star(&self, mc: MutationContext<'gc, '_>) -> AvmString<'gc> {
if self.is_any() {
AvmString::new_utf8(mc, "*")
} else {
self.to_qualified_name(mc)
}
}
// note: I didn't look very deeply into how different exactly this should be
// this is currently generally based on to_qualified_name, without params and leading ::
pub fn to_error_qualified_name(&self, mc: MutationContext<'gc, '_>) -> AvmString<'gc> {

View File

@ -692,6 +692,10 @@ impl<'gc> ClassObject<'gc> {
}
}
pub fn constructor(self) -> Method<'gc> {
self.0.read().constructor.clone()
}
pub fn instance_vtable(self) -> VTable<'gc> {
self.0.read().instance_vtable
}

View File

@ -7,7 +7,7 @@ use crate::avm2::object::script_object::{ScriptObject, ScriptObjectData};
use crate::avm2::object::{ClassObject, Object, ObjectPtr, TObject};
use crate::avm2::scope::ScopeChain;
use crate::avm2::value::Value;
use crate::avm2::Error;
use crate::avm2::{Error, Multiname};
use core::fmt;
use gc_arena::{Collect, Gc, GcCell, MutationContext};
use std::cell::{Ref, RefMut};
@ -31,6 +31,7 @@ pub fn function_allocator<'gc>(
method: |_, _, _| Ok(Value::Undefined),
name: "<Empty Function>",
signature: vec![],
return_type: Multiname::any(activation.context.gc_context),
is_variadic: true,
},
);

View File

@ -7,6 +7,7 @@ use crate::avm2::Error;
use crate::avm2::Multiname;
use crate::avm2::TranslationUnit;
use crate::avm2::Value;
use gc_arena::MutationContext;
use gc_arena::{Collect, Gc};
#[derive(Debug, Collect, Clone, Copy)]
@ -125,6 +126,14 @@ impl<'gc> PropertyClass<'gc> {
Ok((value, changed))
}
}
pub fn get_name(&self, mc: MutationContext<'gc, '_>) -> Multiname<'gc> {
match self {
PropertyClass::Class(class) => class.inner_class_definition().read().name().into(),
PropertyClass::Name(gc) => gc.0.clone(),
PropertyClass::Any => Multiname::any(mc),
}
}
}
enum ResolveOutcome<'gc> {

View File

@ -175,7 +175,7 @@ impl<'gc> Trait<'gc> {
Trait {
name,
attributes: TraitAttributes::empty(),
kind: TraitKind::Slot {
kind: TraitKind::Const {
slot_id: 0,
default_value: default_value.unwrap_or_else(|| default_value_for_type(&type_name)),
type_name,

View File

@ -97,6 +97,23 @@ impl<'gc> VTable<'gc> {
VTable(GcCell::allocate(mc, self.0.read().clone()))
}
pub fn resolved_traits(&self) -> Ref<'_, PropertyMap<'gc, Property>> {
Ref::map(self.0.read(), |v| &v.resolved_traits)
}
pub fn slot_class_name(
&self,
slot_id: u32,
mc: MutationContext<'gc, '_>,
) -> Result<Multiname<'gc>, Error<'gc>> {
self.0
.read()
.slot_classes
.get(slot_id as usize)
.ok_or_else(|| "Invalid slot ID".into())
.map(|c| c.get_name(mc))
}
pub fn get_trait(self, name: &Multiname<'gc>) -> Option<Property> {
self.0
.read()

View File

@ -1,4 +1,4 @@
// compiled with mxmlc
// compiled with mxmlc
package {
import flash.display.MovieClip;
@ -8,27 +8,79 @@ package {
}
}
// note: this entire test is to be replaced by more comprehensive test
// once XML gets implemented.
// This test only checks that `type.@name` looks like a string containing the type name.
import flash.utils.describeType;
import flash.utils.getQualifiedClassName;
import flash.utils.getQualifiedSuperclassName;
import flash.utils.Dictionary;
class C{}
var o = {};
import flash.display.DisplayObject;
var name; // mxmlc disallows .@name.toString() for some reason
// The order of elements in describeType(obj)) depends on the iteration order
// of the internal avmplus Traits hashtable.
// We don't currently reproduce this in Ruffle, so we can't just use 'toXMLString'
// to print the output. Instead, we use this function to re-implement 'toXMLString',
// and normalize the output by printing the children of an element in lexicographic
// order (by their stringified value)
function normalizeXML(data: XML, indent:uint = 0) {
var output = "";
for (var i = 0; i < indent; i++) {
output += " ";
};
output += "<" + data.name();
for each (var attr in data.attributes()) {
output += " " + attr.name() + "=\"" + attr + "\"";
}
if (data.children().length() == 0) {
output += "/>";
return output;
}
output += ">\n";
var childStrs = []
for each (var child in data.children()) {
childStrs.push(normalizeXML(child, indent + 2));
}
childStrs.sort()
for each (var childStr in childStrs) {
for (var i = 0 ; i < indent; i++) {
output += " ";
}
output += childStr;
output += "\n"
}
for (var i = 0; i < indent; i++) {
output += " ";
};
output += "</" + data.name() + ">";
return output;
}
trace(describeType(o).@name == "Object");
name = describeType(o).@name;
trace(name.toString() == "Object");
function describeXMLNormalized(val: *) {
trace(normalizeXML(describeType(val)));
}
trace(describeType(C).@name);
name = describeType(C).@name;
trace(name.toString());
trace(describeType(new C()).@name);
trace(describeType(int).@name);
trace(describeType(1).@name);
trace(describeType(Class).@name);
trace(describeType(Dictionary).@name);
trace(describeType(new Dictionary()).@name);
class C {}
class Base {
public function Base(optParam:* = null) {}
public var baseProp:Object;
public function baseMethod(): Boolean { return true }
public function overridenMethod(firstParam: *, secondParam: Dictionary, thirdParam: DisplayObject = null): Object { return null; }
AS3 function as3Method() {}
}
class Subclass extends Base {
public var subProp:Object;
public function subMethod() {}
public override function overridenMethod(firstParam: *, secondParam: Dictionary, thirdParam: DisplayObject = null): Object { return null; }
}
describeXMLNormalized(Object);
describeXMLNormalized(new Object());
describeXMLNormalized(Subclass);
describeXMLNormalized(new Subclass());
describeXMLNormalized(C);
describeXMLNormalized(new C());
describeXMLNormalized(int);
describeXMLNormalized(1);
describeXMLNormalized(Class);
describeXMLNormalized(Dictionary);
describeXMLNormalized(new Dictionary());

View File

@ -1,10 +1,97 @@
true
true
FilePrivateNS:Test::C
FilePrivateNS:Test::C
FilePrivateNS:Test::C
int
int
Class
flash.utils::Dictionary
flash.utils::Dictionary
<type name="Object" base="Class" isDynamic="true" isFinal="true" isStatic="true">
<accessor name="prototype" access="readonly" type="*" declaredBy="Class"/>
<constant name="length" type="int"/>
<extendsClass type="Class"/>
<extendsClass type="Object"/>
<factory type="Object"/>
</type>
<type name="Object" isDynamic="true" isFinal="false" isStatic="false"/>
<type name="Test.as$38::Subclass" base="Class" isDynamic="true" isFinal="true" isStatic="true">
<accessor name="prototype" access="readonly" type="*" declaredBy="Class"/>
<extendsClass type="Class"/>
<extendsClass type="Object"/>
<factory type="Test.as$38::Subclass">
<extendsClass type="Object"/>
<extendsClass type="Test.as$38::Base"/>
<method name="baseMethod" declaredBy="Test.as$38::Base" returnType="Boolean"/>
<method name="overridenMethod" declaredBy="Test.as$38::Subclass" returnType="Object">
<parameter index="1" type="*" optional="false"/>
<parameter index="2" type="flash.utils::Dictionary" optional="false"/>
<parameter index="3" type="flash.display::DisplayObject" optional="true"/>
</method>
<method name="subMethod" declaredBy="Test.as$38::Subclass" returnType="*"/>
<variable name="baseProp" type="Object"/>
<variable name="subProp" type="Object"/>
</factory>
</type>
<type name="Test.as$38::Subclass" base="Test.as$38::Base" isDynamic="false" isFinal="false" isStatic="false">
<extendsClass type="Object"/>
<extendsClass type="Test.as$38::Base"/>
<method name="baseMethod" declaredBy="Test.as$38::Base" returnType="Boolean"/>
<method name="overridenMethod" declaredBy="Test.as$38::Subclass" returnType="Object">
<parameter index="1" type="*" optional="false"/>
<parameter index="2" type="flash.utils::Dictionary" optional="false"/>
<parameter index="3" type="flash.display::DisplayObject" optional="true"/>
</method>
<method name="subMethod" declaredBy="Test.as$38::Subclass" returnType="*"/>
<variable name="baseProp" type="Object"/>
<variable name="subProp" type="Object"/>
</type>
<type name="Test.as$38::C" base="Class" isDynamic="true" isFinal="true" isStatic="true">
<accessor name="prototype" access="readonly" type="*" declaredBy="Class"/>
<extendsClass type="Class"/>
<extendsClass type="Object"/>
<factory type="Test.as$38::C">
<extendsClass type="Object"/>
</factory>
</type>
<type name="Test.as$38::C" base="Object" isDynamic="false" isFinal="false" isStatic="false">
<extendsClass type="Object"/>
</type>
<type name="int" base="Class" isDynamic="true" isFinal="true" isStatic="true">
<accessor name="prototype" access="readonly" type="*" declaredBy="Class"/>
<constant name="MAX_VALUE" type="int"/>
<constant name="MIN_VALUE" type="int"/>
<constant name="length" type="int"/>
<extendsClass type="Class"/>
<extendsClass type="Object"/>
<factory type="int">
<constructor>
<parameter index="1" type="*" optional="true"/>
</constructor>
<extendsClass type="Object"/>
</factory>
</type>
<type name="int" base="Object" isDynamic="false" isFinal="true" isStatic="false">
<constructor>
<parameter index="1" type="*" optional="true"/>
</constructor>
<extendsClass type="Object"/>
</type>
<type name="Class" base="Class" isDynamic="true" isFinal="true" isStatic="true">
<accessor name="prototype" access="readonly" type="*" declaredBy="Class"/>
<constant name="length" type="int"/>
<extendsClass type="Class"/>
<extendsClass type="Object"/>
<factory type="Class">
<accessor name="prototype" access="readonly" type="*" declaredBy="Class"/>
<extendsClass type="Object"/>
</factory>
</type>
<type name="flash.utils::Dictionary" base="Class" isDynamic="true" isFinal="true" isStatic="true">
<accessor name="prototype" access="readonly" type="*" declaredBy="Class"/>
<extendsClass type="Class"/>
<extendsClass type="Object"/>
<factory type="flash.utils::Dictionary">
<constructor>
<parameter index="1" type="Boolean" optional="true"/>
</constructor>
<extendsClass type="Object"/>
</factory>
</type>
<type name="flash.utils::Dictionary" base="Object" isDynamic="true" isFinal="false" isStatic="false">
<constructor>
<parameter index="1" type="Boolean" optional="true"/>
</constructor>
<extendsClass type="Object"/>
</type>

Binary file not shown.