avm1: Introduce `NativeObject`

The existing `Object` enum representation is problematic for inherited
native objects, since "regular" `ScriptObject`s cannot be turned into
native objects, but rather a completely new native object needs to be
created. `TObject::create_bare_object` is an attempt to aid this
situation, but it works only for `ActionExtends` inheritance, and not
when the user manually wires up `prototype`/`__proto__` (#701).

In Flash, it seems like derived constructors initially have a "regular"
`this` object. But once the `super()` constructor is invoked, the same
`this` object becomes a native object.

To allow this in Ruffle, introduce a new `NativeObject` enum, and
store it as a member in `ScriptObject`. For a start, move `TextFormatObject`
from the `Object` enum to `NativeObject`. The plan is to gradually
move all `Object` enum variants to `NativeObject`, except for `ScriptObject`.
This commit is contained in:
relrelb 2022-08-19 18:31:35 +03:00 committed by Mike Welsh
parent c17da6e91a
commit af006a3053
5 changed files with 191 additions and 202 deletions

View File

@ -1,13 +1,13 @@
use crate::avm1::activation::Activation; use crate::avm1::activation::Activation;
use crate::avm1::error::Error; use crate::avm1::error::Error;
use crate::avm1::object::text_format_object::TextFormatObject; use crate::avm1::object::NativeObject;
use crate::avm1::property_decl::{define_properties_on, Declaration}; use crate::avm1::property_decl::{define_properties_on, Declaration};
use crate::avm1::{globals, Object, ScriptObject, TObject, Value}; use crate::avm1::{globals, Object, ScriptObject, TObject, Value};
use crate::display_object::{AutoSizeMode, EditText, TDisplayObject, TextSelection}; use crate::display_object::{AutoSizeMode, EditText, TDisplayObject, TextSelection};
use crate::font::round_down_to_pixel; use crate::font::round_down_to_pixel;
use crate::html::TextFormat; use crate::html::TextFormat;
use crate::string::{AvmString, WStr}; use crate::string::{AvmString, WStr};
use gc_arena::MutationContext; use gc_arena::{GcCell, MutationContext};
macro_rules! tf_method { macro_rules! tf_method {
($fn:expr) => { ($fn:expr) => {
@ -121,13 +121,26 @@ pub fn set_password<'gc>(
Ok(()) Ok(())
} }
fn new_text_format<'gc>(
activation: &mut Activation<'_, 'gc, '_>,
text_format: TextFormat,
) -> ScriptObject<'gc> {
let proto = activation.context.avm1.prototypes().text_format;
let object = ScriptObject::new(activation.context.gc_context, Some(proto));
object.set_native(
activation.context.gc_context,
NativeObject::TextFormat(GcCell::allocate(activation.context.gc_context, text_format)),
);
object
}
fn get_new_text_format<'gc>( fn get_new_text_format<'gc>(
text_field: EditText<'gc>, text_field: EditText<'gc>,
activation: &mut Activation<'_, 'gc, '_>, activation: &mut Activation<'_, 'gc, '_>,
_args: &[Value<'gc>], _args: &[Value<'gc>],
) -> Result<Value<'gc>, Error<'gc>> { ) -> Result<Value<'gc>, Error<'gc>> {
let tf = text_field.new_text_format(); let text_format = text_field.new_text_format();
Ok(TextFormatObject::new(activation, tf).into()) Ok(new_text_format(activation, text_format).into())
} }
fn set_new_text_format<'gc>( fn set_new_text_format<'gc>(
@ -135,11 +148,9 @@ fn set_new_text_format<'gc>(
activation: &mut Activation<'_, 'gc, '_>, activation: &mut Activation<'_, 'gc, '_>,
args: &[Value<'gc>], args: &[Value<'gc>],
) -> Result<Value<'gc>, Error<'gc>> { ) -> Result<Value<'gc>, Error<'gc>> {
let tf = args.get(0).unwrap_or(&Value::Undefined); if let [Value::Object(text_format), ..] = args {
if let NativeObject::TextFormat(text_format) = text_format.native() {
if let Value::Object(tf) = tf { text_field.set_new_text_format(text_format.read().clone(), &mut activation.context);
if let Some(tf) = tf.as_text_format_object() {
text_field.set_new_text_format(tf.text_format().clone(), &mut activation.context);
} }
} }
@ -151,20 +162,26 @@ fn get_text_format<'gc>(
activation: &mut Activation<'_, 'gc, '_>, activation: &mut Activation<'_, 'gc, '_>,
args: &[Value<'gc>], args: &[Value<'gc>],
) -> Result<Value<'gc>, Error<'gc>> { ) -> Result<Value<'gc>, Error<'gc>> {
let (from, to) = match (args.get(0), args.get(1)) { let (begin_index, end_index) = match args {
(Some(f), Some(t)) => ( [begin_index, end_index, ..] => {
f.coerce_to_f64(activation)? as usize, let begin_index = begin_index.coerce_to_u32(activation)? as usize;
t.coerce_to_f64(activation)? as usize, let end_index = end_index.coerce_to_u32(activation)? as usize;
), (begin_index, end_index)
(Some(f), None) => { }
let v = f.coerce_to_f64(activation)? as usize; [begin_index] => {
(v, v.saturating_add(1)) let begin_index = begin_index.coerce_to_u32(activation)? as usize;
let end_index = begin_index + 1;
(begin_index, end_index)
}
[] => {
let begin_index = 0;
let end_index = text_field.text_length();
(begin_index, end_index)
} }
_ => (0, text_field.text_length()),
}; };
let tf = text_field.text_format(from, to); let text_format = text_field.text_format(begin_index, end_index);
Ok(TextFormatObject::new(activation, tf).into()) Ok(new_text_format(activation, text_format).into())
} }
fn set_text_format<'gc>( fn set_text_format<'gc>(
@ -172,23 +189,33 @@ fn set_text_format<'gc>(
activation: &mut Activation<'_, 'gc, '_>, activation: &mut Activation<'_, 'gc, '_>,
args: &[Value<'gc>], args: &[Value<'gc>],
) -> Result<Value<'gc>, Error<'gc>> { ) -> Result<Value<'gc>, Error<'gc>> {
let tf = args.last().unwrap_or(&Value::Undefined); let (begin_index, end_index, text_format) = match args {
[begin_index, end_index, text_format, ..] => {
let begin_index = begin_index.coerce_to_u32(activation)? as usize;
let end_index = end_index.coerce_to_u32(activation)? as usize;
(begin_index, end_index, text_format)
}
[begin_index, text_format, ..] => {
let begin_index = begin_index.coerce_to_u32(activation)? as usize;
let end_index = begin_index + 1;
(begin_index, end_index, text_format)
}
[text_format] => {
let begin_index = 0;
let end_index = text_field.text_length();
(begin_index, end_index, text_format)
}
_ => return Ok(Value::Undefined),
};
if let Value::Object(tf) = tf { if let Value::Object(text_format) = text_format {
if let Some(tf) = tf.as_text_format_object() { if let NativeObject::TextFormat(text_format) = text_format.native() {
let (from, to) = match (args.get(0), args.get(1)) { text_field.set_text_format(
(Some(f), Some(t)) if args.len() > 2 => ( begin_index,
f.coerce_to_f64(activation)? as usize, end_index,
t.coerce_to_f64(activation)? as usize, text_format.read().clone(),
), &mut activation.context,
(Some(f), _) if args.len() > 1 => { );
let v = f.coerce_to_f64(activation)? as usize;
(v, v.saturating_add(1))
}
_ => (0, text_field.text_length()),
};
text_field.set_text_format(from, to, tf.text_format().clone(), &mut activation.context);
} }
} }
@ -316,12 +343,17 @@ pub fn set_text_color<'gc>(
value: Value<'gc>, value: Value<'gc>,
) -> Result<(), Error<'gc>> { ) -> Result<(), Error<'gc>> {
let rgb = value.coerce_to_u32(activation)?; let rgb = value.coerce_to_u32(activation)?;
let tf = TextFormat { let text_format = TextFormat {
color: Some(swf::Color::from_rgb(rgb, 0)), color: Some(swf::Color::from_rgb(rgb, 0)),
..TextFormat::default() ..Default::default()
}; };
this.set_text_format(0, this.text_length(), tf.clone(), &mut activation.context); this.set_text_format(
this.set_new_text_format(tf, &mut activation.context); 0,
this.text_length(),
text_format.clone(),
&mut activation.context,
);
this.set_new_text_format(text_format, &mut activation.context);
Ok(()) Ok(())
} }
@ -564,7 +596,7 @@ pub fn get_type<'gc>(
true => "input", true => "input",
false => "dynamic", false => "dynamic",
}; };
Ok(AvmString::from(tf_type).into()) Ok(tf_type.into())
} }
pub fn set_type<'gc>( pub fn set_type<'gc>(

View File

@ -1,19 +1,21 @@
//! `TextFormat` impl //! `TextFormat` impl
use crate::avm1::object::text_format_object::TextFormatObject; use crate::avm1::object::NativeObject;
use crate::avm1::property_decl::{define_properties_on, Declaration}; use crate::avm1::property_decl::{define_properties_on, Declaration};
use crate::avm1::{Activation, ArrayObject, AvmString, Error, Object, TObject, Value}; use crate::avm1::{
Activation, ArrayObject, AvmString, Error, Object, ScriptObject, TObject, Value,
};
use crate::avm_warn; use crate::avm_warn;
use crate::ecma_conversions::round_to_even; use crate::ecma_conversions::round_to_even;
use crate::html::TextFormat; use crate::html::TextFormat;
use crate::string::WStr; use crate::string::WStr;
use gc_arena::MutationContext; use gc_arena::{GcCell, MutationContext};
macro_rules! getter { macro_rules! getter {
($name:ident) => { ($name:ident) => {
|activation, this, _args| { |activation, this, _args| {
if let Some(text_format) = this.as_text_format_object() { if let NativeObject::TextFormat(text_format) = this.native() {
return Ok($name(activation, &text_format.text_format())); return Ok($name(activation, &text_format.read()));
} }
Ok(Value::Undefined) Ok(Value::Undefined)
} }
@ -23,11 +25,11 @@ macro_rules! getter {
macro_rules! setter { macro_rules! setter {
($name:ident) => { ($name:ident) => {
|activation, this, args| { |activation, this, args| {
if let Some(text_format) = this.as_text_format_object() { if let NativeObject::TextFormat(text_format) = this.native() {
let value = args.get(0).unwrap_or(&Value::Undefined); let value = args.get(0).unwrap_or(&Value::Undefined);
$name( $name(
activation, activation,
&mut text_format.text_format_mut(activation.context.gc_context), &mut text_format.write(activation.context.gc_context),
value, value,
)?; )?;
} }
@ -470,75 +472,76 @@ pub fn constructor<'gc>(
this: Object<'gc>, this: Object<'gc>,
args: &[Value<'gc>], args: &[Value<'gc>],
) -> Result<Value<'gc>, Error<'gc>> { ) -> Result<Value<'gc>, Error<'gc>> {
if let Some(this) = this.as_text_format_object() { let mut text_format = Default::default();
let mut text_format = this.text_format_mut(activation.context.gc_context); set_font(
set_font( activation,
activation, &mut text_format,
&mut text_format, args.get(0).unwrap_or(&Value::Undefined),
args.get(0).unwrap_or(&Value::Undefined), )?;
)?; set_size(
set_size( activation,
activation, &mut text_format,
&mut text_format, args.get(1).unwrap_or(&Value::Undefined),
args.get(1).unwrap_or(&Value::Undefined), )?;
)?; set_color(
set_color( activation,
activation, &mut text_format,
&mut text_format, args.get(2).unwrap_or(&Value::Undefined),
args.get(2).unwrap_or(&Value::Undefined), )?;
)?; set_bold(
set_bold( activation,
activation, &mut text_format,
&mut text_format, args.get(3).unwrap_or(&Value::Undefined),
args.get(3).unwrap_or(&Value::Undefined), )?;
)?; set_italic(
set_italic( activation,
activation, &mut text_format,
&mut text_format, args.get(4).unwrap_or(&Value::Undefined),
args.get(4).unwrap_or(&Value::Undefined), )?;
)?; set_underline(
set_underline( activation,
activation, &mut text_format,
&mut text_format, args.get(5).unwrap_or(&Value::Undefined),
args.get(5).unwrap_or(&Value::Undefined), )?;
)?; set_url(
set_url( activation,
activation, &mut text_format,
&mut text_format, args.get(6).unwrap_or(&Value::Undefined),
args.get(6).unwrap_or(&Value::Undefined), )?;
)?; set_target(
set_target( activation,
activation, &mut text_format,
&mut text_format, args.get(7).unwrap_or(&Value::Undefined),
args.get(7).unwrap_or(&Value::Undefined), )?;
)?; set_align(
set_align( activation,
activation, &mut text_format,
&mut text_format, args.get(8).unwrap_or(&Value::Undefined),
args.get(8).unwrap_or(&Value::Undefined), )?;
)?; set_left_margin(
set_left_margin( activation,
activation, &mut text_format,
&mut text_format, args.get(9).unwrap_or(&Value::Undefined),
args.get(9).unwrap_or(&Value::Undefined), )?;
)?; set_right_margin(
set_right_margin( activation,
activation, &mut text_format,
&mut text_format, args.get(10).unwrap_or(&Value::Undefined),
args.get(10).unwrap_or(&Value::Undefined), )?;
)?; set_indent(
set_indent( activation,
activation, &mut text_format,
&mut text_format, args.get(11).unwrap_or(&Value::Undefined),
args.get(11).unwrap_or(&Value::Undefined), )?;
)?; set_leading(
set_leading( activation,
activation, &mut text_format,
&mut text_format, args.get(12).unwrap_or(&Value::Undefined),
args.get(12).unwrap_or(&Value::Undefined), )?;
)?; this.set_native(
} activation.context.gc_context,
NativeObject::TextFormat(GcCell::allocate(activation.context.gc_context, text_format)),
);
Ok(this.into()) Ok(this.into())
} }
@ -548,8 +551,7 @@ pub fn create_proto<'gc>(
proto: Object<'gc>, proto: Object<'gc>,
fn_proto: Object<'gc>, fn_proto: Object<'gc>,
) -> Object<'gc> { ) -> Object<'gc> {
let text_format = TextFormatObject::empty_object(gc_context, Some(proto)); let object = ScriptObject::new(gc_context, Some(proto));
let object = text_format.as_script_object().unwrap();
define_properties_on(PROTO_DECLS, gc_context, object, fn_proto); define_properties_on(PROTO_DECLS, gc_context, object, fn_proto);
text_format.into() object.into()
} }

View File

@ -1,13 +1,8 @@
//! Object trait to expose objects to AVM //! Object trait to expose objects to AVM
use crate::avm1::activation::Activation;
use crate::avm1::error::Error; use crate::avm1::error::Error;
use crate::avm1::function::{Executable, ExecutionName, ExecutionReason, FunctionObject}; use crate::avm1::function::{Executable, ExecutionName, ExecutionReason, FunctionObject};
use crate::avm1::object::shared_object::SharedObject;
use crate::avm1::object::super_object::SuperObject;
use crate::avm1::object::value_object::ValueObject;
use crate::avm1::property::Attribute;
use crate::avm1::activation::Activation;
use crate::avm1::object::array_object::ArrayObject; use crate::avm1::object::array_object::ArrayObject;
use crate::avm1::object::bevel_filter::BevelFilterObject; use crate::avm1::object::bevel_filter::BevelFilterObject;
use crate::avm1::object::bitmap_data::BitmapDataObject; use crate::avm1::object::bitmap_data::BitmapDataObject;
@ -21,14 +16,18 @@ use crate::avm1::object::drop_shadow_filter::DropShadowFilterObject;
use crate::avm1::object::glow_filter::GlowFilterObject; use crate::avm1::object::glow_filter::GlowFilterObject;
use crate::avm1::object::gradient_bevel_filter::GradientBevelFilterObject; use crate::avm1::object::gradient_bevel_filter::GradientBevelFilterObject;
use crate::avm1::object::gradient_glow_filter::GradientGlowFilterObject; use crate::avm1::object::gradient_glow_filter::GradientGlowFilterObject;
use crate::avm1::object::text_format_object::TextFormatObject; use crate::avm1::object::shared_object::SharedObject;
use crate::avm1::object::super_object::SuperObject;
use crate::avm1::object::transform_object::TransformObject; use crate::avm1::object::transform_object::TransformObject;
use crate::avm1::object::value_object::ValueObject;
use crate::avm1::object::xml_node_object::XmlNodeObject; use crate::avm1::object::xml_node_object::XmlNodeObject;
use crate::avm1::object::xml_object::XmlObject; use crate::avm1::object::xml_object::XmlObject;
use crate::avm1::property::Attribute;
use crate::avm1::{AvmString, ScriptObject, SoundObject, StageObject, Value}; use crate::avm1::{AvmString, ScriptObject, SoundObject, StageObject, Value};
use crate::display_object::DisplayObject; use crate::display_object::DisplayObject;
use crate::html::TextFormat;
use crate::xml::XmlNode; use crate::xml::XmlNode;
use gc_arena::{Collect, MutationContext}; use gc_arena::{Collect, GcCell, MutationContext};
use ruffle_macros::enum_trait_object; use ruffle_macros::enum_trait_object;
use std::fmt::Debug; use std::fmt::Debug;
@ -51,12 +50,18 @@ pub mod shared_object;
pub mod sound_object; pub mod sound_object;
pub mod stage_object; pub mod stage_object;
pub mod super_object; pub mod super_object;
pub mod text_format_object;
pub mod transform_object; pub mod transform_object;
pub mod value_object; pub mod value_object;
pub mod xml_node_object; pub mod xml_node_object;
pub mod xml_object; pub mod xml_object;
#[derive(Clone, Collect)]
#[collect(no_drop)]
pub enum NativeObject<'gc> {
None,
TextFormat(GcCell<'gc, TextFormat>),
}
/// Represents an object that can be directly interacted with by the AVM /// Represents an object that can be directly interacted with by the AVM
/// runtime. /// runtime.
#[enum_trait_object( #[enum_trait_object(
@ -87,7 +92,6 @@ pub mod xml_object;
GradientGlowFilterObject(GradientGlowFilterObject<'gc>), GradientGlowFilterObject(GradientGlowFilterObject<'gc>),
DateObject(DateObject<'gc>), DateObject(DateObject<'gc>),
BitmapData(BitmapDataObject<'gc>), BitmapData(BitmapDataObject<'gc>),
TextFormatObject(TextFormatObject<'gc>),
} }
)] )]
pub trait TObject<'gc>: 'gc + Collect + Debug + Into<Object<'gc>> + Clone + Copy { pub trait TObject<'gc>: 'gc + Collect + Debug + Into<Object<'gc>> + Clone + Copy {
@ -493,6 +497,12 @@ pub trait TObject<'gc>: 'gc + Collect + Debug + Into<Object<'gc>> + Clone + Copy
Ok(false) Ok(false)
} }
fn native(&self) -> NativeObject<'gc> {
NativeObject::None
}
fn set_native(&self, _gc_context: MutationContext<'gc, '_>, _native: NativeObject<'gc>) {}
/// Get the underlying script object, if it exists. /// Get the underlying script object, if it exists.
fn as_script_object(&self) -> Option<ScriptObject<'gc>>; fn as_script_object(&self) -> Option<ScriptObject<'gc>>;
@ -611,11 +621,6 @@ pub trait TObject<'gc>: 'gc + Collect + Debug + Into<Object<'gc>> + Clone + Copy
None None
} }
/// Get the underlying `TextFormatObject`, if it exists
fn as_text_format_object(&self) -> Option<TextFormatObject<'gc>> {
None
}
fn as_ptr(&self) -> *const ObjectPtr; fn as_ptr(&self) -> *const ObjectPtr;
/// Check if this object is in the prototype chain of the specified test object. /// Check if this object is in the prototype chain of the specified test object.

