avm2: Properly make all classes an instance of `Class`. (#57)

* avm2: Properly make all classes an instance of `Class`.

Also, does this technically mean that `Class` is a metaclass?

* avm2: Remove `Function::from_method_and_proto` as it will no longer be needed

* avm2: Ensure builtin classes are also instances of `Class`.

This requires tying a veritable gordian knot of classes; everything needs to be allocated up-front, linked together, and then properly initialized later on. This necessitated splitting the whole class construction process up into three steps:

1. Allocation via `from_class_partial`, which does everything that can be done without any other classes
2. Weaving via `link_prototype` and `link_type`, which links all of the allocated parts together correctly. This also includes initializing `SystemClasses` and `SystemPrototypes`.
3. Initialization via `into_finished_class`, which must be done *after* the weave has finished.

Once complete you have core classes that are all instances of `Class`, along with prototypes that have their usual legacy quirks.

Note that this does *not* make prototypes instances of their class. We do need to do that, but doing so breaks ES3 legacy support. This is because we currently only work with bound methods, but need to be able to call unbound methods in `callproperty`.

* tests: Add a test for all core classes' instance-of relationships
This commit is contained in:
kmeisthax 2021-09-21 15:50:06 -06:00 committed by Adrian Wielgosik
parent f8c32d3a68
commit 42275f43f3
12 changed files with 404 additions and 436 deletions

View File

@ -279,17 +279,17 @@ impl<'gc> SystemClasses<'gc> {
/// Add a free-function builtin to the global scope.
fn function<'gc>(
mc: MutationContext<'gc, '_>,
activation: &mut Activation<'_, 'gc, '_>,
package: impl Into<AvmString<'gc>>,
name: &'static str,
nf: NativeMethodImpl,
fn_proto: Object<'gc>,
mut domain: Domain<'gc>,
script: Script<'gc>,
) -> Result<(), Error> {
let mc = activation.context.gc_context;
let qname = QName::new(Namespace::package(package), name);
let method = Method::from_builtin(nf, name, mc);
let as3fn = FunctionObject::from_method_and_proto(mc, method, None, fn_proto, None).into();
let as3fn = FunctionObject::from_method(activation, method, None, None).into();
domain.export_definition(qname.clone(), script, mc)?;
script
.init()
@ -422,23 +422,53 @@ pub fn load_player_globals<'gc>(
// public / root package
//
// We have to do this particular dance so that we have Object methods whose
// functions have call/apply in their prototypes, and that Function is also
// a subclass of Object.
let object_proto = object::create_proto(activation);
let fn_proto = function::create_proto(activation, object_proto);
// This part of global initialization is very complicated, because
// everything has to circularly reference everything else:
//
// - Object is an instance of itself, as well as it's prototype
// - All other types are instances of Class, which is an instance of
// itself
// - Function's prototype is an instance of itself
// - All methods created by the above-mentioned classes are also instances
// of Function
//
// Hence, this ridiculously complicated dance of classdef, type allocation,
// and partial initialization.
let object_scope = Some(Scope::push_scope(gs.get_scope(), gs, mc));
let object_classdef = object::create_class(mc);
let object_class =
ClassObject::from_class_partial(activation, object_classdef, None, object_scope)?;
let object_proto = ScriptObject::bare_object(mc);
let (mut object_class, object_cinit) =
object::fill_proto(activation, gs, object_proto, fn_proto)?;
let (mut function_class, function_cinit) =
function::fill_proto(activation, gs, fn_proto, object_class)?;
let fn_scope = Some(Scope::push_scope(gs.get_scope(), gs, mc));
let fn_classdef = function::create_class(mc);
let fn_class = ClassObject::from_class_partial(
activation,
fn_classdef,
Some(object_class.into()),
fn_scope,
)?;
let fn_proto = ScriptObject::object(mc, object_proto);
let (mut class_class, class_proto, class_cinit) =
class::create_class(activation, gs, object_class, object_proto, fn_proto)?;
let class_scope = Some(Scope::push_scope(gs.get_scope(), gs, mc));
let class_classdef = class::create_class(mc);
let class_class = ClassObject::from_class_partial(
activation,
class_classdef,
Some(object_class.into()),
class_scope,
)?;
let class_proto = ScriptObject::object(mc, object_proto);
dynamic_class(mc, object_class, domain, script)?;
dynamic_class(mc, function_class, domain, script)?;
dynamic_class(mc, class_class, domain, script)?;
// Now to weave the Gordian knot...
object_class.link_prototype(activation, object_proto)?;
object_class.link_type(activation, class_proto, class_class.into());
fn_class.link_prototype(activation, fn_proto)?;
fn_class.link_type(activation, class_proto, class_class.into());
class_class.link_prototype(activation, class_proto)?;
class_class.link_type(activation, class_proto, class_class.into());
// At this point, we need at least a partial set of system prototypes in
// order to continue initializing the player. The rest of the prototypes
@ -451,44 +481,26 @@ pub fn load_player_globals<'gc>(
));
activation.context.avm2.system_classes = Some(SystemClasses::new(
object_class,
function_class,
class_class,
object_class.into(),
fn_class.into(),
class_class.into(),
ScriptObject::bare_object(mc),
));
// We can now run all of the steps that would ordinarily be run
// automatically had we not been so early in VM setup. This means things
// like installing class traits and running class initializers, which
// usually are done in the associated constructor for `ClassObject`.
object_class.install_traits(
activation,
object_class
.as_class_definition()
.unwrap()
.read()
.class_traits(),
)?;
function_class.install_traits(
activation,
function_class
.as_class_definition()
.unwrap()
.read()
.class_traits(),
)?;
class_class.install_traits(
activation,
class_class
.as_class_definition()
.unwrap()
.read()
.class_traits(),
)?;
// Our activation environment is now functional enough to finish
// initializing the core class weave. The order of initialization shouldn't
// matter here, as long as all the initialization machinery can see and
// link the various system types together correctly.
let object_class = object_class.into_finished_class(activation)?;
let fn_class = fn_class.into_finished_class(activation)?;
let class_class = class_class.into_finished_class(activation)?;
object_cinit.call(Some(object_class), &[], activation, Some(object_class))?;
function_cinit.call(Some(function_class), &[], activation, Some(function_class))?;
class_cinit.call(Some(class_class), &[], activation, Some(class_class))?;
dynamic_class(mc, object_class, domain, script)?;
dynamic_class(mc, fn_class, domain, script)?;
dynamic_class(mc, class_class, domain, script)?;
// After this point, it is safe to initialize any other classes.
// Make sure to initialize superclasses *before* their subclasses!
avm2_system_class!(
global,
@ -497,6 +509,12 @@ pub fn load_player_globals<'gc>(
domain,
script
);
// Oh, one more small hitch: the domain everything gets put into was
// actually made *before* the core class weave, so let's fix that up now
// that the global class actually exists.
gs.set_proto(mc, activation.avm2().prototypes().global);
avm2_system_class!(string, activation, string::create_class(mc), domain, script);
avm2_system_class!(
boolean,
@ -517,13 +535,9 @@ pub fn load_player_globals<'gc>(
);
avm2_system_class!(array, activation, array::create_class(mc), domain, script);
// At this point we have to hide the fact that we had to create the player
// globals scope *before* the `Object` class
gs.set_proto(mc, activation.avm2().prototypes().global);
function(mc, "", "trace", trace, fn_proto, domain, script)?;
function(mc, "", "isFinite", is_finite, fn_proto, domain, script)?;
function(mc, "", "isNaN", is_nan, fn_proto, domain, script)?;
function(activation, "", "trace", trace, domain, script)?;
function(activation, "", "isFinite", is_finite, domain, script)?;
function(activation, "", "isNaN", is_nan, domain, script)?;
constant(mc, "", "undefined", Value::Undefined, domain, script)?;
constant(mc, "", "null", Value::Null, domain, script)?;
constant(mc, "", "NaN", f64::NAN.into(), domain, script)?;
@ -635,41 +649,37 @@ pub fn load_player_globals<'gc>(
)?;
function(
mc,
activation,
"flash.utils",
"getTimer",
flash::utils::get_timer,
fn_proto,
domain,
script,
)?;
function(
mc,
activation,
"flash.utils",
"getQualifiedClassName",
flash::utils::get_qualified_class_name,
fn_proto,
domain,
script,
)?;
function(
mc,
activation,
"flash.utils",
"getQualifiedSuperclassName",
flash::utils::get_qualified_super_class_name,
fn_proto,
domain,
script,
)?;
function(
mc,
activation,
"flash.utils",
"getDefinitionByName",
flash::utils::get_definition_by_name,
fn_proto,
domain,
script,
)?;
@ -925,11 +935,10 @@ pub fn load_player_globals<'gc>(
// package `flash.crypto`
function(
mc,
activation,
"flash.crypto",
"generateRandomBytes",
flash::crypto::generate_random_bytes,
fn_proto,
domain,
script,
)?;

