avm2: Track progress of avm2 and generate an implementation.json

This commit is contained in:
Nathan Adams 2023-08-05 21:49:11 +02:00 committed by Aaron Hill
parent abc8815445
commit 25b5c7b4e2
8 changed files with 573 additions and 3 deletions

View File

@ -58,6 +58,7 @@ mod qname;
mod regexp;
mod scope;
mod script;
pub mod specification;
mod string;
mod stubs;
mod traits;

View File

@ -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<PropertyMap<'gc, Script<'gc>>> {
Ref::map(self.0.read(), |this| &this.defs)
}
pub fn classes(&self) -> Ref<PropertyMap<'gc, GcCell<'gc, Class<'gc>>>> {
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()

View File

@ -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> {

View File

@ -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>>);

View File

@ -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<StringOrObject<'gc>, Value<'gc>> {
&self.values
}
pub fn get_property_local(
&self,
multiname: &Multiname<'gc>,

View File

@ -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<String> {
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<ParamInfo> {
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<String>,
#[serde(skip_serializing_if = "Option::is_none")]
implements: Option<String>,
#[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<String>,
#[serde(skip_serializing_if = "Option::is_none")]
value: Option<String>,
#[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<String>,
#[serde(skip_serializing_if = "is_false")]
variadic: bool,
}
#[derive(Serialize)]
struct FunctionInfo {
args: Vec<ParamInfo>,
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<String, VariableInfo>,
#[serde(rename = "var")]
#[serde(skip_serializing_if = "FnvHashMap::is_empty")]
variables: FnvHashMap<String, VariableInfo>,
#[serde(skip_serializing_if = "FnvHashMap::is_empty")]
function: FnvHashMap<String, FunctionInfo>,
#[serde(skip_serializing_if = "FnvHashMap::is_empty")]
getter: FnvHashMap<String, VariableInfo>,
#[serde(skip_serializing_if = "FnvHashMap::is_empty")]
setter: FnvHashMap<String, VariableInfo>,
}
#[derive(Serialize, Default)]
struct Definition {
#[serde(skip_serializing_if = "Option::is_none")]
classinfo: Option<ClassInfo>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(rename = "static")]
static_traits: Option<TraitList>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(rename = "instance")]
instance_traits: Option<TraitList>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(rename = "prototype")]
prototype: Option<TraitList>,
}
#[derive(Default)]
struct ClassStubs {
methods: FnvHashSet<Cow<'static, str>>,
getters: FnvHashSet<Cow<'static, str>>,
setters: FnvHashSet<Cow<'static, str>>,
}
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<StringOrObject<'gc>, 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<TraitList>,
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<TraitList>,
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::<String, Definition>::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);
}

View File

@ -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();

View File

@ -54,6 +54,18 @@ pub enum Stub {
Other(Cow<'static, str>),
}
impl Stub {
pub fn avm2_class(&self) -> Option<Cow<'static, str>> {
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 {