core: Added ability to mark stubs inside actionscript

This commit is contained in:
Nathan Adams 2023-02-01 21:45:10 +01:00
parent dff558170e
commit bdbf54ecd4
10 changed files with 245 additions and 19 deletions

2
Cargo.lock generated
View File

@ -307,9 +307,11 @@ dependencies = [
"convert_case",
"proc-macro2",
"quote",
"regex",
"serde",
"serde-xml-rs",
"swf",
"walkdir",
]
[[package]]

View File

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

View File

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

View File

@ -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<dyn std::error::Error>> {
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<Vec<u8>, Box<dyn st
Ok(out_bytes)
}
fn collect_stubs(root: &Path, out_dir: &Path) -> Result<(), Box<dyn std::error::Error>> {
let pattern = RegexBuilder::new(
r#"
\b (?P<type> stub_method | stub_getter | stub_setter) \s*
\( \s*
"(?P<class> .+)" \s*
, \s*
"(?P<property> .+)" \s*
(?:|
, \s*
"(?P<specifics> .+)" \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(())
}

View File

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

View File

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

View File

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

View File

@ -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<Object<'gc>>,
args: &[Value<'gc>],
) -> Result<Value<'gc>, 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<Object<'gc>>,
args: &[Value<'gc>],
) -> Result<Value<'gc>, 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<Object<'gc>>,
args: &[Value<'gc>],
) -> Result<Value<'gc>, 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<Object<'gc>>,

View File

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

View File

@ -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<Cow<'static, str>>,
},
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,