core: Merge #100, Object prototypes

Object prototypes
This commit is contained in:
Mike Welsh 2019-11-28 16:03:25 -08:00 committed by GitHub
commit 75fd2b6a52
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
52 changed files with 2200 additions and 869 deletions

View File

@ -1,6 +1,5 @@
use crate::avm1::function::Avm1Function;
use crate::avm1::globals::create_globals;
use crate::avm1::object::Object;
use crate::backend::navigator::NavigationMethod;
use crate::context::UpdateContext;
use crate::prelude::*;
@ -17,11 +16,12 @@ use crate::tag_utils::SwfSlice;
mod activation;
mod fscommand;
mod function;
mod globals;
pub mod movie_clip;
pub mod globals;
pub mod object;
mod property;
mod return_value;
mod scope;
pub mod script_object;
mod value;
#[cfg(test)]
@ -31,7 +31,10 @@ mod test_utils;
mod tests;
use activation::Activation;
pub use globals::SystemPrototypes;
pub use object::{Object, ObjectCell};
use scope::Scope;
pub use script_object::ScriptObject;
pub use value::Value;
pub struct Avm1<'gc> {
@ -42,7 +45,10 @@ pub struct Avm1<'gc> {
constant_pool: Vec<String>,
/// The global object.
globals: GcCell<'gc, Object<'gc>>,
globals: ObjectCell<'gc>,
/// System builtins that we use internally to construct new objects.
prototypes: globals::SystemPrototypes<'gc>,
/// All activation records for the current execution context.
stack_frames: Vec<GcCell<'gc, Activation<'gc>>>,
@ -59,8 +65,13 @@ unsafe impl<'gc> gc_arena::Collect for Avm1<'gc> {
#[inline]
fn trace(&self, cc: gc_arena::CollectionContext) {
self.globals.trace(cc);
self.prototypes.trace(cc);
self.stack_frames.trace(cc);
self.stack.trace(cc);
for register in &self.registers {
register.trace(cc);
}
}
}
@ -68,10 +79,13 @@ type Error = Box<dyn std::error::Error>;
impl<'gc> Avm1<'gc> {
pub fn new(gc_context: MutationContext<'gc, '_>, player_version: u8) -> Self {
let (prototypes, globals) = create_globals(gc_context);
Self {
player_version,
constant_pool: vec![],
globals: GcCell::allocate(gc_context, create_globals(gc_context)),
globals: GcCell::allocate(gc_context, globals),
prototypes,
stack_frames: vec![],
stack: vec![],
registers: [
@ -390,6 +404,7 @@ impl<'gc> Avm1<'gc> {
Action::Increment => self.action_increment(context),
Action::InitArray => self.action_init_array(context),
Action::InitObject => self.action_init_object(context),
Action::InstanceOf => self.action_instance_of(context),
Action::Jump { offset } => self.action_jump(context, offset, reader),
Action::Less => self.action_less(context),
Action::Less2 => self.action_less_2(context),
@ -799,7 +814,19 @@ impl<'gc> Avm1<'gc> {
context.gc_context,
);
let func = Avm1Function::from_df1(swf_version, func_data, name, params, scope);
let func_obj = GcCell::allocate(context.gc_context, Object::action_function(func));
let prototype = GcCell::allocate(
context.gc_context,
Box::new(ScriptObject::object(
context.gc_context,
Some(self.prototypes.object),
)) as Box<dyn Object<'gc>>,
);
let func_obj = ScriptObject::function(
context.gc_context,
func,
Some(self.prototypes.function),
Some(prototype),
);
if name == "" {
self.push(func_obj);
} else {
@ -830,7 +857,19 @@ impl<'gc> Avm1<'gc> {
context.gc_context,
);
let func = Avm1Function::from_df2(swf_version, func_data, action_func, scope);
let func_obj = GcCell::allocate(context.gc_context, Object::action_function(func));
let prototype = GcCell::allocate(
context.gc_context,
Box::new(ScriptObject::object(
context.gc_context,
Some(self.prototypes.object),
)) as Box<dyn Object<'gc>>,
);
let func_obj = ScriptObject::function(
context.gc_context,
func,
Some(self.prototypes.function),
Some(prototype),
);
if action_func.name == "" {
self.push(func_obj);
} else {
@ -1062,10 +1101,15 @@ impl<'gc> Avm1<'gc> {
}
/// Obtain a reference to `_global`.
pub fn global_object_cell(&self) -> GcCell<'gc, Object<'gc>> {
pub fn global_object_cell(&self) -> ObjectCell<'gc> {
self.globals
}
/// Obtain system built-in prototypes for this instance.
pub fn prototypes(&self) -> &globals::SystemPrototypes<'gc> {
&self.prototypes
}
fn action_get_variable(
&mut self,
context: &mut UpdateContext<'_, 'gc, '_>,
@ -1275,15 +1319,54 @@ impl<'gc> Avm1<'gc> {
Err("Unimplemented action: InitArray".into())
}
fn action_init_object(&mut self, _context: &mut UpdateContext) -> Result<(), Error> {
fn action_init_object(
&mut self,
context: &mut UpdateContext<'_, 'gc, '_>,
) -> Result<(), Error> {
let num_props = self.pop()?.as_i64()?;
let mut object = ScriptObject::object(context.gc_context, Some(self.prototypes.object));
for _ in 0..num_props {
let _value = self.pop()?;
let _name = self.pop()?;
let value = self.pop()?;
let name = self.pop()?.into_string();
let this = self.current_stack_frame().unwrap().read().this_cell();
object.set(&name, value, self, context, this)?;
}
// TODO(Herschel)
Err("Unimplemented action: InitObject".into())
self.push(Value::Object(GcCell::allocate(
context.gc_context,
Box::new(object),
)));
Ok(())
}
fn action_instance_of(
&mut self,
context: &mut UpdateContext<'_, 'gc, '_>,
) -> Result<(), Error> {
let constr = self.pop()?.as_object()?;
let obj = self.pop()?.as_object()?;
//TODO: Interface detection on SWF7
let prototype = constr
.read()
.get("prototype", self, context, constr)?
.resolve(self, context)?
.as_object()?;
let mut proto = obj.read().proto();
while let Some(this_proto) = proto {
if GcCell::ptr_eq(this_proto, prototype) {
self.push(true);
return Ok(());
}
proto = this_proto.read().proto();
}
self.push(false);
Ok(())
}
fn action_jump(
@ -1400,24 +1483,72 @@ impl<'gc> Avm1<'gc> {
Ok(())
}
fn action_new_method(&mut self, _context: &mut UpdateContext) -> Result<(), Error> {
let _name = self.pop()?.as_string()?;
let _object = self.pop()?.as_object()?;
let _num_args = self.pop()?.as_i64()?;
self.push(Value::Undefined);
// TODO(Herschel)
Err("Unimplemented action: NewMethod".into())
fn action_new_method(&mut self, context: &mut UpdateContext<'_, 'gc, '_>) -> Result<(), Error> {
let method_name = self.pop()?;
let object = self.pop()?.as_object()?;
let num_args = self.pop()?.as_i64()?;
let mut args = Vec::new();
for _ in 0..num_args {
args.push(self.pop()?);
}
let constructor = object
.read()
.get(&method_name.as_string()?, self, context, object.to_owned())?
.resolve(self, context)?
.as_object()?;
let prototype = constructor
.read()
.get("prototype", self, context, constructor)?
.resolve(self, context)?
.as_object()?;
let this = prototype.read().new(self, context, prototype, &args)?;
//TODO: What happens if you `ActionNewMethod` without a method name?
constructor
.read()
.call(self, context, this, &args)?
.resolve(self, context)?;
self.push(this);
Ok(())
}
fn action_new_object(&mut self, _context: &mut UpdateContext) -> Result<(), Error> {
let _object = self.pop()?.as_string()?;
fn action_new_object(&mut self, context: &mut UpdateContext<'_, 'gc, '_>) -> Result<(), Error> {
let fn_name = self.pop()?;
let num_args = self.pop()?.as_i64()?;
let mut args = Vec::new();
for _ in 0..num_args {
let _arg = self.pop()?;
args.push(self.pop()?);
}
self.push(Value::Undefined);
// TODO(Herschel)
Err("Unimplemented action: NewObject".into())
let constructor = self
.stack_frames
.last()
.unwrap()
.clone()
.read()
.resolve(fn_name.as_string()?, self, context)?
.resolve(self, context)?
.as_object()?;
let prototype = constructor
.read()
.get("prototype", self, context, constructor)?
.resolve(self, context)?
.as_object()?;
let this = prototype.read().new(self, context, prototype, &args)?;
constructor
.read()
.call(self, context, this, &args)?
.resolve(self, context)?;
self.push(this);
Ok(())
}
fn action_or(&mut self, _context: &mut UpdateContext<'_, 'gc, '_>) -> Result<(), Error> {
@ -1528,7 +1659,7 @@ impl<'gc> Avm1<'gc> {
fn action_set_member(&mut self, context: &mut UpdateContext<'_, 'gc, '_>) -> Result<(), Error> {
let value = self.pop()?;
let name_val = self.pop()?;
let name = name_val.as_string()?;
let name = name_val.into_string();
let object = self.pop()?.as_object()?;
let this = self.current_stack_frame().unwrap().read().this_cell();

View File

@ -1,9 +1,8 @@
//! Activation records
use crate::avm1::object::Object;
use crate::avm1::return_value::ReturnValue;
use crate::avm1::scope::Scope;
use crate::avm1::{Avm1, Error, Value};
use crate::avm1::{Avm1, Error, ObjectCell, Value};
use crate::context::UpdateContext;
use crate::tag_utils::SwfSlice;
use gc_arena::{GcCell, MutationContext};
@ -68,10 +67,10 @@ pub struct Activation<'gc> {
scope: GcCell<'gc, Scope<'gc>>,
/// The immutable value of `this`.
this: GcCell<'gc, Object<'gc>>,
this: ObjectCell<'gc>,
/// The arguments this function was called by.
arguments: Option<GcCell<'gc, Object<'gc>>>,
arguments: Option<ObjectCell<'gc>>,
/// The return value of the activation.
return_value: Option<Value<'gc>>,
@ -115,8 +114,8 @@ impl<'gc> Activation<'gc> {
swf_version: u8,
code: SwfSlice,
scope: GcCell<'gc, Scope<'gc>>,
this: GcCell<'gc, Object<'gc>>,
arguments: Option<GcCell<'gc, Object<'gc>>>,
this: ObjectCell<'gc>,
arguments: Option<ObjectCell<'gc>>,
) -> Activation<'gc> {
Activation {
swf_version,
@ -136,8 +135,8 @@ impl<'gc> Activation<'gc> {
swf_version: u8,
code: SwfSlice,
scope: GcCell<'gc, Scope<'gc>>,
this: GcCell<'gc, Object<'gc>>,
arguments: Option<GcCell<'gc, Object<'gc>>>,
this: ObjectCell<'gc>,
arguments: Option<ObjectCell<'gc>>,
) -> Activation<'gc> {
Activation {
swf_version,
@ -161,7 +160,7 @@ impl<'gc> Activation<'gc> {
/// it.
pub fn from_nothing(
swf_version: u8,
globals: GcCell<'gc, Object<'gc>>,
globals: ObjectCell<'gc>,
mc: MutationContext<'gc, '_>,
) -> Activation<'gc> {
let global_scope = GcCell::allocate(mc, Scope::from_global_object(globals));
@ -297,7 +296,7 @@ impl<'gc> Activation<'gc> {
}
/// Returns value of `this` as a reference.
pub fn this_cell(&self) -> GcCell<'gc, Object<'gc>> {
pub fn this_cell(&self) -> ObjectCell<'gc> {
self.this
}

View File

@ -1,13 +1,13 @@
//! Code relating to executable functions + calling conventions.
use crate::avm1::activation::Activation;
use crate::avm1::object::{Attribute::*, Object};
use crate::avm1::property::Attribute::*;
use crate::avm1::return_value::ReturnValue;
use crate::avm1::scope::Scope;
use crate::avm1::value::Value;
use crate::avm1::{Avm1, Error, UpdateContext};
use crate::avm1::{Avm1, Error, Object, ObjectCell, ScriptObject, UpdateContext};
use crate::tag_utils::SwfSlice;
use gc_arena::GcCell;
use gc_arena::{Collect, CollectionContext, GcCell};
use swf::avm1::types::FunctionParam;
/// Represents a function defined in Ruffle's code.
@ -27,13 +27,14 @@ use swf::avm1::types::FunctionParam;
pub type NativeFunction<'gc> = fn(
&mut Avm1<'gc>,
&mut UpdateContext<'_, 'gc, '_>,
GcCell<'gc, Object<'gc>>,
ObjectCell<'gc>,
&[Value<'gc>],
) -> Result<ReturnValue<'gc>, Error>;
/// Represents a function defined in the AVM1 runtime, either through
/// `DefineFunction` or `DefineFunction2`.
#[derive(Clone)]
#[derive(Clone, Collect)]
#[collect(no_drop)]
pub struct Avm1Function<'gc> {
/// The file format version of the SWF that generated this function.
swf_version: u8,
@ -171,6 +172,15 @@ pub enum Executable<'gc> {
Action(Avm1Function<'gc>),
}
unsafe impl<'gc> Collect for Executable<'gc> {
fn trace(&self, cc: CollectionContext) {
match self {
Self::Native(_) => {}
Self::Action(af) => af.trace(cc),
}
}
}
impl<'gc> Executable<'gc> {
/// Execute the given code.
///
@ -182,7 +192,7 @@ impl<'gc> Executable<'gc> {
&self,
avm: &mut Avm1<'gc>,
ac: &mut UpdateContext<'_, 'gc, '_>,
this: GcCell<'gc, Object<'gc>>,
this: ObjectCell<'gc>,
args: &[Value<'gc>],
) -> Result<ReturnValue<'gc>, Error> {
match self {
@ -192,29 +202,29 @@ impl<'gc> Executable<'gc> {
ac.gc_context,
Scope::new_local_scope(af.scope(), ac.gc_context),
);
let mut arguments = Object::object(ac.gc_context);
let mut arguments =
ScriptObject::object(ac.gc_context, Some(avm.prototypes().object));
if !af.suppress_arguments {
for i in 0..args.len() {
arguments.force_set(
arguments.define_value(
&format!("{}", i),
args.get(i).unwrap().clone(),
DontDelete,
DontDelete.into(),
)
}
arguments.force_set(
"length",
Value::Number(args.len() as f64),
DontDelete | DontEnum,
);
arguments.define_value("length", args.len().into(), DontDelete | DontEnum);
}
let argcell = GcCell::allocate(ac.gc_context, arguments);
let argcell = GcCell::allocate(
ac.gc_context,
Box::new(arguments) as Box<dyn Object<'gc> + 'gc>,
);
let effective_ver = if avm.current_swf_version() > 5 {
af.swf_version()
} else {
this.read()
.display_node()
.as_display_node()
.map(|dn| dn.read().swf_version())
.unwrap_or(ac.player_version)
};
@ -301,3 +311,15 @@ impl<'gc> Executable<'gc> {
}
}
}
impl<'gc> From<NativeFunction<'gc>> for Executable<'gc> {
fn from(nf: NativeFunction<'gc>) -> Self {
Executable::Native(nf)
}
}
impl<'gc> From<Avm1Function<'gc>> for Executable<'gc> {
fn from(af: Avm1Function<'gc>) -> Self {
Executable::Action(af)
}
}

View File

@ -1,18 +1,22 @@
use crate::avm1::fscommand;
use crate::avm1::function::Executable;
use crate::avm1::return_value::ReturnValue;
use crate::avm1::{Avm1, Error, Object, UpdateContext, Value};
use crate::avm1::{Avm1, Error, Object, ObjectCell, ScriptObject, UpdateContext, Value};
use crate::backend::navigator::NavigationMethod;
use enumset::EnumSet;
use gc_arena::{GcCell, MutationContext};
use gc_arena::MutationContext;
use rand::Rng;
mod function;
mod math;
mod movie_clip;
mod object;
#[allow(non_snake_case, unused_must_use)] //can't use errors yet
pub fn getURL<'a, 'gc>(
avm: &mut Avm1<'gc>,
context: &mut UpdateContext<'a, 'gc, '_>,
_this: GcCell<'gc, Object<'gc>>,
_this: ObjectCell<'gc>,
args: &[Value<'gc>],
) -> Result<ReturnValue<'gc>, Error> {
//TODO: Error behavior if no arguments are present
@ -40,7 +44,7 @@ pub fn getURL<'a, 'gc>(
pub fn random<'gc>(
_avm: &mut Avm1<'gc>,
action_context: &mut UpdateContext<'_, 'gc, '_>,
_this: GcCell<'gc, Object<'gc>>,
_this: ObjectCell<'gc>,
args: &[Value<'gc>],
) -> Result<ReturnValue<'gc>, Error> {
match args.get(0) {
@ -52,7 +56,7 @@ pub fn random<'gc>(
pub fn boolean<'gc>(
avm: &mut Avm1<'gc>,
_action_context: &mut UpdateContext<'_, 'gc, '_>,
_this: GcCell<'gc, Object<'gc>>,
_this: ObjectCell<'gc>,
args: &[Value<'gc>],
) -> Result<ReturnValue<'gc>, Error> {
if let Some(val) = args.get(0) {
@ -65,7 +69,7 @@ pub fn boolean<'gc>(
pub fn number<'gc>(
_avm: &mut Avm1<'gc>,
_action_context: &mut UpdateContext<'_, 'gc, '_>,
_this: GcCell<'gc, Object<'gc>>,
_this: ObjectCell<'gc>,
args: &[Value<'gc>],
) -> Result<ReturnValue<'gc>, Error> {
if let Some(val) = args.get(0) {
@ -78,7 +82,7 @@ pub fn number<'gc>(
pub fn is_nan<'gc>(
_avm: &mut Avm1<'gc>,
_action_context: &mut UpdateContext<'_, 'gc, '_>,
_this: GcCell<'gc, Object<'gc>>,
_this: ObjectCell<'gc>,
args: &[Value<'gc>],
) -> Result<ReturnValue<'gc>, Error> {
if let Some(val) = args.get(0) {
@ -88,24 +92,121 @@ pub fn is_nan<'gc>(
}
}
pub fn create_globals<'gc>(gc_context: MutationContext<'gc, '_>) -> Object<'gc> {
let mut globals = Object::object(gc_context);
/// This structure represents all system builtins that are used regardless of
/// whatever the hell happens to `_global`. These are, of course,
/// user-modifiable.
#[derive(Clone)]
pub struct SystemPrototypes<'gc> {
pub object: ObjectCell<'gc>,
pub function: ObjectCell<'gc>,
pub movie_clip: ObjectCell<'gc>,
}
globals.force_set_function("isNaN", is_nan, gc_context, EnumSet::empty());
globals.force_set_function("Boolean", boolean, gc_context, EnumSet::empty());
globals.force_set("Math", math::create(gc_context), EnumSet::empty());
globals.force_set_function("getURL", getURL, gc_context, EnumSet::empty());
globals.force_set_function("Number", number, gc_context, EnumSet::empty());
globals.force_set_function("random", random, gc_context, EnumSet::empty());
unsafe impl<'gc> gc_arena::Collect for SystemPrototypes<'gc> {
#[inline]
fn trace(&self, cc: gc_arena::CollectionContext) {
self.object.trace(cc);
self.function.trace(cc);
self.movie_clip.trace(cc);
}
}
globals.force_set("NaN", Value::Number(std::f64::NAN), EnumSet::empty());
globals.force_set(
/// Initialize default global scope and builtins for an AVM1 instance.
pub fn create_globals<'gc>(
gc_context: MutationContext<'gc, '_>,
) -> (SystemPrototypes<'gc>, Box<dyn Object<'gc> + 'gc>) {
let object_proto = ScriptObject::object_cell(gc_context, None);
let function_proto = function::create_proto(gc_context, object_proto);
object::fill_proto(gc_context, object_proto, function_proto);
let movie_clip_proto: ObjectCell<'gc> =
movie_clip::create_proto(gc_context, object_proto, function_proto);
//TODO: These need to be constructors and should also set `.prototype` on each one
let object = ScriptObject::function(
gc_context,
Executable::Native(object::constructor),
Some(function_proto),
Some(object_proto),
);
let function = ScriptObject::function(
gc_context,
Executable::Native(function::constructor),
Some(function_proto),
Some(function_proto),
);
let movie_clip = ScriptObject::function(
gc_context,
Executable::Native(movie_clip::constructor),
Some(function_proto),
Some(movie_clip_proto),
);
let mut globals = ScriptObject::object(gc_context, Some(object_proto));
globals.define_value("Object", object.into(), EnumSet::empty());
globals.define_value("Function", function.into(), EnumSet::empty());
globals.define_value("MovieClip", movie_clip.into(), EnumSet::empty());
globals.force_set_function(
"Number",
number,
gc_context,
EnumSet::empty(),
Some(function_proto),
);
globals.force_set_function(
"Boolean",
boolean,
gc_context,
EnumSet::empty(),
Some(function_proto),
);
globals.define_value(
"Math",
Value::Object(math::create(
gc_context,
Some(object_proto),
Some(function_proto),
)),
EnumSet::empty(),
);
globals.force_set_function(
"isNaN",
is_nan,
gc_context,
EnumSet::empty(),
Some(function_proto),
);
globals.force_set_function(
"getURL",
getURL,
gc_context,
EnumSet::empty(),
Some(function_proto),
);
globals.force_set_function(
"random",
random,
gc_context,
EnumSet::empty(),
Some(function_proto),
);
globals.define_value("NaN", Value::Number(std::f64::NAN), EnumSet::empty());
globals.define_value(
"Infinity",
Value::Number(std::f64::INFINITY),
EnumSet::empty(),
);
globals
(
SystemPrototypes {
object: object_proto,
function: function_proto,
movie_clip: movie_clip_proto,
},
Box::new(globals),
)
}
#[cfg(test)]

View File

@ -0,0 +1,73 @@
//! Function prototype
use crate::avm1::return_value::ReturnValue;
use crate::avm1::{Avm1, Error, ObjectCell, ScriptObject, UpdateContext, Value};
use enumset::EnumSet;
use gc_arena::MutationContext;
/// Implements `Function`
pub fn constructor<'gc>(
_avm: &mut Avm1<'gc>,
_action_context: &mut UpdateContext<'_, 'gc, '_>,
_this: ObjectCell<'gc>,
_args: &[Value<'gc>],
) -> Result<ReturnValue<'gc>, Error> {
Ok(Value::Undefined.into())
}
pub fn call<'gc>(
_avm: &mut Avm1<'gc>,
_action_context: &mut UpdateContext<'_, 'gc, '_>,
_this: ObjectCell<'gc>,
_args: &[Value<'gc>],
) -> Result<ReturnValue<'gc>, Error> {
Ok(Value::Undefined.into())
}
pub fn apply<'gc>(
_avm: &mut Avm1<'gc>,
_action_context: &mut UpdateContext<'_, 'gc, '_>,
_this: ObjectCell<'gc>,
_args: &[Value<'gc>],
) -> Result<ReturnValue<'gc>, Error> {
Ok(Value::Undefined.into())
}
/// Partially construct `Function.prototype`.
///
/// `__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 `Function` prototype. The
/// returned object is also a bare object, which will need to be linked into
/// the prototype of `Object`.
pub fn create_proto<'gc>(
gc_context: MutationContext<'gc, '_>,
proto: ObjectCell<'gc>,
) -> ObjectCell<'gc> {
let function_proto = ScriptObject::object_cell(gc_context, Some(proto));
function_proto
.write(gc_context)
.as_script_object_mut()
.unwrap()
.force_set_function(
"call",
call,
gc_context,
EnumSet::empty(),
Some(function_proto),
);
function_proto
.write(gc_context)
.as_script_object_mut()
.unwrap()
.force_set_function(
"apply",
apply,
gc_context,
EnumSet::empty(),
Some(function_proto),
);
function_proto
}

View File

@ -1,12 +1,12 @@
use crate::avm1::object::Attribute::*;
use crate::avm1::property::Attribute::*;
use crate::avm1::return_value::ReturnValue;
use crate::avm1::{Avm1, Error, Object, UpdateContext, Value};
use crate::avm1::{Avm1, Error, Object, ObjectCell, ScriptObject, UpdateContext, Value};
use gc_arena::{GcCell, MutationContext};
use rand::Rng;
use std::f64::NAN;
macro_rules! wrap_std {
( $object: ident, $gc_context: ident, $($name:expr => $std:path),* ) => {{
( $object: ident, $gc_context: ident, $proto: ident, $($name:expr => $std:path),* ) => {{
$(
$object.force_set_function(
$name,
@ -19,6 +19,7 @@ macro_rules! wrap_std {
},
$gc_context,
DontDelete | ReadOnly | DontEnum,
$proto
);
)*
}};
@ -27,7 +28,7 @@ macro_rules! wrap_std {
fn atan2<'gc>(
_avm: &mut Avm1<'gc>,
_context: &mut UpdateContext<'_, 'gc, '_>,
_this: GcCell<'gc, Object<'gc>>,
_this: ObjectCell<'gc>,
args: &[Value<'gc>],
) -> Result<ReturnValue<'gc>, Error> {
if let Some(y) = args.get(0) {
@ -43,57 +44,61 @@ fn atan2<'gc>(
pub fn random<'gc>(
_avm: &mut Avm1<'gc>,
action_context: &mut UpdateContext<'_, 'gc, '_>,
_this: GcCell<'gc, Object<'gc>>,
_this: ObjectCell<'gc>,
_args: &[Value<'gc>],
) -> Result<ReturnValue<'gc>, Error> {
Ok(action_context.rng.gen_range(0.0f64, 1.0f64).into())
}
pub fn create<'gc>(gc_context: MutationContext<'gc, '_>) -> GcCell<'gc, Object<'gc>> {
let mut math = Object::object(gc_context);
pub fn create<'gc>(
gc_context: MutationContext<'gc, '_>,
proto: Option<ObjectCell<'gc>>,
fn_proto: Option<ObjectCell<'gc>>,
) -> ObjectCell<'gc> {
let mut math = ScriptObject::object(gc_context, proto);
math.force_set(
math.define_value(
"E",
Value::Number(std::f64::consts::E),
std::f64::consts::E.into(),
DontDelete | ReadOnly | DontEnum,
);
math.force_set(
math.define_value(
"LN10",
Value::Number(std::f64::consts::LN_10),
std::f64::consts::LN_10.into(),
DontDelete | ReadOnly | DontEnum,
);
math.force_set(
math.define_value(
"LN2",
Value::Number(std::f64::consts::LN_2),
std::f64::consts::LN_2.into(),
DontDelete | ReadOnly | DontEnum,
);
math.force_set(
math.define_value(
"LOG10E",
Value::Number(std::f64::consts::LOG10_E),
std::f64::consts::LOG10_E.into(),
DontDelete | ReadOnly | DontEnum,
);
math.force_set(
math.define_value(
"LOG2E",
Value::Number(std::f64::consts::LOG2_E),
std::f64::consts::LOG2_E.into(),
DontDelete | ReadOnly | DontEnum,
);
math.force_set(
math.define_value(
"PI",
Value::Number(std::f64::consts::PI),
std::f64::consts::PI.into(),
DontDelete | ReadOnly | DontEnum,
);
math.force_set(
math.define_value(
"SQRT1_2",
Value::Number(std::f64::consts::FRAC_1_SQRT_2),
std::f64::consts::FRAC_1_SQRT_2.into(),
DontDelete | ReadOnly | DontEnum,
);
math.force_set(
math.define_value(
"SQRT2",
Value::Number(std::f64::consts::SQRT_2),
std::f64::consts::SQRT_2.into(),
DontDelete | ReadOnly | DontEnum,
);
wrap_std!(math, gc_context,
wrap_std!(math, gc_context, fn_proto,
"abs" => f64::abs,
"acos" => f64::acos,
"asin" => f64::asin,
@ -108,15 +113,22 @@ pub fn create<'gc>(gc_context: MutationContext<'gc, '_>) -> GcCell<'gc, Object<'
"tan" => f64::tan
);
math.force_set_function("atan2", atan2, gc_context, DontDelete | ReadOnly | DontEnum);
math.force_set_function(
"atan2",
atan2,
gc_context,
DontDelete | ReadOnly | DontEnum,
fn_proto,
);
math.force_set_function(
"random",
random,
gc_context,
DontDelete | ReadOnly | DontEnum,
fn_proto,
);
GcCell::allocate(gc_context, math)
GcCell::allocate(gc_context, Box::new(math))
}
#[cfg(test)]
@ -130,7 +142,7 @@ mod tests {
#[test]
fn $test() -> Result<(), Error> {
with_avm(19, |avm, context, _root| {
let math = create(context.gc_context);
let math = create(context.gc_context, Some(avm.prototypes().object), Some(avm.prototypes().function));
let function = math.read().get($name, avm, context, math)?.unwrap_immediate();
$(
@ -236,7 +248,15 @@ mod tests {
#[test]
fn test_atan2_nan() {
with_avm(19, |avm, context, _root| {
let math = GcCell::allocate(context.gc_context, create(context.gc_context));
let math = GcCell::allocate(
context.gc_context,
create(
context.gc_context,
Some(avm.prototypes().object),
Some(avm.prototypes().function),
),
);
assert_eq!(atan2(avm, context, *math.read(), &[]).unwrap(), NAN.into());
assert_eq!(
atan2(avm, context, *math.read(), &[1.0.into(), Value::Null]).unwrap(),
@ -256,7 +276,15 @@ mod tests {
#[test]
fn test_atan2_valid() {
with_avm(19, |avm, context, _root| {
let math = GcCell::allocate(context.gc_context, create(context.gc_context));
let math = GcCell::allocate(
context.gc_context,
create(
context.gc_context,
Some(avm.prototypes().object),
Some(avm.prototypes().function),
),
);
assert_eq!(
atan2(avm, context, *math.read(), &[10.0.into()]).unwrap(),
std::f64::consts::FRAC_PI_2.into()

View File

@ -1,18 +1,30 @@
//! MovieClip prototype
use crate::avm1::function::Executable;
use crate::avm1::object::{Attribute::*, Object};
use crate::avm1::property::Attribute::*;
use crate::avm1::return_value::ReturnValue;
use crate::avm1::{Avm1, Error, UpdateContext, Value};
use crate::avm1::{Avm1, Error, Object, ObjectCell, ScriptObject, UpdateContext, Value};
use crate::display_object::{DisplayNode, DisplayObject, MovieClip};
use enumset::EnumSet;
use gc_arena::{GcCell, MutationContext};
/// Implements `MovieClip`
pub fn constructor<'gc>(
_avm: &mut Avm1<'gc>,
_action_context: &mut UpdateContext<'_, 'gc, '_>,
_this: ObjectCell<'gc>,
_args: &[Value<'gc>],
) -> Result<ReturnValue<'gc>, Error> {
Ok(Value::Undefined.into())
}
macro_rules! with_movie_clip {
( $gc_context: ident, $object:ident, $($name:expr => $fn:expr),* ) => {{
( $gc_context: ident, $object:ident, $fn_proto: expr, $($name:expr => $fn:expr),* ) => {{
$(
$object.force_set_function(
$name,
|_avm, _context, this, args| -> Result<ReturnValue<'gc>, Error> {
if let Some(display_object) = this.read().display_node() {
if let Some(display_object) = this.read().as_display_node() {
if let Some(movie_clip) = display_object.read().as_movie_clip() {
return Ok($fn(movie_clip, args));
}
@ -21,18 +33,19 @@ macro_rules! with_movie_clip {
},
$gc_context,
DontDelete | ReadOnly | DontEnum,
$fn_proto
);
)*
}};
}
macro_rules! with_movie_clip_mut {
( $gc_context: ident, $object:ident, $($name:expr => $fn:expr),* ) => {{
( $gc_context: ident, $object:ident, $fn_proto: expr, $($name:expr => $fn:expr),* ) => {{
$(
$object.force_set_function(
$name,
|_avm, context: &mut UpdateContext<'_, 'gc, '_>, this, args| -> Result<ReturnValue<'gc>, Error> {
if let Some(display_object) = this.read().display_node() {
if let Some(display_object) = this.read().as_display_node() {
if let Some(movie_clip) = display_object.write(context.gc_context).as_movie_clip_mut() {
return Ok($fn(movie_clip, context, display_object, args).into());
}
@ -41,6 +54,7 @@ macro_rules! with_movie_clip_mut {
} as crate::avm1::function::NativeFunction<'gc>,
$gc_context,
DontDelete | ReadOnly | DontEnum,
$fn_proto
);
)*
}};
@ -49,7 +63,7 @@ macro_rules! with_movie_clip_mut {
pub fn overwrite_root<'gc>(
_avm: &mut Avm1<'gc>,
ac: &mut UpdateContext<'_, 'gc, '_>,
this: GcCell<'gc, Object<'gc>>,
this: ObjectCell<'gc>,
args: &[Value<'gc>],
) -> Result<ReturnValue<'gc>, Error> {
let new_val = args
@ -57,7 +71,7 @@ pub fn overwrite_root<'gc>(
.map(|v| v.to_owned())
.unwrap_or(Value::Undefined);
this.write(ac.gc_context)
.force_set("_root", new_val, EnumSet::new());
.define_value("_root", new_val, EnumSet::new());
Ok(Value::Undefined.into())
}
@ -65,7 +79,7 @@ pub fn overwrite_root<'gc>(
pub fn overwrite_global<'gc>(
_avm: &mut Avm1<'gc>,
ac: &mut UpdateContext<'_, 'gc, '_>,
this: GcCell<'gc, Object<'gc>>,
this: ObjectCell<'gc>,
args: &[Value<'gc>],
) -> Result<ReturnValue<'gc>, Error> {
let new_val = args
@ -73,17 +87,22 @@ pub fn overwrite_global<'gc>(
.map(|v| v.to_owned())
.unwrap_or(Value::Undefined);
this.write(ac.gc_context)
.force_set("_global", new_val, EnumSet::new());
.define_value("_global", new_val, EnumSet::new());
Ok(Value::Undefined.into())
}
pub fn create_movie_object<'gc>(gc_context: MutationContext<'gc, '_>) -> Object<'gc> {
let mut object = Object::object(gc_context);
pub fn create_proto<'gc>(
gc_context: MutationContext<'gc, '_>,
proto: ObjectCell<'gc>,
fn_proto: ObjectCell<'gc>,
) -> ObjectCell<'gc> {
let mut object = ScriptObject::object(gc_context, Some(proto));
with_movie_clip_mut!(
gc_context,
object,
Some(fn_proto),
"nextFrame" => |movie_clip: &mut MovieClip<'gc>, context: &mut UpdateContext<'_, 'gc, '_>, _cell: DisplayNode<'gc>, _args| {
movie_clip.next_frame(context);
Value::Undefined
@ -105,6 +124,7 @@ pub fn create_movie_object<'gc>(gc_context: MutationContext<'gc, '_>) -> Object<
with_movie_clip!(
gc_context,
object,
Some(fn_proto),
"getBytesLoaded" => |_movie_clip: &MovieClip<'gc>, _args| {
// TODO find a correct value
1.0.into()
@ -118,26 +138,26 @@ pub fn create_movie_object<'gc>(gc_context: MutationContext<'gc, '_>) -> Object<
}
);
object.force_set_virtual(
object.add_property(
"_global",
Executable::Native(|avm, context, _this, _args| Ok(avm.global_object(context).into())),
Some(Executable::Native(overwrite_global)),
EnumSet::new(),
);
object.force_set_virtual(
object.add_property(
"_root",
Executable::Native(|avm, context, _this, _args| Ok(avm.root_object(context).into())),
Some(Executable::Native(overwrite_root)),
EnumSet::new(),
);
object.force_set_virtual(
object.add_property(
"_parent",
Executable::Native(|_avm, _context, this, _args| {
Ok(this
.read()
.display_node()
.as_display_node()
.and_then(|mc| mc.read().parent())
.and_then(|dn| dn.read().object().as_object().ok())
.map(|o| Value::Object(o.to_owned()))
@ -148,5 +168,5 @@ pub fn create_movie_object<'gc>(gc_context: MutationContext<'gc, '_>) -> Object<
EnumSet::new(),
);
object
GcCell::allocate(gc_context, Box::new(object))
}

View File

@ -0,0 +1,214 @@
//! Object prototype
use crate::avm1::property::Attribute::*;
use crate::avm1::return_value::ReturnValue;
use crate::avm1::{Avm1, Error, ObjectCell, UpdateContext, Value};
use enumset::EnumSet;
use gc_arena::{GcCell, MutationContext};
/// Implements `Object`
pub fn constructor<'gc>(
_avm: &mut Avm1<'gc>,
_action_context: &mut UpdateContext<'_, 'gc, '_>,
_this: ObjectCell<'gc>,
_args: &[Value<'gc>],
) -> Result<ReturnValue<'gc>, Error> {
Ok(Value::Undefined.into())
}
/// Implements `Object.prototype.addProperty`
pub fn add_property<'gc>(
_avm: &mut Avm1<'gc>,
context: &mut UpdateContext<'_, 'gc, '_>,
this: ObjectCell<'gc>,
args: &[Value<'gc>],
) -> Result<ReturnValue<'gc>, Error> {
let name = args.get(0).unwrap_or(&Value::Undefined);
let getter = args.get(1).unwrap_or(&Value::Undefined);
let setter = args.get(2).unwrap_or(&Value::Undefined);
match (name, getter) {
(Value::String(name), Value::Object(get)) if !name.is_empty() => {
if let Some(get_func) = get.read().as_executable() {
if let Value::Object(set) = setter {
if let Some(set_func) = set.read().as_executable() {
this.write(context.gc_context).add_property(
name,
get_func,
Some(set_func),
EnumSet::empty(),
);
} else {
return Ok(false.into());
}
} else if let Value::Null = setter {
this.write(context.gc_context).add_property(
name,
get_func,
None,
ReadOnly.into(),
);
} else {
return Ok(false.into());
}
}
Ok(false.into())
}
_ => Ok(false.into()),
}
}
/// Implements `Object.prototype.hasOwnProperty`
pub fn has_own_property<'gc>(
_avm: &mut Avm1<'gc>,
_action_context: &mut UpdateContext<'_, 'gc, '_>,
this: ObjectCell<'gc>,
args: &[Value<'gc>],
) -> Result<ReturnValue<'gc>, Error> {
match args.get(0) {
Some(Value::String(name)) => Ok(Value::Bool(this.read().has_own_property(name)).into()),
_ => Ok(Value::Bool(false).into()),
}
}
/// Implements `Object.prototype.toString`
fn to_string<'gc>(
_: &mut Avm1<'gc>,
_: &mut UpdateContext<'_, 'gc, '_>,
_: ObjectCell<'gc>,
_: &[Value<'gc>],
) -> Result<ReturnValue<'gc>, Error> {
Ok(ReturnValue::Immediate("[object Object]".into()))
}
/// Implements `Object.prototype.isPropertyEnumerable`
fn is_property_enumerable<'gc>(
_: &mut Avm1<'gc>,
_: &mut UpdateContext<'_, 'gc, '_>,
this: ObjectCell<'gc>,
args: &[Value<'gc>],
) -> Result<ReturnValue<'gc>, Error> {
match args.get(0) {
Some(Value::String(name)) => {
Ok(Value::Bool(this.read().is_property_enumerable(name)).into())
}
_ => Ok(Value::Bool(false).into()),
}
}
/// Implements `Object.prototype.isPrototypeOf`
fn is_prototype_of<'gc>(
_: &mut Avm1<'gc>,
_: &mut UpdateContext<'_, 'gc, '_>,
this: ObjectCell<'gc>,
args: &[Value<'gc>],
) -> Result<ReturnValue<'gc>, Error> {
match args.get(0) {
Some(val) => {
let ob = match val.as_object() {
Ok(ob) => ob,
Err(_) => return Ok(Value::Bool(false).into()),
};
let mut proto = ob.read().proto();
while let Some(proto_ob) = proto {
if GcCell::ptr_eq(this, proto_ob) {
return Ok(Value::Bool(true).into());
}
proto = proto_ob.read().proto();
}
Ok(Value::Bool(false).into())
}
_ => Ok(Value::Bool(false).into()),
}
}
/// Implements `Object.prototype.valueOf`
fn value_of<'gc>(
_: &mut Avm1<'gc>,
_: &mut UpdateContext<'_, 'gc, '_>,
this: ObjectCell<'gc>,
_: &[Value<'gc>],
) -> Result<ReturnValue<'gc>, Error> {
Ok(ReturnValue::Immediate(this.into()))
}
/// Partially construct `Object.prototype`.
///
/// `__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.
pub fn fill_proto<'gc>(
gc_context: MutationContext<'gc, '_>,
object_proto: ObjectCell<'gc>,
fn_proto: ObjectCell<'gc>,
) {
let mut ob_proto_write = object_proto.write(gc_context);
ob_proto_write
.as_script_object_mut()
.unwrap()
.force_set_function(
"addProperty",
add_property,
gc_context,
DontDelete | DontEnum,
Some(fn_proto),
);
ob_proto_write
.as_script_object_mut()
.unwrap()
.force_set_function(
"hasOwnProperty",
has_own_property,
gc_context,
DontDelete | DontEnum,
Some(fn_proto),
);
ob_proto_write
.as_script_object_mut()
.unwrap()
.force_set_function(
"isPropertyEnumerable",
is_property_enumerable,
gc_context,
DontDelete | DontEnum,
Some(fn_proto),
);
ob_proto_write
.as_script_object_mut()
.unwrap()
.force_set_function(
"isPrototypeOf",
is_prototype_of,
gc_context,
DontDelete | DontEnum,
Some(fn_proto),
);
ob_proto_write
.as_script_object_mut()
.unwrap()
.force_set_function(
"toString",
to_string,
gc_context,
DontDelete | DontEnum,
Some(fn_proto),
);
ob_proto_write
.as_script_object_mut()
.unwrap()
.force_set_function(
"valueOf",
value_of,
gc_context,
DontDelete | DontEnum,
Some(fn_proto),
);
}

View File

@ -1,681 +1,187 @@
use self::Attribute::*;
use crate::avm1::function::{Avm1Function, Executable, NativeFunction};
//! Object trait to expose objects to AVM
use crate::avm1::function::Executable;
use crate::avm1::property::Attribute;
use crate::avm1::return_value::ReturnValue;
use crate::avm1::{Avm1, Error, UpdateContext, Value};
use crate::avm1::{Avm1, Error, ScriptObject, UpdateContext, Value};
use crate::display_object::DisplayNode;
use core::fmt;
use enumset::{EnumSet, EnumSetType};
use gc_arena::{GcCell, MutationContext};
use std::collections::hash_map::Entry;
use std::collections::HashMap;
use std::mem::replace;
use enumset::EnumSet;
use gc_arena::{Collect, GcCell};
use std::collections::HashSet;
use std::fmt::Debug;
pub const TYPE_OF_OBJECT: &str = "object";
pub const TYPE_OF_FUNCTION: &str = "function";
pub const TYPE_OF_MOVIE_CLIP: &str = "movieclip";
pub type ObjectCell<'gc> = GcCell<'gc, Box<dyn Object<'gc> + 'gc>>;
fn default_to_string<'gc>(
_: &mut Avm1<'gc>,
_: &mut UpdateContext<'_, 'gc, '_>,
_: GcCell<'gc, Object<'gc>>,
_: &[Value<'gc>],
) -> Result<ReturnValue<'gc>, Error> {
Ok(ReturnValue::Immediate("[Object object]".into()))
}
#[derive(EnumSetType, Debug)]
pub enum Attribute {
DontDelete,
DontEnum,
ReadOnly,
}
#[derive(Clone)]
pub enum Property<'gc> {
Virtual {
get: Executable<'gc>,
set: Option<Executable<'gc>>,
attributes: EnumSet<Attribute>,
},
Stored {
value: Value<'gc>,
attributes: EnumSet<Attribute>,
},
}
impl<'gc> Property<'gc> {
/// Get the value of a property slot.
/// Represents an object that can be directly interacted with by the AVM
/// runtime.
pub trait Object<'gc>: 'gc + Collect + Debug {
/// Retrieve a named property from this object exclusively.
///
/// This function yields `None` if the value is being determined on the AVM
/// stack. Otherwise, if the value can be determined on the Rust stack,
/// then this function returns the value.
pub fn get(
&self,
avm: &mut Avm1<'gc>,
context: &mut UpdateContext<'_, 'gc, '_>,
this: GcCell<'gc, Object<'gc>>,
) -> Result<ReturnValue<'gc>, Error> {
match self {
Property::Virtual { get, .. } => get.exec(avm, context, this, &[]),
Property::Stored { value, .. } => Ok(value.to_owned().into()),
}
}
/// Set a property slot.
/// This function takes a redundant `this` parameter which should be
/// the object's own `GcCell`, so that it can pass it to user-defined
/// overrides that may need to interact with the underlying object.
///
/// This function returns `true` if the set has completed, or `false` if
/// it has not yet occured. If `false`, and you need to run code after the
/// set has occured, you must recursively execute the top-most frame via
/// `run_current_frame`.
pub fn set(
&mut self,
avm: &mut Avm1<'gc>,
context: &mut UpdateContext<'_, 'gc, '_>,
this: GcCell<'gc, Object<'gc>>,
new_value: impl Into<Value<'gc>>,
) -> Result<bool, Error> {
match self {
Property::Virtual { set, .. } => {
if let Some(function) = set {
let return_value = function.exec(avm, context, this, &[new_value.into()])?;
Ok(return_value.is_immediate())
} else {
Ok(true)
}
}
Property::Stored {
value, attributes, ..
} => {
if !attributes.contains(ReadOnly) {
replace::<Value<'gc>>(value, new_value.into());
}
Ok(true)
}
}
}
pub fn can_delete(&self) -> bool {
match self {
Property::Virtual { attributes, .. } => !attributes.contains(DontDelete),
Property::Stored { attributes, .. } => !attributes.contains(DontDelete),
}
}
pub fn is_enumerable(&self) -> bool {
match self {
Property::Virtual { attributes, .. } => !attributes.contains(DontEnum),
Property::Stored { attributes, .. } => !attributes.contains(DontEnum),
}
}
pub fn is_overwritable(&self) -> bool {
match self {
Property::Virtual {
attributes, set, ..
} => !attributes.contains(ReadOnly) && !set.is_none(),
Property::Stored { attributes, .. } => !attributes.contains(ReadOnly),
}
}
}
unsafe impl<'gc> gc_arena::Collect for Property<'gc> {
fn trace(&self, cc: gc_arena::CollectionContext) {
match self {
Property::Virtual { get, set, .. } => {
get.trace(cc);
set.trace(cc);
}
Property::Stored { value, .. } => value.trace(cc),
}
}
}
impl fmt::Debug for Property<'_> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
Property::Virtual {
get: _,
set,
attributes,
} => f
.debug_struct("Property::Virtual")
.field("get", &true)
.field("set", &set.is_some())
.field("attributes", &attributes)
.finish(),
Property::Stored { value, attributes } => f
.debug_struct("Property::Stored")
.field("value", &value)
.field("attributes", &attributes)
.finish(),
}
}
}
#[derive(Clone)]
pub struct Object<'gc> {
display_node: Option<DisplayNode<'gc>>,
values: HashMap<String, Property<'gc>>,
function: Option<Executable<'gc>>,
type_of: &'static str,
}
unsafe impl<'gc> gc_arena::Collect for Object<'gc> {
fn trace(&self, cc: gc_arena::CollectionContext) {
self.display_node.trace(cc);
self.values.trace(cc);
}
}
impl fmt::Debug for Object<'_> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.debug_struct("Object")
.field("display_node", &self.display_node)
.field("values", &self.values)
.field("function", &self.function.is_some())
.finish()
}
}
impl<'gc> Object<'gc> {
pub fn object(gc_context: MutationContext<'gc, '_>) -> Self {
let mut result = Self {
type_of: TYPE_OF_OBJECT,
display_node: None,
values: HashMap::new(),
function: None,
};
result.force_set_function(
"toString",
default_to_string,
gc_context,
DontDelete | DontEnum,
);
result
}
/// Constructs an object with no values, not even builtins.
///
/// Intended for constructing scope chains, since they exclusively use the
/// object values, but can't just have a hashmap because of `with` and
/// friends.
pub fn bare_object() -> Self {
Self {
type_of: TYPE_OF_OBJECT,
display_node: None,
values: HashMap::new(),
function: None,
}
}
pub fn native_function(function: NativeFunction<'gc>) -> Self {
Self {
type_of: TYPE_OF_FUNCTION,
function: Some(Executable::Native(function)),
display_node: None,
values: HashMap::new(),
}
}
pub fn action_function(func: Avm1Function<'gc>) -> Self {
Self {
type_of: TYPE_OF_FUNCTION,
function: Some(Executable::Action(func)),
display_node: None,
values: HashMap::new(),
}
}
pub fn set_display_node(&mut self, display_node: DisplayNode<'gc>) {
self.display_node = Some(display_node);
}
pub fn display_node(&self) -> Option<DisplayNode<'gc>> {
self.display_node
}
pub fn set(
&mut self,
name: &str,
value: impl Into<Value<'gc>>,
avm: &mut Avm1<'gc>,
context: &mut UpdateContext<'_, 'gc, '_>,
this: GcCell<'gc, Object<'gc>>,
) -> Result<(), Error> {
match self.values.entry(name.to_owned()) {
Entry::Occupied(mut entry) => {
entry.get_mut().set(avm, context, this, value)?;
Ok(())
}
Entry::Vacant(entry) => {
entry.insert(Property::Stored {
value: value.into(),
attributes: Default::default(),
});
Ok(())
}
}
}
pub fn force_set_virtual<A>(
&mut self,
name: &str,
get: Executable<'gc>,
set: Option<Executable<'gc>>,
attributes: A,
) where
A: Into<EnumSet<Attribute>>,
{
self.values.insert(
name.to_owned(),
Property::Virtual {
get,
set,
attributes: attributes.into(),
},
);
}
pub fn force_set<A>(&mut self, name: &str, value: impl Into<Value<'gc>>, attributes: A)
where
A: Into<EnumSet<Attribute>>,
{
self.values.insert(
name.to_string(),
Property::Stored {
value: value.into(),
attributes: attributes.into(),
},
);
}
pub fn force_set_function<A>(
&mut self,
name: &str,
function: NativeFunction<'gc>,
gc_context: MutationContext<'gc, '_>,
attributes: A,
) where
A: Into<EnumSet<Attribute>>,
{
self.force_set(
name,
GcCell::allocate(gc_context, Object::native_function(function)),
attributes,
)
}
/// Get the value of a particular property on this object.
///
/// The `avm`, `context`, and `this` parameters exist so that this object
/// can call virtual properties. Likewise, this function returns a
/// `ReturnValue` which allows pulling data from the return values of user
/// functions.
pub fn get(
/// This function should not inspect prototype chains. Instead, use `get`
/// to do ordinary property look-up and resolution.
fn get_local(
&self,
name: &str,
avm: &mut Avm1<'gc>,
context: &mut UpdateContext<'_, 'gc, '_>,
this: GcCell<'gc, Object<'gc>>,
) -> Result<ReturnValue<'gc>, Error> {
if let Some(value) = self.values.get(name) {
return value.get(avm, context, this);
}
this: ObjectCell<'gc>,
) -> Result<ReturnValue<'gc>, Error>;
Ok(Value::Undefined.into())
}
/// Delete a given value off the object.
pub fn delete(&mut self, name: &str) -> bool {
if let Some(prop) = self.values.get(name) {
if prop.can_delete() {
self.values.remove(name);
return true;
}
}
false
}
pub fn has_property(&self, name: &str) -> bool {
self.values.contains_key(name)
}
pub fn has_own_property(&self, name: &str) -> bool {
self.values.contains_key(name)
}
pub fn is_property_overwritable(&self, name: &str) -> bool {
self.values
.get(name)
.map(|p| p.is_overwritable())
.unwrap_or(false)
}
pub fn get_keys(&self) -> Vec<String> {
self.values
.iter()
.filter_map(|(k, p)| {
if p.is_enumerable() {
Some(k.to_string())
} else {
None
}
})
.collect()
}
pub fn call(
/// Retrieve a named property from the object, or it's prototype.
fn get(
&self,
name: &str,
avm: &mut Avm1<'gc>,
context: &mut UpdateContext<'_, 'gc, '_>,
this: GcCell<'gc, Object<'gc>>,
args: &[Value<'gc>],
this: ObjectCell<'gc>,
) -> Result<ReturnValue<'gc>, Error> {
if let Some(function) = &self.function {
function.exec(avm, context, this, args)
if self.has_own_property(name) {
self.get_local(name, avm, context, this)
} else {
let mut depth = 0;
let mut proto = self.proto();
while proto.is_some() {
if depth == 255 {
return Err("Encountered an excessively deep prototype chain.".into());
}
if proto.unwrap().read().has_own_property(name) {
return proto.unwrap().read().get_local(name, avm, context, this);
}
proto = proto.unwrap().read().proto();
depth += 1;
}
Ok(Value::Undefined.into())
}
}
pub fn as_string(&self) -> String {
if self.function.is_some() {
"[type Function]".to_string()
} else {
"[object Object]".to_string()
}
}
/// Set a named property on this object, or it's prototype.
///
/// This function takes a redundant `this` parameter which should be
/// the object's own `GcCell`, so that it can pass it to user-defined
/// overrides that may need to interact with the underlying object.
fn set(
&mut self,
name: &str,
value: Value<'gc>,
avm: &mut Avm1<'gc>,
context: &mut UpdateContext<'_, 'gc, '_>,
this: ObjectCell<'gc>,
) -> Result<(), Error>;
pub fn set_type_of(&mut self, type_of: &'static str) {
self.type_of = type_of;
}
/// Call the underlying object.
///
/// This function takes a redundant `this` parameter which should be
/// the object's own `GcCell`, so that it can pass it to user-defined
/// overrides that may need to interact with the underlying object.
fn call(
&self,
avm: &mut Avm1<'gc>,
context: &mut UpdateContext<'_, 'gc, '_>,
this: ObjectCell<'gc>,
args: &[Value<'gc>],
) -> Result<ReturnValue<'gc>, Error>;
pub fn type_of(&self) -> &'static str {
self.type_of
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::avm1::activation::Activation;
use crate::backend::audio::NullAudioBackend;
use crate::backend::navigator::NullNavigatorBackend;
use crate::backend::render::NullRenderer;
use crate::display_object::{DisplayObject, MovieClip};
use crate::library::Library;
use crate::prelude::*;
use gc_arena::rootless_arena;
use rand::{rngs::SmallRng, SeedableRng};
use std::sync::Arc;
fn with_object<F, R>(swf_version: u8, test: F) -> R
where
F: for<'a, 'gc> FnOnce(
&mut Avm1<'gc>,
&mut UpdateContext<'a, 'gc, '_>,
GcCell<'gc, Object<'gc>>,
) -> R,
{
rootless_arena(|gc_context| {
let mut avm = Avm1::new(gc_context, swf_version);
let movie_clip: Box<dyn DisplayObject> =
Box::new(MovieClip::new(swf_version, gc_context));
let root = GcCell::allocate(gc_context, movie_clip);
let mut context = UpdateContext {
gc_context,
global_time: 0,
player_version: 32,
swf_version,
root,
start_clip: root,
active_clip: root,
target_clip: Some(root),
target_path: Value::Undefined,
rng: &mut SmallRng::from_seed([0u8; 16]),
action_queue: &mut crate::context::ActionQueue::new(),
audio: &mut NullAudioBackend::new(),
background_color: &mut Color {
r: 0,
g: 0,
b: 0,
a: 0,
},
library: &mut Library::new(),
navigator: &mut NullNavigatorBackend::new(),
renderer: &mut NullRenderer::new(),
swf_data: &mut Arc::new(vec![]),
};
let object = GcCell::allocate(gc_context, Object::object(gc_context));
let globals = avm.global_object_cell();
avm.insert_stack_frame(GcCell::allocate(
gc_context,
Activation::from_nothing(swf_version, globals, gc_context),
));
test(&mut avm, &mut context, object)
})
}
#[test]
fn test_get_undefined() {
with_object(0, |avm, context, object| {
assert_eq!(
object
.read()
.get("not_defined", avm, context, object)
.unwrap(),
Value::Undefined.into()
);
})
}
#[test]
fn test_set_get() {
with_object(0, |avm, context, object| {
object
.write(context.gc_context)
.force_set("forced", "forced", EnumSet::empty());
object
.write(context.gc_context)
.set("natural", "natural", avm, context, object)
.unwrap();
assert_eq!(
object.read().get("forced", avm, context, object).unwrap(),
ReturnValue::Immediate("forced".into())
);
assert_eq!(
object.read().get("natural", avm, context, object).unwrap(),
ReturnValue::Immediate("natural".into())
);
})
}
#[test]
fn test_set_readonly() {
with_object(0, |avm, context, object| {
object
.write(context.gc_context)
.force_set("normal", "initial", EnumSet::empty());
object
.write(context.gc_context)
.force_set("readonly", "initial", ReadOnly);
object
.write(context.gc_context)
.set("normal", "replaced", avm, context, object)
.unwrap();
object
.write(context.gc_context)
.set("readonly", "replaced", avm, context, object)
.unwrap();
assert_eq!(
object.read().get("normal", avm, context, object).unwrap(),
ReturnValue::Immediate("replaced".into())
);
assert_eq!(
object.read().get("readonly", avm, context, object).unwrap(),
ReturnValue::Immediate("initial".into())
);
})
}
#[test]
fn test_deletable_not_readonly() {
with_object(0, |avm, context, object| {
object
.write(context.gc_context)
.force_set("test", "initial", DontDelete);
assert_eq!(object.write(context.gc_context).delete("test"), false);
assert_eq!(
object.read().get("test", avm, context, object).unwrap(),
ReturnValue::Immediate("initial".into())
);
object
.write(context.gc_context)
.set("test", "replaced", avm, context, object)
.unwrap();
assert_eq!(object.write(context.gc_context).delete("test"), false);
assert_eq!(
object.read().get("test", avm, context, object).unwrap(),
ReturnValue::Immediate("replaced".into())
);
})
}
#[test]
fn test_virtual_get() {
with_object(0, |avm, context, object| {
let getter = Executable::Native(|_avm, _context, _this, _args| {
Ok(ReturnValue::Immediate("Virtual!".into()))
});
object.write(context.gc_context).force_set_virtual(
"test",
getter,
None,
EnumSet::empty(),
);
assert_eq!(
object.read().get("test", avm, context, object).unwrap(),
ReturnValue::Immediate("Virtual!".into())
);
// This set should do nothing
object
.write(context.gc_context)
.set("test", "Ignored!", avm, context, object)
.unwrap();
assert_eq!(
object.read().get("test", avm, context, object).unwrap(),
ReturnValue::Immediate("Virtual!".into())
);
})
}
#[test]
fn test_delete() {
with_object(0, |avm, context, object| {
let getter = Executable::Native(|_avm, _context, _this, _args| {
Ok(ReturnValue::Immediate("Virtual!".into()))
});
object.write(context.gc_context).force_set_virtual(
"virtual",
getter.clone(),
None,
EnumSet::empty(),
);
object.write(context.gc_context).force_set_virtual(
"virtual_un",
getter,
None,
DontDelete,
);
object
.write(context.gc_context)
.force_set("stored", "Stored!", EnumSet::empty());
object
.write(context.gc_context)
.force_set("stored_un", "Stored!", DontDelete);
assert_eq!(object.write(context.gc_context).delete("virtual"), true);
assert_eq!(object.write(context.gc_context).delete("virtual_un"), false);
assert_eq!(object.write(context.gc_context).delete("stored"), true);
assert_eq!(object.write(context.gc_context).delete("stored_un"), false);
assert_eq!(
object.write(context.gc_context).delete("non_existent"),
false
);
assert_eq!(
object.read().get("virtual", avm, context, object).unwrap(),
Value::Undefined.into()
);
assert_eq!(
object
.read()
.get("virtual_un", avm, context, object)
.unwrap(),
ReturnValue::Immediate("Virtual!".into())
);
assert_eq!(
object.read().get("stored", avm, context, object).unwrap(),
Value::Undefined.into()
);
assert_eq!(
object
.read()
.get("stored_un", avm, context, object)
.unwrap(),
ReturnValue::Immediate("Stored!".into())
);
})
}
#[test]
fn test_iter_values() {
with_object(0, |_avm, context, object| {
let getter = Executable::Native(|_avm, _context, _this, _args| Ok(Value::Null.into()));
object
.write(context.gc_context)
.force_set("stored", Value::Null, EnumSet::empty());
object
.write(context.gc_context)
.force_set("stored_hidden", Value::Null, DontEnum);
object.write(context.gc_context).force_set_virtual(
"virtual",
getter.clone(),
None,
EnumSet::empty(),
);
object.write(context.gc_context).force_set_virtual(
"virtual_hidden",
getter,
None,
DontEnum,
);
let keys = object.read().get_keys();
assert_eq!(keys.len(), 2);
assert_eq!(keys.contains(&"stored".to_string()), true);
assert_eq!(keys.contains(&"stored_hidden".to_string()), false);
assert_eq!(keys.contains(&"virtual".to_string()), true);
assert_eq!(keys.contains(&"virtual_hidden".to_string()), false);
})
}
/// Construct a host object of some kind and return it's cell.
///
/// As the first step in object construction, the `new` method is called on
/// the prototype to initialize an object. The prototype may construct any
/// object implementation it wants, with itself as the new object's proto.
/// Then, the constructor is `call`ed with the new object as `this` to
/// initialize the object.
///
/// The arguments passed to the constructor are provided here; however, all
/// object construction should happen in `call`, not `new`. `new` exists
/// purely so that host objects can be constructed by the VM.
fn new(
&self,
avm: &mut Avm1<'gc>,
context: &mut UpdateContext<'_, 'gc, '_>,
this: ObjectCell<'gc>,
args: &[Value<'gc>],
) -> Result<ObjectCell<'gc>, Error>;
/// Delete a named property from the object.
///
/// Returns false if the property cannot be deleted.
fn delete(&mut self, name: &str) -> bool;
/// Retrieve the `__proto__` of a given object.
///
/// The proto is another object used to resolve methods across a class of
/// multiple objects. It should also be accessible as `__proto__` from
/// `get`.
fn proto(&self) -> Option<ObjectCell<'gc>>;
/// Define a value on an object.
///
/// Unlike setting a value, this function is intended to replace any
/// existing virtual or built-in properties already installed on a given
/// object. As such, this should not run any setters; the resulting name
/// slot should either be completely replaced with the value or completely
/// untouched.
///
/// It is not guaranteed that all objects accept value definitions,
/// especially if a property name conflicts with a built-in property, such
/// as `__proto__`.
fn define_value(&mut self, name: &str, value: Value<'gc>, attributes: EnumSet<Attribute>);
/// Define a virtual property onto a given object.
///
/// A virtual property is a set of get/set functions that are called when a
/// given named property is retrieved or stored on an object. These
/// functions are then responsible for providing or accepting the value
/// that is given to or taken from the AVM.
///
/// It is not guaranteed that all objects accept virtual properties,
/// especially if a property name conflicts with a built-in property, such
/// as `__proto__`.
fn add_property(
&mut self,
name: &str,
get: Executable<'gc>,
set: Option<Executable<'gc>>,
attributes: EnumSet<Attribute>,
);
/// Checks if the object has a given named property.
fn has_property(&self, name: &str) -> bool;
/// Checks if the object has a given named property on itself (and not,
/// say, the object's prototype or superclass)
fn has_own_property(&self, name: &str) -> bool;
/// Checks if a named property can be overwritten.
fn is_property_overwritable(&self, name: &str) -> bool;
/// Checks if a named property appears when enumerating the object.
fn is_property_enumerable(&self, name: &str) -> bool;
/// Enumerate the object.
fn get_keys(&self) -> HashSet<String>;
/// Coerce the object into a string.
fn as_string(&self) -> String;
/// Get the object's type string.
fn type_of(&self) -> &'static str;
/// Get the underlying script object, if it exists.
fn as_script_object(&self) -> Option<&ScriptObject<'gc>>;
/// Get the underlying script object, if it exists.
fn as_script_object_mut(&mut self) -> Option<&mut ScriptObject<'gc>>;
/// Get the underlying display node for this object, if it exists.
fn as_display_node(&self) -> Option<DisplayNode<'gc>>;
/// Get the underlying executable for this object, if it exists.
fn as_executable(&self) -> Option<Executable<'gc>>;
}

138
core/src/avm1/property.rs Normal file
View File

@ -0,0 +1,138 @@
//! User-defined properties
use self::Attribute::*;
use crate::avm1::function::Executable;
use crate::avm1::return_value::ReturnValue;
use crate::avm1::{Avm1, Error, ObjectCell, UpdateContext, Value};
use core::fmt;
use enumset::{EnumSet, EnumSetType};
use std::mem::replace;
#[derive(EnumSetType, Debug)]
pub enum Attribute {
DontDelete,
DontEnum,
ReadOnly,
}
#[derive(Clone)]
pub enum Property<'gc> {
Virtual {
get: Executable<'gc>,
set: Option<Executable<'gc>>,
attributes: EnumSet<Attribute>,
},
Stored {
value: Value<'gc>,
attributes: EnumSet<Attribute>,
},
}
impl<'gc> Property<'gc> {
/// Get the value of a property slot.
///
/// This function yields `ReturnValue` because some properties may be
/// user-defined.
pub fn get(
&self,
avm: &mut Avm1<'gc>,
context: &mut UpdateContext<'_, 'gc, '_>,
this: ObjectCell<'gc>,
) -> Result<ReturnValue<'gc>, Error> {
match self {
Property::Virtual { get, .. } => get.exec(avm, context, this, &[]),
Property::Stored { value, .. } => Ok(value.to_owned().into()),
}
}
/// Set a property slot.
///
/// This function returns `true` if the set has completed, or `false` if
/// it has not yet occured. If `false`, and you need to run code after the
/// set has occured, you must recursively execute the top-most frame via
/// `run_current_frame`.
pub fn set(
&mut self,
avm: &mut Avm1<'gc>,
context: &mut UpdateContext<'_, 'gc, '_>,
this: ObjectCell<'gc>,
new_value: impl Into<Value<'gc>>,
) -> Result<bool, Error> {
match self {
Property::Virtual { set, .. } => {
if let Some(function) = set {
let return_value = function.exec(avm, context, this, &[new_value.into()])?;
Ok(return_value.is_immediate())
} else {
Ok(true)
}
}
Property::Stored {
value, attributes, ..
} => {
if !attributes.contains(ReadOnly) {
replace::<Value<'gc>>(value, new_value.into());
}
Ok(true)
}
}
}
pub fn can_delete(&self) -> bool {
match self {
Property::Virtual { attributes, .. } => !attributes.contains(DontDelete),
Property::Stored { attributes, .. } => !attributes.contains(DontDelete),
}
}
pub fn is_enumerable(&self) -> bool {
match self {
Property::Virtual { attributes, .. } => !attributes.contains(DontEnum),
Property::Stored { attributes, .. } => !attributes.contains(DontEnum),
}
}
pub fn is_overwritable(&self) -> bool {
match self {
Property::Virtual {
attributes, set, ..
} => !attributes.contains(ReadOnly) && !set.is_none(),
Property::Stored { attributes, .. } => !attributes.contains(ReadOnly),
}
}
}
unsafe impl<'gc> gc_arena::Collect for Property<'gc> {
fn trace(&self, cc: gc_arena::CollectionContext) {
match self {
Property::Virtual { get, set, .. } => {
get.trace(cc);
set.trace(cc);
}
Property::Stored { value, .. } => value.trace(cc),
}
}
}
impl fmt::Debug for Property<'_> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
Property::Virtual {
get: _,
set,
attributes,
} => f
.debug_struct("Property::Virtual")
.field("get", &true)
.field("set", &set.is_some())
.field("attributes", &attributes)
.finish(),
Property::Stored { value, attributes } => f
.debug_struct("Property::Stored")
.field("value", &value)
.field("attributes", &attributes)
.finish(),
}
}
}

View File

@ -1,7 +1,7 @@
//! Return value enum
use crate::avm1::activation::Activation;
use crate::avm1::{Avm1, Error, Object, Value};
use crate::avm1::{Avm1, Error, ObjectCell, Value};
use crate::context::UpdateContext;
use gc_arena::{Collect, GcCell};
use std::fmt;
@ -99,12 +99,6 @@ impl<'gc> ReturnValue<'gc> {
}
}
/// Consumes the given return value.
///
/// This exists primarily so that users of return values can indicate that
/// they do not plan to use them.
pub fn ignore(self) {}
pub fn is_immediate(&self) -> bool {
use ReturnValue::*;
@ -152,8 +146,8 @@ impl<'gc> From<bool> for ReturnValue<'gc> {
}
}
impl<'gc> From<GcCell<'gc, Object<'gc>>> for ReturnValue<'gc> {
fn from(object: GcCell<'gc, Object<'gc>>) -> Self {
impl<'gc> From<ObjectCell<'gc>> for ReturnValue<'gc> {
fn from(object: ObjectCell<'gc>) -> Self {
ReturnValue::Immediate(Value::Object(object))
}
}

View File

@ -1,7 +1,7 @@
//! Represents AVM1 scope chain resolution.
use crate::avm1::return_value::ReturnValue;
use crate::avm1::{Avm1, Error, Object, UpdateContext, Value};
use crate::avm1::{Avm1, Error, Object, ObjectCell, ScriptObject, UpdateContext, Value};
use enumset::EnumSet;
use gc_arena::{GcCell, MutationContext};
use std::cell::{Ref, RefMut};
@ -30,7 +30,7 @@ pub enum ScopeClass {
pub struct Scope<'gc> {
parent: Option<GcCell<'gc, Scope<'gc>>>,
class: ScopeClass,
values: GcCell<'gc, Object<'gc>>,
values: ObjectCell<'gc>,
}
unsafe impl<'gc> gc_arena::Collect for Scope<'gc> {
@ -43,7 +43,7 @@ unsafe impl<'gc> gc_arena::Collect for Scope<'gc> {
impl<'gc> Scope<'gc> {
/// Construct a global scope (one without a parent).
pub fn from_global_object(globals: GcCell<'gc, Object<'gc>>) -> Scope<'gc> {
pub fn from_global_object(globals: ObjectCell<'gc>) -> Scope<'gc> {
Scope {
parent: None,
class: ScopeClass::Global,
@ -56,7 +56,7 @@ impl<'gc> Scope<'gc> {
Scope {
parent: Some(parent),
class: ScopeClass::Local,
values: GcCell::allocate(mc, Object::bare_object()),
values: ScriptObject::object_cell(mc, None),
}
}
@ -109,7 +109,7 @@ impl<'gc> Scope<'gc> {
Self {
parent: None,
class: ScopeClass::Global,
values: GcCell::allocate(mc, Object::bare_object()),
values: ScriptObject::object_cell(mc, None),
},
)
})
@ -119,7 +119,7 @@ impl<'gc> Scope<'gc> {
/// scope has been replaced with another given object.
pub fn new_target_scope(
mut parent: GcCell<'gc, Self>,
clip: GcCell<'gc, Object<'gc>>,
clip: ObjectCell<'gc>,
mc: MutationContext<'gc, '_>,
) -> GcCell<'gc, Self> {
let mut bottom_scope = None;
@ -163,7 +163,7 @@ impl<'gc> Scope<'gc> {
Self {
parent: None,
class: ScopeClass::Global,
values: GcCell::allocate(mc, Object::bare_object()),
values: ScriptObject::object_cell(mc, None),
},
)
})
@ -176,7 +176,7 @@ impl<'gc> Scope<'gc> {
/// scope. This requires some scope chain juggling.
pub fn new_with_scope(
locals: GcCell<'gc, Self>,
with_object: GcCell<'gc, Object<'gc>>,
with_object: ObjectCell<'gc>,
mc: MutationContext<'gc, '_>,
) -> GcCell<'gc, Self> {
let parent_scope = locals.read().parent;
@ -204,7 +204,7 @@ impl<'gc> Scope<'gc> {
pub fn new(
parent: GcCell<'gc, Self>,
class: ScopeClass,
with_object: GcCell<'gc, Object<'gc>>,
with_object: ObjectCell<'gc>,
) -> Scope<'gc> {
Scope {
parent: Some(parent),
@ -214,17 +214,17 @@ impl<'gc> Scope<'gc> {
}
/// Returns a reference to the current local scope object.
pub fn locals(&self) -> Ref<Object<'gc>> {
pub fn locals(&self) -> Ref<Box<dyn Object<'gc>>> {
self.values.read()
}
/// Returns a gc cell of the current local scope object.
pub fn locals_cell(&self) -> GcCell<'gc, Object<'gc>> {
pub fn locals_cell(&self) -> ObjectCell<'gc> {
self.values.to_owned()
}
/// Returns a reference to the current local scope object for mutation.
pub fn locals_mut(&self, mc: MutationContext<'gc, '_>) -> RefMut<Object<'gc>> {
pub fn locals_mut(&self, mc: MutationContext<'gc, '_>) -> RefMut<Box<dyn Object<'gc>>> {
self.values.write(mc)
}
@ -246,7 +246,7 @@ impl<'gc> Scope<'gc> {
name: &str,
avm: &mut Avm1<'gc>,
context: &mut UpdateContext<'_, 'gc, '_>,
this: GcCell<'gc, Object<'gc>>,
this: ObjectCell<'gc>,
) -> Result<ReturnValue<'gc>, Error> {
if self.locals().has_property(name) {
return self.locals().get(name, avm, context, this);
@ -286,7 +286,7 @@ impl<'gc> Scope<'gc> {
value: Value<'gc>,
avm: &mut Avm1<'gc>,
context: &mut UpdateContext<'_, 'gc, '_>,
this: GcCell<'gc, Object<'gc>>,
this: ObjectCell<'gc>,
) -> Result<Option<Value<'gc>>, Error> {
if self.locals().has_property(name) && self.locals().is_property_overwritable(name) {
self.locals_mut(context.gc_context)
@ -308,7 +308,8 @@ impl<'gc> Scope<'gc> {
/// chain. As a result, this function always force sets a property on the
/// local object and does not traverse the scope chain.
pub fn define(&self, name: &str, value: impl Into<Value<'gc>>, mc: MutationContext<'gc, '_>) {
self.locals_mut(mc).force_set(name, value, EnumSet::empty());
self.locals_mut(mc)
.define_value(name, value.into(), EnumSet::empty());
}
/// Delete a value from scope

View File

@ -0,0 +1,707 @@
use crate::avm1::function::{Executable, NativeFunction};
use crate::avm1::property::{Attribute, Property};
use crate::avm1::return_value::ReturnValue;
use crate::avm1::{Avm1, Error, Object, ObjectCell, UpdateContext, Value};
use crate::display_object::DisplayNode;
use core::fmt;
use enumset::EnumSet;
use gc_arena::{GcCell, MutationContext};
use std::collections::hash_map::Entry;
use std::collections::{HashMap, HashSet};
pub const TYPE_OF_OBJECT: &str = "object";
pub const TYPE_OF_FUNCTION: &str = "function";
pub const TYPE_OF_MOVIE_CLIP: &str = "movieclip";
#[derive(Clone)]
pub struct ScriptObject<'gc> {
prototype: Option<ObjectCell<'gc>>,
display_node: Option<DisplayNode<'gc>>,
values: HashMap<String, Property<'gc>>,
function: Option<Executable<'gc>>,
type_of: &'static str,
}
unsafe impl<'gc> gc_arena::Collect for ScriptObject<'gc> {
fn trace(&self, cc: gc_arena::CollectionContext) {
self.prototype.trace(cc);
self.display_node.trace(cc);
self.values.trace(cc);
self.function.trace(cc);
}
}
impl fmt::Debug for ScriptObject<'_> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.debug_struct("Object")
.field("prototype", &self.prototype)
.field("display_node", &self.display_node)
.field("values", &self.values)
.field("function", &self.function.is_some())
.finish()
}
}
impl<'gc> ScriptObject<'gc> {
pub fn object(
_gc_context: MutationContext<'gc, '_>,
proto: Option<ObjectCell<'gc>>,
) -> ScriptObject<'gc> {
ScriptObject {
prototype: proto,
type_of: TYPE_OF_OBJECT,
display_node: None,
values: HashMap::new(),
function: None,
}
}
/// Constructs and allocates an empty but normal object in one go.
pub fn object_cell(
gc_context: MutationContext<'gc, '_>,
proto: Option<ObjectCell<'gc>>,
) -> ObjectCell<'gc> {
GcCell::allocate(
gc_context,
Box::new(ScriptObject {
prototype: proto,
type_of: TYPE_OF_OBJECT,
display_node: None,
values: HashMap::new(),
function: None,
}),
)
}
/// Constructs an object with no values, not even builtins.
///
/// Intended for constructing scope chains, since they exclusively use the
/// object values, but can't just have a hashmap because of `with` and
/// friends.
pub fn bare_object() -> Self {
ScriptObject {
prototype: None,
type_of: TYPE_OF_OBJECT,
display_node: None,
values: HashMap::new(),
function: None,
}
}
/// Construct a function sans prototype.
pub fn bare_function(
function: impl Into<Executable<'gc>>,
fn_proto: Option<ObjectCell<'gc>>,
) -> Self {
ScriptObject {
prototype: fn_proto,
type_of: TYPE_OF_FUNCTION,
function: Some(function.into()),
display_node: None,
values: HashMap::new(),
}
}
/// Construct a function from an executable and associated protos.
///
/// Since prototypes need to link back to themselves, this function builds
/// both objects itself and returns the function to you, fully allocated.
///
/// `fn_proto` refers to the implicit proto of the function object, and the
/// `prototype` refers to the explicit prototype of the function. If
/// provided, the function and it's prototype will be linked to each other.
pub fn function(
gc_context: MutationContext<'gc, '_>,
function: impl Into<Executable<'gc>>,
fn_proto: Option<ObjectCell<'gc>>,
prototype: Option<ObjectCell<'gc>>,
) -> ObjectCell<'gc> {
let function = GcCell::allocate(
gc_context,
Box::new(Self::bare_function(function, fn_proto)) as Box<dyn Object<'gc> + 'gc>,
);
//TODO: Can we make these proper sets or no?
if let Some(p) = prototype {
p.write(gc_context).define_value(
"constructor",
Value::Object(function),
Attribute::DontEnum.into(),
);
function
.write(gc_context)
.define_value("prototype", p.into(), EnumSet::empty());
}
function
}
pub fn set_display_node(&mut self, display_node: DisplayNode<'gc>) {
self.display_node = Some(display_node);
}
pub fn display_node(&self) -> Option<DisplayNode<'gc>> {
self.display_node
}
/// Declare a native function on the current object.
///
/// This is intended for use with defining host object prototypes. Notably,
/// this creates a function object without an explicit `prototype`, which
/// is only possible when defining host functions. User-defined functions
/// always get a fresh explicit prototype, so you should never force set a
/// user-defined function.
pub fn force_set_function<A>(
&mut self,
name: &str,
function: NativeFunction<'gc>,
gc_context: MutationContext<'gc, '_>,
attributes: A,
fn_proto: Option<ObjectCell<'gc>>,
) where
A: Into<EnumSet<Attribute>>,
{
self.define_value(
name,
Value::Object(ScriptObject::function(gc_context, function, fn_proto, None)),
attributes.into(),
)
}
pub fn set_prototype(&mut self, prototype: ObjectCell<'gc>) {
self.prototype = Some(prototype);
}
pub fn set_type_of(&mut self, type_of: &'static str) {
self.type_of = type_of;
}
}
impl<'gc> Object<'gc> for ScriptObject<'gc> {
/// Get the value of a particular property on this object.
///
/// The `avm`, `context`, and `this` parameters exist so that this object
/// can call virtual properties. Furthermore, since some virtual properties
/// may resolve on the AVM stack, this function may return `None` instead
/// of a `Value`. *This is not equivalent to `undefined`.* Instead, it is a
/// signal that your value will be returned on the ActionScript stack, and
/// that you should register a stack continuation in order to get it.
fn get_local(
&self,
name: &str,
avm: &mut Avm1<'gc>,
context: &mut UpdateContext<'_, 'gc, '_>,
this: ObjectCell<'gc>,
) -> Result<ReturnValue<'gc>, Error> {
if name == "__proto__" {
return Ok(self
.prototype
.map_or(Value::Undefined, Value::Object)
.into());
}
if let Some(value) = self.values.get(name) {
return value.get(avm, context, this);
}
Ok(Value::Undefined.into())
}
/// Set a named property on the object.
///
/// This function takes a redundant `this` parameter which should be
/// the object's own `GcCell`, so that it can pass it to user-defined
/// overrides that may need to interact with the underlying object.
fn set(
&mut self,
name: &str,
value: Value<'gc>,
avm: &mut Avm1<'gc>,
context: &mut UpdateContext<'_, 'gc, '_>,
this: ObjectCell<'gc>,
) -> Result<(), Error> {
if name == "__proto__" {
self.prototype = value.as_object().ok().to_owned();
} else {
match self.values.entry(name.to_owned()) {
Entry::Occupied(mut entry) => {
entry.get_mut().set(avm, context, this, value)?;
}
Entry::Vacant(entry) => {
entry.insert(Property::Stored {
value,
attributes: Default::default(),
});
}
}
}
Ok(())
}
/// Call the underlying object.
///
/// This function takes a redundant `this` parameter which should be
/// the object's own `GcCell`, so that it can pass it to user-defined
/// overrides that may need to interact with the underlying object.
fn call(
&self,
avm: &mut Avm1<'gc>,
context: &mut UpdateContext<'_, 'gc, '_>,
this: ObjectCell<'gc>,
args: &[Value<'gc>],
) -> Result<ReturnValue<'gc>, Error> {
if let Some(function) = &self.function {
function.exec(avm, context, this, args)
} else {
Ok(Value::Undefined.into())
}
}
#[allow(clippy::new_ret_no_self)]
fn new(
&self,
_avm: &mut Avm1<'gc>,
context: &mut UpdateContext<'_, 'gc, '_>,
this: ObjectCell<'gc>,
_args: &[Value<'gc>],
) -> Result<ObjectCell<'gc>, Error> {
Ok(GcCell::allocate(
context.gc_context,
Box::new(ScriptObject::object(context.gc_context, Some(this))) as Box<dyn Object<'gc>>,
))
}
/// Delete a named property from the object.
///
/// Returns false if the property cannot be deleted.
fn delete(&mut self, name: &str) -> bool {
if let Some(prop) = self.values.get(name) {
if prop.can_delete() {
self.values.remove(name);
return true;
}
}
false
}
fn add_property(
&mut self,
name: &str,
get: Executable<'gc>,
set: Option<Executable<'gc>>,
attributes: EnumSet<Attribute>,
) {
self.values.insert(
name.to_owned(),
Property::Virtual {
get,
set,
attributes,
},
);
}
fn define_value(&mut self, name: &str, value: Value<'gc>, attributes: EnumSet<Attribute>) {
self.values
.insert(name.to_string(), Property::Stored { value, attributes });
}
fn proto(&self) -> Option<ObjectCell<'gc>> {
self.prototype
}
/// Checks if the object has a given named property.
fn has_property(&self, name: &str) -> bool {
self.has_own_property(name)
|| self
.prototype
.as_ref()
.map_or(false, |p| p.read().has_property(name))
}
/// Checks if the object has a given named property on itself (and not,
/// say, the object's prototype or superclass)
fn has_own_property(&self, name: &str) -> bool {
if name == "__proto__" {
return true;
}
self.values.contains_key(name)
}
fn is_property_overwritable(&self, name: &str) -> bool {
self.values
.get(name)
.map(|p| p.is_overwritable())
.unwrap_or(false)
}
/// Checks if a named property appears when enumerating the object.
fn is_property_enumerable(&self, name: &str) -> bool {
if let Some(prop) = self.values.get(name) {
prop.is_enumerable()
} else {
false
}
}
/// Enumerate the object.
fn get_keys(&self) -> HashSet<String> {
let mut result = self
.prototype
.map_or_else(HashSet::new, |p| p.read().get_keys());
self.values
.iter()
.filter_map(|(k, p)| {
if p.is_enumerable() {
Some(k.to_string())
} else {
None
}
})
.for_each(|k| {
result.insert(k);
});
result
}
fn as_string(&self) -> String {
if self.function.is_some() {
"[type Function]".to_string()
} else {
"[object Object]".to_string()
}
}
fn type_of(&self) -> &'static str {
self.type_of
}
fn as_script_object(&self) -> Option<&ScriptObject<'gc>> {
Some(self)
}
fn as_script_object_mut(&mut self) -> Option<&mut ScriptObject<'gc>> {
Some(self)
}
/// Get the underlying display node for this object, if it exists.
fn as_display_node(&self) -> Option<DisplayNode<'gc>> {
self.display_node
}
/// Returns a copy of a given function.
///
/// TODO: We have to clone here because of how executables are stored on
/// objects directly. This might not be a good idea for performance.
fn as_executable(&self) -> Option<Executable<'gc>> {
self.function.clone()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::avm1::activation::Activation;
use crate::avm1::property::Attribute::*;
use crate::backend::audio::NullAudioBackend;
use crate::backend::navigator::NullNavigatorBackend;
use crate::backend::render::NullRenderer;
use crate::display_object::{DisplayObject, MovieClip};
use crate::library::Library;
use crate::prelude::*;
use gc_arena::rootless_arena;
use rand::{rngs::SmallRng, SeedableRng};
use std::sync::Arc;
fn with_object<F, R>(swf_version: u8, test: F) -> R
where
F: for<'a, 'gc> FnOnce(
&mut Avm1<'gc>,
&mut UpdateContext<'a, 'gc, '_>,
ObjectCell<'gc>,
) -> R,
{
rootless_arena(|gc_context| {
let mut avm = Avm1::new(gc_context, swf_version);
let movie_clip: Box<dyn DisplayObject> =
Box::new(MovieClip::new(swf_version, gc_context));
let root = GcCell::allocate(gc_context, movie_clip);
let mut context = UpdateContext {
gc_context,
global_time: 0,
player_version: 32,
swf_version,
root,
start_clip: root,
active_clip: root,
target_clip: Some(root),
target_path: Value::Undefined,
rng: &mut SmallRng::from_seed([0u8; 16]),
action_queue: &mut crate::context::ActionQueue::new(),
audio: &mut NullAudioBackend::new(),
background_color: &mut Color {
r: 0,
g: 0,
b: 0,
a: 0,
},
library: &mut Library::new(),
navigator: &mut NullNavigatorBackend::new(),
renderer: &mut NullRenderer::new(),
swf_data: &mut Arc::new(vec![]),
system_prototypes: avm.prototypes().clone(),
};
let object = GcCell::allocate(
gc_context,
Box::new(ScriptObject::object(
gc_context,
Some(avm.prototypes().object),
)) as Box<dyn Object<'_>>,
);
let globals = avm.global_object_cell();
avm.insert_stack_frame(GcCell::allocate(
gc_context,
Activation::from_nothing(swf_version, globals, gc_context),
));
test(&mut avm, &mut context, object)
})
}
#[test]
fn test_get_undefined() {
with_object(0, |avm, context, object| {
assert_eq!(
object
.read()
.get("not_defined", avm, context, object)
.unwrap(),
ReturnValue::Immediate(Value::Undefined)
);
})
}
#[test]
fn test_set_get() {
with_object(0, |avm, context, object| {
object
.write(context.gc_context)
.as_script_object_mut()
.unwrap()
.define_value("forced", "forced".into(), EnumSet::empty());
object
.write(context.gc_context)
.set("natural", "natural".into(), avm, context, object)
.unwrap();
assert_eq!(
object.read().get("forced", avm, context, object).unwrap(),
ReturnValue::Immediate("forced".into())
);
assert_eq!(
object.read().get("natural", avm, context, object).unwrap(),
ReturnValue::Immediate("natural".into())
);
})
}
#[test]
fn test_set_readonly() {
with_object(0, |avm, context, object| {
object
.write(context.gc_context)
.as_script_object_mut()
.unwrap()
.define_value("normal", "initial".into(), EnumSet::empty());
object
.write(context.gc_context)
.as_script_object_mut()
.unwrap()
.define_value("readonly", "initial".into(), ReadOnly.into());
object
.write(context.gc_context)
.set("normal", "replaced".into(), avm, context, object)
.unwrap();
object
.write(context.gc_context)
.set("readonly", "replaced".into(), avm, context, object)
.unwrap();
assert_eq!(
object.read().get("normal", avm, context, object).unwrap(),
ReturnValue::Immediate("replaced".into())
);
assert_eq!(
object.read().get("readonly", avm, context, object).unwrap(),
ReturnValue::Immediate("initial".into())
);
})
}
#[test]
fn test_deletable_not_readonly() {
with_object(0, |avm, context, object| {
object
.write(context.gc_context)
.as_script_object_mut()
.unwrap()
.define_value("test", "initial".into(), DontDelete.into());
assert_eq!(object.write(context.gc_context).delete("test"), false);
assert_eq!(
object.read().get("test", avm, context, object).unwrap(),
ReturnValue::Immediate("initial".into())
);
object
.write(context.gc_context)
.as_script_object_mut()
.unwrap()
.set("test", "replaced".into(), avm, context, object)
.unwrap();
assert_eq!(object.write(context.gc_context).delete("test"), false);
assert_eq!(
object.read().get("test", avm, context, object).unwrap(),
ReturnValue::Immediate("replaced".into())
);
})
}
#[test]
fn test_virtual_get() {
with_object(0, |avm, context, object| {
let getter = Executable::Native(|_avm, _context, _this, _args| {
Ok(ReturnValue::Immediate("Virtual!".into()))
});
object
.write(context.gc_context)
.as_script_object_mut()
.unwrap()
.add_property("test", getter, None, EnumSet::empty());
assert_eq!(
object.read().get("test", avm, context, object).unwrap(),
ReturnValue::Immediate("Virtual!".into())
);
// This set should do nothing
object
.write(context.gc_context)
.set("test", "Ignored!".into(), avm, context, object)
.unwrap();
assert_eq!(
object.read().get("test", avm, context, object).unwrap(),
ReturnValue::Immediate("Virtual!".into())
);
})
}
#[test]
fn test_delete() {
with_object(0, |avm, context, object| {
let getter = Executable::Native(|_avm, _context, _this, _args| {
Ok(ReturnValue::Immediate("Virtual!".into()))
});
object
.write(context.gc_context)
.as_script_object_mut()
.unwrap()
.add_property("virtual", getter.clone(), None, EnumSet::empty());
object
.write(context.gc_context)
.as_script_object_mut()
.unwrap()
.add_property("virtual_un", getter, None, DontDelete.into());
object
.write(context.gc_context)
.as_script_object_mut()
.unwrap()
.define_value("stored", "Stored!".into(), EnumSet::empty());
object
.write(context.gc_context)
.as_script_object_mut()
.unwrap()
.define_value("stored_un", "Stored!".into(), DontDelete.into());
assert_eq!(object.write(context.gc_context).delete("virtual"), true);
assert_eq!(object.write(context.gc_context).delete("virtual_un"), false);
assert_eq!(object.write(context.gc_context).delete("stored"), true);
assert_eq!(object.write(context.gc_context).delete("stored_un"), false);
assert_eq!(
object.write(context.gc_context).delete("non_existent"),
false
);
assert_eq!(
object.read().get("virtual", avm, context, object).unwrap(),
ReturnValue::Immediate(Value::Undefined)
);
assert_eq!(
object
.read()
.get("virtual_un", avm, context, object)
.unwrap(),
ReturnValue::Immediate("Virtual!".into())
);
assert_eq!(
object.read().get("stored", avm, context, object).unwrap(),
ReturnValue::Immediate(Value::Undefined)
);
assert_eq!(
object
.read()
.get("stored_un", avm, context, object)
.unwrap(),
ReturnValue::Immediate("Stored!".into())
);
})
}
#[test]
fn test_iter_values() {
with_object(0, |_avm, context, object| {
let getter = Executable::Native(|_avm, _context, _this, _args| {
Ok(ReturnValue::Immediate(Value::Null))
});
object
.write(context.gc_context)
.as_script_object_mut()
.unwrap()
.define_value("stored", Value::Null, EnumSet::empty());
object
.write(context.gc_context)
.as_script_object_mut()
.unwrap()
.define_value("stored_hidden", Value::Null, DontEnum.into());
object
.write(context.gc_context)
.as_script_object_mut()
.unwrap()
.add_property("virtual", getter.clone(), None, EnumSet::empty());
object
.write(context.gc_context)
.as_script_object_mut()
.unwrap()
.add_property("virtual_hidden", getter, None, DontEnum.into());
let keys = object.read().get_keys();
assert_eq!(keys.len(), 2);
assert_eq!(keys.contains(&"stored".to_string()), true);
assert_eq!(keys.contains(&"stored_hidden".to_string()), false);
assert_eq!(keys.contains(&"virtual".to_string()), true);
assert_eq!(keys.contains(&"virtual_hidden".to_string()), false);
})
}
}

View File

@ -1,5 +1,5 @@
use crate::avm1::activation::Activation;
use crate::avm1::{Avm1, Object, UpdateContext, Value};
use crate::avm1::{Avm1, ObjectCell, UpdateContext, Value};
use crate::backend::audio::NullAudioBackend;
use crate::backend::navigator::NullNavigatorBackend;
use crate::backend::render::NullRenderer;
@ -7,19 +7,18 @@ use crate::context::ActionQueue;
use crate::display_object::{DisplayObject, MovieClip};
use crate::library::Library;
use crate::prelude::*;
use gc_arena::{rootless_arena, GcCell};
use gc_arena::{rootless_arena, GcCell, MutationContext};
use rand::{rngs::SmallRng, SeedableRng};
use std::sync::Arc;
pub fn with_avm<F, R>(swf_version: u8, test: F) -> R
where
F: for<'a, 'gc> FnOnce(
&mut Avm1<'gc>,
&mut UpdateContext<'a, 'gc, '_>,
GcCell<'gc, Object<'gc>>,
) -> R,
F: for<'a, 'gc> FnOnce(&mut Avm1<'gc>, &mut UpdateContext<'a, 'gc, '_>, ObjectCell<'gc>) -> R,
{
rootless_arena(|gc_context| {
fn in_the_arena<'gc, F, R>(swf_version: u8, test: F, gc_context: MutationContext<'gc, '_>) -> R
where
F: for<'a> FnOnce(&mut Avm1<'gc>, &mut UpdateContext<'a, 'gc, '_>, ObjectCell<'gc>) -> R,
{
let mut avm = Avm1::new(gc_context, swf_version);
let movie_clip: Box<dyn DisplayObject> = Box::new(MovieClip::new(swf_version, gc_context));
let root = GcCell::allocate(gc_context, movie_clip);
@ -46,6 +45,7 @@ where
navigator: &mut NullNavigatorBackend::new(),
renderer: &mut NullRenderer::new(),
swf_data: &mut Arc::new(vec![]),
system_prototypes: avm.prototypes().clone(),
};
let globals = avm.global_object_cell();
@ -57,5 +57,7 @@ where
let this = root.read().object().as_object().unwrap().to_owned();
test(&mut avm, &mut context, this)
})
}
rootless_arena(|gc_context| in_the_arena(swf_version, test, gc_context))
}

View File

@ -11,11 +11,11 @@ fn locals_into_form_values() {
my_locals
.write(context.gc_context)
.set("value1", "string", avm, context, my_locals)
.set("value1", "string".into(), avm, context, my_locals)
.unwrap();
my_locals
.write(context.gc_context)
.set("value2", 2.0, avm, context, my_locals)
.set("value2", 2.0.into(), avm, context, my_locals)
.unwrap();
avm.insert_stack_frame(GcCell::allocate(context.gc_context, my_activation));

View File

@ -1,7 +1,5 @@
use crate::avm1::object::Object;
use crate::avm1::return_value::ReturnValue;
use crate::avm1::{Avm1, Error, UpdateContext};
use gc_arena::GcCell;
use crate::avm1::{Avm1, Error, ObjectCell, UpdateContext};
#[derive(Clone, Debug)]
#[allow(dead_code)]
@ -11,7 +9,7 @@ pub enum Value<'gc> {
Bool(bool),
Number(f64),
String(String),
Object(GcCell<'gc, Object<'gc>>),
Object(ObjectCell<'gc>),
}
impl<'gc> From<String> for Value<'gc> {
@ -32,8 +30,8 @@ impl<'gc> From<bool> for Value<'gc> {
}
}
impl<'gc> From<GcCell<'gc, Object<'gc>>> for Value<'gc> {
fn from(object: GcCell<'gc, Object<'gc>>) -> Self {
impl<'gc> From<ObjectCell<'gc>> for Value<'gc> {
fn from(object: ObjectCell<'gc>) -> Self {
Value::Object(object)
}
}
@ -68,6 +66,12 @@ impl<'gc> From<u32> for Value<'gc> {
}
}
impl<'gc> From<usize> for Value<'gc> {
fn from(value: usize) -> Self {
Value::Number(value as f64)
}
}
unsafe impl<'gc> gc_arena::Collect for Value<'gc> {
fn trace(&self, cc: gc_arena::CollectionContext) {
if let Value::Object(object) = self {
@ -230,6 +234,10 @@ impl<'gc> Value<'gc> {
self.as_f64().map(|n| n as i64)
}
pub fn as_usize(&self) -> Result<usize, Error> {
self.as_f64().map(|n| n as usize)
}
pub fn as_f64(&self) -> Result<f64, Error> {
match *self {
Value::Number(v) => Ok(v),
@ -244,7 +252,7 @@ impl<'gc> Value<'gc> {
}
}
pub fn as_object(&self) -> Result<GcCell<'gc, Object<'gc>>, Error> {
pub fn as_object(&self) -> Result<ObjectCell<'gc>, Error> {
if let Value::Object(object) = self {
Ok(object.to_owned())
} else {
@ -256,7 +264,7 @@ impl<'gc> Value<'gc> {
&self,
avm: &mut Avm1<'gc>,
context: &mut UpdateContext<'_, 'gc, '_>,
this: GcCell<'gc, Object<'gc>>,
this: ObjectCell<'gc>,
args: &[Value<'gc>],
) -> Result<ReturnValue<'gc>, Error> {
if let Value::Object(object) = self {

View File

@ -81,6 +81,10 @@ pub struct UpdateContext<'a, 'gc, 'gc_context> {
/// request, but this requires us to implement auto-generated
/// _names ("instanceN" etc. for unnamed clips).
pub target_path: avm1::Value<'gc>,
/// The current set of system-specified prototypes to use when constructing
/// new built-in objects.
pub system_prototypes: avm1::SystemPrototypes<'gc>,
}
/// A queued ActionScript call.

View File

@ -1,4 +1,4 @@
use crate::avm1::Value;
use crate::avm1::{ObjectCell, Value};
use crate::context::{RenderContext, UpdateContext};
use crate::player::NEWEST_PLAYER_VERSION;
use crate::prelude::*;
@ -259,6 +259,7 @@ pub trait DisplayObject<'gc>: 'gc + Collect + Debug {
&mut self,
_gc_context: MutationContext<'gc, '_>,
_display_object: DisplayNode<'gc>,
_proto: ObjectCell<'gc>,
) {
}

View File

@ -73,10 +73,11 @@ impl<'gc> Button<'gc> {
self.children.clear();
for record in &self.static_data.records {
if record.states.contains(&swf_state) {
if let Ok(child) = context
.library
.instantiate_display_object(record.id, context.gc_context)
{
if let Ok(child) = context.library.instantiate_display_object(
record.id,
context.gc_context,
&context.system_prototypes,
) {
child
.write(context.gc_context)
.set_parent(Some(context.active_clip));
@ -161,10 +162,11 @@ impl<'gc> DisplayObject<'gc> for Button<'gc> {
for record in &self.static_data.records {
if record.states.contains(&swf::ButtonState::HitTest) {
match context
.library
.instantiate_display_object(record.id, context.gc_context)
{
match context.library.instantiate_display_object(
record.id,
context.gc_context,
&context.system_prototypes,
) {
Ok(child) => {
{
let mut child = child.write(context.gc_context);

View File

@ -1,7 +1,6 @@
//! `MovieClip` display object and support code.
use crate::avm1::movie_clip::create_movie_object;
use crate::avm1::object::{Object, TYPE_OF_MOVIE_CLIP};
use crate::avm1::Value;
use crate::avm1::script_object::TYPE_OF_MOVIE_CLIP;
use crate::avm1::{ObjectCell, ScriptObject, Value};
use crate::backend::audio::AudioStreamHandle;
use crate::character::Character;
use crate::context::{RenderContext, UpdateContext};
@ -34,7 +33,7 @@ pub struct MovieClip<'gc> {
current_frame: FrameNumber,
audio_stream: Option<AudioStreamHandle>,
children: BTreeMap<Depth, DisplayNode<'gc>>,
object: GcCell<'gc, Object<'gc>>,
object: ObjectCell<'gc>,
}
impl<'gc> MovieClip<'gc> {
@ -48,7 +47,7 @@ impl<'gc> MovieClip<'gc> {
current_frame: 0,
audio_stream: None,
children: BTreeMap::new(),
object: GcCell::allocate(gc_context, create_movie_object(gc_context)),
object: GcCell::allocate(gc_context, Box::new(ScriptObject::bare_object())),
}
}
@ -79,7 +78,7 @@ impl<'gc> MovieClip<'gc> {
current_frame: 0,
audio_stream: None,
children: BTreeMap::new(),
object: GcCell::allocate(gc_context, create_movie_object(gc_context)),
object: GcCell::allocate(gc_context, Box::new(ScriptObject::bare_object())),
}
}
@ -303,10 +302,11 @@ impl<'gc> MovieClip<'gc> {
depth: Depth,
copy_previous_properties: bool,
) -> Option<DisplayNode<'gc>> {
if let Ok(child_cell) = context
.library
.instantiate_display_object(id, context.gc_context)
{
if let Ok(child_cell) = context.library.instantiate_display_object(
id,
context.gc_context,
&context.system_prototypes,
) {
// Remove previous child from children list,
// and add new childonto front of the list.
let prev_child = self.children.insert(depth, child_cell);
@ -626,10 +626,18 @@ impl<'gc> DisplayObject<'gc> for MovieClip<'gc> {
&mut self,
gc_context: MutationContext<'gc, '_>,
display_object: DisplayNode<'gc>,
proto: ObjectCell<'gc>,
) {
let mut object = self.object.write(gc_context);
object.set_display_node(display_object);
object.set_type_of(TYPE_OF_MOVIE_CLIP);
object
.as_script_object_mut()
.unwrap()
.set_display_node(display_object);
object
.as_script_object_mut()
.unwrap()
.set_type_of(TYPE_OF_MOVIE_CLIP);
object.as_script_object_mut().unwrap().set_prototype(proto);
}
fn object(&self) -> Value<'gc> {

View File

@ -1,3 +1,5 @@
use crate::avm1::globals::SystemPrototypes;
use crate::avm1::ObjectCell;
use crate::backend::audio::SoundHandle;
use crate::character::Character;
use crate::display_object::DisplayObject;
@ -47,15 +49,19 @@ impl<'gc> Library<'gc> {
&self,
id: CharacterId,
gc_context: MutationContext<'gc, '_>,
prototypes: &SystemPrototypes<'gc>,
) -> Result<DisplayNode<'gc>, Box<dyn std::error::Error>> {
let obj: Box<dyn DisplayObject<'gc>> = match self.characters.get(&id) {
Some(Character::Bitmap(bitmap)) => bitmap.clone(),
Some(Character::EditText(edit_text)) => edit_text.clone(),
Some(Character::Graphic(graphic)) => graphic.clone(),
Some(Character::MorphShape(morph_shape)) => morph_shape.clone(),
Some(Character::MovieClip(movie_clip)) => movie_clip.clone(),
Some(Character::Button(button)) => button.clone(),
Some(Character::Text(text)) => text.clone(),
let (obj, proto): (Box<dyn DisplayObject<'gc>>, ObjectCell<'gc>) = match self
.characters
.get(&id)
{
Some(Character::Bitmap(bitmap)) => (bitmap.clone(), prototypes.object),
Some(Character::EditText(edit_text)) => (edit_text.clone(), prototypes.object),
Some(Character::Graphic(graphic)) => (graphic.clone(), prototypes.object),
Some(Character::MorphShape(morph_shape)) => (morph_shape.clone(), prototypes.object),
Some(Character::MovieClip(movie_clip)) => (movie_clip.clone(), prototypes.movie_clip),
Some(Character::Button(button)) => (button.clone(), prototypes.object),
Some(Character::Text(text)) => (text.clone(), prototypes.object),
Some(_) => return Err("Not a DisplayObject".into()),
None => {
log::error!("Tried to instantiate non-registered character ID {}", id);
@ -65,7 +71,7 @@ impl<'gc> Library<'gc> {
let result = GcCell::allocate(gc_context, obj);
result
.write(gc_context)
.post_instantiation(gc_context, result);
.post_instantiation(gc_context, result, proto);
Ok(result)
}

View File

@ -1,4 +1,4 @@
use crate::avm1::{self, Avm1};
use crate::avm1::{Avm1, Value};
use crate::backend::{
audio::AudioBackend, navigator::NavigatorBackend, render::Letterbox, render::RenderBackend,
};
@ -188,10 +188,11 @@ impl<Audio: AudioBackend, Renderer: RenderBackend, Navigator: NavigatorBackend>
};
player.gc_arena.mutate(|gc_context, gc_root| {
gc_root
.root
.write(gc_context)
.post_instantiation(gc_context, gc_root.root)
gc_root.root.write(gc_context).post_instantiation(
gc_context,
gc_root.root,
gc_root.avm.read().prototypes().movie_clip,
)
});
player.build_matrices();
@ -336,7 +337,8 @@ impl<Audio: AudioBackend, Renderer: RenderBackend, Navigator: NavigatorBackend>
start_clip: gc_root.root,
target_clip: Some(gc_root.root),
root: gc_root.root,
target_path: avm1::Value::Undefined,
target_path: Value::Undefined,
system_prototypes: gc_root.avm.read().prototypes().clone(),
};
if let Some(node) = &*gc_root.mouse_hover_node.read() {
@ -414,7 +416,8 @@ impl<Audio: AudioBackend, Renderer: RenderBackend, Navigator: NavigatorBackend>
start_clip: gc_root.root,
target_clip: Some(gc_root.root),
root: gc_root.root,
target_path: avm1::Value::Undefined,
target_path: Value::Undefined,
system_prototypes: gc_root.avm.read().prototypes().clone(),
};
// RollOut of previous node.
@ -484,7 +487,8 @@ impl<Audio: AudioBackend, Renderer: RenderBackend, Navigator: NavigatorBackend>
start_clip: gc_root.root,
target_clip: Some(gc_root.root),
root: gc_root.root,
target_path: avm1::Value::Undefined,
target_path: Value::Undefined,
system_prototypes: gc_root.avm.read().prototypes().clone(),
};
let mut morph_shapes = fnv::FnvHashMap::default();
@ -547,7 +551,8 @@ impl<Audio: AudioBackend, Renderer: RenderBackend, Navigator: NavigatorBackend>
start_clip: gc_root.root,
target_clip: Some(gc_root.root),
root: gc_root.root,
target_path: avm1::Value::Undefined,
target_path: Value::Undefined,
system_prototypes: gc_root.avm.read().prototypes().clone(),
};
gc_root

View File

@ -1,3 +1,4 @@
use gc_arena::Collect;
use std::sync::Arc;
use swf::TagCode;
@ -5,7 +6,8 @@ pub type DecodeResult = Result<(), Box<dyn std::error::Error>>;
pub type SwfStream<R> = swf::read::Reader<std::io::Cursor<R>>;
/// A shared-ownership reference to some portion of an immutable datastream.
#[derive(Debug, Clone)]
#[derive(Debug, Clone, Collect)]
#[collect(no_drop)]
pub struct SwfSlice {
pub data: Arc<Vec<u8>>,
pub start: usize,

View File

@ -60,14 +60,49 @@ swf_tests! {
(timeline_function_def, "avm1/timeline_function_def", 3),
(root_global_parent, "avm1/root_global_parent", 3),
(register_underflow, "avm1/register_underflow", 1),
(object_prototypes, "avm1/object_prototypes", 1),
(movieclip_prototype_extension, "avm1/movieclip_prototype_extension", 1),
(recursive_prototypes, "avm1/recursive_prototypes", 1),
(has_own_property, "avm1/has_own_property", 1),
#[ignore] (extends_chain, "avm1/extends_chain", 1),
(is_prototype_of, "avm1/is_prototype_of", 1),
}
#[test]
fn test_prototype_enumerate() -> Result<(), Error> {
let trace_log = run_swf("tests/swfs/avm1/prototype_enumerate/test.swf", 1)?;
let mut actual: Vec<String> = trace_log.lines().map(|s| s.to_string()).collect();
let mut expected = vec!["a", "b", "c", "d", "e"];
actual.sort();
expected.sort();
assert_eq!(actual, expected);
Ok(())
}
/// Loads an SWF and runs it through the Ruffle core for a number of frames.
/// Tests that the trace output matches the given expected output.
fn test_swf(swf_path: &str, num_frames: u32, expected_output_path: &str) -> Result<(), Error> {
let _ = log::set_logger(&TRACE_LOGGER).map(|()| log::set_max_level(log::LevelFilter::Info));
let expected_output = std::fs::read_to_string(expected_output_path)?.replace("\r\n", "\n");
let trace_log = run_swf(swf_path, num_frames)?;
if trace_log != expected_output {
println!(
"Ruffle output:\n{}\nExpected output:\n{}",
trace_log, expected_output
);
panic!("Ruffle output did not match expected output.");
}
Ok(())
}
/// Loads an SWF and runs it through the Ruffle core for a number of frames.
/// Tests that the trace output matches the given expected output.
fn run_swf(swf_path: &str, num_frames: u32) -> Result<String, Error> {
let _ = log::set_logger(&TRACE_LOGGER).map(|()| log::set_max_level(log::LevelFilter::Info));
let swf_data = std::fs::read(swf_path)?;
let mut player = Player::new(
NullRenderer,
@ -80,16 +115,7 @@ fn test_swf(swf_path: &str, num_frames: u32, expected_output_path: &str) -> Resu
player.run_frame();
}
let trace_log = trace_log();
if trace_log != expected_output {
println!(
"Ruffle output:\n{}\nExpected output:\n{}",
trace_log, expected_output
);
panic!("Ruffle output did not match expected output.");
}
Ok(())
Ok(trace_log())
}
thread_local! {

View File

@ -0,0 +1,3 @@
interface Blue {
}

View File

@ -0,0 +1,11 @@
class ChildA extends Super {
function ChildA() {
super();
trace("ChildA constructor");
}
function work() {
super.work();
trace("ChildA work");
}
}

View File

@ -0,0 +1,11 @@
class ChildB extends Super implements Blue {
function ChildB() {
super();
trace("ChildA constructor");
}
function work() {
super.work();
trace("ChildA work");
}
}

View File

@ -0,0 +1,11 @@
class GrandchildBA extends ChildB {
function GrandchildBA() {
super();
trace("GrandchildBA constructor");
}
function work() {
super.work();
trace("GrandchildBA work");
}
}

View File

@ -0,0 +1,11 @@
class GrandchildBB extends ChildB implements Pink {
function GrandchildBB() {
super();
trace("GrandchildBB constructor");
}
function work() {
super.work();
trace("GrandchildBB work");
}
}

View File

@ -0,0 +1,3 @@
interface Pink extends Red {
}

View File

@ -0,0 +1,3 @@
interface Red {
}

View File

@ -0,0 +1,9 @@
class Super {
function Super() {
trace("Super constructor");
}
function work() {
trace("Super work");
}
}

View File

@ -0,0 +1,134 @@
// ChildA constructor
Super constructor
ChildA constructor
// ChildA work
Super work
ChildA work
// ChildA instanceof Super
true
// ChildA instanceof ChildA
true
// ChildA instanceof ChildB
false
// ChildA instanceof GrandchildBA
false
// ChildA instanceof GrandchildBB
false
// ChildA instanceof Red
false
// ChildA instanceof Blue
false
// ChildA instanceof Pink
false
// ChildB constructor
Super constructor
ChildA constructor
// ChildB work
Super work
ChildA work
// ChildB instanceof Super
true
// ChildB instanceof ChildA
false
// ChildB instanceof ChildB
true
// ChildB instanceof GrandchildBA
false
// ChildB instanceof GrandchildBB
false
// ChildB instanceof Red
false
// ChildB instanceof Blue
true
// ChildB instanceof Pink
false
// GrandchildBA constructor
Super constructor
ChildA constructor
GrandchildBA constructor
// GrandchildBA work
Super work
ChildA work
GrandchildBA work
// GrandchildBA instanceof Super
true
// GrandchildBA instanceof ChildA
false
// GrandchildBA instanceof ChildB
true
// GrandchildBA instanceof GrandchildBA
true
// GrandchildBA instanceof GrandchildBB
false
// GrandchildBA instanceof Red
false
// GrandchildBA instanceof Blue
true
// GrandchildBA instanceof Pink
false
// GrandchildBB constructor
Super constructor
ChildA constructor
GrandchildBB constructor
// GrandchildBB work
Super work
ChildA work
GrandchildBB work
// GrandchildBB instanceof Super
true
// GrandchildBB instanceof ChildA
false
// GrandchildBB instanceof ChildB
true
// GrandchildBB instanceof GrandchildBA
false
// GrandchildBB instanceof GrandchildBB
true
// GrandchildBB instanceof Red
true
// GrandchildBB instanceof Blue
true
// GrandchildBB instanceof Pink
true

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,18 @@
// base.hasOwnProperty("name")
true
// child.hasOwnProperty("name")
false
// base.hasOwnProperty("__proto__")
true
// child.hasOwnProperty("__proto__")
true
// base.hasOwnProperty("prototype")
false
// child.hasOwnProperty("prototype")
false

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,90 @@
// a.isPrototypeOf(a)
false
// a.isPrototypeOf(b)
true
// a.isPrototypeOf(c)
false
// a.isPrototypeOf(d)
false
// a.isPrototypeOf(Fun)
false
// b.isPrototypeOf(a)
false
// b.isPrototypeOf(b)
false
// b.isPrototypeOf(c)
false
// b.isPrototypeOf(d)
false
// b.isPrototypeOf(Fun)
false
// c.isPrototypeOf(a)
false
// c.isPrototypeOf(b)
false
// c.isPrototypeOf(c)
false
// c.isPrototypeOf(d)
true
// c.isPrototypeOf(Fun)
false
// d.isPrototypeOf(a)
false
// d.isPrototypeOf(b)
false
// d.isPrototypeOf(c)
false
// d.isPrototypeOf(d)
false
// d.isPrototypeOf(Fun)
false
// Fun.prototype.isPrototypeOf(a)
false
// Fun.prototype.isPrototypeOf(b)
false
// Fun.prototype.isPrototypeOf(c)
true
// Fun.prototype.isPrototypeOf(d)
true
// Fun.prototype.isPrototypeOf(Fun)
false
// Object.prototype.isPrototypeOf(a)
true
// Object.prototype.isPrototypeOf(b)
true
// Object.prototype.isPrototypeOf(c)
true
// Object.prototype.isPrototypeOf(d)
true
// Object.prototype.isPrototypeOf(Fun)
true

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,6 @@
// this.three
undefined
// this.three
3

View File

@ -0,0 +1,23 @@
Base constructed!
Base constructed!
// a.name
foo
// b.name
foo
// c.name
foo
// a.name = "bar"
// a.name
bar
// b.name
bar
// c.name
foo

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.