View File

@ -4,10 +4,10 @@ use crate::avm2::activation::Activation;
use crate::avm2::class::Class;
use crate::avm2::method::Method;
use crate::avm2::names::{Namespace, QName};
use crate::avm2::object::{ClassObject, Object, ScriptObject, TObject};
use crate::avm2::scope::Scope;
use crate::avm2::object::Object;
use crate::avm2::value::Value;
use crate::avm2::Error;
use gc_arena::{GcCell, MutationContext};
/// Implements `Class`'s instance initializer.
///
@ -30,44 +30,15 @@ pub fn class_init<'gc>(
Ok(Value::Undefined)
}
/// Construct `Class` and `Class.prototype`, respectively.
///
/// This function additionally returns the class initializer method for `Class`,
/// which must be called before user code runs.
pub fn create_class<'gc>(
activation: &mut Activation<'_, 'gc, '_>,
globals: Object<'gc>,
superclass: Object<'gc>,
super_proto: Object<'gc>,
fn_proto: Object<'gc>,
) -> Result<(Object<'gc>, Object<'gc>, Object<'gc>), Error> {
/// Construct `Class`'s class.
pub fn create_class<'gc>(gc_context: MutationContext<'gc, '_>) -> GcCell<'gc, Class<'gc>> {
let class_class = Class::new(
QName::new(Namespace::public(), "Class"),
Some(QName::new(Namespace::public(), "Object").into()),
Method::from_builtin(
instance_init,
"<Class instance initializer>",
activation.context.gc_context,
),
Method::from_builtin(
class_init,
"<Class class initializer>",
activation.context.gc_context,
),
activation.context.gc_context,
Method::from_builtin(instance_init, "<Class instance initializer>", gc_context),
Method::from_builtin(class_init, "<Class class initializer>", gc_context),
gc_context,
);
let scope = Scope::push_scope(globals.get_scope(), globals, activation.context.gc_context);
let proto = ScriptObject::object(activation.context.gc_context, super_proto);
let (class_object, cinit) = ClassObject::from_builtin_class(
activation.context.gc_context,
Some(superclass),
class_class,
Some(scope),
proto,
fn_proto,
)?;
Ok((class_object, proto, cinit))
class_class
}

