Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

avm2: Implement support for native methods in playerglobal #7244

Merged
merged 1 commit into from
Jul 15, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions core/build_playerglobal/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,7 @@ version = "0.1.0"
edition = "2021"

[dependencies]
convert_case = "0.5.0"
proc-macro2 = "1.0.40"
quote = "1.0.20"
swf = { path = "../../swf" }
191 changes: 190 additions & 1 deletion core/build_playerglobal/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
//! An internal Ruffle utility to build our playerglobal
//! `library.swf`

use convert_case::{Case, Casing};
use proc_macro2::TokenStream;
use quote::quote;
use std::fs::File;
use std::path::PathBuf;
use std::io::Write;
use std::path::{Path, PathBuf};
use std::process::Command;
use std::str::FromStr;
use swf::avm2::types::*;
use swf::DoAbc;
use swf::Header;
use swf::SwfStr;
Expand Down Expand Up @@ -50,6 +56,8 @@ pub fn build_playerglobal(
std::fs::remove_file(playerglobal.with_extension("cpp"))?;
std::fs::remove_file(playerglobal.with_extension("h"))?;

write_native_table(&bytes, &out_dir)?;

let tags = vec![Tag::DoAbc(DoAbc {
name: SwfStr::from_utf8_str(""),
is_lazy_initialize: true,
Expand All @@ -62,3 +70,184 @@ pub fn build_playerglobal(

Ok(())
}

// Resolve the 'name' field of a `Multiname`. This only handles the cases
// that we need for our custom `playerglobal.swf` (
fn resolve_multiname_name<'a>(abc: &'a AbcFile, multiname: &Multiname) -> &'a str {
if let Multiname::QName { name, .. } | Multiname::Multiname { name, .. } = multiname {
&abc.constant_pool.strings[name.0 as usize - 1]
} else {
panic!("Unexpected Multiname {:?}", multiname);
}
}

// Like `resolve_multiname_name`, but for namespaces instead.
fn resolve_multiname_ns<'a>(abc: &'a AbcFile, multiname: &Multiname) -> &'a str {
if let Multiname::QName { namespace, .. } = multiname {
let ns = &abc.constant_pool.namespaces[namespace.0 as usize - 1];
if let Namespace::Package(p) = ns {
&abc.constant_pool.strings[p.0 as usize - 1]
} else {
panic!("Unexpected Namespace {:?}", ns);
}
} else {
panic!("Unexpected Multiname {:?}", multiname);
}
}

