aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorNarfinger <Narfinger@users.noreply.github.com>2025-05-13 14:54:18 +0200
committerGitHub <noreply@github.com>2025-05-13 12:54:18 +0000
commit991be359a3b896c204778cd3f3b226e16f9435b8 (patch)
tree8dc9b81799a9afee02de84aed25c2901028a35e2
parent91c4c7b9982556bc11a39d47081984cbdfa6280e (diff)
downloadservo-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.rs73
-rw-r--r--components/constellation/tracing.rs5
-rw-r--r--components/script/messaging.rs1
-rw-r--r--components/script/script_thread.rs57
-rw-r--r--components/servo/javascript_evaluator.rs65
-rw-r--r--components/servo/lib.rs16
-rw-r--r--components/servo/tests/webview.rs82
-rw-r--r--components/servo/webview.rs23
-rw-r--r--components/shared/constellation/from_script_message.rs9
-rw-r--r--components/shared/constellation/lib.rs7
-rw-r--r--components/shared/embedder/lib.rs64
-rw-r--r--components/shared/script/lib.rs7
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 {