View File

@ -1,6 +1,7 @@
use crate::avm1::activation::Activation; use crate::avm1::activation::Activation;
use crate::avm1::error::Error; use crate::avm1::error::Error;
use crate::avm1::function::{ExecutionName, ExecutionReason}; use crate::avm1::function::{ExecutionName, ExecutionReason};
use crate::avm1::object::NativeObject;
use crate::avm1::property::{Attribute, Property}; use crate::avm1::property::{Attribute, Property};
use crate::avm1::property_map::{Entry, PropertyMap}; use crate::avm1::property_map::{Entry, PropertyMap};
use crate::avm1::{Object, ObjectPtr, TObject, Value}; use crate::avm1::{Object, ObjectPtr, TObject, Value};
@ -52,6 +53,7 @@ pub struct ScriptObject<'gc>(GcCell<'gc, ScriptObjectData<'gc>>);
#[derive(Collect)] #[derive(Collect)]
#[collect(no_drop)] #[collect(no_drop)]
pub struct ScriptObjectData<'gc> { pub struct ScriptObjectData<'gc> {
native: NativeObject<'gc>,
properties: PropertyMap<'gc, Property<'gc>>, properties: PropertyMap<'gc, Property<'gc>>,
interfaces: Vec<Object<'gc>>, interfaces: Vec<Object<'gc>>,
watchers: PropertyMap<'gc, Watcher<'gc>>, watchers: PropertyMap<'gc, Watcher<'gc>>,
@ -71,6 +73,7 @@ impl<'gc> ScriptObject<'gc> {
let object = Self(GcCell::allocate( let object = Self(GcCell::allocate(
gc_context, gc_context,
ScriptObjectData { ScriptObjectData {
native: NativeObject::None,
properties: PropertyMap::new(), properties: PropertyMap::new(),
interfaces: vec![], interfaces: vec![],
watchers: PropertyMap::new(), watchers: PropertyMap::new(),
@ -481,6 +484,20 @@ impl<'gc> TObject<'gc> for ScriptObject<'gc> {
self.0.write(gc_context).interfaces = iface_list; self.0.write(gc_context).interfaces = iface_list;
} }
fn native(&self) -> NativeObject<'gc> {
self.0.read().native.clone()
}
fn set_native(&self, gc_context: MutationContext<'gc, '_>, native: NativeObject<'gc>) {
// Native object should be introduced at most once.
debug_assert!(matches!(self.0.read().native, NativeObject::None));
// Native object must not be `None`.
debug_assert!(!matches!(native, NativeObject::None));
self.0.write(gc_context).native = native;
}
fn as_script_object(&self) -> Option<ScriptObject<'gc>> { fn as_script_object(&self) -> Option<ScriptObject<'gc>> {
Some(*self) Some(*self)
} }

View File

@ -1,67 +0,0 @@
use crate::avm1::{Activation, Object, ScriptObject, TObject};
use crate::html::TextFormat;
use crate::impl_custom_object;
use gc_arena::{Collect, GcCell, MutationContext};
use std::cell::{Ref, RefMut};
use std::fmt;
#[derive(Clone, Copy, Collect)]
#[collect(no_drop)]
pub struct TextFormatObject<'gc>(GcCell<'gc, TextFormatData<'gc>>);
#[derive(Collect)]
#[collect(no_drop)]
pub struct TextFormatData<'gc> {
/// The underlying script object.
base: ScriptObject<'gc>,
text_format: TextFormat,
}
impl fmt::Debug for TextFormatObject<'_> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let this = self.0.read();
f.debug_struct("TextFormatObject")
.field("text_format", &this.text_format)
.finish()
}
}
impl<'gc> TextFormatObject<'gc> {
pub fn empty_object(gc_context: MutationContext<'gc, '_>, proto: Option<Object<'gc>>) -> Self {
Self(GcCell::allocate(
gc_context,
TextFormatData {
base: ScriptObject::new(gc_context, proto),
text_format: TextFormat::default(),
},
))
}
pub fn new(activation: &mut Activation<'_, 'gc, '_>, text_format: TextFormat) -> Self {
Self(GcCell::allocate(
activation.context.gc_context,
TextFormatData {
base: ScriptObject::new(
activation.context.gc_context,
Some(activation.context.avm1.prototypes.text_format),
),
text_format,
},
))
}
pub fn text_format(&self) -> Ref<TextFormat> {
Ref::map(self.0.read(), |o| &o.text_format)
}
pub fn text_format_mut(&self, gc_context: MutationContext<'gc, '_>) -> RefMut<TextFormat> {
RefMut::map(self.0.write(gc_context), |o| &mut o.text_format)
}
}
impl<'gc> TObject<'gc> for TextFormatObject<'gc> {
impl_custom_object!(base {
bare_object(as_text_format_object -> TextFormatObject::empty_object);
});
}