Skip to content

Commit ca7e746

Browse files
committed
Return immediately from run_app on web
This avoids using JavaScript exceptions to support `EventLoop::run_app` on the web, which is a huge hack, and doesn't work with the Exception Handling Proposal for WebAssembly: https://github.com/WebAssembly/exception-handling This needs the application handler passed to `run_app` to be `'static`, but that works better on iOS too anyhow (since you can't accidentally forget to pass in state that then wouldn't be dropped when terminating).
1 parent f946859 commit ca7e746

File tree

10 files changed

+72
-106
lines changed

10 files changed

+72
-106
lines changed

src/application.rs

+1-2
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,7 @@ use crate::window::WindowId;
1111
/// See [the top-level docs] for example usage, and [`EventLoop::run_app`] for an overview of when
1212
/// events are delivered.
1313
///
14-
/// This is [dropped] when the event loop is shut down. Note that this only works if you're passing
15-
/// the entire state to [`EventLoop::run_app`] (passing `&mut app` won't work).
14+
/// This is [dropped] when the event loop is shut down.
1615
///
1716
/// [the top-level docs]: crate
1817
/// [`EventLoop::run_app`]: crate::event_loop::EventLoop::run_app

src/changelog/unreleased.md

+4
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,8 @@ changelog entry.
194194
- Removed `KeyEventExtModifierSupplement`, and made the fields `text_with_all_modifiers` and
195195
`key_without_modifiers` public on `KeyEvent` instead.
196196
- Move `window::Fullscreen` to `monitor::Fullscreen`.
197+
- On web, avoid throwing an exception in `EventLoop::run_app`, instead preferring to return to the caller.
198+
This requires passing a `'static` application to ensure that the application state will live as long as necessary.
197199

198200
### Removed
199201

@@ -230,6 +232,8 @@ changelog entry.
230232
- Remove `CustomCursor::from_rgba`, use `CustomCursorSource` instead.
231233
- Removed `ApplicationHandler::exited`, the event loop being shut down can now be listened to in
232234
the `Drop` impl on the application handler.
235+
- Removed `EventLoopExtWeb::spawn_app`, the exception throwing that made this workaround necessary
236+
has been removed from `EventLoop::run_app`.
233237

234238
### Fixed
235239

src/event_loop.rs

+58-22
Original file line numberDiff line numberDiff line change
@@ -184,7 +184,7 @@ impl EventLoop {
184184
}
185185

