avm2: Refactor Executable->BoundMethod, introduce freestanding exec()

This commit is contained in:
Adrian Wielgosik 2024-05-19 20:27:59 +02:00 committed by Nathan Adams
parent 3bb47db5f9
commit 2532d7e927
7 changed files with 205 additions and 238 deletions

View File

@ -4,7 +4,6 @@ use std::rc::Rc;
use crate::avm2::class::AllocatorFn;
use crate::avm2::error::make_error_1107;
use crate::avm2::function::Executable;
use crate::avm2::globals::SystemClasses;
use crate::avm2::method::{Method, NativeMethodImpl};
use crate::avm2::scope::ScopeChain;
@ -598,8 +597,13 @@ impl<'gc> Avm2<'gc> {
}
/// Pushes an executable on the call stack
pub fn push_call(&self, mc: &Mutation<'gc>, calling: &Executable<'gc>) {
self.call_stack.write(mc).push(calling)
pub fn push_call(
&self,
mc: &Mutation<'gc>,
method: Method<'gc>,
superclass: Option<ClassObject<'gc>>,
) {
self.call_stack.write(mc).push(method, superclass)
}
/// Pushes script initializer (global init) on the call stack

View File

@ -1,4 +1,4 @@
use crate::avm2::function::{display_function, Executable};
use crate::avm2::function::display_function;
use crate::avm2::method::Method;
use crate::avm2::object::ClassObject;
use crate::string::WString;
@ -27,11 +27,8 @@ impl<'gc> CallStack<'gc> {
Self { stack: Vec::new() }
}
pub fn push(&mut self, exec: &Executable<'gc>) {
self.stack.push(CallNode::Method {
method: exec.as_method(),
superclass: exec.bound_superclass(),
})
pub fn push(&mut self, method: Method<'gc>, superclass: Option<ClassObject<'gc>>) {
self.stack.push(CallNode::Method { method, superclass })
}
pub fn push_global_init(&mut self, script: Script<'gc>) {

View File

@ -1,7 +1,5 @@
//! AVM2 executables.
use crate::avm2::activation::Activation;
use crate::avm2::method::{BytecodeMethod, Method, NativeMethod, ParamConfig};
use crate::avm2::method::{Method, ParamConfig};
use crate::avm2::object::{ClassObject, Object};
use crate::avm2::scope::ScopeChain;
use crate::avm2::traits::TraitKind;
@ -11,13 +9,12 @@ use crate::string::WString;
use gc_arena::{Collect, Gc};
use std::fmt;
/// Represents code written in AVM2 bytecode that can be executed by some
/// means.
/// Represents a bound method.
#[derive(Clone, Collect)]
#[collect(no_drop)]
pub struct BytecodeExecutable<'gc> {
pub struct BoundMethod<'gc> {
/// The method code to execute from a given ABC file.
method: Gc<'gc, BytecodeMethod<'gc>>,
method: Method<'gc>,
/// The scope this method was defined in.
scope: ScopeChain<'gc>,
@ -26,206 +23,66 @@ pub struct BytecodeExecutable<'gc> {
///
/// If `None`, then the receiver provided by the caller is used. A
/// `Some` value indicates a bound executable.
receiver: Option<Object<'gc>>,
/// The bound superclass for this method.
///
/// The `superclass` is the class that defined this method. If `None`,
/// then there is no defining superclass and `super` operations should fall
/// back to the `receiver`.
bound_superclass: Option<ClassObject<'gc>>,
}
#[derive(Clone, Collect)]
#[collect(no_drop)]
pub struct NativeExecutable<'gc> {
/// The method associated with the executable.
method: Gc<'gc, NativeMethod<'gc>>,
/// The scope this method was defined in.
scope: ScopeChain<'gc>,
/// The bound receiver for this method.
bound_receiver: Option<Object<'gc>>,
/// The bound superclass for this method.
/// The bound class for this method.
///
/// The `superclass` is the class that defined this method. If `None`,
/// then there is no defining superclass and `super` operations should fall
/// The `class` is the class that defined this method. If `None`,
/// then there is no defining class and `super` operations should fall
/// back to the `receiver`.
bound_superclass: Option<ClassObject<'gc>>,
bound_class: Option<ClassObject<'gc>>,
}
/// Represents code that can be executed by some means.
#[derive(Clone, Collect)]
#[collect(no_drop)]
pub enum Executable<'gc> {
/// Code defined in Ruffle's binary.
Native(NativeExecutable<'gc>),
/// Code defined in a loaded ABC file.
Action(BytecodeExecutable<'gc>),
}
impl<'gc> Executable<'gc> {
/// Convert a method into an executable.
impl<'gc> BoundMethod<'gc> {
pub fn from_method(
method: Method<'gc>,
scope: ScopeChain<'gc>,
receiver: Option<Object<'gc>>,
superclass: Option<ClassObject<'gc>>,
) -> Self {
match method {
Method::Native(method) => Self::Native(NativeExecutable {
method,
scope,
bound_receiver: receiver,
bound_superclass: superclass,
}),
Method::Bytecode(method) => Self::Action(BytecodeExecutable {
method,
scope,
receiver,
bound_superclass: superclass,
}),
Self {
method,
scope,
bound_receiver: receiver,
bound_class: superclass,
}
}
/// Execute a method.
///
/// The function will either be called directly if it is a Rust builtin, or
/// executed on the same AVM2 instance as the activation passed in here.
/// The value returned in either case will be provided here.
///
/// It is a panicking logic error to attempt to execute user code while any
/// reachable object is currently under a GcCell write lock.
///
/// Passed-in arguments will be conformed to the set of method parameters
/// declared on the function.
pub fn exec(
&self,
unbound_receiver: Value<'gc>,
mut arguments: &[Value<'gc>],
arguments: &[Value<'gc>],
activation: &mut Activation<'_, 'gc>,
callee: Object<'gc>,
) -> Result<Value<'gc>, Error<'gc>> {
let ret = match self {
Executable::Native(bm) => {
let method = bm.method.method;
let receiver = if let Some(receiver) = bm.bound_receiver {
receiver
} else if matches!(unbound_receiver, Value::Null | Value::Undefined) {
bm.scope
.get(0)
.expect("No global scope for function call")
.values()
} else {
unbound_receiver.coerce_to_object(activation)?
};
let caller_domain = activation.caller_domain();
let caller_movie = activation.caller_movie();
let subclass_object = bm.bound_superclass;
let mut activation = Activation::from_builtin(
activation.context.reborrow(),
subclass_object,
bm.scope,
caller_domain,
caller_movie,
);
if arguments.len() > bm.method.signature.len() && !bm.method.is_variadic {
return Err(format!(
"Attempted to call {:?} with {} arguments (more than {} is prohibited)",
bm.method.name,
arguments.len(),
bm.method.signature.len()
)
.into());
}
if bm.method.resolved_signature.read().is_none() {
bm.method.resolve_signature(&mut activation)?;
}
let resolved_signature = bm.method.resolved_signature.read();
let resolved_signature = resolved_signature.as_ref().unwrap();
let arguments = activation.resolve_parameters(
Method::Native(bm.method),
arguments,
resolved_signature,
Some(callee),
)?;
activation
.context
.avm2
.push_call(activation.context.gc_context, self);
method(&mut activation, receiver, &arguments)
}
Executable::Action(bm) => {
if bm.method.is_unchecked() {
let max_args = bm.method.signature().len();
if arguments.len() > max_args && !bm.method.is_variadic() {
arguments = &arguments[..max_args];
}
}
let receiver = if let Some(receiver) = bm.receiver {
receiver
} else if matches!(unbound_receiver, Value::Null | Value::Undefined) {
bm.scope
.get(0)
.expect("No global scope for function call")
.values()
} else {
unbound_receiver.coerce_to_object(activation)?
};
let subclass_object = bm.bound_superclass;
// This used to be a one step called Activation::from_method,
// but avoiding moving an Activation around helps perf
let mut activation = Activation::from_nothing(activation.context.reborrow());
activation.init_from_method(
bm.method,
bm.scope,
receiver,
arguments,
subclass_object,
callee,
)?;
activation
.context
.avm2
.push_call(activation.context.gc_context, self);
activation.run_actions(bm.method)
}
let receiver = if let Some(receiver) = self.bound_receiver {
receiver
} else if matches!(unbound_receiver, Value::Null | Value::Undefined) {
self.scope
.get(0)
.expect("No global scope for function call")
.values()
} else {
unbound_receiver.coerce_to_object(activation)?
};
activation
.context
.avm2
.pop_call(activation.context.gc_context);
ret
exec(
self.method,
self.scope,
receiver,
self.bound_class,
arguments,
activation,
callee,
)
}
pub fn bound_superclass(&self) -> Option<ClassObject<'gc>> {
match self {
Executable::Native(NativeExecutable {
bound_superclass, ..
}) => *bound_superclass,
Executable::Action(BytecodeExecutable {
bound_superclass, ..
}) => *bound_superclass,
}
self.bound_class
}
pub fn as_method(&self) -> Method<'gc> {
match self {
Executable::Native(nm) => Method::Native(nm.method),
Executable::Action(bm) => Method::Bytecode(bm.method),
}
self.method
}
pub fn debug_full_name(&self) -> WString {
@ -235,47 +92,135 @@ impl<'gc> Executable<'gc> {
}
pub fn num_parameters(&self) -> usize {
match self {
Executable::Native(NativeExecutable { method, .. }) => method.signature.len(),
Executable::Action(BytecodeExecutable { method, .. }) => method.signature.len(),
match self.method {
Method::Native(method) => method.signature.len(),
Method::Bytecode(method) => method.signature.len(),
}
}
pub fn signature(&self) -> &[ParamConfig<'gc>] {
match self {
Executable::Native(NativeExecutable { method, .. }) => &method.signature,
Executable::Action(BytecodeExecutable { method, .. }) => method.signature(),
match &self.method {
Method::Native(method) => &method.signature,
Method::Bytecode(method) => method.signature(),
}
}
pub fn is_variadic(&self) -> bool {
match self {
Executable::Native(NativeExecutable { method, .. }) => method.is_variadic,
Executable::Action(BytecodeExecutable { method, .. }) => method.is_variadic(),
match self.method {
Method::Native(method) => method.is_variadic,
Method::Bytecode(method) => method.is_variadic(),
}
}
pub fn return_type(&self) -> &Multiname<'gc> {
match self {
Executable::Native(NativeExecutable { method, .. }) => &method.return_type,
Executable::Action(BytecodeExecutable { method, .. }) => &method.return_type,
match &self.method {
Method::Native(method) => &method.return_type,
Method::Bytecode(method) => &method.return_type,
}
}
}
impl<'gc> fmt::Debug for Executable<'gc> {
/// Execute a method.
///
/// The function will either be called directly if it is a Rust builtin, or
/// executed on the same AVM2 instance as the activation passed in here.
/// The value returned in either case will be provided here.
///
/// It is a panicking logic error to attempt to execute user code while any
/// reachable object is currently under a GcCell write lock.
///
/// Passed-in arguments will be conformed to the set of method parameters
/// declared on the function.
pub fn exec<'gc>(
method: Method<'gc>,
scope: ScopeChain<'gc>,
receiver: Object<'gc>,
bound_class: Option<ClassObject<'gc>>,
mut arguments: &[Value<'gc>],
activation: &mut Activation<'_, 'gc>,
callee: Object<'gc>,
) -> Result<Value<'gc>, Error<'gc>> {
let ret = match method {
Method::Native(bm) => {
let caller_domain = activation.caller_domain();
let caller_movie = activation.caller_movie();
let mut activation = Activation::from_builtin(
activation.context.reborrow(),
bound_class,
scope,
caller_domain,
caller_movie,
);
if arguments.len() > bm.signature.len() && !bm.is_variadic {
return Err(format!(
"Attempted to call {:?} with {} arguments (more than {} is prohibited)",
bm.name,
arguments.len(),
bm.signature.len()
)
.into());
}
if bm.resolved_signature.read().is_none() {
bm.resolve_signature(&mut activation)?;
}
let resolved_signature = bm.resolved_signature.read();
let resolved_signature = resolved_signature.as_ref().unwrap();
let arguments = activation.resolve_parameters(
method,
arguments,
resolved_signature,
Some(callee),
)?;
activation
.context
.avm2
.push_call(activation.context.gc_context, method, bound_class);
(bm.method)(&mut activation, receiver, &arguments)
}
Method::Bytecode(bm) => {
if bm.is_unchecked() {
let max_args = bm.signature().len();
if arguments.len() > max_args && !bm.is_variadic() {
arguments = &arguments[..max_args];
}
}
// This used to be a one step called Activation::from_method,
// but avoiding moving an Activation around helps perf
let mut activation = Activation::from_nothing(activation.context.reborrow());
activation.init_from_method(bm, scope, receiver, arguments, bound_class, callee)?;
activation
.context
.avm2
.push_call(activation.context.gc_context, method, bound_class);
activation.run_actions(bm)
}
};
activation
.context
.avm2
.pop_call(activation.context.gc_context);
ret
}
impl<'gc> fmt::Debug for BoundMethod<'gc> {
fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Action(be) => fmt
.debug_struct("Executable::Action")
.field("method", &Gc::as_ptr(be.method))
.field("scope", &be.scope)
.field("receiver", &be.receiver)
match self.method {
Method::Bytecode(be) => fmt
.debug_struct("BoundMethod")
.field("method", &Gc::as_ptr(be))
.field("scope", &self.scope)
.field("receiver", &self.bound_receiver)
.finish(),
Self::Native(bm) => fmt
.debug_struct("Executable::Native")
.field("method", &bm.method)
.field("bound_receiver", &bm.bound_receiver)
Method::Native(bm) => fmt
.debug_struct("BoundMethod")
.field("method", &bm)
.field("scope", &self.scope)
.field("bound_receiver", &self.bound_receiver)
.finish(),
}
}

View File

@ -7,7 +7,7 @@ use crate::avm2::class::Class;
use crate::avm2::domain::Domain;
use crate::avm2::error;
use crate::avm2::events::{DispatchList, Event};
use crate::avm2::function::Executable;
use crate::avm2::function::{exec, BoundMethod};
use crate::avm2::property::Property;
use crate::avm2::regexp::RegExp;
use crate::avm2::value::{Hint, Value};
@ -600,11 +600,14 @@ pub trait TObject<'gc>: 'gc + Collect + Debug + Into<Object<'gc>> + Clone + Copy
class,
} = full_method;
return Executable::from_method(method, scope, None, Some(class)).exec(
Value::from(self.into()),
return exec(
method,
scope,
self.into(),
Some(class),
arguments,
activation,
class.into(), //Deliberately invalid.
class.into(), //Callee deliberately invalid.
);
}
@ -1153,8 +1156,8 @@ pub trait TObject<'gc>: 'gc + Collect + Debug + Into<Object<'gc>> + Clone + Copy
None
}
/// Get this object's `Executable`, if it has one.
fn as_executable(&self) -> Option<Ref<Executable<'gc>>> {
/// Get this object's `BoundMethod`, if it has one.
fn as_executable(&self) -> Option<Ref<BoundMethod<'gc>>> {
None
}

View File

@ -3,7 +3,7 @@
use crate::avm2::activation::Activation;
use crate::avm2::class::{Allocator, AllocatorFn, Class, ClassHashWrapper};
use crate::avm2::error::{argument_error, make_error_1127, reference_error, type_error};
use crate::avm2::function::Executable;
use crate::avm2::function::exec;
use crate::avm2::method::Method;
use crate::avm2::object::function_object::FunctionObject;
use crate::avm2::object::script_object::{scriptobject_allocator, ScriptObjectData};
@ -477,10 +477,16 @@ impl<'gc> ClassObject<'gc> {
activation: &mut Activation<'_, 'gc>,
) -> Result<Value<'gc>, Error<'gc>> {
let scope = self.0.read().instance_scope;
let constructor =
Executable::from_method(self.0.read().constructor, scope, None, Some(self));
constructor.exec(receiver, arguments, activation, self.into())
let method = self.0.read().constructor;
exec(
method,
scope,
receiver.coerce_to_object(activation)?,
Some(self),
arguments,
activation,
self.into(),
)
}
/// Call the instance's native initializer.
@ -495,10 +501,16 @@ impl<'gc> ClassObject<'gc> {
activation: &mut Activation<'_, 'gc>,
) -> Result<Value<'gc>, Error<'gc>> {
let scope = self.0.read().instance_scope;
let constructor =
Executable::from_method(self.0.read().native_constructor, scope, None, Some(self));
constructor.exec(receiver, arguments, activation, self.into())
let method = self.0.read().native_constructor;
exec(
method,
scope,
receiver.coerce_to_object(activation)?,
Some(self),
arguments,
activation,
self.into(),
)
}
/// Supercall a method defined in this class.
@ -842,9 +854,15 @@ impl<'gc> TObject<'gc> for ClassObject<'gc> {
) -> Result<Value<'gc>, Error<'gc>> {
if let Some(call_handler) = self.0.read().call_handler {
let scope = self.0.read().class_scope;
let func = Executable::from_method(call_handler, scope, None, Some(self));
func.exec(receiver, arguments, activation, self.into())
exec(
call_handler,
scope,
receiver.coerce_to_object(activation)?,
Some(self),
arguments,
activation,
self.into(),
)
} else if arguments.len() == 1 {
arguments[0].coerce_to_type(activation, self.inner_class_definition())
} else {

View File

@ -1,7 +1,7 @@
//! Function object impl
use crate::avm2::activation::Activation;
use crate::avm2::function::Executable;
use crate::avm2::function::BoundMethod;
use crate::avm2::method::{Method, NativeMethod};
use crate::avm2::object::script_object::{ScriptObject, ScriptObjectData};
use crate::avm2::object::{ClassObject, Object, ObjectPtr, TObject};
@ -41,7 +41,7 @@ pub fn function_allocator<'gc>(
activation.context.gc_context,
FunctionObjectData {
base,
exec: Executable::from_method(
exec: BoundMethod::from_method(
Method::Native(dummy),
activation.create_scopechain(),
None,
@ -78,7 +78,7 @@ pub struct FunctionObjectData<'gc> {
base: ScriptObjectData<'gc>,
/// Executable code
exec: Executable<'gc>,
exec: BoundMethod<'gc>,
/// Attached prototype (note: not the same thing as base object's proto)
prototype: Option<Object<'gc>>,
@ -120,7 +120,7 @@ impl<'gc> FunctionObject<'gc> {
subclass_object: Option<ClassObject<'gc>>,
) -> FunctionObject<'gc> {
let fn_class = activation.avm2().classes().function;
let exec = Executable::from_method(method, scope, receiver, subclass_object);
let exec = BoundMethod::from_method(method, scope, receiver, subclass_object);
FunctionObject(GcCell::new(
activation.context.gc_context,
@ -169,7 +169,7 @@ impl<'gc> TObject<'gc> for FunctionObject<'gc> {
Ok(Value::Object(Object::from(*self)))
}
fn as_executable(&self) -> Option<Ref<Executable<'gc>>> {
fn as_executable(&self) -> Option<Ref<BoundMethod<'gc>>> {
Some(Ref::map(self.0.read(), |r| &r.exec))
}

View File

@ -1,5 +1,5 @@
use crate::avm2::dynamic_map::DynamicKey;
use crate::avm2::function::Executable;
use crate::avm2::function::BoundMethod;
use crate::avm2::method::{Method, ParamConfig};
use crate::avm2::object::TObject;
use crate::avm2::traits::{Trait, TraitKind};
@ -166,7 +166,7 @@ impl FunctionInfo {
}
}
pub fn from_executable(executable: &Executable, stubbed: bool) -> Self {
pub fn from_bound_method(executable: &BoundMethod, stubbed: bool) -> Self {
Self {
returns: executable
.return_type()
@ -345,7 +345,7 @@ impl Definition {
if let Some(executable) = object.as_executable() {
output.get_or_insert_with(Default::default).function.insert(
name.to_string(),
FunctionInfo::from_executable(&executable, false),
FunctionInfo::from_bound_method(&executable, false),
);
}
} else {
@ -494,7 +494,7 @@ pub fn capture_specification(context: &mut UpdateContext, output: &Path) {
.get_or_insert_with(Default::default);
instance_traits.function.insert(
name.to_string(),
FunctionInfo::from_executable(
FunctionInfo::from_bound_method(
&executable,
namespace_stubs.has_method(&name.to_string()),
),