ruffle/core/src/avm2.rs

335 lines
9.8 KiB
Rust
Raw Normal View History

//! ActionScript Virtual Machine 2 (AS3) support
use crate::avm2::globals::{SystemConstructors, SystemPrototypes};
use crate::avm2::method::Method;
use crate::avm2::object::EventObject;
use crate::avm2::script::{Script, TranslationUnit};
use crate::avm2::string::AvmString;
use crate::context::UpdateContext;
use crate::tag_utils::SwfSlice;
use gc_arena::{Collect, MutationContext};
use std::collections::HashMap;
use std::rc::Rc;
use swf::avm2::read::Reader;
2020-02-20 19:41:15 +00:00
#[macro_export]
macro_rules! avm_debug {
($avm: expr, $($arg:tt)*) => (
if $avm.show_debug_output() {
log::debug!($($arg)*)
}
2020-02-20 19:41:15 +00:00
)
}
mod activation;
mod array;
2021-03-05 23:01:02 +00:00
mod bytearray;
mod class;
mod domain;
mod events;
mod function;
mod globals;
mod method;
mod names;
mod object;
2020-02-15 01:30:19 +00:00
mod property;
mod property_map;
2021-02-11 07:56:21 +00:00
mod regexp;
mod return_value;
2020-02-10 19:54:55 +00:00
mod scope;
mod script;
mod slot;
mod string;
mod traits;
mod value;
2020-08-06 02:11:52 +00:00
pub use crate::avm2::activation::Activation;
pub use crate::avm2::array::ArrayStorage;
pub use crate::avm2::domain::Domain;
pub use crate::avm2::events::Event;
pub use crate::avm2::names::{Namespace, QName};
pub use crate::avm2::object::{ArrayObject, Object, ScriptObject, StageObject, TObject};
pub use crate::avm2::value::Value;
const BROADCAST_WHITELIST: [&str; 3] = ["enterFrame", "exitFrame", "frameConstructed"];
/// Boxed error alias.
///
/// As AVM2 is a far stricter VM than AVM1, this may eventually be replaced
/// with a proper Avm2Error enum.
pub type Error = Box<dyn std::error::Error>;
/// The state of an AVM2 interpreter.
#[derive(Collect)]
#[collect(no_drop)]
pub struct Avm2<'gc> {
/// Values currently present on the operand stack.
stack: Vec<Value<'gc>>,
/// Global scope object.
globals: Domain<'gc>,
/// System prototypes.
system_prototypes: Option<SystemPrototypes<'gc>>,
/// System constructors.
system_constructors: Option<SystemConstructors<'gc>>,
/// A list of objects which are capable of recieving broadcasts.
///
/// Certain types of events are "broadcast events" that are emitted on all
/// constructed objects in order of their creation, whether or not they are
/// currently present on the display list. This list keeps track of that.
///
/// TODO: These should be weak object pointers, but our current garbage
/// collector does not support weak references.
broadcast_list: HashMap<AvmString<'gc>, Vec<Object<'gc>>>,
#[cfg(feature = "avm_debug")]
pub debug_output: bool,
}
impl<'gc> Avm2<'gc> {
/// Construct a new AVM interpreter.
pub fn new(mc: MutationContext<'gc, '_>) -> Self {
let globals = Domain::global_domain(mc);
Self {
stack: Vec::new(),
globals,
system_prototypes: None,
system_constructors: None,
broadcast_list: HashMap::new(),
#[cfg(feature = "avm_debug")]
debug_output: false,
}
}
pub fn load_player_globals(context: &mut UpdateContext<'_, 'gc, '_>) -> Result<(), Error> {
let globals = context.avm2.globals;
let mut activation = Activation::from_nothing(context.reborrow());
globals::load_player_globals(&mut activation, globals)
}
Completely overhaul the way traits are defined on objects. Previously, we were treating ES4 classes like syntactic sugar over a prototype chain (like ES6 classes); e.g. each declared trait was set in the given prototype and then property look-ups happened as normal. This already caused problems with virtual properties, which could be partially-defined in subclasses and required careful checks to make sure we stopped checking the prototype chain on the *correct* half of the property. However, this is a hint of a larger problem, which is that ES4 classes don't actually define anything on the prototype chain. Instead, the instance itself constructs class properties and methods on itself. This allows things like methods automatically binding `this`, which isn't included in this commit but will be implemented really soon. The prototype chain still exists even on pure ES4 classes, due to the need for backwards compatibility with ES3 code. Object, for example, still defines it's methods as prototype methods and thus there needs to be a prototype chain to reach them. I actually could have gotten away with using the prototype chain if AS3 *hadn't* retained this "legacy" detail of ES3 allowing this class/prototype distinction to leak out into upcoming tests. We still actually use the prototype chain for one other thing: trait resolution. When we look for a trait to install onto an object, we pull traits from the prototype chain using a special set of `TObject` methods. This happens in opposite order from normal prototype lookups so that subclassing and verification can proceed correctly. `super` somehow became even harder to implement: we now actually construct the parent class so we can get traits from it, which is going to complicate method binding as mentioned above.
2020-03-04 00:39:49 +00:00
/// Return the current set of system prototypes.
///
/// This function panics if the interpreter has not yet been initialized.
Completely overhaul the way traits are defined on objects. Previously, we were treating ES4 classes like syntactic sugar over a prototype chain (like ES6 classes); e.g. each declared trait was set in the given prototype and then property look-ups happened as normal. This already caused problems with virtual properties, which could be partially-defined in subclasses and required careful checks to make sure we stopped checking the prototype chain on the *correct* half of the property. However, this is a hint of a larger problem, which is that ES4 classes don't actually define anything on the prototype chain. Instead, the instance itself constructs class properties and methods on itself. This allows things like methods automatically binding `this`, which isn't included in this commit but will be implemented really soon. The prototype chain still exists even on pure ES4 classes, due to the need for backwards compatibility with ES3 code. Object, for example, still defines it's methods as prototype methods and thus there needs to be a prototype chain to reach them. I actually could have gotten away with using the prototype chain if AS3 *hadn't* retained this "legacy" detail of ES3 allowing this class/prototype distinction to leak out into upcoming tests. We still actually use the prototype chain for one other thing: trait resolution. When we look for a trait to install onto an object, we pull traits from the prototype chain using a special set of `TObject` methods. This happens in opposite order from normal prototype lookups so that subclassing and verification can proceed correctly. `super` somehow became even harder to implement: we now actually construct the parent class so we can get traits from it, which is going to complicate method binding as mentioned above.
2020-03-04 00:39:49 +00:00
pub fn prototypes(&self) -> &SystemPrototypes<'gc> {
self.system_prototypes.as_ref().unwrap()
Completely overhaul the way traits are defined on objects. Previously, we were treating ES4 classes like syntactic sugar over a prototype chain (like ES6 classes); e.g. each declared trait was set in the given prototype and then property look-ups happened as normal. This already caused problems with virtual properties, which could be partially-defined in subclasses and required careful checks to make sure we stopped checking the prototype chain on the *correct* half of the property. However, this is a hint of a larger problem, which is that ES4 classes don't actually define anything on the prototype chain. Instead, the instance itself constructs class properties and methods on itself. This allows things like methods automatically binding `this`, which isn't included in this commit but will be implemented really soon. The prototype chain still exists even on pure ES4 classes, due to the need for backwards compatibility with ES3 code. Object, for example, still defines it's methods as prototype methods and thus there needs to be a prototype chain to reach them. I actually could have gotten away with using the prototype chain if AS3 *hadn't* retained this "legacy" detail of ES3 allowing this class/prototype distinction to leak out into upcoming tests. We still actually use the prototype chain for one other thing: trait resolution. When we look for a trait to install onto an object, we pull traits from the prototype chain using a special set of `TObject` methods. This happens in opposite order from normal prototype lookups so that subclassing and verification can proceed correctly. `super` somehow became even harder to implement: we now actually construct the parent class so we can get traits from it, which is going to complicate method binding as mentioned above.
2020-03-04 00:39:49 +00:00
}
/// Return the current set of system constructors.
///
/// This function panics if the interpreter has not yet been initialized.
pub fn constructors(&self) -> &SystemConstructors<'gc> {
self.system_constructors.as_ref().unwrap()
}
/// Run a script's initializer method.
pub fn run_script_initializer(
script: Script<'gc>,
context: &mut UpdateContext<'_, 'gc, '_>,
) -> Result<(), Error> {
let mut init_activation = Activation::from_script(context.reborrow(), script)?;
let (method, scope) = script.init();
match method {
Method::Native(nf) => {
nf(&mut init_activation, Some(scope), &[])?;
}
Method::Entry(_) => {
init_activation.run_stack_frame_for_script(script)?;
}
};
Ok(())
}
/// Dispatch an event on an object.
///
/// The `bool` parameter reads true if the event was cancelled.
pub fn dispatch_event(
context: &mut UpdateContext<'_, 'gc, '_>,
event: Event<'gc>,
target: Object<'gc>,
) -> Result<bool, Error> {
use crate::avm2::events::dispatch_event;
let event_constr = context.avm2.constructors().event;
let mut activation = Activation::from_nothing(context.reborrow());
let event_object = EventObject::from_event(&mut activation, event_constr, event)?;
dispatch_event(&mut activation, target, event_object)
}
/// Add an object to the broadcast list.
///
/// Each broadcastable event contains it's own broadcast list. You must
/// register all objects that have event handlers with that event's
/// broadcast list by calling this function. Attempting to register a
/// broadcast listener for a non-broadcast event will do nothing.
///
/// Attempts to register the same listener for the same event will also do
/// nothing.
pub fn register_broadcast_listener(
context: &mut UpdateContext<'_, 'gc, '_>,
object: Object<'gc>,
event_name: AvmString<'gc>,
) {
if !BROADCAST_WHITELIST.iter().any(|x| *x == event_name) {
return;
}
let bucket = context.avm2.broadcast_list.entry(event_name).or_default();
if bucket.iter().any(|x| Object::ptr_eq(*x, object)) {
return;
}
bucket.push(object);
}
/// Dispatch an event on all objects in the current execution list.
///
/// `on_type` specifies a class or interface constructor whose instances,
/// implementers, and/or subclasses define the set of objects that will
/// receive the event. You can broadcast to just display objects, or
/// specific interfaces, and so on.
///
/// Attempts to broadcast a non-broadcast event will do nothing. To add a
/// new broadcast type, you must add it to the `BROADCAST_WHITELIST` first.
pub fn broadcast_event(
context: &mut UpdateContext<'_, 'gc, '_>,
event: Event<'gc>,
on_type: Object<'gc>,
) -> Result<(), Error> {
let event_name = event.event_type();
if !BROADCAST_WHITELIST.iter().any(|x| *x == event_name) {
return Ok(());
}
let el_length = context
.avm2
.broadcast_list
.entry(event_name)
.or_default()
.len();
for i in 0..el_length {
let object = context
.avm2
.broadcast_list
.get(&event_name)
.unwrap()
.get(i)
.copied();
if let Some(object) = object {
if object.is_of_type(on_type)? {
Avm2::dispatch_event(context, event.clone(), object)?;
}
}
}
Ok(())
}
pub fn run_stack_frame_for_callable(
callable: Object<'gc>,
reciever: Option<Object<'gc>>,
args: &[Value<'gc>],
context: &mut UpdateContext<'_, 'gc, '_>,
) -> Result<(), Error> {
let mut evt_activation = Activation::from_nothing(context.reborrow());
callable.call(
reciever,
args,
&mut evt_activation,
reciever.and_then(|r| r.proto()),
)?;
Ok(())
}
/// Load an ABC file embedded in a `SwfSlice`.
///
/// The `SwfSlice` must resolve to the contents of an ABC file.
pub fn load_abc(
abc: SwfSlice,
_abc_name: &str,
lazy_init: bool,
context: &mut UpdateContext<'_, 'gc, '_>,
domain: Domain<'gc>,
) -> Result<(), Error> {
let mut read = Reader::new(abc.as_ref());
let abc_file = Rc::new(read.read()?);
let tunit = TranslationUnit::from_abc(abc_file.clone(), domain, context.gc_context);
for i in (0..abc_file.scripts.len()).rev() {
let mut script = tunit.load_script(i as u32, context)?;
if !lazy_init {
script.globals(context)?;
}
}
Ok(())
}
pub fn global_domain(&self) -> Domain<'gc> {
self.globals
}
/// Push a value onto the operand stack.
fn push(&mut self, value: impl Into<Value<'gc>>) {
let value = value.into();
avm_debug!(self, "Stack push {}: {:?}", self.stack.len(), value);
self.stack.push(value);
}
/// Retrieve the top-most value on the operand stack.
#[allow(clippy::let_and_return)]
fn pop(&mut self) -> Value<'gc> {
let value = self.stack.pop().unwrap_or_else(|| {
log::warn!("Avm1::pop: Stack underflow");
Value::Undefined
});
avm_debug!(self, "Stack pop {}: {:?}", self.stack.len(), value);
value
}
fn pop_args(&mut self, arg_count: u32) -> Vec<Value<'gc>> {
let mut args = Vec::with_capacity(arg_count as usize);
args.resize(arg_count as usize, Value::Undefined);
for arg in args.iter_mut().rev() {
*arg = self.pop();
}
args
}
#[cfg(feature = "avm_debug")]
#[inline]
pub fn show_debug_output(&self) -> bool {
self.debug_output
}
#[cfg(not(feature = "avm_debug"))]
pub const fn show_debug_output(&self) -> bool {
false
}
#[cfg(feature = "avm_debug")]
pub fn set_show_debug_output(&mut self, visible: bool) {
self.debug_output = visible;
}
#[cfg(not(feature = "avm_debug"))]
pub const fn set_show_debug_output(&self, _visible: bool) {}
}