avm2: Add support for playerglobal instance allocators

This commit adds support for combining instance allocators with
ActionScript playerglobal class definitions. This is activated
by defining the metadata `[Ruffle(InstanceAllocator = "true")]`
on the ActionScript class definition.

The implementation of this feature is very similar to native methods:
`build_playerglobal` checks for the metadata described above,
and defines a const `NATIVE_INSTANCE_ALLOCATOR_TABLE` mapping
class ids to function pointers.

To demonstrate this feature, I've converted `Event` to ActionScript
(keeping the existing instance allocator function).
I've also converted `ActivityEvent` and `ContextMenuEvent` to
`ActionScript`, to demonstrate how this simplifies inheritance.
In a future PR, we can convert the remaining events to ActionScript,
and remove the `EventData` enum entirely.

Unfortunately, `flex-sdk`'s `asc.jar` compiler strips out all metadata
when the `-optimize` option is passed. As a result, I forked
`flex-sdk` and disabled this behavior:
https://github.com/ruffle-rs/flex-sdk/releases/tag/ruffle-1.0.0

The modified `asc.jar` (built from the forked repository)
is included in this PR, and replaces the our previous 'asc.jar'
downloaded from the official Flex SDK release.

* Change metadata to `[Ruffle(InstanceAllocator)]`

* Strip out metadata before saving bytecode
This commit is contained in:
Aaron Hill 2022-07-21 01:11:46 -05:00 committed by GitHub
parent 01c8c38762
commit bb6f07ee1a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 528 additions and 514 deletions

Binary file not shown.

View File