/// Handles native functons defined in our `playerglobal`
///
/// The high-level idea is to generate code (specifically, a `TokenStream`)
/// which builds a table - mapping from the method ids of native functions,
/// to Rust function pointers which implement them.
///
/// This table gets used when we first load a method from an ABC file.
/// If it's a native method in our `playerglobal`, we swap it out
/// with a `NativeMethod` retrieved from the table. To the rest of
/// the Ruffle codebase, it appears as though the method was always defined
/// as a native method, and never existed in the bytecode at all.
///
/// See `flash.system.Security.allowDomain` for an example of defining
/// and using a native method.
fn write_native_table(data: &[u8], out_dir: &Path) -> Result<(), Box<dyn std::error::Error>> {
let mut reader = swf::avm2::read::Reader::new(data);
let abc = reader.read()?;

let none_tokens = quote! { None };
let mut rust_paths = vec![none_tokens; abc.methods.len()];

let mut check_trait = |trait_: &Trait, parent: Option<Index<Multiname>>| {
let method_id = match trait_.kind {
TraitKind::Method { method, .. }
| TraitKind::Getter { method, .. }
| TraitKind::Setter { method, .. } => {
let abc_method = &abc.methods[method.0 as usize];
// We only want to process native methods
if !abc_method.flags.contains(MethodFlags::NATIVE) {
return;
}
method
}
TraitKind::Function { .. } => {
panic!("TraitKind::Function is not supported: {:?}", trait_)
}
_ => return,
};

// Note - technically, this could conflict with
// a method with a name starting with `get_` or `set_`.
// However, all Flash methods are named with lowerCamelCase,
// so we'll never actually need to implement a native method that
// would cause such a conflict.
let method_prefix = match trait_.kind {
TraitKind::Getter { .. } => "get_",
TraitKind::Setter { .. } => "set_",
_ => "",
};

let flash_to_rust_path = |path: &str| {
// Convert each component of the path to snake-case.
// This correctly handles sequences of upper-case letters,
// so 'URLLoader' becomes 'url_loader'
let components = path
.split('.')
.map(|component| component.to_case(Case::Snake))
.collect::<Vec<_>>();
// Form a Rust path from the snake-case components
components.join("::")
};

let mut path = "crate::avm2::globals::".to_string();

let trait_name = &abc.constant_pool.multinames[trait_.name.0 as usize - 1];

if let Some(parent) = parent {
// This is a method defined inside the class. Append the class namespace
// (the package) and the class name.
// For example, a namespace of "flash.system" and a name of "Security"
// turns into the path "flash::system::security"
let multiname = &abc.constant_pool.multinames[parent.0 as usize - 1];
path += &flash_to_rust_path(resolve_multiname_ns(&abc, multiname));
path += "::";
path += &flash_to_rust_path(resolve_multiname_name(&abc, multiname));
path += "::";
} else {
// This is a freestanding function. Append its namespace (the package).
// For example, the freestanding function "flash.utils.getDefinitionByName"
// has a namespace of "flash.utils", which turns into the path
// "flash::utils"
path += &flash_to_rust_path(resolve_multiname_ns(&abc, trait_name));
path += "::";
}

// Append the trait name - this corresponds to the actual method
// name (e.g. `getDefinitionByName`)
path += method_prefix;
path += &flash_to_rust_path(resolve_multiname_name(&abc, trait_name));

// Now that we've built up the path, convert it into a `TokenStream`.
// This gives us something like
// `crate::avm2::globals::flash::system::Security::allowDomain`
//
// The resulting `TokenStream` is suitable for usage with `quote!` to
// generate a reference to the function pointer that should exist
// at that path in Rust code.
let path_tokens = TokenStream::from_str(&path).unwrap();
rust_paths[method_id.0 as usize] = quote! { Some(#path_tokens) };
};

// We support three kinds of native methods:
// instance methods, class methods, and freestanding functions.
// We're going to insert them into an array indexed by `MethodId`,
// so it doesn't matter what order we visit them in.
for (i, instance) in abc.instances.iter().enumerate() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not entirely sure how it's matters, but it seems like nativegen.py recursively traverses through Script -> Method/Class, rather than a linear Instance scan.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nothing depends on the iteration order, since we just use the method ids.

// Look for native instance methods
for trait_ in &instance.traits {
check_trait(trait_, Some(instance.name));
}
// Look for native class methods (in the corresponding
// `Class` definition)
for trait_ in &abc.classes[i].traits {
check_trait(trait_, Some(instance.name));
}
}

// Look for freestanding methods
for script in &abc.scripts {
for trait_ in &script.traits {
check_trait(trait_, None);
}
}

// Finally, generate the actual code. This is a Rust array -
// the entry at index `i` is a Rust function pointer for the native
// method with id `i`. Not all methods in playerglobal will be native
// methods, so we store `None` in the entries corresponding to non-native
// functions. We expect the majority of the methods in playerglobal to be
// native, so this should only waste a small amount of memory.
//
// If a function pointer doesn't exist at the expected path,
// then Ruffle compilation will fail
// with an error message that mentions the non-existent path.
//
// When we initially load a method from an ABC file, we check if it's from our playerglobal,
// and if its ID exists in this table.
// If so, we replace it with a `NativeMethod` constructed
// from the function pointer we looked up in the table.

let make_native_table = quote! {
const NATIVE_TABLE: &[Option<crate::avm2::method::NativeMethodImpl>] = &[
#(#rust_paths,)*
];
}
.to_string();

// Each table entry ends with ') ,' - insert a newline so that
// each entry is on its own line. This makes error messages more readable.
let make_native_table = make_native_table.replace(") ,", ") ,\n");
Comment on lines +238 to +247
Copy link
Member

@Herschel Herschel Jul 14, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if it's worth using, for example, prettyplease to make the output nicer in the future. Overkill for this specific case, but I expect we will be generating more and more code that it might be worthwhile at some point.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, it might be worth adding along with custom instance allocator support (which I'm working on in a follow-up PR).


let mut native_table_file = File::create(out_dir.join("native_table.rs"))?;
native_table_file.write_all(make_native_table.as_bytes())?;

Ok(())
}
8 changes: 6 additions & 2 deletions core/src/avm2.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
//! ActionScript Virtual Machine 2 (AS3) support

use crate::avm2::globals::SystemClasses;
use crate::avm2::method::Method;
use crate::avm2::method::{Method, NativeMethodImpl};
use crate::avm2::object::EventObject;
use crate::avm2::script::{Script, TranslationUnit};
use crate::context::UpdateContext;
Expand Down Expand Up @@ -75,6 +75,9 @@ pub struct Avm2<'gc> {
/// System classes.
system_classes: Option<SystemClasses<'gc>>,

#[collect(require_static)]
native_table: &'static [Option<NativeMethodImpl>],