View File

@ -5,10 +5,10 @@ use crate::avm2::class::Class;
use crate::avm2::globals::array::resolve_array_hole;
use crate::avm2::method::Method;
use crate::avm2::names::{Namespace, QName};
use crate::avm2::object::{ClassObject, FunctionObject, Object, ScriptObject, TObject};
use crate::avm2::scope::Scope;
use crate::avm2::object::{FunctionObject, Object, TObject};
use crate::avm2::value::Value;
use crate::avm2::Error;
use gc_arena::{GcCell, MutationContext};
/// Implements `Function`'s instance initializer.
pub fn instance_init<'gc>(
@ -25,10 +25,38 @@ pub fn instance_init<'gc>(
/// Implements `Function`'s class initializer.
pub fn class_init<'gc>(
_activation: &mut Activation<'_, 'gc, '_>,
_this: Option<Object<'gc>>,
activation: &mut Activation<'_, 'gc, '_>,
this: Option<Object<'gc>>,
_args: &[Value<'gc>],
) -> Result<Value<'gc>, Error> {
if let Some(this) = this {
let mut function_proto = this
.get_property(this, &QName::dynamic_name("prototype"), activation)?
.coerce_to_object(activation)?;
function_proto.install_dynamic_property(
activation.context.gc_context,
QName::new(Namespace::as3_namespace(), "call"),
FunctionObject::from_method(
activation,
Method::from_builtin(call, "call", activation.context.gc_context),
None,
None,
)
.into(),
)?;
function_proto.install_dynamic_property(
activation.context.gc_context,
QName::new(Namespace::as3_namespace(), "apply"),
FunctionObject::from_method(
activation,
Method::from_builtin(apply, "apply", activation.context.gc_context),
None,
None,
)
.into(),
)?;
}
Ok(Value::Undefined)
}
@ -95,74 +123,15 @@ fn apply<'gc>(
}
}
/// Create Function prototype.
///
/// This function creates a suitable prototype and returns it.
pub fn create_proto<'gc>(
activation: &mut Activation<'_, 'gc, '_>,
super_proto: Object<'gc>,
) -> Object<'gc> {
ScriptObject::object(activation.context.gc_context, super_proto)
}
/// Fill `Function.prototype` and allocate it's class object.
///
/// This function returns both the class object and it's class initializer,
/// which must be called before user code runs.
pub fn fill_proto<'gc>(
activation: &mut Activation<'_, 'gc, '_>,
globals: Object<'gc>,
mut function_proto: Object<'gc>,
superclass: Object<'gc>,
) -> Result<(Object<'gc>, Object<'gc>), Error> {
/// Construct `Function`'s class.
pub fn create_class<'gc>(gc_context: MutationContext<'gc, '_>) -> GcCell<'gc, Class<'gc>> {
let function_class = Class::new(
QName::new(Namespace::public(), "Function"),
Some(QName::new(Namespace::public(), "Object").into()),
Method::from_builtin(
instance_init,
"<Function instance initializer>",
activation.context.gc_context,
),
Method::from_builtin(
class_init,
"<Function class initializer>",
activation.context.gc_context,
),
activation.context.gc_context,
Method::from_builtin(instance_init, "<Function instance initializer>", gc_context),
Method::from_builtin(class_init, "<Function class initializer>", gc_context),
gc_context,
);
let scope = Scope::push_scope(globals.get_scope(), globals, activation.context.gc_context);
function_proto.install_dynamic_property(
activation.context.gc_context,
QName::new(Namespace::as3_namespace(), "call"),
FunctionObject::from_method_and_proto(
activation.context.gc_context,
Method::from_builtin(call, "call", activation.context.gc_context),
None,
function_proto,
None,
)
.into(),
)?;
function_proto.install_dynamic_property(
activation.context.gc_context,
QName::new(Namespace::as3_namespace(), "apply"),
FunctionObject::from_method_and_proto(
activation.context.gc_context,
Method::from_builtin(apply, "apply", activation.context.gc_context),
None,
function_proto,
None,
)
.into(),
)?;
ClassObject::from_builtin_class(
activation.context.gc_context,
Some(superclass),
function_class,
Some(scope),
function_proto,
function_proto,
)
function_class
}

