diff --git a/web/src/external_interface.rs b/web/src/external_interface.rs new file mode 100644 index 000000000..0eb67a1f6 --- /dev/null +++ b/web/src/external_interface.rs @@ -0,0 +1,167 @@ +use crate::{JavascriptPlayer, CURRENT_CONTEXT}; +use js_sys::{Array, Function, Object}; +use ruffle_core::context::UpdateContext; +use ruffle_core::external::{ + ExternalInterfaceMethod, ExternalInterfaceProvider, FsCommandProvider, Value as ExternalValue, + Value, +}; +use std::collections::BTreeMap; +use wasm_bindgen::{JsCast, JsValue}; + +#[derive(Clone)] +pub struct JavascriptInterface { + js_player: JavascriptPlayer, +} + +struct JavascriptMethod { + this: JsValue, + function: JsValue, +} + +impl ExternalInterfaceMethod for JavascriptMethod { + fn call(&self, context: &mut UpdateContext<'_, '_>, args: &[ExternalValue]) -> ExternalValue { + let old_context = CURRENT_CONTEXT.with(|v| { + v.replace(Some(unsafe { + std::mem::transmute::<&mut UpdateContext, &mut UpdateContext<'static, 'static>>( + context, + ) + } as *mut UpdateContext)) + }); + let result = if let Some(function) = self.function.dyn_ref::() { + let args_array = Array::new(); + for arg in args { + args_array.push(&external_to_js_value(arg.to_owned())); + } + if let Ok(result) = function.apply(&self.this, &args_array) { + js_to_external_value(&result) + } else { + ExternalValue::Undefined + } + } else { + ExternalValue::Undefined + }; + CURRENT_CONTEXT.with(|v| v.replace(old_context)); + result + } +} + +impl JavascriptInterface { + pub fn new(js_player: JavascriptPlayer) -> Self { + Self { js_player } + } + + fn find_method(&self, root: JsValue, name: &str) -> Option { + let mut parent = JsValue::UNDEFINED; + let mut value = root; + for key in name.split('.') { + parent = value; + value = crate::get_property(&parent, &JsValue::from_str(key)).ok()?; + } + if value.is_function() { + Some(JavascriptMethod { + this: parent, + function: value, + }) + } else { + None + } + } +} + +impl ExternalInterfaceProvider for JavascriptInterface { + fn get_method(&self, name: &str) -> Option> { + if let Some(method) = self.find_method(self.js_player.clone().into(), name) { + return Some(Box::new(method)); + } + if let Some(window) = web_sys::window() { + if let Some(method) = self.find_method(window.into(), name) { + return Some(Box::new(method)); + } + } + + // Return a dummy method, as `ExternalInterface.call` must return `undefined`, not `null`. + Some(Box::new(JavascriptMethod { + this: JsValue::UNDEFINED, + function: JsValue::UNDEFINED, + })) + } + + fn on_callback_available(&self, name: &str) { + self.js_player.on_callback_available(name); + } + + fn get_id(&self) -> Option { + self.js_player.get_object_id() + } +} + +impl FsCommandProvider for JavascriptInterface { + fn on_fs_command(&self, command: &str, args: &str) -> bool { + self.js_player + .on_fs_command(command, args) + .unwrap_or_default() + } +} + +pub fn js_to_external_value(js: &JsValue) -> ExternalValue { + if let Some(value) = js.as_f64() { + ExternalValue::Number(value) + } else if let Some(value) = js.as_string() { + ExternalValue::String(value) + } else if let Some(value) = js.as_bool() { + ExternalValue::Bool(value) + } else if let Some(array) = js.dyn_ref::() { + let values: Vec<_> = array + .values() + .into_iter() + .flatten() + .map(|v| js_to_external_value(&v)) + .collect(); + ExternalValue::List(values) + } else if let Some(object) = js.dyn_ref::() { + let mut values = BTreeMap::new(); + for entry in Object::entries(object).values() { + if let Ok(entry) = entry.and_then(|v| v.dyn_into::()) { + if let Some(key) = entry.get(0).as_string() { + values.insert(key, js_to_external_value(&entry.get(1))); + } + } + } + ExternalValue::Object(values) + } else if js.is_null() { + ExternalValue::Null + } else { + ExternalValue::Undefined + } +} + +pub fn external_to_js_value(external: ExternalValue) -> JsValue { + match external { + Value::Undefined => JsValue::UNDEFINED, + Value::Null => JsValue::NULL, + Value::Bool(value) => JsValue::from_bool(value), + Value::Number(value) => JsValue::from_f64(value), + Value::String(value) => JsValue::from_str(&value), + Value::Object(object) => { + let entries = Array::new(); + for (key, value) in object { + entries.push(&Array::of2( + &JsValue::from_str(&key), + &external_to_js_value(value), + )); + } + if let Ok(result) = Object::from_entries(&entries) { + result.into() + } else { + JsValue::NULL + } + } + Value::List(values) => { + let array = Array::new(); + for value in values { + array.push(&external_to_js_value(value)); + } + array.into() + } + } +} diff --git a/web/src/lib.rs b/web/src/lib.rs index 53a98e69d..a35907c61 100644 --- a/web/src/lib.rs +++ b/web/src/lib.rs @@ -3,24 +3,22 @@ //! Ruffle web frontend. mod audio; +mod external_interface; mod input; mod log_adapter; mod navigator; mod storage; mod ui; +use external_interface::{external_to_js_value, js_to_external_value, JavascriptInterface}; use input::{web_key_to_codepoint, web_to_ruffle_key_code, web_to_ruffle_text_control}; -use js_sys::{Array, Error as JsError, Function, Object, Promise, Uint8Array}; +use js_sys::{Error as JsError, Promise, Uint8Array}; use ruffle_core::backend::navigator::OpenURLMode; use ruffle_core::backend::ui::FontDefinition; use ruffle_core::compatibility_rules::CompatibilityRules; use ruffle_core::config::{Letterbox, NetworkingAccessMode}; use ruffle_core::context::UpdateContext; use ruffle_core::events::{MouseButton, MouseWheelDelta, TextControlCode}; -use ruffle_core::external::{ - ExternalInterfaceMethod, ExternalInterfaceProvider, FsCommandProvider, Value as ExternalValue, - Value, -}; use ruffle_core::tag_utils::SwfMovie; use ruffle_core::{swf, DefaultFont}; use ruffle_core::{ @@ -32,7 +30,6 @@ use ruffle_video_software::backend::SoftwareVideoBackend; use ruffle_web_common::JsResult; use serde::{Deserialize, Serialize}; use slotmap::{DefaultKey, SlotMap}; -use std::collections::BTreeMap; use std::rc::Rc; use std::str::FromStr; use std::sync::Once; @@ -135,11 +132,6 @@ extern "C" { fn display_unsupported_video(this: &JavascriptPlayer, url: &str); } -#[derive(Clone)] -struct JavascriptInterface { - js_player: JavascriptPlayer, -} - fn deserialize_color<'de, D>(deserializer: D) -> Result, D::Error> where D: serde::Deserializer<'de>, @@ -556,7 +548,7 @@ impl Ruffle { #[allow(clippy::boxed_local)] // for js_bind pub fn call_exposed_callback(&self, name: &str, args: Box<[JsValue]>) -> JsValue { - let args: Vec = args.iter().map(js_to_external_value).collect(); + let args: Vec<_> = args.iter().map(js_to_external_value).collect(); // Re-entrant callbacks need to return through the hole that was punched through for them // We record the context of external functions, and then if we get an internal callback @@ -1508,165 +1500,12 @@ pub enum RuffleInstanceError { InstanceNotFound, } -struct JavascriptMethod { - this: JsValue, - function: JsValue, -} - -impl ExternalInterfaceMethod for JavascriptMethod { - fn call(&self, context: &mut UpdateContext<'_, '_>, args: &[ExternalValue]) -> ExternalValue { - let old_context = CURRENT_CONTEXT.with(|v| { - v.replace(Some(unsafe { - std::mem::transmute::<&mut UpdateContext, &mut UpdateContext<'static, 'static>>( - context, - ) - } as *mut UpdateContext)) - }); - let result = if let Some(function) = self.function.dyn_ref::() { - let args_array = Array::new(); - for arg in args { - args_array.push(&external_to_js_value(arg.to_owned())); - } - if let Ok(result) = function.apply(&self.this, &args_array) { - js_to_external_value(&result) - } else { - ExternalValue::Undefined - } - } else { - ExternalValue::Undefined - }; - CURRENT_CONTEXT.with(|v| v.replace(old_context)); - result - } -} - #[wasm_bindgen(raw_module = "./ruffle-imports")] extern "C" { #[wasm_bindgen(catch, js_name = "getProperty")] pub fn get_property(target: &JsValue, key: &JsValue) -> Result; } -impl JavascriptInterface { - fn new(js_player: JavascriptPlayer) -> Self { - Self { js_player } - } - - fn find_method(&self, root: JsValue, name: &str) -> Option { - let mut parent = JsValue::UNDEFINED; - let mut value = root; - for key in name.split('.') { - parent = value; - value = get_property(&parent, &JsValue::from_str(key)).ok()?; - } - if value.is_function() { - Some(JavascriptMethod { - this: parent, - function: value, - }) - } else { - None - } - } -} - -impl ExternalInterfaceProvider for JavascriptInterface { - fn get_method(&self, name: &str) -> Option> { - if let Some(method) = self.find_method(self.js_player.clone().into(), name) { - return Some(Box::new(method)); - } - if let Some(window) = web_sys::window() { - if let Some(method) = self.find_method(window.into(), name) { - return Some(Box::new(method)); - } - } - - // Return a dummy method, as `ExternalInterface.call` must return `undefined`, not `null`. - Some(Box::new(JavascriptMethod { - this: JsValue::UNDEFINED, - function: JsValue::UNDEFINED, - })) - } - - fn on_callback_available(&self, name: &str) { - self.js_player.on_callback_available(name); - } - - fn get_id(&self) -> Option { - self.js_player.get_object_id() - } -} - -impl FsCommandProvider for JavascriptInterface { - fn on_fs_command(&self, command: &str, args: &str) -> bool { - self.js_player - .on_fs_command(command, args) - .unwrap_or_default() - } -} - -fn js_to_external_value(js: &JsValue) -> ExternalValue { - if let Some(value) = js.as_f64() { - ExternalValue::Number(value) - } else if let Some(value) = js.as_string() { - ExternalValue::String(value) - } else if let Some(value) = js.as_bool() { - ExternalValue::Bool(value) - } else if let Some(array) = js.dyn_ref::() { - let values: Vec<_> = array - .values() - .into_iter() - .flatten() - .map(|v| js_to_external_value(&v)) - .collect(); - ExternalValue::List(values) - } else if let Some(object) = js.dyn_ref::() { - let mut values = BTreeMap::new(); - for entry in Object::entries(object).values() { - if let Ok(entry) = entry.and_then(|v| v.dyn_into::()) { - if let Some(key) = entry.get(0).as_string() { - values.insert(key, js_to_external_value(&entry.get(1))); - } - } - } - ExternalValue::Object(values) - } else if js.is_null() { - ExternalValue::Null - } else { - ExternalValue::Undefined - } -} - -fn external_to_js_value(external: ExternalValue) -> JsValue { - match external { - Value::Undefined => JsValue::UNDEFINED, - Value::Null => JsValue::NULL, - Value::Bool(value) => JsValue::from_bool(value), - Value::Number(value) => JsValue::from_f64(value), - Value::String(value) => JsValue::from_str(&value), - Value::Object(object) => { - let entries = Array::new(); - for (key, value) in object { - entries.push(&Array::of2( - &JsValue::from_str(&key), - &external_to_js_value(value), - )); - } - if let Ok(result) = Object::from_entries(&entries) { - result.into() - } else { - JsValue::NULL - } - } - Value::List(values) => { - let array = Array::new(); - for value in values { - array.push(&external_to_js_value(value)); - } - array.into() - } - } -} - async fn create_renderer( builder: PlayerBuilder, document: &web_sys::Document,