/// A list of objects which are capable of recieving broadcasts.
///
/// Certain types of events are "broadcast events" that are emitted on all
Expand All @@ -98,6 +101,7 @@ impl<'gc> Avm2<'gc> {
stack: Vec::new(),
globals,
system_classes: None,
native_table: Default::default(),
broadcast_list: Default::default(),

#[cfg(feature = "avm_debug")]
Expand Down Expand Up @@ -130,7 +134,7 @@ impl<'gc> Avm2<'gc> {
Method::Native(method) => {
//This exists purely to check if the builtin is OK with being called with
//no parameters.
init_activation.resolve_parameters(method.name, &[], &method.signature)?;
init_activation.resolve_parameters(&method.name, &[], &method.signature)?;

(method.method)(&mut init_activation, Some(scope), &[])?;
}
Expand Down
4 changes: 4 additions & 0 deletions core/src/avm2/domain.rs
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,10 @@ impl<'gc> Domain<'gc> {
))
}

pub fn is_avm2_global_domain(&self, activation: &mut Activation<'_, 'gc, '_>) -> bool {
activation.avm2().global_domain().0.as_ptr() == self.0.as_ptr()
}

/// Create a new domain with a given parent.
///
/// This function must not be called before the player globals have been
Expand Down
2 changes: 1 addition & 1 deletion core/src/avm2/function.rs
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ impl<'gc> Executable<'gc> {
}

let arguments = activation.resolve_parameters(
bm.method.name,
&bm.method.name,
arguments,
&bm.method.signature,
)?;
Expand Down
24 changes: 5 additions & 19 deletions core/src/avm2/globals.rs
Original file line number Diff line number Diff line change
Expand Up @@ -443,11 +443,6 @@ pub fn load_player_globals<'gc>(
flash::system::capabilities::create_class(mc),
script,
)?;
class(
activation,
flash::system::security::create_class(mc),
script,
)?;
class(activation, flash::system::system::create_class(mc), script)?;

// package `flash.events`
Expand Down Expand Up @@ -580,14 +575,6 @@ pub fn load_player_globals<'gc>(
script,
)?;

function(
activation,
"flash.utils",
"getDefinitionByName",
flash::utils::get_definition_by_name,
script,
)?;

// package `flash.display`
class(
activation,
Expand Down Expand Up @@ -756,12 +743,6 @@ pub fn load_player_globals<'gc>(
flash::net::object_encoding::create_class(mc),
script,
)?;
class(activation, flash::net::url_loader::create_class(mc), script)?;
class(
activation,
flash::net::url_request::create_class(mc),
script,
)?;

// package `flash.text`
avm2_system_class!(
Expand Down Expand Up @@ -806,12 +787,17 @@ pub fn load_player_globals<'gc>(
/// See that tool, and 'core/src/avm2/globals/README.md', for more details
const PLAYERGLOBAL: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/playerglobal.swf"));

// This defines a const named `NATIVE_TABLE`
include!(concat!(env!("OUT_DIR"), "/native_table.rs"));

/// Loads classes from our custom 'playerglobal' (which are written in ActionScript)
/// into the environment. See 'core/src/avm2/globals/README.md' for more information
fn load_playerglobal<'gc>(
activation: &mut Activation<'_, 'gc, '_>,
domain: Domain<'gc>,
) -> Result<(), Error> {
activation.avm2().native_table = NATIVE_TABLE;

let movie = Arc::new(SwfMovie::from_data(PLAYERGLOBAL, None, None)?);

let slice = SwfSlice::from(movie);
Expand Down
22 changes: 20 additions & 2 deletions core/src/avm2/globals/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,26 @@ In addition to potential copyright issues around redistributing Flash's `playerg
many of its classes rely on specific 'native' methods being provided
by the Flash VM, which Ruffle does not implement.

## Native methods

We support defining native methods (instance methods, class methods, and freestanding functions)
in ActionScript classes in playerglobal. During the build process, we automatically
generate a reference to a Rust function at the corresponding path in Ruffle.

For example, the native method function `flash.system.Security.allowDomain`
expects a Rust function to be defined at `crate::avm2::globals::flash::system::security::allowDomain`.

This function is cast to a `NativeMethodImpl` function pointer, exactly like
functions defined on a pure-Rust class definition.

If you're unsure of the path to use, just build Ruffle after marking the
`ActionScript` method as `native` - the compiler will produce an error
explaining where the Rust function needs to be defined.

The ActionScript method and the Rust function are automatically linked
together, and the Rust function will be invoked when the corresponding
function is called from ActionScript.

## Compiling

Java must be installed for the build process to complete.
Expand All @@ -53,8 +73,6 @@ when any of our ActionScript classes are changed.

## Limitations

* Only pure ActionScript classes are currently supported. Classes with
'native' methods are not yet supported.
* 'Special' classes which are loaded early during player initialization
(e.g. `Object`, `Function`, `Class`) cannot currently
be implemented in `playerglobal`, since they are initialized in a special
Expand Down
Loading