Skip to content

Commit 991be35

Browse files
Narfingermrobinson
andauthored
libservo: Allow embedders to execute JavaScript scripts via the API (servo#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 <[email protected]> Signed-off-by: Martin Robinson <[email protected]> Co-authored-by: Martin Robinson <[email protected]>
1 parent 91c4c7b commit 991be35

File tree

12 files changed

+391
-18
lines changed

12 files changed

+391
-18
lines changed

components/constellation/constellation.rs

Lines changed: 70 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -129,9 +129,10 @@ use embedder_traits::resources::{self, Resource};
129129
use embedder_traits::user_content_manager::UserContentManager;
130130
use embedder_traits::{
131131
AnimationState, CompositorHitTestResult, Cursor, EmbedderMsg, EmbedderProxy,
132-
FocusSequenceNumber, ImeEvent, InputEvent, MediaSessionActionType, MediaSessionEvent,
133-
MediaSessionPlaybackState, MouseButton, MouseButtonAction, MouseButtonEvent, Theme,
134-
ViewportDetails, WebDriverCommandMsg, WebDriverLoadStatus,
132+
FocusSequenceNumber, ImeEvent, InputEvent, JSValue, JavaScriptEvaluationError,
133+
JavaScriptEvaluationId, MediaSessionActionType, MediaSessionEvent, MediaSessionPlaybackState,
134+
MouseButton, MouseButtonAction, MouseButtonEvent, Theme, ViewportDetails, WebDriverCommandMsg,
135+
WebDriverLoadStatus,
135136
};
136137
use euclid::Size2D;
137138
use euclid::default::Size2D as UntypedSize2D;
@@ -1477,6 +1478,52 @@ where
14771478
EmbedderToConstellationMessage::PaintMetric(pipeline_id, paint_metric_event) => {
14781479
self.handle_paint_metric(pipeline_id, paint_metric_event);
14791480
},
1481+
EmbedderToConstellationMessage::EvaluateJavaScript(
1482+
webview_id,
1483+
evaluation_id,
1484+
script,
1485+
) => {
1486+
self.handle_evaluate_javascript(webview_id, evaluation_id, script);
1487+
},
1488+
}
1489+
}
1490+
1491+
#[cfg_attr(
1492+
feature = "tracing",
1493+
tracing::instrument(skip_all, fields(servo_profiling = true), level = "trace")
1494+
)]
1495+
fn handle_evaluate_javascript(
1496+
&mut self,
1497+
webview_id: WebViewId,
1498+
evaluation_id: JavaScriptEvaluationId,
1499+
script: String,
1500+
) {
1501+
let browsing_context_id = BrowsingContextId::from(webview_id);
1502+
let Some(pipeline) = self
1503+
.browsing_contexts
1504+
.get(&browsing_context_id)
1505+
.and_then(|browsing_context| self.pipelines.get(&browsing_context.pipeline_id))
1506+
else {
1507+
self.handle_finish_javascript_evaluation(
1508+
evaluation_id,
1509+
Err(JavaScriptEvaluationError::InternalError),
1510+
);
1511+
return;
1512+
};
1513+
1514+
if pipeline
1515+
.event_loop
1516+
.send(ScriptThreadMessage::EvaluateJavaScript(
1517+
pipeline.id,
1518+
evaluation_id,
1519+
script,
1520+
))
1521+
.is_err()
1522+
{
1523+
self.handle_finish_javascript_evaluation(
1524+
evaluation_id,
1525+
Err(JavaScriptEvaluationError::InternalError),
1526+
);
14801527
}
14811528
}
14821529

@@ -1817,6 +1864,9 @@ where
18171864
self.mem_profiler_chan
18181865
.send(mem::ProfilerMsg::Report(sender));
18191866
},
1867+
ScriptToConstellationMessage::FinishJavaScriptEvaluation(evaluation_id, result) => {
1868+
self.handle_finish_javascript_evaluation(evaluation_id, result)
1869+
},
18201870
}
18211871
}
18221872

@@ -3178,6 +3228,22 @@ where
31783228
}
31793229
}
31803230

