From 25b5c7b4e2de56cd31d65f4cda201458b5eb7b40 Mon Sep 17 00:00:00 2001 From: Nathan Adams Date: Sat, 5 Aug 2023 21:49:11 +0200 Subject: [PATCH] avm2: Track progress of avm2 and generate an implementation.json --- core/src/avm2.rs | 1 + core/src/avm2/domain.rs | 9 + core/src/avm2/function.rs | 25 +- core/src/avm2/namespace.rs | 2 +- core/src/avm2/object/script_object.rs | 7 + core/src/avm2/specification.rs | 516 ++++++++++++++++++++++++++ core/src/player.rs | 4 + core/src/stub.rs | 12 + 8 files changed, 573 insertions(+), 3 deletions(-) create mode 100644 core/src/avm2/specification.rs diff --git a/core/src/avm2.rs b/core/src/avm2.rs index 2ebaad088..c5d828c54 100644 --- a/core/src/avm2.rs +++ b/core/src/avm2.rs @@ -58,6 +58,7 @@ mod qname; mod regexp; mod scope; mod script; +pub mod specification; mod string; mod stubs; mod traits; diff --git a/core/src/avm2/domain.rs b/core/src/avm2/domain.rs index f184d55d7..78d0d2960 100644 --- a/core/src/avm2/domain.rs +++ b/core/src/avm2/domain.rs @@ -12,6 +12,7 @@ use crate::avm2::Multiname; use crate::avm2::QName; use gc_arena::{Collect, GcCell, GcWeakCell, Mutation}; use ruffle_wstr::WStr; +use std::cell::Ref; use super::class::Class; use super::error::error; @@ -344,6 +345,14 @@ impl<'gc> Domain<'gc> { self.0.write(mc).classes.insert(export_name, class); } + pub fn defs(&self) -> Ref>> { + Ref::map(self.0.read(), |this| &this.defs) + } + + pub fn classes(&self) -> Ref>>> { + Ref::map(self.0.read(), |this| &this.classes) + } + pub fn is_default_domain_memory(&self) -> bool { let read = self.0.read(); read.domain_memory.expect("Missing domain memory").as_ptr() diff --git a/core/src/avm2/function.rs b/core/src/avm2/function.rs index c17f43dcb..5613b64ec 100644 --- a/core/src/avm2/function.rs +++ b/core/src/avm2/function.rs @@ -1,12 +1,12 @@ //! AVM2 executables. use crate::avm2::activation::Activation; -use crate::avm2::method::{BytecodeMethod, Method, NativeMethod}; +use crate::avm2::method::{BytecodeMethod, Method, NativeMethod, ParamConfig}; use crate::avm2::object::{ClassObject, Object}; use crate::avm2::scope::ScopeChain; use crate::avm2::traits::TraitKind; use crate::avm2::value::Value; -use crate::avm2::Error; +use crate::avm2::{Error, Multiname}; use crate::string::WString; use gc_arena::{Collect, Gc}; use std::fmt; @@ -231,6 +231,27 @@ impl<'gc> Executable<'gc> { Executable::Action(BytecodeExecutable { method, .. }) => method.signature.len(), } } + + pub fn signature(&self) -> &[ParamConfig<'gc>] { + match self { + Executable::Native(NativeExecutable { method, .. }) => &method.signature, + Executable::Action(BytecodeExecutable { 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(), + } + } + + pub fn return_type(&self) -> &Multiname<'gc> { + match self { + Executable::Native(NativeExecutable { method, .. }) => &method.return_type, + Executable::Action(BytecodeExecutable { method, .. }) => &method.return_type, + } + } } impl<'gc> fmt::Debug for Executable<'gc> { diff --git a/core/src/avm2/namespace.rs b/core/src/avm2/namespace.rs index c36392625..70fef7e8a 100644 --- a/core/src/avm2/namespace.rs +++ b/core/src/avm2/namespace.rs @@ -10,7 +10,7 @@ use swf::avm2::types::{Index, Namespace as AbcNamespace}; use super::api_version::ApiVersion; -#[derive(Clone, Copy, Collect, Debug)] +#[derive(Clone, Copy, Collect, Debug, PartialEq)] #[collect(no_drop)] pub struct Namespace<'gc>(Gc<'gc, NamespaceData<'gc>>); diff --git a/core/src/avm2/object/script_object.rs b/core/src/avm2/object/script_object.rs index 37609de4c..7dd51810f 100644 --- a/core/src/avm2/object/script_object.rs +++ b/core/src/avm2/object/script_object.rs @@ -145,6 +145,13 @@ impl<'gc> ScriptObjectData<'gc> { } } + /// Retrieve the values stored directly on this ScriptObjectData. + /// + /// This should only be used for debugging purposes. + pub fn values(&self) -> &DynamicMap, Value<'gc>> { + &self.values + } + pub fn get_property_local( &self, multiname: &Multiname<'gc>, diff --git a/core/src/avm2/specification.rs b/core/src/avm2/specification.rs new file mode 100644 index 000000000..6fc16f94d --- /dev/null +++ b/core/src/avm2/specification.rs @@ -0,0 +1,516 @@ +use crate::avm2::dynamic_map::{DynamicMap, StringOrObject}; +use crate::avm2::function::Executable; +use crate::avm2::method::{Method, ParamConfig}; +use crate::avm2::object::TObject; +use crate::avm2::traits::{Trait, TraitKind}; +use crate::avm2::{Activation, Avm2, ClassObject, QName, Value}; +use crate::context::UpdateContext; +use crate::string::AvmString; +use crate::stub::Stub; +use fnv::{FnvHashMap, FnvHashSet}; +use serde::Serialize; +use std::borrow::Cow; +use std::fs::File; +use std::process::exit; + +fn is_false(b: &bool) -> bool { + !(*b) +} + +fn escape_string(string: &AvmString) -> String { + let mut output = "".to_string(); + output.push('\"'); + + for c in string.chars() { + let c = c.unwrap_or(char::REPLACEMENT_CHARACTER); + let escape = match u8::try_from(c as u32) { + Ok(b'"') => "\\\"", + Ok(b'\\') => "\\\\", + Ok(b'\n') => "\\n", + Ok(b'\r') => "\\r", + Ok(b'\t') => "\\t", + Ok(0x08) => "\\b", + Ok(0x0C) => "\\f", + _ => { + output.push(c); + continue; + } + }; + + output.push_str(escape); + } + + output.push('\"'); + output +} + +fn format_value(value: &Value) -> Option { + match value { + Value::Undefined => None, + Value::Null => Some("null".to_string()), + Value::Bool(value) => Some(value.to_string()), + Value::Number(value) => Some(value.to_string()), + Value::Integer(value) => Some(value.to_string()), + Value::String(value) => Some(escape_string(value)), + Value::Object(_) => None, + } +} + +fn format_signature(params: &[ParamConfig], is_variadic: bool) -> Vec { + let mut result = Vec::with_capacity(params.len()); + + for param in params { + result.push(ParamInfo { + type_info: param + .param_type_name + .local_name() + .map(|n| n.to_string()) + .unwrap_or_else(|| "*".to_string()), + value: param.default_value.and_then(|v| format_value(&v)), + variadic: false, + }); + } + + if is_variadic { + result.push(ParamInfo { + type_info: "*".to_string(), + value: None, + variadic: true, + }) + } + + result +} + +#[derive(Serialize, Default)] +struct ClassInfo { + #[serde(skip_serializing_if = "is_false")] + dynamic: bool, + + #[serde(skip_serializing_if = "Option::is_none")] + extends: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + implements: Option, + + #[serde(skip_serializing_if = "is_false")] + #[serde(rename = "final")] + is_final: bool, +} + +#[derive(Serialize, Default)] +struct VariableInfo { + #[serde(skip_serializing_if = "Option::is_none")] + #[serde(rename = "type")] + type_info: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + value: Option, + + #[serde(skip_serializing_if = "is_false")] + stubbed: bool, +} + +impl VariableInfo { + pub fn from_value<'gc>(value: Value<'gc>, activation: &mut Activation<'_, 'gc>) -> Self { + Self { + type_info: match value { + Value::Bool(_) => Some("Boolean".to_string()), + Value::Number(_) => Some("Number".to_string()), + Value::Integer(_) => Some("int".to_string()), + Value::String(_) => Some("String".to_string()), + Value::Object(_) => Some("Object".to_string()), + _ => Some("*".to_string()), + }, + value: value + .coerce_to_string(activation) + .ok() + .map(|v| v.to_string()), + stubbed: false, + } + } +} + +#[derive(Serialize, Default)] +struct ParamInfo { + #[serde(rename = "type")] + type_info: String, + + #[serde(skip_serializing_if = "Option::is_none")] + #[serde(rename = "default")] + value: Option, + + #[serde(skip_serializing_if = "is_false")] + variadic: bool, +} + +#[derive(Serialize)] +struct FunctionInfo { + args: Vec, + returns: String, + #[serde(skip_serializing_if = "is_false")] + stubbed: bool, +} + +impl FunctionInfo { + pub fn from_method(method: &Method, stubbed: bool) -> Self { + Self { + returns: method + .return_type() + .local_name() + .map(|n| n.to_string()) + .unwrap_or_else(|| "void".to_string()), + args: format_signature(method.signature(), method.is_variadic()), + stubbed, + } + } + + pub fn from_executable(executable: &Executable, stubbed: bool) -> Self { + Self { + returns: executable + .return_type() + .local_name() + .map(|n| n.to_string()) + .unwrap_or_else(|| "void".to_string()), + args: format_signature(executable.signature(), executable.is_variadic()), + stubbed, + } + } +} + +#[derive(Serialize, Default)] +struct TraitList { + #[serde(rename = "const")] + #[serde(skip_serializing_if = "FnvHashMap::is_empty")] + constants: FnvHashMap, + + #[serde(rename = "var")] + #[serde(skip_serializing_if = "FnvHashMap::is_empty")] + variables: FnvHashMap, + + #[serde(skip_serializing_if = "FnvHashMap::is_empty")] + function: FnvHashMap, + + #[serde(skip_serializing_if = "FnvHashMap::is_empty")] + getter: FnvHashMap, + + #[serde(skip_serializing_if = "FnvHashMap::is_empty")] + setter: FnvHashMap, +} + +#[derive(Serialize, Default)] +struct Definition { + #[serde(skip_serializing_if = "Option::is_none")] + classinfo: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + #[serde(rename = "static")] + static_traits: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + #[serde(rename = "instance")] + instance_traits: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + #[serde(rename = "prototype")] + prototype: Option, +} + +#[derive(Default)] +struct ClassStubs { + methods: FnvHashSet>, + getters: FnvHashSet>, + setters: FnvHashSet>, +} + +impl ClassStubs { + pub fn for_class(class_name: &str, stubs: &FnvHashSet<&Stub>) -> Self { + let mut result = ClassStubs::default(); + + for stub in stubs + .iter() + .filter(|s| s.avm2_class() == Some(Cow::Borrowed(class_name))) + { + match stub { + Stub::Avm2Method { method, .. } => { + result.methods.insert(method.clone()); + } + Stub::Avm2Getter { property, .. } => { + result.getters.insert(property.clone()); + } + Stub::Avm2Setter { property, .. } => { + result.setters.insert(property.clone()); + } + _ => {} + } + } + + result + } + + pub fn has_method(&self, name: &str) -> bool { + self.methods.contains(name) + } + + pub fn has_getter(&self, name: &str) -> bool { + self.getters.contains(name) + } + + pub fn has_setter(&self, name: &str) -> bool { + self.setters.contains(name) + } +} + +impl Definition { + fn from_class<'gc>( + class_object: ClassObject<'gc>, + activation: &mut Activation<'_, 'gc>, + stubs: &ClassStubs, + ) -> Self { + let mut definition = Self::default(); + let class = class_object.inner_class_definition(); + + if class.read().is_final() { + definition + .classinfo + .get_or_insert_with(Default::default) + .is_final = true; + } + if !class.read().is_sealed() { + definition + .classinfo + .get_or_insert_with(Default::default) + .dynamic = true; + } + if let Some(super_name) = class + .read() + .super_class_name() + .as_ref() + .and_then(|n| n.local_name()) + { + if &super_name != b"Object" { + definition + .classinfo + .get_or_insert_with(Default::default) + .extends = Some(super_name.to_string()); + } + } + + let prototype = class_object.prototype(); + let prototype_base = prototype.base(); + let prototype_values: &DynamicMap, Value<'gc>> = + prototype_base.values(); + for (key, value) in prototype_values.as_hashmap().iter() { + let name = match key { + StringOrObject::String(name) => *name, + StringOrObject::Object(object) => { + Value::Object(*object).coerce_to_string(activation).unwrap() + } + }; + if &name != b"constructor" { + Self::add_prototype_value( + &name, + value.value, + &mut definition.prototype, + activation, + ); + } + } + + Self::fill_traits( + activation.avm2(), + class.read().class_traits(), + &mut definition.static_traits, + &stubs, + ); + Self::fill_traits( + activation.avm2(), + class.read().instance_traits(), + &mut definition.instance_traits, + &stubs, + ); + + definition + } + + fn add_prototype_value<'gc>( + name: &AvmString<'gc>, + value: Value<'gc>, + output: &mut Option, + activation: &mut Activation<'_, 'gc>, + ) { + if let Some(object) = value.as_object() { + if let Some(executable) = object.as_executable() { + output.get_or_insert_with(Default::default).function.insert( + name.to_string(), + FunctionInfo::from_executable(&executable, false), + ); + } + } else { + output + .get_or_insert_with(Default::default) + .variables + .insert( + name.to_string(), + VariableInfo::from_value(value, activation), + ); + } + } + + fn fill_traits<'gc>( + avm2: &Avm2<'gc>, + traits: &[Trait<'gc>], + output: &mut Option, + stubs: &ClassStubs, + ) { + for class_trait in traits { + if !class_trait.name().namespace().is_public() + && class_trait.name().namespace() != avm2.as3_namespace + { + continue; + } + let trait_name = class_trait.name().local_name().to_string(); + match class_trait.kind() { + TraitKind::Slot { + type_name, + default_value, + .. + } => { + output + .get_or_insert_with(Default::default) + .variables + .insert( + trait_name, + VariableInfo { + type_info: type_name.local_name().map(|n| n.to_string()), + value: format_value(default_value), + stubbed: false, + }, + ); + } + TraitKind::Method { method, .. } => { + let stubbed = stubs.has_method(&trait_name); + output + .get_or_insert_with(Default::default) + .function + .insert(trait_name, FunctionInfo::from_method(method, stubbed)); + } + TraitKind::Getter { method, .. } => { + let stubbed = stubs.has_getter(&trait_name); + output.get_or_insert_with(Default::default).getter.insert( + trait_name, + VariableInfo { + type_info: Some( + method + .return_type() + .local_name() + .map(|n| n.to_string()) + .unwrap_or_else(|| "*".to_string()), + ), + value: None, + stubbed, + }, + ); + } + TraitKind::Setter { method, .. } => { + let stubbed = stubs.has_setter(&trait_name); + output.get_or_insert_with(Default::default).setter.insert( + trait_name, + VariableInfo { + type_info: Some( + method + .signature() + .first() + .and_then(|p| p.param_type_name.local_name()) + .map(|t| t.to_string()) + .unwrap_or_else(|| "*".to_string()), + ), + value: None, + stubbed, + }, + ); + } + TraitKind::Class { .. } => {} + TraitKind::Function { .. } => {} + TraitKind::Const { + type_name, + default_value, + .. + } => { + output + .get_or_insert_with(Default::default) + .constants + .insert( + trait_name, + VariableInfo { + type_info: type_name.local_name().map(|n| n.to_string()), + value: format_value(default_value), + stubbed: false, + }, + ); + } + } + } + } +} + +pub fn capture_specification(context: &mut UpdateContext) { + let mut definitions = FnvHashMap::::default(); + let stubs = crate::stub::get_known_stubs(); + + let defs = context.avm2.playerglobals_domain.defs().clone(); + let mut activation = Activation::from_nothing(context.reborrow()); + for (name, namespace, _) in defs.iter() { + let value = activation + .context + .avm2 + .playerglobals_domain + .get_defined_value(&mut activation, QName::new(namespace, name)) + .expect("Builtins shouldn't error"); + if let Some(object) = value.as_object() { + if let Some(class) = object.as_class_object() { + let class_name = class + .inner_class_definition() + .read() + .name() + .to_qualified_name_err_message(activation.context.gc_context) + .to_string(); + let class_stubs = ClassStubs::for_class(&class_name, &stubs); + definitions.insert( + class_name, + Definition::from_class(class, &mut activation, &class_stubs), + ); + } else if let Some(executable) = object.as_executable() { + let namespace_stubs = + ClassStubs::for_class(&namespace.as_uri().to_string(), &stubs); + let definition = definitions + .entry(namespace.as_uri().to_string()) + .or_default(); + let instance_traits = definition + .instance_traits + .get_or_insert_with(Default::default); + instance_traits.function.insert( + name.to_string(), + FunctionInfo::from_executable( + &executable, + namespace_stubs.has_method(&name.to_string()), + ), + ); + } + } else { + let definition = definitions + .entry(namespace.as_uri().to_string()) + .or_default(); + let instance_traits = definition + .instance_traits + .get_or_insert_with(Default::default); + instance_traits.constants.insert( + name.to_string(), + VariableInfo::from_value(value, &mut activation), + ); + } + } + serde_json::to_writer_pretty(&File::create("implementation.json").unwrap(), &definitions) + .unwrap(); + exit(0); +} diff --git a/core/src/player.rs b/core/src/player.rs index a537abf9e..efe744a12 100644 --- a/core/src/player.rs +++ b/core/src/player.rs @@ -5,7 +5,10 @@ use crate::avm1::Object; use crate::avm1::SystemProperties; use crate::avm1::VariableDumper; use crate::avm1::{Activation, ActivationIdentifier}; +use crate::avm1::{ScriptObject, TObject, Value}; use crate::avm1::{TObject, Value}; +use crate::avm2::api_version::ApiVersion; +use crate::avm2::specification::capture_specification; use crate::avm2::{ object::TObject as _, Activation as Avm2Activation, Avm2, CallStack, Object as Avm2Object, }; @@ -2548,6 +2551,7 @@ impl PlayerBuilder { stage.set_allow_fullscreen(context, self.allow_fullscreen); stage.post_instantiation(context, None, Instantiator::Movie, false); stage.build_matrices(context); + capture_specification(context); }); player_lock.gc_arena.borrow().mutate(|context, root| { let call_stack = root.data.read().avm2.call_stack(); diff --git a/core/src/stub.rs b/core/src/stub.rs index e1d4c0c2f..e1857a69d 100644 --- a/core/src/stub.rs +++ b/core/src/stub.rs @@ -54,6 +54,18 @@ pub enum Stub { Other(Cow<'static, str>), } +impl Stub { + pub fn avm2_class(&self) -> Option> { + match self { + Stub::Avm2Method { class, .. } => Some(class.clone()), + Stub::Avm2Getter { class, .. } => Some(class.clone()), + Stub::Avm2Setter { class, .. } => Some(class.clone()), + Stub::Avm2Constructor { class, .. } => Some(class.clone()), + _ => None, + } + } +} + impl Display for Stub { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { match self {