186186
impl EventLoop {
187-
/// Run the application with the event loop on the calling thread.
187+
/// Run the event loop with the given application on the calling thread.
188188
///
189189
/// ## Event loop flow
190190
///
@@ -237,34 +237,70 @@ impl EventLoop {
237237
/// [`ControlFlow::WaitUntil`] and life-cycle methods like [`ApplicationHandler::resumed`], but
238238
/// it should give you an idea of how things fit together.
239239
///
240-
/// ## Platform-specific
240+
/// ## Returns
241+
///
242+
/// The semantics of this function can be a bit confusing, because the way different platforms
243+
/// control their event loop varies significantly.
244+
///
245+
/// On most platforms (Android, X11, Wayland, Windows, macOS), this blocks the caller, runs the
246+
/// event loop internally, and then returns once [`ActiveEventLoop::exit`] is called.
247+
///
248+
/// On iOS, this will register the application handler, and then call [`UIApplicationMain`]
249+
/// (which is the only way to run the system event loop), which never returns to the caller
250+
/// (the process instead exits after the handler has been dropped).
251+
///
252+
/// On the web, this works by registering the application handler, and then immediately
253+
/// returning to the caller. This is necessary because WebAssembly (and JavaScript) is always
254+
/// executed in the context of the browser's own (internal) event loop, and thus we need to
255+
/// return to avoid blocking that and allow events to later be delivered asynchronously.
256+
///
257+
/// If you call this function inside `fn main`, you usually do not need to think about these
258+
/// details.
259+
///
260+
/// [`UIApplicationMain`]: https://developer.apple.com/documentation/uikit/uiapplicationmain(_:_:_:_:)-1yub7?language=objc
241261
///
242-
/// - **iOS:** Will never return to the caller and so values not passed to this function will
243-
/// *not* be dropped before the process exits.
244-
/// - **Web:** Will _act_ as if it never returns to the caller by throwing a Javascript
245-
/// exception (that Rust doesn't see) that will also mean that the rest of the function is
246-
/// never executed and any values not passed to this function will *not* be dropped.
262+
/// ## Static
247263
///
248-
/// Web applications are recommended to use
264+
/// To alleviate the issues noted above, this function requires that you pass in a `'static`
265+
/// handler, to ensure that any state your application uses will be alive as long as the
266+
/// application is running.
267+
///
268+
/// To be clear, you should avoid doing e.g. `event_loop.run_app(&mut app)?`, and prefer
269+
/// `event_loop.run_app(app)?` instead.
270+
///
271+
/// If this requirement is prohibitive for you, consider using
249272
#[cfg_attr(
250-
web_platform,
251-
doc = " [`EventLoopExtWeb::spawn_app()`][crate::platform::web::EventLoopExtWeb::spawn_app()]"
273+
any(
274+
windows_platform,
275+
macos_platform,
276+
android_platform,
277+
x11_platform,
278+
wayland_platform,
279+
docsrs,
280+
),
281+
doc = "[`EventLoopExtRunOnDemand::run_app_on_demand`](crate::platform::run_on_demand::EventLoopExtRunOnDemand::run_app_on_demand)"
252282
)]
253-
#[cfg_attr(not(web_platform), doc = " `EventLoopExtWeb::spawn_app()`")]
254-
/// [^1] instead of [`run_app()`] to avoid the need for the Javascript exception trick, and to
255-
/// make it clearer that the event loop runs asynchronously (via the browser's own,
256-
/// internal, event loop) and doesn't block the current thread of execution like it does
257-
/// on other platforms.
258-
///
259-
/// This function won't be available with `target_feature = "exception-handling"`.
283+
#[cfg_attr(
284+
not(any(
285+
windows_platform,
286+
macos_platform,
287+
android_platform,
288+
x11_platform,
289+
wayland_platform,
290+
docsrs,
291+
)),
292+
doc = "`EventLoopExtRunOnDemand::run_app_on_demand`"
293+
)]
294+
/// instead (though note that this is not available on iOS and web).
260295
///
261-
/// [^1]: `spawn_app()` is only available on the Web platform.
296+
/// ## Platform-specific
262297
///
263-
/// [`set_control_flow()`]: ActiveEventLoop::set_control_flow()
264-
/// [`run_app()`]: Self::run_app()
298+
/// - **Web** Once your handler has been dropped, it's possible to reinitialize another event
299+
/// loop by calling this function again. This can be useful if you want to recreate the event
300+
/// loop while the WebAssembly module is still loaded. For example, this can be used to
301+
/// recreate the event loop when switching between tabs on a single page application.
265302
#[inline]
266-
#[cfg(not(all(web_platform, target_feature = "exception-handling")))]
267-
pub fn run_app<A: ApplicationHandler>(self, app: A) -> Result<(), EventLoopError> {
303+
pub fn run_app<A: ApplicationHandler + 'static>(self, app: A) -> Result<(), EventLoopError> {
268304
self.event_loop.run_app(app)
269305
}
270306

src/platform/run_on_demand.rs