3231+
#[cfg_attr(
3232+
feature = "tracing",
3233+
tracing::instrument(skip_all, fields(servo_profiling = true), level = "trace")
3234+
)]
3235+
fn handle_finish_javascript_evaluation(
3236+
&mut self,
3237+
evaluation_id: JavaScriptEvaluationId,
3238+
result: Result<JSValue, JavaScriptEvaluationError>,
3239+
) {
3240+
self.embedder_proxy
3241+
.send(EmbedderMsg::FinishJavaScriptEvaluation(
3242+
evaluation_id,
3243+
result,
3244+
));
3245+
}
3246+
31813247
#[cfg_attr(
31823248
feature = "tracing",
31833249
tracing::instrument(skip_all, fields(servo_profiling = true), level = "trace")
@@ -4691,6 +4757,7 @@ where
46914757
NavigationHistoryBehavior::Replace,
46924758
);
46934759
},
4760+
// TODO: This should use the ScriptThreadMessage::EvaluateJavaScript command
46944761
WebDriverCommandMsg::ScriptCommand(browsing_context_id, cmd) => {
46954762
let pipeline_id = match self.browsing_contexts.get(&browsing_context_id) {
46964763
Some(browsing_context) => browsing_context.pipeline_id,

components/constellation/tracing.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ mod from_compositor {
7777
Self::SetWebViewThrottled(_, _) => target!("SetWebViewThrottled"),
7878
Self::SetScrollStates(..) => target!("SetScrollStates"),
7979
Self::PaintMetric(..) => target!("PaintMetric"),
80+
Self::EvaluateJavaScript(..) => target!("EvaluateJavaScript"),
8081
}
8182
}
8283
}
@@ -176,6 +177,7 @@ mod from_script {
176177
Self::TitleChanged(..) => target!("TitleChanged"),
177178
Self::IFrameSizes(..) => target!("IFrameSizes"),
178179
Self::ReportMemory(..) => target!("ReportMemory"),
180+
Self::FinishJavaScriptEvaluation(..) => target!("FinishJavaScriptEvaluation"),
179181
}
180182
}
181183
}
@@ -238,6 +240,9 @@ mod from_script {
238240
Self::ShutdownComplete => target_variant!("ShutdownComplete"),
239241
Self::ShowNotification(..) => target_variant!("ShowNotification"),
240242
Self::ShowSelectElementMenu(..) => target_variant!("ShowSelectElementMenu"),
243+
Self::FinishJavaScriptEvaluation(..) => {
244+
target_variant!("FinishJavaScriptEvaluation")
245+
},
241246
}
242247
}
243248
}

