Skip to content

Commit

Permalink
avm2: Properly make all classes an instance of Class. (#57)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
kmeisthax authored and adrian17 committed Sep 21, 2021
1 parent f8c32d3 commit 42275f4
Show file tree
Hide file tree
Showing 12 changed files with 404 additions and 436 deletions.
145 changes: 77 additions & 68 deletions core/src/avm2/globals.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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
Expand All @@ -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,
Expand All @@ -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,
Expand All @@ -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)?;
Expand Down Expand Up @@ -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,
)?;
Expand Down Expand Up @@ -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,
)?;
Expand Down
45 changes: 8 additions & 37 deletions core/src/avm2/globals/class.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
///
Expand All @@ -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
}
Loading

0 comments on commit 42275f4

Please sign in to comment.