+2-11
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ pub trait EventLoopExtRunOnDemand {
1111
/// Run the application with the event loop on the calling thread.
1212
///
1313
/// Unlike [`EventLoop::run_app`], this function accepts non-`'static` (i.e. non-`move`)
14-
/// closures and it is possible to return control back to the caller without
14+
/// state and it is possible to return control back to the caller without
1515
/// consuming the `EventLoop` (by using [`exit()`]) and
1616
/// so the event loop can be re-run after it has exit.
1717
///
@@ -30,14 +30,7 @@ pub trait EventLoopExtRunOnDemand {
3030
///
3131
/// # Caveats
3232
/// - This extension isn't available on all platforms, since it's not always possible to return
33-
/// to the caller (specifically this is impossible on iOS and Web - though with the Web
34-
/// backend it is possible to use
35-
#[cfg_attr(
36-
web_platform,
37-
doc = " [`EventLoopExtWeb::spawn_app()`][crate::platform::web::EventLoopExtWeb::spawn_app()]"
38-
)]
39-
#[cfg_attr(not(web_platform), doc = " `EventLoopExtWeb::spawn_app()`")]
40-
/// [^1] more than once instead).
33+
/// to the caller (specifically this is impossible on iOS and Web).
4134
/// - No [`Window`] state can be carried between separate runs of the event loop.
4235
///
4336
/// You are strongly encouraged to use [`EventLoop::run_app()`] for portability, unless you
@@ -56,8 +49,6 @@ pub trait EventLoopExtRunOnDemand {
5649
/// are delivered via callbacks based on an event loop that is internal to the browser itself.
5750
/// - **iOS:** It's not possible to stop and start an `UIApplication` repeatedly on iOS.
5851
///
59-
/// [^1]: `spawn_app()` is only available on the Web platforms.
60-
///
6152
/// [`exit()`]: ActiveEventLoop::exit()
6253
/// [`set_control_flow()`]: ActiveEventLoop::set_control_flow()
6354
fn run_app_on_demand<A: ApplicationHandler>(&mut self, app: A) -> Result<(), EventLoopError>;

src/platform/web.rs

-29
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,6 @@ use serde::{Deserialize, Serialize};
5454
#[cfg(web_platform)]
5555
use web_sys::HtmlCanvasElement;
5656

57-
use crate::application::ApplicationHandler;
5857
use crate::cursor::CustomCursorSource;
5958
use crate::error::NotSupportedError;
6059
use crate::event_loop::{ActiveEventLoop, EventLoop};
@@ -181,30 +180,6 @@ impl WindowAttributesExtWeb for WindowAttributes {
181180

182181
/// Additional methods on `EventLoop` that are specific to the Web.
183182
pub trait EventLoopExtWeb {
184-
/// Initializes the winit event loop.
185-
///
186-
/// Unlike
187-
#[cfg_attr(all(web_platform, target_feature = "exception-handling"), doc = "`run_app()`")]
188-
#[cfg_attr(
189-
not(all(web_platform, target_feature = "exception-handling")),
190-
doc = "[`run_app()`]"
191-
)]
192-
/// [^1], this returns immediately, and doesn't throw an exception in order to
193-
/// satisfy its [`!`] return type.
194-
///
195-
/// Once the event loop has been destroyed, it's possible to reinitialize another event loop
196-
/// by calling this function again. This can be useful if you want to recreate the event loop
197-
/// while the WebAssembly module is still loaded. For example, this can be used to recreate the
198-
/// event loop when switching between tabs on a single page application.
199-
#[rustfmt::skip]
200-
///
201-
#[cfg_attr(
202-
not(all(web_platform, target_feature = "exception-handling")),
203-
doc = "[`run_app()`]: EventLoop::run_app()"
204-
)]
205-
/// [^1]: `run_app()` is _not_ available on Wasm when the target supports `exception-handling`.
206-
fn spawn_app<A: ApplicationHandler + 'static>(self, app: A);
207-
208183
/// Sets the strategy for [`ControlFlow::Poll`].
209184
///
210185
/// See [`PollStrategy`].
@@ -263,10 +238,6 @@ pub trait EventLoopExtWeb {
263238
}
264239

265240
impl EventLoopExtWeb for EventLoop {
266-
fn spawn_app<A: ApplicationHandler + 'static>(self, app: A) {
267-
self.event_loop.spawn_app(app);
268-
}
269-
270241
fn set_poll_strategy(&self, strategy: PollStrategy) {
271242
self.event_loop.set_poll_strategy(strategy);
272243
}

src/platform_impl/apple/uikit/event_loop.rs

+2-1
Original file line numberDiff line numberDiff line change
@@ -243,7 +243,8 @@ impl EventLoop {
243243
})
244244
}
245245