components/script/messaging.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@ impl MixedMessage {
9191
#[cfg(feature = "webgpu")]
9292
ScriptThreadMessage::SetWebGPUPort(..) => None,
9393
ScriptThreadMessage::SetScrollStates(id, ..) => Some(*id),
94+
ScriptThreadMessage::EvaluateJavaScript(id, _, _) => Some(*id),
9495
},
9596
MixedMessage::FromScript(inner_msg) => match inner_msg {
9697
MainThreadScriptMsg::Common(CommonScriptMsg::Task(_, _, pipeline_id, _)) => {

components/script/script_thread.rs

Lines changed: 54 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -50,9 +50,9 @@ use devtools_traits::{
5050
};
5151
use embedder_traits::user_content_manager::UserContentManager;
5252
use embedder_traits::{
53-
CompositorHitTestResult, EmbedderMsg, FocusSequenceNumber, InputEvent, MediaSessionActionType,
54-
MouseButton, MouseButtonAction, MouseButtonEvent, Theme, ViewportDetails,
55-
WebDriverScriptCommand,
53+
CompositorHitTestResult, EmbedderMsg, FocusSequenceNumber, InputEvent,
54+
JavaScriptEvaluationError, JavaScriptEvaluationId, MediaSessionActionType, MouseButton,
55+
MouseButtonAction, MouseButtonEvent, Theme, ViewportDetails, WebDriverScriptCommand,
5656
};
5757
use euclid::Point2D;
5858
use euclid::default::Rect;
@@ -156,6 +156,7 @@ use crate::script_runtime::{
156156
};
157157
use crate::task_queue::TaskQueue;
158158
use crate::task_source::{SendableTaskSource, TaskSourceName};
159+
use crate::webdriver_handlers::jsval_to_webdriver;
159160
use crate::{devtools, webdriver_handlers};
160161

161162
thread_local!(static SCRIPT_THREAD_ROOT: Cell<Option<*const ScriptThread>> = const { Cell::new(None) });
@@ -1878,6 +1879,9 @@ impl ScriptThread {
18781879
ScriptThreadMessage::SetScrollStates(pipeline_id, scroll_states) => {
18791880
self.handle_set_scroll_states(pipeline_id, scroll_states)
18801881
},
1882+
ScriptThreadMessage::EvaluateJavaScript(pipeline_id, evaluation_id, script) => {
1883+
self.handle_evaluate_javascript(pipeline_id, evaluation_id, script, can_gc);
1884+
},
18811885
}
18821886
}
18831887

@@ -3815,6 +3819,53 @@ impl ScriptThread {
38153819
)
38163820
}
38173821
}
3822+
3823+
fn handle_evaluate_javascript(
3824+
&self,
3825+
pipeline_id: PipelineId,
3826+
evaluation_id: JavaScriptEvaluationId,
3827+
script: String,
3828+
can_gc: CanGc,
3829+
) {
3830+
let Some(window) = self.documents.borrow().find_window(pipeline_id) else {
3831+
let _ = self.senders.pipeline_to_constellation_sender.send((
3832+
pipeline_id,
3833+
ScriptToConstellationMessage::FinishJavaScriptEvaluation(
3834+
evaluation_id,
3835+
Err(JavaScriptEvaluationError::WebViewNotReady),
3836+
),
3837+
));
3838+
return;
3839+
};
3840+
3841+
let global_scope = window.as_global_scope();
3842+
let realm = enter_realm(global_scope);
3843+
let context = window.get_cx();
3844+
3845+
rooted!(in(*context) let mut return_value = UndefinedValue());
3846+
global_scope.evaluate_js_on_global_with_result(
3847+
&script,
3848+
return_value.handle_mut(),
3849+
ScriptFetchOptions::default_classic_script(global_scope),
3850+
global_scope.api_base_url(),
3851+
can_gc,
3852+
);
3853+
let result = match jsval_to_webdriver(
3854+
context,
3855+
global_scope,
3856+
return_value.handle(),
3857+
(&realm).into(),
3858+
can_gc,
3859+
) {
3860+
Ok(ref value) => Ok(value.into()),
3861+
Err(_) => Err(JavaScriptEvaluationError::SerializationError),
3862+
};
3863+
3864+
let _ = self.senders.pipeline_to_constellation_sender.send((
3865+
pipeline_id,
3866+
ScriptToConstellationMessage::FinishJavaScriptEvaluation(evaluation_id, result),
3867+
));
3868+
}
38183869
}
38193870