View File

@ -4,11 +4,11 @@ use crate::avm2::activation::Activation;
use crate::avm2::class::Class;
use crate::avm2::method::{Method, NativeMethodImpl};
use crate::avm2::names::{Namespace, QName};
use crate::avm2::object::{ClassObject, FunctionObject, Object, ScriptObject, TObject};
use crate::avm2::scope::Scope;
use crate::avm2::object::{FunctionObject, Object, TObject};
use crate::avm2::traits::Trait;
use crate::avm2::value::Value;
use crate::avm2::Error;
use gc_arena::{GcCell, MutationContext};
/// Implements `Object`'s instance initializer.
pub fn instance_init<'gc>(
@ -21,10 +21,99 @@ pub fn instance_init<'gc>(
/// Implements `Object`'s class initializer
pub fn class_init<'gc>(
_activation: &mut Activation<'_, 'gc, '_>,
_this: Option<Object<'gc>>,
activation: &mut Activation<'_, 'gc, '_>,
this: Option<Object<'gc>>,
_args: &[Value<'gc>],
) -> Result<Value<'gc>, Error> {
if let Some(this) = this {
let mut object_proto = this
.get_property(this, &QName::dynamic_name("prototype"), activation)?
.coerce_to_object(activation)?;
let gc_context = activation.context.gc_context;
object_proto.install_dynamic_property(
gc_context,
QName::new(Namespace::public(), "hasOwnProperty"),
FunctionObject::from_method(
activation,
Method::from_builtin(has_own_property, "hasOwnProperty", gc_context),
None,
None,
)
.into(),
)?;
object_proto.install_dynamic_property(
gc_context,
QName::new(Namespace::public(), "propertyIsEnumerable"),
FunctionObject::from_method(
activation,
Method::from_builtin(property_is_enumerable, "propertyIsEnumerable", gc_context),
None,
None,
)
.into(),
)?;
object_proto.install_dynamic_property(
gc_context,
QName::new(Namespace::public(), "setPropertyIsEnumerable"),
FunctionObject::from_method(
activation,
Method::from_builtin(
set_property_is_enumerable,
"setPropertyIsEnumerable",
gc_context,
),
None,
None,
)
.into(),
)?;
object_proto.install_dynamic_property(
gc_context,
QName::new(Namespace::public(), "isPrototypeOf"),
FunctionObject::from_method(
activation,
Method::from_builtin(is_prototype_of, "isPrototypeOf", gc_context),
None,
None,
)
.into(),
)?;
object_proto.install_dynamic_property(
gc_context,
QName::new(Namespace::public(), "toString"),
FunctionObject::from_method(
activation,
Method::from_builtin(to_string, "toString", gc_context),
None,
None,
)
.into(),
)?;
object_proto.install_dynamic_property(
gc_context,
QName::new(Namespace::public(), "toLocaleString"),
FunctionObject::from_method(
activation,
Method::from_builtin(to_locale_string, "toLocaleString", gc_context),
None,
None,
)
.into(),
)?;
object_proto.install_dynamic_property(
gc_context,
QName::new(Namespace::public(), "valueOf"),
FunctionObject::from_method(
activation,
Method::from_builtin(value_of, "valueOf", gc_context),
None,
None,
)
.into(),
)?;
}
Ok(Value::Undefined)
}
@ -149,124 +238,8 @@ pub fn set_property_is_enumerable<'gc>(
Ok(Value::Undefined)
}
/// Create object prototype.
///
/// This function creates a suitable class and object prototype attached to it,
/// but does not actually fill it with methods. That requires a valid function
/// prototype, and is thus done by `fill_proto` below.
pub fn create_proto<'gc>(activation: &mut Activation<'_, 'gc, '_>) -> Object<'gc> {
ScriptObject::bare_object(activation.context.gc_context)
}
/// Finish constructing `Object.prototype`, and also construct `Object`.
///
/// `__proto__` and other cross-linked properties of this object will *not*
/// be defined here. The caller of this function is responsible for linking
/// them in order to obtain a valid ECMAScript `Object` prototype.
///
/// Since Object and Function are so heavily intertwined, this function does
/// not allocate an object to store either proto. Instead, you must allocate
/// bare objects for both and let this function fill Object for you.
///
/// This function returns both the class object and it's class initializer
/// method, which must be called before user code runs.
pub fn fill_proto<'gc>(
activation: &mut Activation<'_, 'gc, '_>,
globals: Object<'gc>,
mut object_proto: Object<'gc>,
fn_proto: Object<'gc>,
) -> Result<(Object<'gc>, Object<'gc>), Error> {
let gc_context = activation.context.gc_context;
object_proto.install_dynamic_property(
gc_context,
QName::new(Namespace::public(), "hasOwnProperty"),
FunctionObject::from_method_and_proto(
gc_context,
Method::from_builtin(has_own_property, "hasOwnProperty", gc_context),
None,
fn_proto,
None,
)
.into(),
)?;
object_proto.install_dynamic_property(
gc_context,
QName::new(Namespace::public(), "propertyIsEnumerable"),
FunctionObject::from_method_and_proto(
gc_context,
Method::from_builtin(property_is_enumerable, "propertyIsEnumerable", gc_context),
None,
fn_proto,
None,
)
.into(),
)?;
object_proto.install_dynamic_property(
gc_context,
QName::new(Namespace::public(), "setPropertyIsEnumerable"),
FunctionObject::from_method_and_proto(
gc_context,
Method::from_builtin(
set_property_is_enumerable,
"setPropertyIsEnumerable",
gc_context,
),
None,
fn_proto,
None,
)
.into(),
)?;
object_proto.install_dynamic_property(
gc_context,
QName::new(Namespace::public(), "isPrototypeOf"),
FunctionObject::from_method_and_proto(
gc_context,
Method::from_builtin(is_prototype_of, "isPrototypeOf", gc_context),
None,
fn_proto,
None,
)
.into(),
)?;
object_proto.install_dynamic_property(
gc_context,
QName::new(Namespace::public(), "toString"),
FunctionObject::from_method_and_proto(
gc_context,
Method::from_builtin(to_string, "toString", gc_context),
None,
fn_proto,
None,
)
.into(),
)?;
object_proto.install_dynamic_property(
gc_context,
QName::new(Namespace::public(), "toLocaleString"),
FunctionObject::from_method_and_proto(
gc_context,
Method::from_builtin(to_locale_string, "toLocaleString", gc_context),
None,
fn_proto,
None,
)
.into(),
)?;
object_proto.install_dynamic_property(
gc_context,
QName::new(Namespace::public(), "valueOf"),
FunctionObject::from_method_and_proto(
gc_context,
Method::from_builtin(value_of, "valueOf", gc_context),
None,
fn_proto,
None,
)
.into(),
)?;
/// Construct `Object`'s class.
pub fn create_class<'gc>(gc_context: MutationContext<'gc, '_>) -> GcCell<'gc, Class<'gc>> {
let object_class = Class::new(
QName::new(Namespace::public(), "Object"),
None,
@ -290,16 +263,5 @@ pub fn fill_proto<'gc>(
];
write.define_as3_builtin_instance_methods(gc_context, PUBLIC_INSTANCE_METHODS);
drop(write);
let scope = Scope::push_scope(globals.get_scope(), globals, gc_context);
ClassObject::from_builtin_class(
gc_context,
None,
object_class,
Some(scope),
object_proto,
fn_proto,
)
object_class
}