246-
pub fn run_app<A: ApplicationHandler>(self, app: A) -> ! {
246+
// Require `'static` for correctness, we won't be able to `Drop` the user's state otherwise.
247+
pub fn run_app<A: ApplicationHandler + 'static>(self, app: A) -> ! {
247248
let application: Option<Retained<UIApplication>> =
248249
unsafe { msg_send![UIApplication::class(), sharedApplication] };
249250
assert!(

src/platform_impl/web/event_loop/mod.rs

+3-26
Original file line numberDiff line numberDiff line change
@@ -24,32 +24,9 @@ impl EventLoop {
2424
Ok(EventLoop { elw: ActiveEventLoop::new() })
2525
}
2626

27-
pub fn run_app<A: ApplicationHandler>(self, app: A) -> ! {
28-
let app = Box::new(app);
29-
30-
// SAFETY: The `transmute` is necessary because `run()` requires `'static`. This is safe
31-
// because this function will never return and all resources not cleaned up by the point we
32-
// `throw` will leak, making this actually `'static`.
33-
let app = unsafe {
34-
std::mem::transmute::<
35-
Box<dyn ApplicationHandler + '_>,
36-
Box<dyn ApplicationHandler + 'static>,
37-
>(app)
38-
};
39-
40-
self.elw.run(app, false);
41-
42-
// Throw an exception to break out of Rust execution and use unreachable to tell the
43-
// compiler this function won't return, giving it a return type of '!'
44-
backend::throw(
45-
"Using exceptions for control flow, don't mind me. This isn't actually an error!",
46-
);
47-
48-
unreachable!();
49-
}
50-
51-
pub fn spawn_app<A: ApplicationHandler + 'static>(self, app: A) {
52-
self.elw.run(Box::new(app), true);
27+
pub fn run_app<A: ApplicationHandler + 'static>(self, app: A) -> Result<(), EventLoopError> {
28+
self.elw.run(Box::new(app));
29+
Ok(())
5330
}
5431

5532
pub fn window_target(&self) -> &dyn RootActiveEventLoop {

src/platform_impl/web/event_loop/runner.rs

+1-9
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,6 @@ struct Execution {
4646
exit: Cell<bool>,
4747
runner: RefCell<RunnerEnum>,
4848
suspended: Cell<bool>,
49-
event_loop_recreation: Cell<bool>,
5049
events: RefCell<VecDeque<Event>>,
5150
id: Cell<usize>,
5251
window: web_sys::Window,
@@ -193,7 +192,6 @@ impl Shared {
193192
exit: Cell::new(false),
194193
runner: RefCell::new(RunnerEnum::Pending),
195194
suspended: Cell::new(false),
196-
event_loop_recreation: Cell::new(false),
197195
events: RefCell::new(VecDeque::new()),
198196
window,
199197
navigator,
@@ -766,9 +764,7 @@ impl Shared {
766764
// * For each undropped `Window`:
767765
// * The `register_redraw_request` closure.
768766
// * The `destroy_fn` closure.
769-
if self.0.event_loop_recreation.get() {
770-
crate::event_loop::EventLoopBuilder::allow_event_loop_recreation();
771-
}
767+
crate::event_loop::EventLoopBuilder::allow_event_loop_recreation();
772768
}
773769

774770
// Check if the event loop is currently closed
@@ -807,10 +803,6 @@ impl Shared {
807803
}
808804
}
809805

810-
pub fn event_loop_recreation(&self, allow: bool) {
811-
self.0.event_loop_recreation.set(allow)
812-
}
813-
814806
pub(crate) fn control_flow(&self) -> ControlFlow {
815807
self.0.control_flow.get()
816808
}

src/platform_impl/web/event_loop/window_target.rs

+1-2
Original file line numberDiff line numberDiff line change
@@ -55,8 +55,7 @@ impl ActiveEventLoop {
5555
Self { runner: runner::Shared::new(), modifiers: ModifiersShared::default() }
5656
}
5757

58-
pub(crate) fn run(&self, app: Box<dyn ApplicationHandler>, event_loop_recreation: bool) {
59-
self.runner.event_loop_recreation(event_loop_recreation);
58+
pub(crate) fn run(&self, app: Box<dyn ApplicationHandler>) {
6059
self.runner.start(app, self.clone());
6160
}
6261

src/platform_impl/web/web_sys/mod.rs

-4
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,6 @@ pub use self::safe_area::SafeAreaHandle;
2525
pub use self::schedule::Schedule;
2626
use crate::dpi::{LogicalPosition, LogicalSize};
2727

28-
pub fn throw(msg: &str) {
29-
wasm_bindgen::throw_str(msg);
30-
}
31-
3228
pub struct PageTransitionEventHandle {
3329
_show_listener: event_handle::EventListenerHandle<dyn FnMut(PageTransitionEvent)>,
3430
_hide_listener: event_handle::EventListenerHandle<dyn FnMut(PageTransitionEvent)>,

0 commit comments

Comments
 (0)