From bdbf54ecd42ae47e7c128e252c0a44e870010be7 Mon Sep 17 00:00:00 2001 From: Nathan Adams Date: Wed, 1 Feb 2023 21:45:10 +0100 Subject: [PATCH] core: Added ability to mark stubs inside actionscript --- Cargo.lock | 2 + core/build.rs | 8 +- core/build_playerglobal/Cargo.toml | 2 + core/build_playerglobal/src/lib.rs | 89 +++++++++++++++++++++ core/build_playerglobal/src/main.rs | 2 +- core/src/avm2/globals.rs | 21 +++++ core/src/avm2/globals/__ruffle__/logging.as | 6 ++ core/src/avm2/globals/toplevel.rs | 85 ++++++++++++++++++++ core/src/avm2/stubs.rs | 18 ++--- core/src/stub.rs | 31 +++++-- 10 files changed, 245 insertions(+), 19 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 16b0ec7a0..01589ec5e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -307,9 +307,11 @@ dependencies = [ "convert_case", "proc-macro2", "quote", + "regex", "serde", "serde-xml-rs", "swf", + "walkdir", ] [[package]] diff --git a/core/build.rs b/core/build.rs index 4d0b30268..c4ce72c0d 100644 --- a/core/build.rs +++ b/core/build.rs @@ -1,6 +1,10 @@ fn main() { - build_playerglobal::build_playerglobal("../".into(), std::env::var("OUT_DIR").unwrap().into()) - .expect("Failed to build playerglobal"); + build_playerglobal::build_playerglobal( + "../".into(), + std::env::var("OUT_DIR").unwrap().into(), + cfg!(feature = "known_stubs"), + ) + .expect("Failed to build playerglobal"); // This is overly conservative - it will cause us to rebuild playerglobal.swf // if *any* files in this directory change, not just .as files. diff --git a/core/build_playerglobal/Cargo.toml b/core/build_playerglobal/Cargo.toml index 8246f01b2..99d0951ac 100644 --- a/core/build_playerglobal/Cargo.toml +++ b/core/build_playerglobal/Cargo.toml @@ -16,3 +16,5 @@ clap = {version = "4.1.4", features = ["derive"]} serde = {version = "1.0.152", features = ["derive"]} serde-xml-rs = "0.6.0" colored = "2.0.0" +regex = "1.7.1" +walkdir = "2.3.2" diff --git a/core/build_playerglobal/src/lib.rs b/core/build_playerglobal/src/lib.rs index 6961b76cb..7c60374be 100644 --- a/core/build_playerglobal/src/lib.rs +++ b/core/build_playerglobal/src/lib.rs @@ -4,6 +4,9 @@ use convert_case::{Boundary, Case, Casing}; use proc_macro2::TokenStream; use quote::quote; +use regex::RegexBuilder; +use std::ffi::OsStr; +use std::fs; use std::fs::File; use std::io::ErrorKind; use std::io::Write; @@ -13,6 +16,7 @@ use std::str::FromStr; use swf::avm2::types::*; use swf::avm2::write::Writer; use swf::{DoAbc, DoAbcFlag, Header, Tag}; +use walkdir::WalkDir; // The metadata name - all metadata in our .as files // should be of the form `[Ruffle(key1 = value1, key2 = value2)]` @@ -30,6 +34,7 @@ const METADATA_NATIVE_INSTANCE_INIT: &str = "NativeInstanceInit"; pub fn build_playerglobal( repo_root: PathBuf, out_dir: PathBuf, + with_stubs: bool, ) -> Result<(), Box> { let classes_dir = repo_root.join("core/src/avm2/globals/"); let asc_path = repo_root.join("core/build_playerglobal/asc.jar"); @@ -77,6 +82,10 @@ pub fn build_playerglobal( std::fs::remove_file(playerglobal.with_extension("cpp"))?; std::fs::remove_file(playerglobal.with_extension("h"))?; + if with_stubs { + collect_stubs(&classes_dir, &out_dir)?; + } + bytes = write_native_table(&bytes, &out_dir)?; let tags = [Tag::DoAbc(DoAbc { @@ -443,3 +452,83 @@ fn write_native_table(data: &[u8], out_dir: &Path) -> Result, Box Result<(), Box> { + let pattern = RegexBuilder::new( + r#" + \b (?P stub_method | stub_getter | stub_setter) \s* + \( \s* + "(?P .+)" \s* + , \s* + "(?P .+)" \s* + (?:| + , \s* + "(?P .+)" \s* + ) + \) \s* + ; + "#, + ) + .ignore_whitespace(true) + .build()?; + + let mut stubs = Vec::new(); + + for entry in WalkDir::new(root) + .into_iter() + .filter_map(|f| f.ok()) + .filter(|f| f.path().extension() == Some(OsStr::new("as"))) + { + let contents = fs::read_to_string(entry.path())?; + for entry in pattern.captures_iter(&contents) { + let class = &entry["class"]; + let property = &entry["property"]; + let specifics = entry.name("specifics").map(|m| m.as_str()); + + match (&entry["type"], specifics) { + ("stub_method", Some(specifics)) => stubs.push(quote! { + crate::stub::Stub::Avm2Method { + class: Cow::Borrowed(#class), + method: Cow::Borrowed(#property), + specifics: Cow::Borrowed(#specifics) + } + }), + ("stub_method", None) => stubs.push(quote! { + crate::stub::Stub::Avm2Method { + class: Cow::Borrowed(#class), + method: Cow::Borrowed(#property), + specifics: None + } + }), + ("stub_getter", _) => stubs.push(quote! { + crate::stub::Stub::Avm2Getter { + class: Cow::Borrowed(#class), + property: Cow::Borrowed(#property) + } + }), + ("stub_setter", _) => stubs.push(quote! { + crate::stub::Stub::Avm2Setter { + class: Cow::Borrowed(#class), + property: Cow::Borrowed(#property) + } + }), + _ => panic!("Unsupported stub type {}", &entry["type"]), + } + } + } + + let stub_block = quote! { + #[cfg(feature = "known_stubs")] + use std::borrow::Cow; + + #[cfg(feature = "known_stubs")] + pub static AS_DEFINED_STUBS: &[crate::stub::Stub] = &[ + #(#stubs,)* + ]; + }; + + let mut as_stub_file = File::create(out_dir.join("actionscript_stubs.rs"))?; + as_stub_file.write_all(stub_block.to_string().as_bytes())?; + + Ok(()) +} diff --git a/core/build_playerglobal/src/main.rs b/core/build_playerglobal/src/main.rs index f245e2263..fff0b1a19 100644 --- a/core/build_playerglobal/src/main.rs +++ b/core/build_playerglobal/src/main.rs @@ -23,7 +23,7 @@ fn main() { let args = cli::Cli::parse(); match args.command { Commands::Compile { out_dir } => { - build_playerglobal::build_playerglobal(repo_root, out_dir.into()).unwrap(); + build_playerglobal::build_playerglobal(repo_root, out_dir.into(), false).unwrap(); } Commands::Lint => { let tmp = root.join("tmp"); diff --git a/core/src/avm2/globals.rs b/core/src/avm2/globals.rs index 54cb1b18b..6f27e4ec6 100644 --- a/core/src/avm2/globals.rs +++ b/core/src/avm2/globals.rs @@ -420,6 +420,27 @@ pub fn load_player_globals<'gc>( toplevel::log_warn, script, )?; + function( + activation, + "__ruffle__", + "stub_method", + toplevel::stub_method, + script, + )?; + function( + activation, + "__ruffle__", + "stub_getter", + toplevel::stub_getter, + script, + )?; + function( + activation, + "__ruffle__", + "stub_setter", + toplevel::stub_setter, + script, + )?; function(activation, "", "isFinite", toplevel::is_finite, script)?; function(activation, "", "isNaN", toplevel::is_nan, script)?; function(activation, "", "parseInt", toplevel::parse_int, script)?; diff --git a/core/src/avm2/globals/__ruffle__/logging.as b/core/src/avm2/globals/__ruffle__/logging.as index 088686a18..94132e25f 100644 --- a/core/src/avm2/globals/__ruffle__/logging.as +++ b/core/src/avm2/globals/__ruffle__/logging.as @@ -1,3 +1,9 @@ package __ruffle__ { public native function log_warn(...arguments); + + public native function stub_method(class_name, method); + + public native function stub_getter(class_name, method); + + public native function stub_setter(class_name, method); } diff --git a/core/src/avm2/globals/toplevel.rs b/core/src/avm2/globals/toplevel.rs index 7bd89aa83..fc47f8534 100644 --- a/core/src/avm2/globals/toplevel.rs +++ b/core/src/avm2/globals/toplevel.rs @@ -5,6 +5,8 @@ use crate::avm2::object::Object; use crate::avm2::value::Value; use crate::avm2::Error; use crate::string::{AvmString, WStr, WString}; +use crate::stub::Stub; +use std::borrow::Cow; pub fn trace<'gc>( activation: &mut Activation<'_, 'gc>, @@ -54,6 +56,89 @@ pub fn log_warn<'gc>( Ok(Value::Undefined) } +pub fn stub_method<'gc>( + activation: &mut Activation<'_, 'gc>, + _this: Option>, + args: &[Value<'gc>], +) -> Result, Error<'gc>> { + match args { + [class, method] => { + let class = class.coerce_to_string(activation)?; + let method = method.coerce_to_string(activation)?; + activation + .context + .stub_tracker + .encounter(&Stub::Avm2Method { + class: Cow::Owned(class.to_utf8_lossy().to_string()), + method: Cow::Owned(method.to_utf8_lossy().to_string()), + specifics: None, + }); + } + [class, method, specifics] => { + let class = class.coerce_to_string(activation)?; + let method = method.coerce_to_string(activation)?; + let specifics = specifics.coerce_to_string(activation)?; + activation + .context + .stub_tracker + .encounter(&Stub::Avm2Method { + class: Cow::Owned(class.to_utf8_lossy().to_string()), + method: Cow::Owned(method.to_utf8_lossy().to_string()), + specifics: Some(Cow::Owned(specifics.to_utf8_lossy().to_string())), + }); + } + _ => tracing::warn!("(__ruffle__.stub_method called with wrong args)"), + } + + Ok(Value::Undefined) +} + +pub fn stub_getter<'gc>( + activation: &mut Activation<'_, 'gc>, + _this: Option>, + args: &[Value<'gc>], +) -> Result, Error<'gc>> { + match args { + [class, property] => { + let class = class.coerce_to_string(activation)?; + let property = property.coerce_to_string(activation)?; + activation + .context + .stub_tracker + .encounter(&Stub::Avm2Getter { + class: Cow::Owned(class.to_utf8_lossy().to_string()), + property: Cow::Owned(property.to_utf8_lossy().to_string()), + }); + } + _ => tracing::warn!("(__ruffle__.stub_getter called with wrong args)"), + } + + Ok(Value::Undefined) +} + +pub fn stub_setter<'gc>( + activation: &mut Activation<'_, 'gc>, + _this: Option>, + args: &[Value<'gc>], +) -> Result, Error<'gc>> { + match args { + [class, property] => { + let class = class.coerce_to_string(activation)?; + let property = property.coerce_to_string(activation)?; + activation + .context + .stub_tracker + .encounter(&Stub::Avm2Setter { + class: Cow::Owned(class.to_utf8_lossy().to_string()), + property: Cow::Owned(property.to_utf8_lossy().to_string()), + }); + } + _ => tracing::warn!("(__ruffle__.stub_setter called with wrong args)"), + } + + Ok(Value::Undefined) +} + pub fn is_finite<'gc>( activation: &mut Activation<'_, 'gc>, _this: Option>, diff --git a/core/src/avm2/stubs.rs b/core/src/avm2/stubs.rs index bf70a2851..ee70b38eb 100644 --- a/core/src/avm2/stubs.rs +++ b/core/src/avm2/stubs.rs @@ -6,8 +6,8 @@ macro_rules! avm2_stub_method { linkme::distributed_slice($crate::stub::KNOWN_STUBS) )] static STUB: $crate::stub::Stub = $crate::stub::Stub::Avm2Method { - class: $class, - method: $method, + class: std::borrow::Cow::Borrowed($class), + method: std::borrow::Cow::Borrowed($method), specifics: None, }; $activation.context.stub_tracker.encounter(&STUB); @@ -18,9 +18,9 @@ macro_rules! avm2_stub_method { linkme::distributed_slice($crate::stub::KNOWN_STUBS) )] static STUB: $crate::stub::Stub = $crate::stub::Stub::Avm2Method { - class: $class, - method: $method, - specifics: Some($specifics), + class: std::borrow::Cow::Borrowed($class), + method: std::borrow::Cow::Borrowed($method), + specifics: Some(std::borrow::Cow::Borrowed($specifics)), }; $activation.context.stub_tracker.encounter(&STUB); }; @@ -46,8 +46,8 @@ macro_rules! avm2_stub_getter { linkme::distributed_slice($crate::stub::KNOWN_STUBS) )] static STUB: $crate::stub::Stub = $crate::stub::Stub::Avm2Getter { - class: $class, - property: $property, + class: std::borrow::Cow::Borrowed($class), + property: std::borrow::Cow::Borrowed($property), }; $activation.context.stub_tracker.encounter(&STUB); }; @@ -61,8 +61,8 @@ macro_rules! avm2_stub_setter { linkme::distributed_slice($crate::stub::KNOWN_STUBS) )] static STUB: $crate::stub::Stub = $crate::stub::Stub::Avm2Setter { - class: $class, - property: $property, + class: std::borrow::Cow::Borrowed($class), + property: std::borrow::Cow::Borrowed($property), }; $activation.context.stub_tracker.encounter(&STUB); }; diff --git a/core/src/stub.rs b/core/src/stub.rs index 91d0ec78e..687538598 100644 --- a/core/src/stub.rs +++ b/core/src/stub.rs @@ -7,6 +7,23 @@ use std::fmt::{Debug, Display, Formatter}; #[linkme::distributed_slice] pub static KNOWN_STUBS: [Stub] = [..]; +#[cfg(feature = "known_stubs")] +mod external { + include!(concat!(env!("OUT_DIR"), "/actionscript_stubs.rs")); +} + +#[cfg(feature = "known_stubs")] +pub fn get_known_stubs() -> FnvHashSet<&'static Stub> { + let mut result = FnvHashSet::default(); + for stub in KNOWN_STUBS.iter() { + result.insert(stub); + } + for stub in external::AS_DEFINED_STUBS { + result.insert(stub); + } + result +} + #[derive(Ord, PartialOrd, Eq, PartialEq, Hash, Debug, Clone)] pub enum Stub { Avm1Method { @@ -15,17 +32,17 @@ pub enum Stub { specifics: Option<&'static str>, }, Avm2Method { - class: &'static str, - method: &'static str, - specifics: Option<&'static str>, + class: Cow<'static, str>, + method: Cow<'static, str>, + specifics: Option>, }, Avm2Getter { - class: &'static str, - property: &'static str, + class: Cow<'static, str>, + property: Cow<'static, str>, }, Avm2Setter { - class: &'static str, - property: &'static str, + class: Cow<'static, str>, + property: Cow<'static, str>, }, Avm2Constructor { class: &'static str,