@ -10,11 +10,19 @@ use std::path::{Path, PathBuf};
use std::process::Command;
use std::str::FromStr;
use swf::avm2::types::*;
use swf::avm2::write::Writer;
use swf::DoAbc;
use swf::Header;
use swf::SwfStr;
use swf::Tag;
// The metadata name - all metadata in our .as files
// should be of the form `[Ruffle(key1 = value1, key2 = value2)]`
const RUFFLE_METADATA_NAME: &str = "Ruffle";
// Indicates that we should generate a reference to an instance allocator
// method (used as a metadata key with `Ruffle` metadata)
const METADATA_INSTANCE_ALLOCATOR: &str = "InstanceAllocator";
/// If successful, returns a list of paths that were used. If this is run
/// from a build script, these paths should be printed with
/// cargo:rerun-if-changed
@ -49,14 +57,14 @@ pub fn build_playerglobal(
}
let playerglobal = out_dir.join("playerglobal");
let bytes = std::fs::read(playerglobal.with_extension("abc"))?;
let mut bytes = std::fs::read(playerglobal.with_extension("abc"))?;
// Cleanup the temporary files written out by 'asc.jar'
std::fs::remove_file(playerglobal.with_extension("abc"))?;
std::fs::remove_file(playerglobal.with_extension("cpp"))?;
std::fs::remove_file(playerglobal.with_extension("h"))?;
write_native_table(&bytes, &out_dir)?;
bytes = write_native_table(&bytes, &out_dir)?;
let tags = vec![Tag::DoAbc(DoAbc {
name: SwfStr::from_utf8_str(""),
@ -95,6 +103,91 @@ fn resolve_multiname_ns<'a>(abc: &'a AbcFile, multiname: &Multiname) -> &'a str
}
}
fn flash_to_rust_path(path: &str) -> String {
// 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("::")
}
fn rust_method_path(
abc: &AbcFile,
trait_: &Trait,
parent: Option<Index<Multiname>>,
prefix: &str,
suffix: &str,
) -> TokenStream {
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 += prefix;
path += &flash_to_rust_path(resolve_multiname_name(&abc, trait_name));
path += suffix;
// 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();
quote! { Some(#path_tokens) }
}
fn strip_metadata(abc: &mut AbcFile) {
abc.metadata.clear();
for instance in &mut abc.instances {
for trait_ in &mut instance.traits {
trait_.metadata.clear();
}
}
for class in &mut abc.classes {
for trait_ in &mut class.traits {
trait_.metadata.clear();
}
}
for script in &mut abc.scripts {
for trait_ in &mut script.traits {
trait_.metadata.clear();
}
}
for body in &mut abc.method_bodies {
for trait_ in &mut body.traits {
trait_.metadata.clear();
}
}
}
/// Handles native functons defined in our `playerglobal`
///
/// The high-level idea is to generate code (specifically, a `TokenStream`)
@ -109,12 +202,16 @@ fn resolve_multiname_ns<'a>(abc: &'a AbcFile, multiname: &Multiname) -> &'a str
///
/// 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>> {
///
/// Returns a modified version of 'data' that should be saved to disk
/// in our generated SWF
fn write_native_table(data: &[u8], out_dir: &Path) -> Result<Vec<u8>, Box<dyn std::error::Error>> {
let mut reader = swf::avm2::read::Reader::new(data);
let abc = reader.read()?;
let mut abc = reader.read()?;
let none_tokens = quote! { None };
let mut rust_paths = vec![none_tokens; abc.methods.len()];
let mut rust_paths = vec![none_tokens.clone(); abc.methods.len()];
let mut rust_instance_allocators = vec![none_tokens; abc.classes.len()];
let mut check_trait = |trait_: &Trait, parent: Option<Index<Multiname>>| {
let method_id = match trait_.kind {
@ -145,55 +242,55 @@ fn write_native_table(data: &[u8], out_dir: &Path) -> Result<(), Box<dyn std::er
_ => "",
};
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("::")
rust_paths[method_id.0 as usize] =
rust_method_path(&abc, trait_, parent, method_prefix, "");
};
// Look for `[Ruffle(InstanceAllocator)]` metadata - if present,
// generate a reference to an allocator function in the native instance
// allocators table.
let mut check_instance_allocator = |trait_: &Trait| {
let class_id = if let TraitKind::Class { slot_id, .. } = trait_.kind {
slot_id
} else {
return;
};
let mut path = "crate::avm2::globals::".to_string();
let class_name_idx = abc.instances[class_id as usize - 1].name.0;
let class_name = resolve_multiname_name(
&abc,
&abc.constant_pool.multinames[class_name_idx as usize - 1],
);
let trait_name = &abc.constant_pool.multinames[trait_.name.0 as usize - 1];
let method_name = "::".to_string() + &flash_to_rust_path(class_name) + "_allocator";
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 += "::";
for metadata_idx in &trait_.metadata {
let metadata = &abc.metadata[metadata_idx.0 as usize];
let name = &abc.constant_pool.strings[metadata.name.0 as usize - 1];
match name.as_str() {
RUFFLE_METADATA_NAME => {}
_ => panic!("Unexpected class metadata {:?}", name),
}
for item in &metadata.items {
let key = if item.key.0 != 0 {
Some(abc.constant_pool.strings[item.key.0 as usize - 1].as_str())
} else {
None
};
let value = &abc.constant_pool.strings[item.value.0 as usize - 1];
match (key, value.as_str()) {
// Match `[Ruffle(InstanceAllocator)]`
(None, METADATA_INSTANCE_ALLOCATOR) => {
// This results in a path of the form
// `crate::avm2::globals::<path::to::class>::<class_allocator>`
rust_instance_allocators[class_id as usize - 1] =
rust_method_path(&abc, trait_, None, "", &method_name);
}
_ => panic!("Unexpected metadata pair ({:?}, {})", key, value),
}
}
}
// 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:
@ -216,29 +313,37 @@ fn write_native_table(data: &[u8], out_dir: &Path) -> Result<(), Box<dyn std::er
for script in &abc.scripts {
for trait_ in &script.traits {
check_trait(trait_, None);
check_instance_allocator(trait_);
}
}
// 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.
// Finally, generate the actual code.
let make_native_table = quote! {
const NATIVE_TABLE: &[Option<crate::avm2::method::NativeMethodImpl>] = &[
// 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.
pub const NATIVE_METHOD_TABLE: &[Option<crate::avm2::method::NativeMethodImpl>] = &[
#(#rust_paths,)*
];
// This is very similar to `NATIVE_METHOD_TABLE`, but we have one entry per
// class, rather than per method. When an entry is `Some(fn_ptr)`, we use
// `fn_ptr` as the instance allocator for the corresponding class when we
// load it into Ruffle.
pub const NATIVE_INSTANCE_ALLOCATOR_TABLE: &[Option<crate::avm2::class::AllocatorFn>] = &[
#(#rust_instance_allocators,)*
];
}
.to_string();
@ -249,5 +354,13 @@ fn write_native_table(data: &[u8], out_dir: &Path) -> Result<(), Box<dyn std::er
let mut native_table_file = File::create(out_dir.join("native_table.rs"))?;
native_table_file.write_all(make_native_table.as_bytes())?;
Ok(())
// Ruffle doesn't need metadata items at runtime, so strip
// them out to save space
strip_metadata(&mut abc);
let mut out_bytes = Vec::new();
let mut writer = Writer::new(&mut out_bytes);
writer.write(abc).expect("Failed to write modified ABC");
Ok(out_bytes)
}

View File

@ -1,5 +1,6 @@
//! ActionScript Virtual Machine 2 (AS3) support
use crate::avm2::class::AllocatorFn;
use crate::avm2::globals::SystemClasses;
use crate::avm2::method::{Method, NativeMethodImpl};
use crate::avm2::object::EventObject;
@ -76,7 +77,10 @@ pub struct Avm2<'gc> {
system_classes: Option<SystemClasses<'gc>>,
#[collect(require_static)]
native_table: &'static [Option<NativeMethodImpl>],
native_method_table: &'static [Option<NativeMethodImpl>],
#[collect(require_static)]
native_instance_allocator_table: &'static [Option<AllocatorFn>],
/// A list of objects which are capable of recieving broadcasts.
///
@ -101,7 +105,8 @@ impl<'gc> Avm2<'gc> {
stack: Vec::new(),
globals,
system_classes: None,
native_table: Default::default(),
native_method_table: Default::default(),
native_instance_allocator_table: Default::default(),
broadcast_list: Default::default(),
#[cfg(feature = "avm_debug")]

View File

@ -354,6 +354,16 @@ impl<'gc> Class<'gc> {
attributes.set(ClassAttributes::FINAL, abc_instance.is_final);
attributes.set(ClassAttributes::INTERFACE, abc_instance.is_interface);
let mut instance_allocator = None;
// When loading a class from our playerglobal, grab the corresponding native
// allocator function from the table (which may be `None`)
if unit.domain().is_avm2_global_domain(activation) {
instance_allocator = activation.avm2().native_instance_allocator_table
[class_index as usize]
.map(Allocator);
}
Ok(GcCell::allocate(
activation.context.gc_context,
Self {
@ -363,7 +373,7 @@ impl<'gc> Class<'gc> {
attributes,
protected_namespace,
interfaces,
instance_allocator: None,
instance_allocator,
instance_init,
native_instance_init,
instance_traits: Vec::new(),

View File

@ -210,9 +210,10 @@ fn class<'gc>(
let class_read = class_def.read();
let super_class = if let Some(sc_name) = class_read.super_class_name() {
let super_class: Result<Object<'gc>, Error> = global
.get_property(sc_name, activation)
let super_class: Result<Object<'gc>, Error> = activation
.resolve_definition(sc_name)
.ok()
.and_then(|v| v)
.and_then(|v| v.as_object())
.ok_or_else(|| {
format!(
@ -441,13 +442,6 @@ pub fn load_player_globals<'gc>(
)?;
class(activation, flash::system::system::create_class(mc), script)?;
// package `flash.events`
avm2_system_class!(
event,
activation,
flash::events::event::create_class(mc),
script
);
class(
activation,
flash::events::ieventdispatcher::create_interface(mc),
@ -458,62 +452,6 @@ pub fn load_player_globals<'gc>(
flash::events::eventdispatcher::create_class(mc),
script,
)?;
avm2_system_class!(
mouseevent,
activation,
flash::events::mouseevent::create_class(mc),
script
);
avm2_system_class!(
textevent,
activation,
flash::events::textevent::create_class(mc),
script
);
avm2_system_class!(
errorevent,
activation,
flash::events::errorevent::create_class(mc),
script
);
avm2_system_class!(
securityerrorevent,
activation,
flash::events::securityerrorevent::create_class(mc),
script
);
avm2_system_class!(
ioerrorevent,
activation,
flash::events::ioerrorevent::create_class(mc),
script
);
class(
activation,
flash::events::contextmenuevent::create_class(mc),
script,
)?;
class(
activation,
flash::events::keyboardevent::create_class(mc),
script,
)?;
class(
activation,
flash::events::progressevent::create_class(mc),
script,
)?;
class(
activation,
flash::events::activityevent::create_class(mc),
script,
)?;
avm2_system_class!(
fullscreenevent,
activation,
flash::events::fullscreenevent::create_class(mc),
script
);
class(
activation,
flash::events::eventphase::create_class(mc),
@ -750,10 +688,64 @@ pub fn load_player_globals<'gc>(
)?;
// Inside this call, the macro `avm2_system_classes_playerglobal`
// triggers classloading. Therefore, we run `load_playerglobal` last,
// so that all of our classes have been defined.
// triggers classloading. Therefore, we run `load_playerglobal`
// relative late, so that it can access classes defined before
// this call.
load_playerglobal(activation, domain)?;
// These are event definitions, which need to be able to
// load "flash.events.Event", which is defined in our playerglobal.
// Therefore, they need to come after "load_playerglobal"
// FIXME: Convert all of these event classes to ActionScript,
// which will allow us to remove all of these calls.
avm2_system_class!(
mouseevent,
activation,
flash::events::mouseevent::create_class(mc),
script
);
avm2_system_class!(
textevent,
activation,
flash::events::textevent::create_class(mc),
script
);
avm2_system_class!(
errorevent,
activation,
flash::events::errorevent::create_class(mc),
script
);
avm2_system_class!(
securityerrorevent,
activation,
flash::events::securityerrorevent::create_class(mc),
script
);
avm2_system_class!(
ioerrorevent,
activation,
flash::events::ioerrorevent::create_class(mc),
script
);
class(
activation,
flash::events::keyboardevent::create_class(mc),
script,
)?;
class(
activation,
flash::events::progressevent::create_class(mc),
script,
)?;
avm2_system_class!(
fullscreenevent,
activation,
flash::events::fullscreenevent::create_class(mc),
script
);
Ok(())
}
@ -761,8 +753,9 @@ 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"));
mod native {
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
@ -770,7 +763,8 @@ fn load_playerglobal<'gc>(
activation: &mut Activation<'_, 'gc, '_>,
domain: Domain<'gc>,
) -> Result<(), Error> {
activation.avm2().native_table = NATIVE_TABLE;
activation.avm2().native_method_table = native::NATIVE_METHOD_TABLE;
activation.avm2().native_instance_allocator_table = native::NATIVE_INSTANCE_ALLOCATOR_TABLE;
let movie = Arc::new(SwfMovie::from_data(PLAYERGLOBAL, None, None)?);
@ -805,7 +799,14 @@ fn load_playerglobal<'gc>(
// This acts the same way as 'avm2_system_class', but for classes
// declared in 'playerglobal'. Classes are declared as ("package", "class", field_name),
// and are stored in 'avm2().system_classes'
avm2_system_classes_playerglobal!(activation, script, [("flash.display", "Scene", scene)]);
avm2_system_classes_playerglobal!(
activation,
script,
[
("flash.display", "Scene", scene),
("flash.events", "Event", event),
]
);
Ok(())
}

View File

@ -52,6 +52,17 @@ 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.
## Custom instance allocator
You can use a custom instance allocator method by applying the metadata
`[Ruffle(InstanceAllocator)]`
to your class definition. A reference to a function named `<classname>_allocator`
will be generated - this should be an `AllocatorFn`, just like when defining
a class in Rust. This allocator will automatically be registered when the corresponding
class is loaded.
See `flash/events/Event.as` for an example
## Compiling
Java must be installed for the build process to complete.

View File

@ -0,0 +1,5 @@
// This is a stub - the actual class is defined in `interactiveobject.rs`
package flash.display {
public class InteractiveObject {
}
}

View File

@ -1,7 +1,5 @@
//! `flash.events` namespace
pub mod activityevent;
pub mod contextmenuevent;
pub mod errorevent;
pub mod event;
pub mod eventdispatcher;

View File

@ -0,0 +1,19 @@
package flash.events {
import flash.events.Event;
public class ActivityEvent extends Event {
public var activating:Boolean;
public function ActivityEvent(type:String, bubbles:Boolean = false, cancelable:Boolean = false, activating:Boolean = false) {
super(type, bubbles, cancelable);
this.activating = activating;
}
override public function clone() : Event {
return new ActivityEvent(this.type, this.bubbles, this.cancelable, this.activating);
}
override public function toString(): String {
return formatToString("ActivityEvent","type","bubbles","cancelable","eventPhase","activating");
}
}
}

View File

@ -0,0 +1,49 @@
package flash.events {
import flash.display.InteractiveObject;
public class ContextMenuEvent extends Event {
public static const MENU_ITEM_SELECT:String = "menuItemSelect";
public static const MENU_SELECT:String = "menuSelect";
private var _mouseTarget:InteractiveObject;
private var _contextMenuOwner:InteractiveObject;
private var _isMouseTargetInaccessible:Boolean;
public function ContextMenuEvent(type:String, bubbles:Boolean = false, cancelable:Boolean = false, mouseTarget:InteractiveObject = null, contextMenuOwner:InteractiveObject = null) {
super(type,bubbles,cancelable);
this._mouseTarget = mouseTarget;
this._contextMenuOwner = contextMenuOwner;
}
override public function clone() : Event {
return new ContextMenuEvent(this.type, this.bubbles, this.cancelable, this._mouseTarget, this._contextMenuOwner);
}
override public function toString() : String {
return this.formatToString("ContextMenuEvent","type","bubbles","cancelable","eventPhase","mouseTarget","contextMenuOwner");
}
public function get mouseTarget() : InteractiveObject {
return this._mouseTarget;
}
public function set mouseTarget(value:InteractiveObject) : void {
this._mouseTarget = value;
}
public function get contextMenuOwner() : InteractiveObject {
return this._contextMenuOwner;
}
public function set contextMenuOwner(value:InteractiveObject) : void {
this._contextMenuOwner = value;
}
public function get isMouseTargetInaccessible() : Boolean {
return this._isMouseTargetInaccessible;
}
public function set isMouseTargetInaccessible(value:Boolean) : void {
this._isMouseTargetInaccessible = value;
}
}
}

View File

@ -0,0 +1,129 @@
package flash.events {
[Ruffle(InstanceAllocator)]
public class Event {
public static const ACTIVATE:String = "activate";
public static const ADDED:String = "added";
public static const ADDED_TO_STAGE:String = "addedToStage";
public static const BROWSER_ZOOM_CHANGE:String = "browserZoomChange";
public static const CANCEL:String = "cancel";
public static const CHANGE:String = "change";
public static const CLEAR:String = "clear";
public static const CLOSE:String = "close";
public static const COMPLETE:String = "complete";
public static const CONNECT:String = "connect";
public static const COPY:String = "copy";
public static const CUT:String = "cut";
public static const DEACTIVATE:String = "deactivate";
public static const ENTER_FRAME:String = "enterFrame";
public static const FRAME_CONSTRUCTED:String = "frameConstructed";
public static const EXIT_FRAME:String = "exitFrame";
public static const FRAME_LABEL:String = "frameLabel";
public static const ID3:String = "id3";
public static const INIT:String = "init";
public static const MOUSE_LEAVE:String = "mouseLeave";
public static const OPEN:String = "open";
public static const PASTE:String = "paste";
public static const REMOVED:String = "removed";
public static const REMOVED_FROM_STAGE:String = "removedFromStage";
public static const RENDER:String = "render";
public static const RESIZE:String = "resize";
public static const SCROLL:String = "scroll";
public static const TEXT_INTERACTION_MODE_CHANGE:String = "textInteractionModeChange";
public static const SELECT:String = "select";
public static const SELECT_ALL:String = "selectAll";
public static const SOUND_COMPLETE:String = "soundComplete";
public static const TAB_CHILDREN_CHANGE:String = "tabChildrenChange";
public static const TAB_ENABLED_CHANGE:String = "tabEnabledChange";
public static const TAB_INDEX_CHANGE:String = "tabIndexChange";
public static const UNLOAD:String = "unload";
public static const FULLSCREEN:String = "fullScreen";
public static const CONTEXT3D_CREATE:String = "context3DCreate";
public static const TEXTURE_READY:String = "textureReady";
public static const VIDEO_FRAME:String = "videoFrame";
public static const SUSPEND:String = "suspend";
public static const CHANNEL_MESSAGE:String = "channelMessage";
public static const CHANNEL_STATE:String = "channelState";
public static const WORKER_STATE:String = "workerState";
public function Event(type:String, bubbles:Boolean = false, cancelable:Boolean = false) {
this.init(type, bubbles, cancelable);
}
private native function init(type:String, bubbles:Boolean = false, cancelable:Boolean = false):void;
public native function get bubbles():Boolean;
public native function get cancelable():Boolean;
public native function get currentTarget():Object;
public native function get eventPhase():uint;
public native function get target():Object;
public native function get type():String;
public function clone():Event {
return new Event(this.type, this.bubbles, this.cancelable);
}
public function toString(): String {
return this.formatToString("Event","type","bubbles","cancelable","eventPhase");
}
public function formatToString(className:String, ... arguments):String {
var fmt = "[" + className;
for each (var key: String in arguments) {
var val = this[key];
if(val is String) {
fmt += " " + key + "=\"" + val + "\"";
} else {
fmt += " " + key + "=" + val;
}
}
return fmt += "]";
}
public native function isDefaultPrevented(): Boolean;
public native function preventDefault():void;
public native function stopPropagation():void;
public native function stopImmediatePropagation():void;
}
}

View File

@ -1,76 +0,0 @@
use crate::avm2::activation::Activation;
use crate::avm2::class::{Class, ClassAttributes};
use crate::avm2::method::{Method, NativeMethodImpl};
use crate::avm2::names::{Namespace, QName};
use crate::avm2::object::Object;
use crate::avm2::value::Value;
use crate::avm2::Error;
use gc_arena::{GcCell, MutationContext};
/// Implements `flash.events.ActivityEvent`'s instance constructor.
pub fn instance_init<'gc>(
activation: &mut Activation<'_, 'gc, '_>,
this: Option<Object<'gc>>,
args: &[Value<'gc>],
) -> Result<Value<'gc>, Error> {
if let Some(this) = this {
activation.super_init(this, args)?; // Event uses the first three parameters
}
Ok(Value::Undefined)
}
/// Implements `flash.events.ActivityEvent`'s class constructor.
pub fn class_init<'gc>(
_activation: &mut Activation<'_, 'gc, '_>,
_this: Option<Object<'gc>>,
_args: &[Value<'gc>],
) -> Result<Value<'gc>, Error> {
Ok(Value::Undefined)
}
pub fn activating<'gc>(
_activation: &mut Activation<'_, 'gc, '_>,
_this: Option<Object<'gc>>,
_args: &[Value<'gc>],
) -> Result<Value<'gc>, Error> {
log::warn!("ActivityEvent.activating - not implemented");
Ok(Value::Undefined)
}
pub fn set_activating<'gc>(
_activation: &mut Activation<'_, 'gc, '_>,
_this: Option<Object<'gc>>,
_args: &[Value<'gc>],
) -> Result<Value<'gc>, Error> {
log::warn!("ActivityEvent.set_activating - not implemented");
Ok(Value::Undefined)
}
/// Construct `ActivityEvent`'s class.
pub fn create_class<'gc>(mc: MutationContext<'gc, '_>) -> GcCell<'gc, Class<'gc>> {
let class = Class::new(
QName::new(Namespace::package("flash.events"), "ActivityEvent"),
Some(QName::new(Namespace::package("flash.events"), "Event").into()),
Method::from_builtin(instance_init, "<ActivityEvent instance initializer>", mc),
Method::from_builtin(class_init, "<ActivityEvent class initializer>", mc),
mc,
);
let mut write = class.write(mc);
const PUBLIC_INSTANCE_PROPERTIES: &[(
&str,
Option<NativeMethodImpl>,
Option<NativeMethodImpl>,
)] = &[("activating", Some(activating), Some(set_activating))];
write.define_public_builtin_instance_properties(mc, PUBLIC_INSTANCE_PROPERTIES);
write.set_attributes(ClassAttributes::SEALED);
const CONSTANTS: &[(&str, &str)] = &[("ACTIVITY", "activity")];
write.define_public_constant_string_class_traits(CONSTANTS);
class
}

View File

@ -1,53 +0,0 @@
use crate::avm2::activation::Activation;
use crate::avm2::class::{Class, ClassAttributes};
use crate::avm2::method::Method;
use crate::avm2::names::{Namespace, QName};
use crate::avm2::object::Object;
use crate::avm2::value::Value;
use crate::avm2::Error;
use gc_arena::{GcCell, MutationContext};
/// Implements `flash.events.ContextMenuEvent`'s instance constructor.
pub fn instance_init<'gc>(
activation: &mut Activation<'_, 'gc, '_>,
this: Option<Object<'gc>>,
args: &[Value<'gc>],
) -> Result<Value<'gc>, Error> {
if let Some(this) = this {
activation.super_init(this, args)?; // ErrorEvent, Event use these
}
Ok(Value::Undefined)
}
/// Implements `flash.events.ContextMenuEvent`'s class constructor.
pub fn class_init<'gc>(
_activation: &mut Activation<'_, 'gc, '_>,
_this: Option<Object<'gc>>,
_args: &[Value<'gc>],
) -> Result<Value<'gc>, Error> {
Ok(Value::Undefined)
}
/// Construct `ContextMenuEvent`'s class.
pub fn create_class<'gc>(mc: MutationContext<'gc, '_>) -> GcCell<'gc, Class<'gc>> {
let class = Class::new(
QName::new(Namespace::package("flash.events"), "ContextMenuEvent"),
Some(QName::new(Namespace::package("flash.events"), "Event").into()),
Method::from_builtin(instance_init, "<ContextMenuEvent instance initializer>", mc),
Method::from_builtin(class_init, "<ContextMenuEvent class initializer>", mc),
mc,
);
let mut write = class.write(mc);
write.set_attributes(ClassAttributes::SEALED);
const CONSTANTS: &[(&str, &str)] = &[
("MENU_ITEM_SELECT", "menuItemSelect"),
("MENU_SELECT", "menuSelect"),
];
write.define_public_constant_string_class_traits(CONSTANTS);
class
}

View File

@ -1,60 +1,42 @@
//! `flash.events.Event` builtin/prototype
use crate::avm2::activation::Activation;
use crate::avm2::class::{Class, ClassAttributes};
use crate::avm2::method::{Method, NativeMethodImpl};
use crate::avm2::names::{Namespace, QName};
use crate::avm2::object::{event_allocator, EventObject, Object, TObject};
use crate::avm2::object::{Object, TObject};
use crate::avm2::value::Value;
use crate::avm2::Error;
use crate::string::AvmString;
use gc_arena::{GcCell, MutationContext};
/// Implements `flash.events.Event`'s instance constructor.
pub fn instance_init<'gc>(
pub use crate::avm2::object::event_allocator;
pub fn init<'gc>(
activation: &mut Activation<'_, 'gc, '_>,
this: Option<Object<'gc>>,
args: &[Value<'gc>],
) -> Result<Value<'gc>, Error> {
if let Some(this) = this {
activation.super_init(this, &[])?;
if let Some(mut evt) = this.as_event_mut(activation.context.gc_context) {
evt.set_event_type(
args.get(0)
.cloned()
.unwrap_or(Value::Undefined)
.coerce_to_string(activation)?,
);
evt.set_bubbles(
args.get(1)
.cloned()
.unwrap_or(Value::Bool(false))
.coerce_to_boolean(),
);
evt.set_cancelable(
args.get(2)
.cloned()
.unwrap_or(Value::Bool(false))
.coerce_to_boolean(),
);
}
}
Ok(Value::Undefined)
}
/// Implements `flash.events.Event`'s class constructor.
pub fn class_init<'gc>(
_activation: &mut Activation<'_, 'gc, '_>,
_this: Option<Object<'gc>>,
_args: &[Value<'gc>],
) -> Result<Value<'gc>, Error> {
let this = this.unwrap();
let mut evt = this.as_event_mut(activation.context.gc_context).unwrap();
evt.set_event_type(
args.get(0)
.cloned()
.unwrap_or(Value::Undefined)
.coerce_to_string(activation)?,
);
evt.set_bubbles(
args.get(1)
.cloned()
.unwrap_or(Value::Bool(false))
.coerce_to_boolean(),
);
evt.set_cancelable(
args.get(2)
.cloned()
.unwrap_or(Value::Bool(false))
.coerce_to_boolean(),
);
Ok(Value::Undefined)
}
/// Implements `bubbles` property's getter
pub fn bubbles<'gc>(
pub fn get_bubbles<'gc>(
_activation: &mut Activation<'_, 'gc, '_>,
this: Option<Object<'gc>>,
_args: &[Value<'gc>],
@ -67,7 +49,7 @@ pub fn bubbles<'gc>(
}
/// Implements `cancelable` property's getter
pub fn cancelable<'gc>(
pub fn get_cancelable<'gc>(
_activation: &mut Activation<'_, 'gc, '_>,
this: Option<Object<'gc>>,
_args: &[Value<'gc>],
@ -93,7 +75,7 @@ pub fn get_type<'gc>(
}
/// Implements `target` property's getter
pub fn target<'gc>(
pub fn get_target<'gc>(
_activation: &mut Activation<'_, 'gc, '_>,
this: Option<Object<'gc>>,
_args: &[Value<'gc>],
@ -106,7 +88,7 @@ pub fn target<'gc>(
}
/// Implements `currentTarget` property's getter
pub fn current_target<'gc>(
pub fn get_current_target<'gc>(
_activation: &mut Activation<'_, 'gc, '_>,
this: Option<Object<'gc>>,
_args: &[Value<'gc>],
@ -122,7 +104,7 @@ pub fn current_target<'gc>(
}
/// Implements `eventPhase` property's getter
pub fn event_phase<'gc>(
pub fn get_event_phase<'gc>(
_activation: &mut Activation<'_, 'gc, '_>,
this: Option<Object<'gc>>,
_args: &[Value<'gc>],
@ -135,66 +117,6 @@ pub fn event_phase<'gc>(
Ok(Value::Undefined)
}
/// Implements `clone`
pub fn clone<'gc>(
activation: &mut Activation<'_, 'gc, '_>,
this: Option<Object<'gc>>,
_args: &[Value<'gc>],
) -> Result<Value<'gc>, Error> {
if let Some(evt) = this.unwrap().as_event() {
return Ok(EventObject::from_event(activation, evt.clone())?.into());
}
Ok(Value::Undefined)
}
/// Implements `formatToString`
pub fn format_to_string<'gc>(
activation: &mut Activation<'_, 'gc, '_>,
this: Option<Object<'gc>>,
args: &[Value<'gc>],
) -> Result<Value<'gc>, Error> {
use std::fmt::Write;
if let Some(this) = this {
let class_name = args
.get(0)
.cloned()
.unwrap_or(Value::Undefined)
.coerce_to_string(activation)?;
let mut stringified_params = String::new();
if let Some(params) = args.get(1..) {
for param_name in params {
let param_name = QName::dynamic_name(match param_name {
Value::Undefined | Value::Null => "null".into(),
_ => param_name.coerce_to_string(activation)?,
})
.into();
let param_value = this
.get_property(&param_name, activation)?
.coerce_to_debug_string(activation)?;
write!(
stringified_params,
" {}={}",
param_name.local_name().unwrap(),
param_value
)
.unwrap();
}
}
return Ok(AvmString::new_utf8(
activation.context.gc_context,
format!("[{}{}]", class_name, stringified_params),
)
.into());
}
Ok(Value::Undefined)
}
/// Implements `isDefaultPrevented`
pub fn is_default_prevented<'gc>(
_activation: &mut Activation<'_, 'gc, '_>,
@ -246,134 +168,3 @@ pub fn stop_immediate_propagation<'gc>(
Ok(Value::Undefined)
}
/// Implements `toString`
pub fn to_string<'gc>(
activation: &mut Activation<'_, 'gc, '_>,
this: Option<Object<'gc>>,
_args: &[Value<'gc>],
) -> Result<Value<'gc>, Error> {
if let Some(this) = this {
if let Some(event) = this.as_event() {
let event_type = event.event_type();
let bubbles = event.is_bubbling();
let cancelable = event.is_cancelable();
let phase = event.phase() as u32;
return Ok(AvmString::new_utf8(
activation.context.gc_context,
format!(
"[Event type=\"{}\" bubbles={} cancelable={} eventPhase={}]",
event_type, bubbles, cancelable, phase
),
)
.into());
}
}
Ok(Value::Undefined)
}
/// Construct `Event`'s class.
pub fn create_class<'gc>(mc: MutationContext<'gc, '_>) -> GcCell<'gc, Class<'gc>> {
let class = Class::new(
QName::new(Namespace::package("flash.events"), "Event"),
Some(QName::new(Namespace::public(), "Object").into()),
Method::from_builtin(instance_init, "<Event instance initializer>", mc),
Method::from_builtin(class_init, "<Event class initializer>", mc),
mc,
);
let mut write = class.write(mc);
write.set_attributes(ClassAttributes::SEALED);
write.set_instance_allocator(event_allocator);
const PUBLIC_INSTANCE_PROPERTIES: &[(
&str,
Option<NativeMethodImpl>,
Option<NativeMethodImpl>,
)] = &[
("bubbles", Some(bubbles), None),
("cancelable", Some(cancelable), None),
("type", Some(get_type), None),
("target", Some(target), None),
("currentTarget", Some(current_target), None),
("eventPhase", Some(event_phase), None),
];
write.define_public_builtin_instance_properties(mc, PUBLIC_INSTANCE_PROPERTIES);
const PUBLIC_INSTANCE_METHODS: &[(&str, NativeMethodImpl)] = &[
("clone", clone),
("formatToString", format_to_string),
("isDefaultPrevented", is_default_prevented),
("preventDefault", prevent_default),
("stopPropagation", stop_propagation),
("stopImmediatePropagation", stop_immediate_propagation),
("toString", to_string),
];
write.define_public_builtin_instance_methods(mc, PUBLIC_INSTANCE_METHODS);
const CONSTANTS: &[(&str, &str)] = &[
("ACTIVATE", "activate"),
("ADDED", "added"),
("ADDED_TO_STAGE", "addedToStage"),
("BROWSER_ZOOM_CHANGE", "browserZoomChange"),
("CANCEL", "cancel"),
("CHANGE", "change"),
("CHANNEL_MESSAGE", "channelMessage"),
("CHANNEL_STATE", "channelState"),
("CLEAR", "clear"),
("CLOSE", "close"),
("CLOSING", "closing"),
("COMPLETE", "complete"),
("CONNECT", "connect"),
("CONTEXT3D_CREATE", "context3DCreate"),
("COPY", "copy"),
("CUT", "cut"),
("DEACTIVATE", "deactivate"),
("DISPLAYING", "displaying"),
("ENTER_FRAME", "enterFrame"),
("EXIT_FRAME", "exitFrame"),
("EXITING", "exiting"),
("FRAME_CONSTRUCTED", "frameConstructed"),
("FRAME_LABEL", "frameLabel"),
("FULLSCREEN", "fullScreen"),
("HTML_BOUNDS_CHANGE", "htmlBoundsChange"),
("HTML_DOM_INITIALIZE", "htmlDOMInitialize"),
("HTML_RENDER", "htmlRender"),
("ID3", "id3"),
("INIT", "init"),
("LOCATION_CHANGE", "locationChange"),
("MOUSE_LEAVE", "mouseLeave"),
("NETWORK_CHANGE", "networkChange"),
("OPEN", "open"),
("PASTE", "paste"),
("PREPARING", "preparing"),
("REMOVED", "removed"),
("REMOVED_FROM_STAGE", "removedFromStage"),
("RENDER", "render"),
("RESIZE", "resize"),
("SCROLL", "scroll"),
("SELECT", "select"),
("SELECT_ALL", "selectAll"),
("SOUND_COMPLETE", "soundComplete"),
("STANDARD_ERROR_CLOSE", "standardErrorClose"),
("STANDARD_INPUT_CLOSE", "standardInputClose"),
("STANDARD_OUTPUT_CLOSE", "standardOutputClose"),
("SUSPEND", "suspend"),
("TAB_CHILDREN_CHANGE", "tabChildrenChange"),
("TAB_ENABLED_CHANGE", "tabEnabledChange"),
("TAB_INDEX_CHANGE", "tabIndexChange"),
("TEXT_INTERACTION_MODE_CHANGE", "textInteractionModeChange"),
("TEXTURE_READY", "textureReady"),
("UNLOAD", "unload"),
("USER_IDLE", "userIdle"),
("USER_PRESENT", "userPresent"),
("VIDEO_FRAME", "videoFrame"),
("WORKER_STATE", "workerState"),
];
write.define_public_constant_string_class_traits(CONSTANTS);
class
}