View File

@ -85,6 +85,53 @@ impl<'gc> ClassObject<'gc> {
superclass_object: Option<Object<'gc>>,
scope: Option<GcCell<'gc, Scope<'gc>>>,
) -> Result<Object<'gc>, Error> {
let class_object = Self::from_class_partial(activation, class, superclass_object, scope)?;
let instance_allocator = class_object.0.read().instance_allocator.0;
//TODO: Class prototypes are *not* instances of their class and should
//not be allocated by the class allocator, but instead should be
//regular objects
let class_proto = if let Some(superclass_object) = superclass_object {
let base_proto = superclass_object
.get_property(
superclass_object,
&QName::new(Namespace::public(), "prototype"),
activation,
)?
.coerce_to_object(activation)?;
instance_allocator(superclass_object, base_proto, activation)?
} else {
ScriptObject::bare_object(activation.context.gc_context)
};
class_object.link_prototype(activation, class_proto)?;
let class_class = activation.avm2().classes().class;
let class_class_proto = activation.avm2().prototypes().class;
class_object.link_type(activation, class_class_proto, class_class);
class_object.into_finished_class(activation)
}
/// Allocate a class but do not properly construct it.
///
/// This function does the bare minimum to allocate classes, without taking
/// any action that would require the existence of any other objects in the
/// object graph. The resulting class will be a bare object and should not
/// be used or presented to user code until you finish initializing it. You
/// do that by calling `link_prototype`, `link_type`, and then
/// `into_finished_class` in that order.
///
/// This returns the class object directly (*not* an `Object`), to allow
/// further manipulation of the class once it's dependent types have been
/// allocated.
pub fn from_class_partial(
activation: &mut Activation<'_, 'gc, '_>,
class: GcCell<'gc, Class<'gc>>,
superclass_object: Option<Object<'gc>>,
scope: Option<GcCell<'gc, Scope<'gc>>>,
) -> Result<Self, Error> {
if let Some(base_class) = superclass_object.and_then(|b| b.as_class_definition()) {
if base_class.read().is_final() {
return Err(format!(
@ -113,24 +160,6 @@ impl<'gc> ClassObject<'gc> {
})
.unwrap_or(scriptobject_allocator);
//TODO: Class prototypes are *not* instances of their class and should
//not be allocated by the class allocator, but instead should be
//regular objects
let class_proto = if let Some(superclass_object) = superclass_object {
let base_proto = superclass_object
.get_property(
superclass_object,
&QName::new(Namespace::public(), "prototype"),
activation,
)?
.coerce_to_object(activation)?;
instance_allocator(superclass_object, base_proto, activation)?
} else {
ScriptObject::bare_object(activation.context.gc_context)
};
let fn_proto = activation.avm2().prototypes().function;
let constructor = Executable::from_method(
class.read().instance_init(),
scope,
@ -144,10 +173,10 @@ impl<'gc> ClassObject<'gc> {
activation.context.gc_context,
);
let mut class_object = ClassObject(GcCell::allocate(
let class_object = ClassObject(GcCell::allocate(
activation.context.gc_context,
ClassObjectData {
base: ScriptObjectData::base_new(Some(fn_proto), None),
base: ScriptObjectData::base_new(None, None),
class,
scope,
superclass_object,
@ -160,93 +189,39 @@ impl<'gc> ClassObject<'gc> {
},
));
class_object.link_prototype(activation, class_proto)?;
class_object.link_interfaces(activation)?;
class_object.install_traits(activation, class.read().class_traits())?;
class_object.run_class_initializer(activation)?;
Ok(class_object.into())
Ok(class_object)
}
/// Construct a builtin type from a Rust constructor and prototype.
/// Finish initialization of the class.
///
/// This function returns both the class constructor object and the
/// class initializer to call before the class is used. The constructor
/// should be used in all cases where the type needs to be referred to. You
/// must call the class initializer yourself.
/// This is intended for classes that were pre-allocated with
/// `from_class_partial`. It skips several critical initialization steps
/// that are necessary to obtain a functioning class object:
///
/// You are also required to install class constructor traits yourself onto
/// the returned object. This is due to the fact that normal trait
/// installation requires a working `context.avm2` with a link to the
/// function prototype, and this is intended to be called before that link
/// has been established.
/// - The `link_type` step, which makes the class an instance of another
/// type
/// - The `link_prototype` step, which installs a prototype for instances
/// of this type to inherit
///
/// `base_class` is allowed to be `None`, corresponding to a `null` value
/// in the VM. This corresponds to no base class, and in practice appears
/// to be limited to interfaces.
pub fn from_builtin_class(
mc: MutationContext<'gc, '_>,
superclass_object: Option<Object<'gc>>,
class: GcCell<'gc, Class<'gc>>,
scope: Option<GcCell<'gc, Scope<'gc>>>,
mut prototype: Object<'gc>,
fn_proto: Object<'gc>,
) -> Result<(Object<'gc>, Object<'gc>), Error> {
let instance_allocator = class
.read()
.instance_allocator()
.or_else(|| {
superclass_object
.and_then(|c| c.as_class_object())
.and_then(|c| c.instance_allocator())
})
.unwrap_or(scriptobject_allocator);
/// Make sure to call them before calling this function, or it may yield an
/// error.
pub fn into_finished_class(
mut self,
activation: &mut Activation<'_, 'gc, '_>,
) -> Result<Object<'gc>, Error> {
let class = self
.as_class_definition()
.ok_or("Cannot finish initialization of core class without a class definition!")?;
let class_class = self.0.read().base.instance_of().ok_or(
"Cannot finish initialization of core class without it being linked to a type!",
)?;
let constructor = Executable::from_method(class.read().instance_init(), scope, None, mc);
let native_constructor =
Executable::from_method(class.read().native_instance_init(), scope, None, mc);
let mut base: Object<'gc> = ClassObject(GcCell::allocate(
mc,
ClassObjectData {
base: ScriptObjectData::base_new(Some(fn_proto), None),
class,
scope,
superclass_object,
instance_allocator: Allocator(instance_allocator),
constructor,
native_constructor,
params: None,
applications: HashMap::new(),
interfaces: Vec::new(),
},
))
.into();
self.link_interfaces(activation)?;
self.install_traits(activation, class.read().class_traits())?;
self.install_instance_traits(activation, class_class)?;
self.run_class_initializer(activation)?;
base.install_slot(
mc,
QName::new(Namespace::public(), "prototype"),
0,
prototype.into(),
false,
);
prototype.install_slot(
mc,
QName::new(Namespace::public(), "constructor"),
0,
base.into(),
false,
);
let class_initializer = class.read().class_init();
let class_object = FunctionObject::from_method_and_proto(
mc,
class_initializer,
scope,
fn_proto,
Some(base),
);
Ok((base, class_object))
Ok(self.into())
}
/// Link this class to a prototype.
@ -314,6 +289,23 @@ impl<'gc> ClassObject<'gc> {
Ok(())
}
/// Manually set the type of this `Class`.
///
/// This is intended to support initialization of early types such as
/// `Class` and `Object`. All other types should pull `Class`'s prototype
/// and type object from the `Avm2` instance.
pub fn link_type(
self,
activation: &mut Activation<'_, 'gc, '_>,
proto: Object<'gc>,
instance_of: Object<'gc>,
) {
let mut write = self.0.write(activation.context.gc_context);
write.base.set_instance_of(instance_of);
write.base.set_proto(proto);
}
/// Run the class's initializer method.
pub fn run_class_initializer(
self,
@ -602,7 +594,8 @@ impl<'gc> TObject<'gc> for ClassObject<'gc> {
ScriptObject::bare_object(activation.context.gc_context)
};
let fn_proto = activation.avm2().prototypes().function;
let class_class = activation.avm2().classes().class;
let class_class_proto = activation.avm2().prototypes().class;
let constructor = self.0.read().constructor.clone();
let native_constructor = self.0.read().native_constructor.clone();
@ -610,7 +603,7 @@ impl<'gc> TObject<'gc> for ClassObject<'gc> {
let mut class_object = ClassObject(GcCell::allocate(
activation.context.gc_context,
ClassObjectData {
base: ScriptObjectData::base_new(Some(fn_proto), None),
base: ScriptObjectData::base_new(Some(class_class_proto), Some(class_class)),
class: parameterized_class,
scope,
superclass_object,
@ -626,6 +619,7 @@ impl<'gc> TObject<'gc> for ClassObject<'gc> {
class_object.link_prototype(activation, class_proto)?;
class_object.link_interfaces(activation)?;
class_object.install_traits(activation, parameterized_class.read().class_traits())?;
class_object.install_instance_traits(activation, class_class)?;
class_object.run_class_initializer(activation)?;
self.0

View File

@ -84,32 +84,6 @@ impl<'gc> FunctionObject<'gc> {
))
.into()
}
/// Construct a function from an ABC method, the current closure scope, and
/// a function prototype.
///
/// The given `reciever`, if supplied, will override any user-specified
/// `this` parameter.
///
/// This function exists primarily for early globals. Unless you are in a
/// position where you cannot access `Function.prototype` yet, you should
/// use `from_method` instead.
pub fn from_method_and_proto(
mc: MutationContext<'gc, '_>,
method: Method<'gc>,
scope: Option<GcCell<'gc, Scope<'gc>>>,
fn_proto: Object<'gc>,
receiver: Option<Object<'gc>>,
) -> Object<'gc> {
FunctionObject(GcCell::allocate(
mc,
FunctionObjectData {
base: ScriptObjectData::base_new(Some(fn_proto), None),
exec: Some(Executable::from_method(method, scope, receiver, mc)),
},
))
.into()
}
}
impl<'gc> TObject<'gc> for FunctionObject<'gc> {

View File

@ -824,4 +824,9 @@ impl<'gc> ScriptObjectData<'gc> {
pub fn instance_of(&self) -> Option<Object<'gc>> {
self.instance_of
}
/// Set the class object for this object.
pub fn set_instance_of(&mut self, instance_of: Object<'gc>) {
self.instance_of = Some(instance_of);
}
}

View File

@ -680,6 +680,7 @@ swf_tests! {
(as3_string_indexof_lastindexof, "avm2/string_indexof_lastindexof", 1),
(as3_string_match, "avm2/string_match", 1),
(as3_string_slice_substr_substring, "avm2/string_slice_substr_substring", 1),
(as3_class_is, "avm2/class_is", 1),
}
// TODO: These tests have some inaccuracies currently, so we use approx_eq to test that numeric values are close enough.

View File

@ -0,0 +1,51 @@
package {
public class Test {}
}
trace("///Test is Test");
trace(Test is Test);
trace("///Test is Class");
trace(Test is Class);
trace("///Test is Function");
trace(Test is Function);
trace("///Test is Object");
trace(Test is Object);
trace("///Class is Test");
trace(Class is Test);
trace("///Class is Class");
trace(Class is Class);
trace("///Class is Function");
trace(Class is Function);
trace("///Class is Object");
trace(Class is Object);
trace("///Function is Test");
trace(Function is Test);
trace("///Function is Class");
trace(Function is Class);
trace("///Function is Function");
trace(Function is Function);
trace("///Function is Object");
trace(Function is Object);
trace("///Object is Test");
trace(Object is Test);
trace("///Object is Class");
trace(Object is Class);
trace("///Object is Function");
trace(Object is Function);
trace("///Object is Object");
trace(Object is Object);

View File

@ -0,0 +1,32 @@
///Test is Test
false
///Test is Class
true
///Test is Function
false
///Test is Object
true
///Class is Test
false
///Class is Class
true
///Class is Function
false
///Class is Object
true
///Function is Test
false
///Function is Class
true
///Function is Function
false
///Function is Object
true
///Object is Test
false
///Object is Class
true
///Object is Function
false
///Object is Object
true

Binary file not shown.

Binary file not shown.