38203871
impl Drop for ScriptThread {
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
/* This Source Code Form is subject to the terms of the Mozilla Public
2+
* License, v. 2.0. If a copy of the MPL was not distributed with this
3+
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4+
5+
use std::collections::HashMap;
6+
7+
use base::id::WebViewId;
8+
use constellation_traits::EmbedderToConstellationMessage;
9+
use embedder_traits::{JSValue, JavaScriptEvaluationError, JavaScriptEvaluationId};
10+
11+
use crate::ConstellationProxy;
12+
13+
struct PendingEvaluation {
14+
callback: Box<dyn FnOnce(Result<JSValue, JavaScriptEvaluationError>)>,
15+
}
16+
17+
pub(crate) struct JavaScriptEvaluator {
18+
current_id: JavaScriptEvaluationId,
19+
constellation_proxy: ConstellationProxy,
20+
pending_evaluations: HashMap<JavaScriptEvaluationId, PendingEvaluation>,
21+
}
22+
23+
impl JavaScriptEvaluator {
24+
pub(crate) fn new(constellation_proxy: ConstellationProxy) -> Self {
25+
Self {
26+
current_id: JavaScriptEvaluationId(0),
27+
constellation_proxy,
28+
pending_evaluations: Default::default(),
29+
}
30+
}
31+
32+
fn generate_id(&mut self) -> JavaScriptEvaluationId {
33+
let next_id = JavaScriptEvaluationId(self.current_id.0 + 1);
34+
std::mem::replace(&mut self.current_id, next_id)
35+
}
36+
37+
pub(crate) fn evaluate(
38+
&mut self,
39+
webview_id: WebViewId,
40+
script: String,
41+
callback: Box<dyn FnOnce(Result<JSValue, JavaScriptEvaluationError>)>,
42+
) {
43+
let evaluation_id = self.generate_id();
44+
self.constellation_proxy
45+
.send(EmbedderToConstellationMessage::EvaluateJavaScript(
46+
webview_id,
47+
evaluation_id,
48+
script,
49+
));
50+
self.pending_evaluations
51+
.insert(evaluation_id, PendingEvaluation { callback });
52+
}
53+
54+
pub(crate) fn finish_evaluation(
55+
&mut self,
56+
evaluation_id: JavaScriptEvaluationId,
57+
result: Result<JSValue, JavaScriptEvaluationError>,
58+
) {
59+
(self
60+
.pending_evaluations
61+
.remove(&evaluation_id)
62+
.expect("Received request to finish unknown JavaScript evaluation.")
63+
.callback)(result)
64+
}
65+
}

components/servo/lib.rs

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
//! `WindowMethods` trait.
1919
2020
mod clipboard_delegate;
21+
mod javascript_evaluator;
2122
mod proxies;
2223
mod responders;
2324
mod servo_delegate;
@@ -82,6 +83,7 @@ pub use gleam::gl;
8283
use gleam::gl::RENDERER;
8384
use ipc_channel::ipc::{self, IpcSender};
8485
use ipc_channel::router::ROUTER;
86+
use javascript_evaluator::JavaScriptEvaluator;
8587
pub use keyboard_types::*;
8688
use layout::LayoutFactoryImpl;
8789
use log::{Log, Metadata, Record, debug, warn};
@@ -196,6 +198,9 @@ pub struct Servo {
196198
compositor: Rc<RefCell<IOCompositor>>,
197199
constellation_proxy: ConstellationProxy,
198200
embedder_receiver: Receiver<EmbedderMsg>,
201+
/// A struct that tracks ongoing JavaScript evaluations and is responsible for
202+
/// calling the callback when the evaluation is complete.
203+
javascript_evaluator: Rc<RefCell<JavaScriptEvaluator>>,
199204
/// Tracks whether we are in the process of shutting down, or have shut down.
200205
/// This is shared with `WebView`s and the `ServoRenderer`.
201206
shutdown_state: Rc<Cell<ShutdownState>>,
@@ -487,10 +492,14 @@ impl Servo {
487492
opts.debug.convert_mouse_to_touch,
488493
);
489494

495+
let constellation_proxy = ConstellationProxy::new(constellation_chan);
490496
Self {
491497
delegate: RefCell::new(Rc::new(DefaultServoDelegate)),
492498
compositor: Rc::new(RefCell::new(compositor)),
493-
constellation_proxy: ConstellationProxy::new(constellation_chan),
499+
javascript_evaluator: Rc::new(RefCell::new(JavaScriptEvaluator::new(
500+
constellation_proxy.clone(),
501+
))),
502+
constellation_proxy,
494503
embedder_receiver,
495504
shutdown_state,
496505
webviews: Default::default(),
@@ -738,6 +747,11 @@ impl Servo {
738747
webview.delegate().request_unload(webview, request);
739748
}
740749
},
750+
EmbedderMsg::FinishJavaScriptEvaluation(evaluation_id, result) => {
751+
self.javascript_evaluator
752+
.borrow_mut()
753+
.finish_evaluation(evaluation_id, result);
754+
},
741755
EmbedderMsg::Keyboard(webview_id, keyboard_event) => {
742756
if let Some(webview) = self.get_webview_handle(webview_id) {
743757
webview

0 commit comments

Comments
 (0)