View File

@ -17,7 +17,9 @@ pub fn instance_init<'gc>(
args: &[Value<'gc>],
) -> Result<Value<'gc>, Error> {
if let Some(this) = this {
activation.super_init(this, args)?;
// Get up to three arguments
let event_args = &args[..(std::cmp::min(args.len(), 2))];
activation.super_init(this, event_args)?;
if let Some(mut evt) = this.as_event_mut(activation.context.gc_context) {
// This is technically duplicative of `Event`'s initializer, but
// we have different default parameters.

View File

@ -1,4 +1,6 @@
// List is ordered alphabetically.
// List is ordered alphabetically, except where superclasses
// need to come before subclasses.
include "flash/accessibility/AccessibilityProperties.as"
include "flash/display/ActionScriptVersion.as"
include "flash/display/BitmapDataChannel.as"
@ -30,6 +32,12 @@ include "flash/display/StageQuality.as"
include "flash/display/StageScaleMode.as"
include "flash/display/SWFVersion.as"
include "flash/display/TriangleCulling.as"
// Event needs to come before its subclasses
include "flash/events/Event.as"
include "flash/events/ActivityEvent.as"
include "flash/events/ContextMenuEvent.as"
include "flash/geom/ColorTransform.as"
include "flash/geom/Orientation3D.as"
include "flash/geom/Matrix.as"

View File

@ -6,6 +6,7 @@ include "Object.as"
// List is ordered alphabetically.
include "Array.as"
include "Boolean.as"
include "flash/display/InteractiveObject.as"
include "flash/events/EventDispatcher.as"
include "Number.as"
include "String.as"

View File

@ -140,7 +140,8 @@ impl<'gc> TranslationUnit<'gc> {
// allowing us to use 'bc_method' later on without a borrow-checker error.
let method = (|| {
if is_global {
if let Some(native) = activation.avm2().native_table[method_index.0 as usize] {
if let Some(native) = activation.avm2().native_method_table[method_index.0 as usize]
{
let variadic = bc_method.is_variadic();
return Method::from_builtin_and_params(
native,