diff options
author | Narfinger <Narfinger@users.noreply.github.com> | 2025-05-13 14:54:18 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2025-05-13 12:54:18 +0000 |
commit | 991be359a3b896c204778cd3f3b226e16f9435b8 (patch) | |
tree | 8dc9b81799a9afee02de84aed25c2901028a35e2 | |
parent | 91c4c7b9982556bc11a39d47081984cbdfa6280e (diff) | |
download | servo-991be359a3b896c204778cd3f3b226e16f9435b8.tar.gz servo-991be359a3b896c204778cd3f3b226e16f9435b8.zip |
libservo: Allow embedders to execute JavaScript scripts via the API (#35720)
This change adds a new `WebView` API `evaluate_javascript()`, which
allows embedders to
execute JavaScript code and wait for a reply asynchronously. Ongoing
script execution is
tracked by a libservo `JavaScriptEvaluator` struct, which maps an id to
the callback passed
to the `evaluate_javascript()` method. The id is used to track the
script and its execution
through the other parts of Servo.
Testing: This changes includes `WebView` unit tests.
---------
Signed-off-by: Narfinger <Narfinger@users.noreply.github.com>
Signed-off-by: Martin Robinson <mrobinson@igalia.com>
Co-authored-by: Martin Robinson <mrobinson@igalia.com>
-rw-r--r-- | components/constellation/constellation.rs | 73 | ||||
-rw-r--r-- | components/constellation/tracing.rs | 5 | ||||
-rw-r--r-- | components/script/messaging.rs | 1 | ||||
-rw-r--r-- | components/script/script_thread.rs | 57 | ||||
-rw-r--r-- | components/servo/javascript_evaluator.rs | 65 | ||||
-rw-r--r-- | components/servo/lib.rs | 16 | ||||
-rw-r--r-- | components/servo/tests/webview.rs | 82 | ||||
-rw-r--r-- | components/servo/webview.rs | 23 | ||||
-rw-r--r-- | components/shared/constellation/from_script_message.rs | 9 | ||||
-rw-r--r-- | components/shared/constellation/lib.rs | 7 | ||||
-rw-r--r-- | components/shared/embedder/lib.rs | 64 | ||||
-rw-r--r-- | components/shared/script/lib.rs | 7 |
12 files changed, 391 insertions, 18 deletions
diff --git a/components/constellation/constellation.rs b/components/constellation/constellation.rs index e493a97d184..5db37800c42 100644 --- a/components/constellation/constellation.rs +++ b/components/constellation/constellation.rs @@ -129,9 +129,10 @@ use embedder_traits::resources::{self, Resource}; use embedder_traits::user_content_manager::UserContentManager; use embedder_traits::{ AnimationState, CompositorHitTestResult, Cursor, EmbedderMsg, EmbedderProxy, - FocusSequenceNumber, ImeEvent, InputEvent, MediaSessionActionType, MediaSessionEvent, - MediaSessionPlaybackState, MouseButton, MouseButtonAction, MouseButtonEvent, Theme, - ViewportDetails, WebDriverCommandMsg, WebDriverLoadStatus, + FocusSequenceNumber, ImeEvent, InputEvent, JSValue, JavaScriptEvaluationError, + JavaScriptEvaluationId, MediaSessionActionType, MediaSessionEvent, MediaSessionPlaybackState, + MouseButton, MouseButtonAction, MouseButtonEvent, Theme, ViewportDetails, WebDriverCommandMsg, + WebDriverLoadStatus, }; use euclid::Size2D; use euclid::default::Size2D as UntypedSize2D; @@ -1477,6 +1478,52 @@ where EmbedderToConstellationMessage::PaintMetric(pipeline_id, paint_metric_event) => { self.handle_paint_metric(pipeline_id, paint_metric_event); }, + EmbedderToConstellationMessage::EvaluateJavaScript( + webview_id, + evaluation_id, + script, + ) => { + self.handle_evaluate_javascript(webview_id, evaluation_id, script); + }, + } + } + + #[cfg_attr( + feature = "tracing", + tracing::instrument(skip_all, fields(servo_profiling = true), level = "trace") + )] + fn handle_evaluate_javascript( + &mut self, + webview_id: WebViewId, + evaluation_id: JavaScriptEvaluationId, + script: String, + ) { + let browsing_context_id = BrowsingContextId::from(webview_id); + let Some(pipeline) = self + .browsing_contexts + .get(&browsing_context_id) + .and_then(|browsing_context| self.pipelines.get(&browsing_context.pipeline_id)) + else { + self.handle_finish_javascript_evaluation( + evaluation_id, + Err(JavaScriptEvaluationError::InternalError), + ); + return; + }; + + if pipeline + .event_loop + .send(ScriptThreadMessage::EvaluateJavaScript( + pipeline.id, + evaluation_id, + script, + )) + .is_err() + { + self.handle_finish_javascript_evaluation( + evaluation_id, + Err(JavaScriptEvaluationError::InternalError), + ); } } @@ -1817,6 +1864,9 @@ where self.mem_profiler_chan .send(mem::ProfilerMsg::Report(sender)); }, + ScriptToConstellationMessage::FinishJavaScriptEvaluation(evaluation_id, result) => { + self.handle_finish_javascript_evaluation(evaluation_id, result) + }, } } @@ -3182,6 +3232,22 @@ where feature = "tracing", tracing::instrument(skip_all, fields(servo_profiling = true), level = "trace") )] + fn handle_finish_javascript_evaluation( + &mut self, + evaluation_id: JavaScriptEvaluationId, + result: Result<JSValue, JavaScriptEvaluationError>, + ) { + self.embedder_proxy + .send(EmbedderMsg::FinishJavaScriptEvaluation( + evaluation_id, + result, + )); + } + + #[cfg_attr( + feature = "tracing", + tracing::instrument(skip_all, fields(servo_profiling = true), level = "trace") + )] fn handle_subframe_loaded(&mut self, pipeline_id: PipelineId) { let browsing_context_id = match self.pipelines.get(&pipeline_id) { Some(pipeline) => pipeline.browsing_context_id, @@ -4691,6 +4757,7 @@ where NavigationHistoryBehavior::Replace, ); }, + // TODO: This should use the ScriptThreadMessage::EvaluateJavaScript command WebDriverCommandMsg::ScriptCommand(browsing_context_id, cmd) => { let pipeline_id = match self.browsing_contexts.get(&browsing_context_id) { Some(browsing_context) => browsing_context.pipeline_id, diff --git a/components/constellation/tracing.rs b/components/constellation/tracing.rs index eff7f755c6b..fd7fe7dd251 100644 --- a/components/constellation/tracing.rs +++ b/components/constellation/tracing.rs @@ -77,6 +77,7 @@ mod from_compositor { Self::SetWebViewThrottled(_, _) => target!("SetWebViewThrottled"), Self::SetScrollStates(..) => target!("SetScrollStates"), Self::PaintMetric(..) => target!("PaintMetric"), + Self::EvaluateJavaScript(..) => target!("EvaluateJavaScript"), } } } @@ -176,6 +177,7 @@ mod from_script { Self::TitleChanged(..) => target!("TitleChanged"), Self::IFrameSizes(..) => target!("IFrameSizes"), Self::ReportMemory(..) => target!("ReportMemory"), + Self::FinishJavaScriptEvaluation(..) => target!("FinishJavaScriptEvaluation"), } } } @@ -238,6 +240,9 @@ mod from_script { Self::ShutdownComplete => target_variant!("ShutdownComplete"), Self::ShowNotification(..) => target_variant!("ShowNotification"), Self::ShowSelectElementMenu(..) => target_variant!("ShowSelectElementMenu"), + Self::FinishJavaScriptEvaluation(..) => { + target_variant!("FinishJavaScriptEvaluation") + }, } } } diff --git a/components/script/messaging.rs b/components/script/messaging.rs index e0ea9e30af2..08d6fc841cf 100644 --- a/components/script/messaging.rs +++ b/components/script/messaging.rs @@ -91,6 +91,7 @@ impl MixedMessage { #[cfg(feature = "webgpu")] ScriptThreadMessage::SetWebGPUPort(..) => None, ScriptThreadMessage::SetScrollStates(id, ..) => Some(*id), + ScriptThreadMessage::EvaluateJavaScript(id, _, _) => Some(*id), }, MixedMessage::FromScript(inner_msg) => match inner_msg { MainThreadScriptMsg::Common(CommonScriptMsg::Task(_, _, pipeline_id, _)) => { diff --git a/components/script/script_thread.rs b/components/script/script_thread.rs index d6ab18be49b..4815e6feae1 100644 --- a/components/script/script_thread.rs +++ b/components/script/script_thread.rs @@ -50,9 +50,9 @@ use devtools_traits::{ }; use embedder_traits::user_content_manager::UserContentManager; use embedder_traits::{ - CompositorHitTestResult, EmbedderMsg, FocusSequenceNumber, InputEvent, MediaSessionActionType, - MouseButton, MouseButtonAction, MouseButtonEvent, Theme, ViewportDetails, - WebDriverScriptCommand, + CompositorHitTestResult, EmbedderMsg, FocusSequenceNumber, InputEvent, + JavaScriptEvaluationError, JavaScriptEvaluationId, MediaSessionActionType, MouseButton, + MouseButtonAction, MouseButtonEvent, Theme, ViewportDetails, WebDriverScriptCommand, }; use euclid::Point2D; use euclid::default::Rect; @@ -156,6 +156,7 @@ use crate::script_runtime::{ }; use crate::task_queue::TaskQueue; use crate::task_source::{SendableTaskSource, TaskSourceName}; +use crate::webdriver_handlers::jsval_to_webdriver; use crate::{devtools, webdriver_handlers}; thread_local!(static SCRIPT_THREAD_ROOT: Cell<Option<*const ScriptThread>> = const { Cell::new(None) }); @@ -1878,6 +1879,9 @@ impl ScriptThread { ScriptThreadMessage::SetScrollStates(pipeline_id, scroll_states) => { self.handle_set_scroll_states(pipeline_id, scroll_states) }, + ScriptThreadMessage::EvaluateJavaScript(pipeline_id, evaluation_id, script) => { + self.handle_evaluate_javascript(pipeline_id, evaluation_id, script, can_gc); + }, } } @@ -3815,6 +3819,53 @@ impl ScriptThread { ) } } + + fn handle_evaluate_javascript( + &self, + pipeline_id: PipelineId, + evaluation_id: JavaScriptEvaluationId, + script: String, + can_gc: CanGc, + ) { + let Some(window) = self.documents.borrow().find_window(pipeline_id) else { + let _ = self.senders.pipeline_to_constellation_sender.send(( + pipeline_id, + ScriptToConstellationMessage::FinishJavaScriptEvaluation( + evaluation_id, + Err(JavaScriptEvaluationError::WebViewNotReady), + ), + )); + return; + }; + + let global_scope = window.as_global_scope(); + let realm = enter_realm(global_scope); + let context = window.get_cx(); + + rooted!(in(*context) let mut return_value = UndefinedValue()); + global_scope.evaluate_js_on_global_with_result( + &script, + return_value.handle_mut(), + ScriptFetchOptions::default_classic_script(global_scope), + global_scope.api_base_url(), + can_gc, + ); + let result = match jsval_to_webdriver( + context, + global_scope, + return_value.handle(), + (&realm).into(), + can_gc, + ) { + Ok(ref value) => Ok(value.into()), + Err(_) => Err(JavaScriptEvaluationError::SerializationError), + }; + + let _ = self.senders.pipeline_to_constellation_sender.send(( + pipeline_id, + ScriptToConstellationMessage::FinishJavaScriptEvaluation(evaluation_id, result), + )); + } } impl Drop for ScriptThread { diff --git a/components/servo/javascript_evaluator.rs b/components/servo/javascript_evaluator.rs new file mode 100644 index 00000000000..41cb5539b05 --- /dev/null +++ b/components/servo/javascript_evaluator.rs @@ -0,0 +1,65 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +use std::collections::HashMap; + +use base::id::WebViewId; +use constellation_traits::EmbedderToConstellationMessage; +use embedder_traits::{JSValue, JavaScriptEvaluationError, JavaScriptEvaluationId}; + +use crate::ConstellationProxy; + +struct PendingEvaluation { + callback: Box<dyn FnOnce(Result<JSValue, JavaScriptEvaluationError>)>, +} + +pub(crate) struct JavaScriptEvaluator { + current_id: JavaScriptEvaluationId, + constellation_proxy: ConstellationProxy, + pending_evaluations: HashMap<JavaScriptEvaluationId, PendingEvaluation>, +} + +impl JavaScriptEvaluator { + pub(crate) fn new(constellation_proxy: ConstellationProxy) -> Self { + Self { + current_id: JavaScriptEvaluationId(0), + constellation_proxy, + pending_evaluations: Default::default(), + } + } + + fn generate_id(&mut self) -> JavaScriptEvaluationId { + let next_id = JavaScriptEvaluationId(self.current_id.0 + 1); + std::mem::replace(&mut self.current_id, next_id) + } + + pub(crate) fn evaluate( + &mut self, + webview_id: WebViewId, + script: String, + callback: Box<dyn FnOnce(Result<JSValue, JavaScriptEvaluationError>)>, + ) { + let evaluation_id = self.generate_id(); + self.constellation_proxy + .send(EmbedderToConstellationMessage::EvaluateJavaScript( + webview_id, + evaluation_id, + script, + )); + self.pending_evaluations + .insert(evaluation_id, PendingEvaluation { callback }); + } + + pub(crate) fn finish_evaluation( + &mut self, + evaluation_id: JavaScriptEvaluationId, + result: Result<JSValue, JavaScriptEvaluationError>, + ) { + (self + .pending_evaluations + .remove(&evaluation_id) + .expect("Received request to finish unknown JavaScript evaluation.") + .callback)(result) + } +} diff --git a/components/servo/lib.rs b/components/servo/lib.rs index b8210450cd8..d2c65429ba9 100644 --- a/components/servo/lib.rs +++ b/components/servo/lib.rs @@ -18,6 +18,7 @@ //! `WindowMethods` trait. mod clipboard_delegate; +mod javascript_evaluator; mod proxies; mod responders; mod servo_delegate; @@ -82,6 +83,7 @@ pub use gleam::gl; use gleam::gl::RENDERER; use ipc_channel::ipc::{self, IpcSender}; use ipc_channel::router::ROUTER; +use javascript_evaluator::JavaScriptEvaluator; pub use keyboard_types::*; use layout::LayoutFactoryImpl; use log::{Log, Metadata, Record, debug, warn}; @@ -196,6 +198,9 @@ pub struct Servo { compositor: Rc<RefCell<IOCompositor>>, constellation_proxy: ConstellationProxy, embedder_receiver: Receiver<EmbedderMsg>, + /// A struct that tracks ongoing JavaScript evaluations and is responsible for + /// calling the callback when the evaluation is complete. + javascript_evaluator: Rc<RefCell<JavaScriptEvaluator>>, /// Tracks whether we are in the process of shutting down, or have shut down. /// This is shared with `WebView`s and the `ServoRenderer`. shutdown_state: Rc<Cell<ShutdownState>>, @@ -487,10 +492,14 @@ impl Servo { opts.debug.convert_mouse_to_touch, ); + let constellation_proxy = ConstellationProxy::new(constellation_chan); Self { delegate: RefCell::new(Rc::new(DefaultServoDelegate)), compositor: Rc::new(RefCell::new(compositor)), - constellation_proxy: ConstellationProxy::new(constellation_chan), + javascript_evaluator: Rc::new(RefCell::new(JavaScriptEvaluator::new( + constellation_proxy.clone(), + ))), + constellation_proxy, embedder_receiver, shutdown_state, webviews: Default::default(), @@ -738,6 +747,11 @@ impl Servo { webview.delegate().request_unload(webview, request); } }, + EmbedderMsg::FinishJavaScriptEvaluation(evaluation_id, result) => { + self.javascript_evaluator + .borrow_mut() + .finish_evaluation(evaluation_id, result); + }, EmbedderMsg::Keyboard(webview_id, keyboard_event) => { if let Some(webview) = self.get_webview_handle(webview_id) { webview diff --git a/components/servo/tests/webview.rs b/components/servo/tests/webview.rs index 89fbe2025a3..41900015b94 100644 --- a/components/servo/tests/webview.rs +++ b/components/servo/tests/webview.rs @@ -11,12 +11,14 @@ mod common; -use std::cell::Cell; +use std::cell::{Cell, RefCell}; use std::rc::Rc; use anyhow::ensure; use common::{ServoTest, run_api_tests}; -use servo::{WebViewBuilder, WebViewDelegate}; +use servo::{ + JSValue, JavaScriptEvaluationError, LoadStatus, WebView, WebViewBuilder, WebViewDelegate, +}; #[derive(Default)] struct WebViewDelegateImpl { @@ -44,6 +46,81 @@ fn test_create_webview(servo_test: &ServoTest) -> Result<(), anyhow::Error> { Ok(()) } +fn evaluate_javascript( + servo_test: &ServoTest, + webview: WebView, + script: impl ToString, +) -> Result<JSValue, JavaScriptEvaluationError> { + let load_webview = webview.clone(); + let _ = servo_test.spin(move || Ok(load_webview.load_status() != LoadStatus::Complete)); + + let saved_result = Rc::new(RefCell::new(None)); + let callback_result = saved_result.clone(); + webview.evaluate_javascript(script, move |result| { + *callback_result.borrow_mut() = Some(result) + }); + + let spin_result = saved_result.clone(); + let _ = servo_test.spin(move || Ok(spin_result.borrow().is_none())); + + (*saved_result.borrow()) + .clone() + .expect("Should have waited until value available") +} + +fn test_evaluate_javascript_basic(servo_test: &ServoTest) -> Result<(), anyhow::Error> { + let delegate = Rc::new(WebViewDelegateImpl::default()); + let webview = WebViewBuilder::new(servo_test.servo()) + .delegate(delegate.clone()) + .build(); + + let result = evaluate_javascript(servo_test, webview.clone(), "undefined"); + ensure!(result == Ok(JSValue::Undefined)); + + let result = evaluate_javascript(servo_test, webview.clone(), "null"); + ensure!(result == Ok(JSValue::Null)); + + let result = evaluate_javascript(servo_test, webview.clone(), "42"); + ensure!(result == Ok(JSValue::Number(42.0))); + + let result = evaluate_javascript(servo_test, webview.clone(), "3 + 4"); + ensure!(result == Ok(JSValue::Number(7.0))); + + let result = evaluate_javascript(servo_test, webview.clone(), "'abc' + 'def'"); + ensure!(result == Ok(JSValue::String("abcdef".into()))); + + let result = evaluate_javascript(servo_test, webview.clone(), "let foo = {blah: 123}; foo"); + ensure!(matches!(result, Ok(JSValue::Object(_)))); + if let Ok(JSValue::Object(values)) = result { + ensure!(values.len() == 1); + ensure!(values.get("blah") == Some(&JSValue::Number(123.0))); + } + + let result = evaluate_javascript(servo_test, webview.clone(), "[1, 2, 3, 4]"); + let expected = JSValue::Array(vec![ + JSValue::Number(1.0), + JSValue::Number(2.0), + JSValue::Number(3.0), + JSValue::Number(4.0), + ]); + ensure!(result == Ok(expected)); + + let result = evaluate_javascript(servo_test, webview.clone(), "window"); + ensure!(matches!(result, Ok(JSValue::Window(..)))); + + let result = evaluate_javascript(servo_test, webview.clone(), "document.body"); + ensure!(matches!(result, Ok(JSValue::Element(..)))); + + let result = evaluate_javascript( + servo_test, + webview.clone(), + "document.body.innerHTML += '<iframe>'; frames[0]", + ); + ensure!(matches!(result, Ok(JSValue::Frame(..)))); + + Ok(()) +} + fn test_create_webview_and_immediately_drop_webview_before_shutdown( servo_test: &ServoTest, ) -> Result<(), anyhow::Error> { @@ -54,6 +131,7 @@ fn test_create_webview_and_immediately_drop_webview_before_shutdown( fn main() { run_api_tests!( test_create_webview, + test_evaluate_javascript_basic, // This test needs to be last, as it tests creating and dropping // a WebView right before shutdown. test_create_webview_and_immediately_drop_webview_before_shutdown diff --git a/components/servo/webview.rs b/components/servo/webview.rs index 95eb6dfd154..10786ad8b69 100644 --- a/components/servo/webview.rs +++ b/components/servo/webview.rs @@ -13,8 +13,8 @@ use compositing_traits::WebViewTrait; use constellation_traits::{EmbedderToConstellationMessage, TraversalDirection}; use dpi::PhysicalSize; use embedder_traits::{ - Cursor, InputEvent, LoadStatus, MediaSessionActionType, ScreenGeometry, Theme, TouchEventType, - ViewportDetails, + Cursor, InputEvent, JSValue, JavaScriptEvaluationError, LoadStatus, MediaSessionActionType, + ScreenGeometry, Theme, TouchEventType, ViewportDetails, }; use euclid::{Point2D, Scale, Size2D}; use servo_geometry::DeviceIndependentPixel; @@ -23,6 +23,7 @@ use webrender_api::ScrollLocation; use webrender_api::units::{DeviceIntPoint, DevicePixel, DeviceRect}; use crate::clipboard_delegate::{ClipboardDelegate, DefaultClipboardDelegate}; +use crate::javascript_evaluator::JavaScriptEvaluator; use crate::webview_delegate::{DefaultWebViewDelegate, WebViewDelegate}; use crate::{ConstellationProxy, Servo, WebRenderDebugOption}; @@ -75,6 +76,7 @@ pub(crate) struct WebViewInner { pub(crate) compositor: Rc<RefCell<IOCompositor>>, pub(crate) delegate: Rc<dyn WebViewDelegate>, pub(crate) clipboard_delegate: Rc<dyn ClipboardDelegate>, + javascript_evaluator: Rc<RefCell<JavaScriptEvaluator>>, rect: DeviceRect, hidpi_scale_factor: Scale<f32, DeviceIndependentPixel, DevicePixel>, @@ -117,9 +119,10 @@ impl WebView { compositor: servo.compositor.clone(), delegate: builder.delegate, clipboard_delegate: Rc::new(DefaultClipboardDelegate), + javascript_evaluator: servo.javascript_evaluator.clone(), rect: DeviceRect::from_origin_and_size(Point2D::origin(), size), hidpi_scale_factor: builder.hidpi_scale_factor, - load_status: LoadStatus::Complete, + load_status: LoadStatus::Started, url: None, status_text: None, page_title: None, @@ -549,6 +552,20 @@ impl WebView { pub fn paint(&self) -> bool { self.inner().compositor.borrow_mut().render() } + + /// Evaluate the specified string of JavaScript code. Once execution is complete or an error + /// occurs, Servo will call `callback`. + pub fn evaluate_javascript<T: ToString>( + &self, + script: T, + callback: impl FnOnce(Result<JSValue, JavaScriptEvaluationError>) + 'static, + ) { + self.inner().javascript_evaluator.borrow_mut().evaluate( + self.id(), + script.to_string(), + Box::new(callback), + ); + } } /// A structure used to expose a view of the [`WebView`] to the Servo diff --git a/components/shared/constellation/from_script_message.rs b/components/shared/constellation/from_script_message.rs index 21665c24e57..3856def660e 100644 --- a/components/shared/constellation/from_script_message.rs +++ b/components/shared/constellation/from_script_message.rs @@ -15,8 +15,8 @@ use base::id::{ use canvas_traits::canvas::{CanvasId, CanvasMsg}; use devtools_traits::{DevtoolScriptControlMsg, ScriptToDevtoolsControlMsg, WorkerId}; use embedder_traits::{ - AnimationState, EmbedderMsg, FocusSequenceNumber, MediaSessionEvent, TouchEventResult, - ViewportDetails, + AnimationState, EmbedderMsg, FocusSequenceNumber, JSValue, JavaScriptEvaluationError, + JavaScriptEvaluationId, MediaSessionEvent, TouchEventResult, ViewportDetails, }; use euclid::default::Size2D as UntypedSize2D; use http::{HeaderMap, Method}; @@ -644,6 +644,11 @@ pub enum ScriptToConstellationMessage { IFrameSizes(Vec<IFrameSizeMsg>), /// Request results from the memory reporter. ReportMemory(IpcSender<MemoryReportResult>), + /// Return the result of the evaluated JavaScript with the given [`JavaScriptEvaluationId`]. + FinishJavaScriptEvaluation( + JavaScriptEvaluationId, + Result<JSValue, JavaScriptEvaluationError>, + ), } impl fmt::Debug for ScriptToConstellationMessage { diff --git a/components/shared/constellation/lib.rs b/components/shared/constellation/lib.rs index 134eed6c1d9..d85fbe31bdf 100644 --- a/components/shared/constellation/lib.rs +++ b/components/shared/constellation/lib.rs @@ -19,8 +19,8 @@ use base::Epoch; use base::cross_process_instant::CrossProcessInstant; use base::id::{MessagePortId, PipelineId, WebViewId}; use embedder_traits::{ - CompositorHitTestResult, Cursor, InputEvent, MediaSessionActionType, Theme, ViewportDetails, - WebDriverCommandMsg, + CompositorHitTestResult, Cursor, InputEvent, JavaScriptEvaluationId, MediaSessionActionType, + Theme, ViewportDetails, WebDriverCommandMsg, }; use euclid::Vector2D; pub use from_script_message::*; @@ -92,6 +92,9 @@ pub enum EmbedderToConstellationMessage { SetScrollStates(PipelineId, Vec<ScrollState>), /// Notify the constellation that a particular paint metric event has happened for the given pipeline. PaintMetric(PipelineId, PaintMetricEvent), + /// Evaluate a JavaScript string in the context of a `WebView`. When execution is complete or an + /// error is encountered, a correpsonding message will be sent to the embedding layer. + EvaluateJavaScript(WebViewId, JavaScriptEvaluationId, String), } /// A description of a paint metric that is sent from the Servo renderer to the diff --git a/components/shared/embedder/lib.rs b/components/shared/embedder/lib.rs index c87fa9019ef..e9427fcc719 100644 --- a/components/shared/embedder/lib.rs +++ b/components/shared/embedder/lib.rs @@ -13,8 +13,10 @@ pub mod resources; pub mod user_content_manager; mod webdriver; +use std::collections::HashMap; use std::ffi::c_void; use std::fmt::{Debug, Display, Error, Formatter}; +use std::hash::Hash; use std::path::PathBuf; use std::sync::Arc; @@ -372,6 +374,12 @@ pub enum EmbedderMsg { DeviceIntRect, IpcSender<Option<usize>>, ), + /// Inform the embedding layer that a JavaScript evaluation has + /// finished with the given result. + FinishJavaScriptEvaluation( + JavaScriptEvaluationId, + Result<JSValue, JavaScriptEvaluationError>, + ), } impl Debug for EmbedderMsg { @@ -857,3 +865,59 @@ impl Display for FocusSequenceNumber { Display::fmt(&self.0, f) } } + +/// An identifier for a particular JavaScript evaluation that is used to track the +/// evaluation from the embedding layer to the script layer and then back. +#[derive(Clone, Copy, Deserialize, Eq, Hash, PartialEq, Serialize)] +pub struct JavaScriptEvaluationId(pub usize); + +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] +pub enum JSValue { + Undefined, + Null, + Boolean(bool), + Number(f64), + String(String), + Element(String), + Frame(String), + Window(String), + Array(Vec<JSValue>), + Object(HashMap<String, JSValue>), +} + +impl From<&WebDriverJSValue> for JSValue { + fn from(value: &WebDriverJSValue) -> Self { + match value { + WebDriverJSValue::Undefined => Self::Undefined, + WebDriverJSValue::Null => Self::Null, + WebDriverJSValue::Boolean(value) => Self::Boolean(*value), + WebDriverJSValue::Int(value) => Self::Number(*value as f64), + WebDriverJSValue::Number(value) => Self::Number(*value), + WebDriverJSValue::String(value) => Self::String(value.clone()), + WebDriverJSValue::Element(web_element) => Self::Element(web_element.0.clone()), + WebDriverJSValue::Frame(web_frame) => Self::Frame(web_frame.0.clone()), + WebDriverJSValue::Window(web_window) => Self::Window(web_window.0.clone()), + WebDriverJSValue::ArrayLike(vector) => { + Self::Array(vector.iter().map(Into::into).collect()) + }, + WebDriverJSValue::Object(map) => Self::Object( + map.iter() + .map(|(key, value)| (key.clone(), value.into())) + .collect(), + ), + } + } +} + +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] +pub enum JavaScriptEvaluationError { + /// An internal Servo error prevented the JavaSript evaluation from completing properly. + /// This indicates a bug in Servo. + InternalError, + /// The `WebView` on which this evaluation request was triggered is not ready. This might + /// happen if the `WebView`'s `Document` is changing due to ongoing load events, for instance. + WebViewNotReady, + /// The script executed successfully, but Servo could not serialize the JavaScript return + /// value into a [`JSValue`]. + SerializationError, +} diff --git a/components/shared/script/lib.rs b/components/shared/script/lib.rs index 748c42400a8..29acc51765c 100644 --- a/components/shared/script/lib.rs +++ b/components/shared/script/lib.rs @@ -27,8 +27,8 @@ use crossbeam_channel::{RecvTimeoutError, Sender}; use devtools_traits::ScriptToDevtoolsControlMsg; use embedder_traits::user_content_manager::UserContentManager; use embedder_traits::{ - CompositorHitTestResult, FocusSequenceNumber, InputEvent, MediaSessionActionType, Theme, - ViewportDetails, WebDriverScriptCommand, + CompositorHitTestResult, FocusSequenceNumber, InputEvent, JavaScriptEvaluationId, + MediaSessionActionType, Theme, ViewportDetails, WebDriverScriptCommand, }; use euclid::{Rect, Scale, Size2D, UnknownUnit}; use ipc_channel::ipc::{IpcReceiver, IpcSender}; @@ -245,6 +245,9 @@ pub enum ScriptThreadMessage { /// The compositor scrolled and is updating the scroll states of the nodes in the given /// pipeline via the Constellation. SetScrollStates(PipelineId, Vec<ScrollState>), + /// Evaluate the given JavaScript and return a result via a corresponding message + /// to the Constellation. + EvaluateJavaScript(PipelineId, JavaScriptEvaluationId, String), } impl fmt::Debug for ScriptThreadMessage { |