From f69b601abbdea5ebb389dc17a72379da0f06c492 Mon Sep 17 00:00:00 2001 From: Mads Marquart Date: Mon, 17 Mar 2025 04:49:58 +0100 Subject: [PATCH] winit-web: 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). --- winit-core/src/event_loop/run_on_demand.rs | 7 +- winit-uikit/src/event_loop.rs | 7 +- winit-web/src/event_loop/mod.rs | 29 +------ winit-web/src/event_loop/runner.rs | 10 +-- winit-web/src/event_loop/window_target.rs | 3 +- winit-web/src/lib.rs | 25 ------ winit-web/src/web_sys/mod.rs | 4 - winit/src/changelog/unreleased.md | 6 ++ winit/src/event_loop.rs | 92 +++++++++++++++------- 9 files changed, 81 insertions(+), 102 deletions(-) diff --git a/winit-core/src/event_loop/run_on_demand.rs b/winit-core/src/event_loop/run_on_demand.rs index 556be393..a95a05bd 100644 --- a/winit-core/src/event_loop/run_on_demand.rs +++ b/winit-core/src/event_loop/run_on_demand.rs @@ -12,7 +12,7 @@ pub trait EventLoopExtRunOnDemand { /// Run the application with the event loop on the calling thread. /// /// Unlike [`EventLoop::run_app`], this function accepts non-`'static` (i.e. non-`move`) - /// closures and it is possible to return control back to the caller without + /// state and it is possible to return control back to the caller without /// consuming the `EventLoop` (by using [`exit()`]) and /// so the event loop can be re-run after it has exit. /// @@ -31,8 +31,7 @@ pub trait EventLoopExtRunOnDemand { /// /// # Caveats /// - This extension isn't available on all platforms, since it's not always possible to return - /// to the caller (specifically this is impossible on iOS and Web - though with the Web - /// backend it is possible to use `EventLoopExtWeb::spawn_app()`[^1] more than once instead). + /// to the caller (specifically this is impossible on iOS and Web). /// - No [`Window`] state can be carried between separate runs of the event loop. /// /// You are strongly encouraged to use [`EventLoop::run_app()`] for portability, unless you @@ -51,8 +50,6 @@ pub trait EventLoopExtRunOnDemand { /// are delivered via callbacks based on an event loop that is internal to the browser itself. /// - **iOS:** It's not possible to stop and start an `UIApplication` repeatedly on iOS. /// - /// [^1]: `spawn_app()` is only available on the Web platforms. - /// /// [`exit()`]: ActiveEventLoop::exit() /// [`set_control_flow()`]: ActiveEventLoop::set_control_flow() fn run_app_on_demand(&mut self, app: A) -> Result<(), EventLoopError>; diff --git a/winit-uikit/src/event_loop.rs b/winit-uikit/src/event_loop.rs index bc3e1fcb..22e94400 100644 --- a/winit-uikit/src/event_loop.rs +++ b/winit-uikit/src/event_loop.rs @@ -238,7 +238,8 @@ impl EventLoop { }) } - pub fn run_app(self, app: A) -> ! { + // Require `'static` for correctness, we won't be able to `Drop` the user's state otherwise. + pub fn run_app(self, app: A) -> ! { let application: Option> = unsafe { msg_send![UIApplication::class(), sharedApplication] }; assert!( @@ -250,6 +251,10 @@ impl EventLoop { // We intentionally override neither the application nor the delegate, // to allow the user to do so themselves! + // + // NOTE: `UIApplicationMain` is _the only way_ to start the event loop on iOS/UIKit, there + // are no other feasible options. See also: + // app_state::launch(self.mtm, app, || UIApplication::main(None, None, self.mtm)) } diff --git a/winit-web/src/event_loop/mod.rs b/winit-web/src/event_loop/mod.rs index 156327f1..2cf2732b 100644 --- a/winit-web/src/event_loop/mod.rs +++ b/winit-web/src/event_loop/mod.rs @@ -39,32 +39,9 @@ impl EventLoop { EVENT_LOOP_CREATED.store(false, Ordering::Relaxed); } - pub fn run_app(self, app: A) -> ! { - let app = Box::new(app); - - // SAFETY: The `transmute` is necessary because `run()` requires `'static`. This is safe - // because this function will never return and all resources not cleaned up by the point we - // `throw` will leak, making this actually `'static`. - let app = unsafe { - std::mem::transmute::< - Box, - Box, - >(app) - }; - - self.elw.run(app, false); - - // Throw an exception to break out of Rust execution and use unreachable to tell the - // compiler this function won't return, giving it a return type of '!' - backend::throw( - "Using exceptions for control flow, don't mind me. This isn't actually an error!", - ); - - unreachable!(); - } - - pub fn spawn_app(self, app: A) { - self.elw.run(Box::new(app), true); + pub fn run_app(self, app: A) -> Result<(), EventLoopError> { + self.elw.run(Box::new(app)); + Ok(()) } pub fn window_target(&self) -> &dyn RootActiveEventLoop { diff --git a/winit-web/src/event_loop/runner.rs b/winit-web/src/event_loop/runner.rs index 74bddb7e..608fe540 100644 --- a/winit-web/src/event_loop/runner.rs +++ b/winit-web/src/event_loop/runner.rs @@ -48,7 +48,6 @@ struct Execution { exit: Cell, runner: RefCell, suspended: Cell, - event_loop_recreation: Cell, events: RefCell>, id: Cell, window: web_sys::Window, @@ -195,7 +194,6 @@ impl Shared { exit: Cell::new(false), runner: RefCell::new(RunnerEnum::Pending), suspended: Cell::new(false), - event_loop_recreation: Cell::new(false), events: RefCell::new(VecDeque::new()), window, navigator, @@ -772,9 +770,7 @@ impl Shared { // * For each undropped `Window`: // * The `register_redraw_request` closure. // * The `destroy_fn` closure. - if self.0.event_loop_recreation.get() { - EventLoop::allow_event_loop_recreation(); - } + EventLoop::allow_event_loop_recreation(); } // Check if the event loop is currently closed @@ -809,10 +805,6 @@ impl Shared { } } - pub fn event_loop_recreation(&self, allow: bool) { - self.0.event_loop_recreation.set(allow) - } - pub(crate) fn control_flow(&self) -> ControlFlow { self.0.control_flow.get() } diff --git a/winit-web/src/event_loop/window_target.rs b/winit-web/src/event_loop/window_target.rs index 6d2d2be9..dcfce3bb 100644 --- a/winit-web/src/event_loop/window_target.rs +++ b/winit-web/src/event_loop/window_target.rs @@ -56,8 +56,7 @@ impl ActiveEventLoop { Self { runner: runner::Shared::new(), modifiers: ModifiersShared::default() } } - pub(crate) fn run(&self, app: Box, event_loop_recreation: bool) { - self.runner.event_loop_recreation(event_loop_recreation); + pub(crate) fn run(&self, app: Box) { self.runner.start(app, self.clone()); } diff --git a/winit-web/src/lib.rs b/winit-web/src/lib.rs index be020e70..70650846 100644 --- a/winit-web/src/lib.rs +++ b/winit-web/src/lib.rs @@ -85,7 +85,6 @@ use std::task::{Context, Poll}; use ::web_sys::HtmlCanvasElement; #[cfg(feature = "serde")] use serde::{Deserialize, Serialize}; -use winit_core::application::ApplicationHandler; use winit_core::cursor::{CustomCursor, CustomCursorSource}; use winit_core::error::NotSupportedError; use winit_core::event_loop::ActiveEventLoop; @@ -237,30 +236,6 @@ impl Default for WindowAttributesWeb { /// Additional methods on `EventLoop` that are specific to the Web. pub trait EventLoopExtWeb { - /// Initializes the winit event loop. - /// - /// Unlike - #[cfg_attr(target_feature = "exception-handling", doc = "`run_app()`")] - #[cfg_attr( - not(target_feature = "exception-handling"), - doc = "[`run_app()`]" - )] - /// [^1], this returns immediately, and doesn't throw an exception in order to - /// satisfy its [`!`] return type. - /// - /// Once the event loop has been destroyed, it's possible to reinitialize another event loop - /// by calling this function again. This can be useful if you want to recreate the event loop - /// while the WebAssembly module is still loaded. For example, this can be used to recreate the - /// event loop when switching between tabs on a single page application. - #[rustfmt::skip] - /// - #[cfg_attr( - not(target_feature = "exception-handling"), - doc = "[`run_app()`]: EventLoop::run_app()" - )] - /// [^1]: `run_app()` is _not_ available on Wasm when the target supports `exception-handling`. - fn spawn_app(self, app: A); - /// Sets the strategy for [`ControlFlow::Poll`]. /// /// See [`PollStrategy`]. diff --git a/winit-web/src/web_sys/mod.rs b/winit-web/src/web_sys/mod.rs index 0b1e5c05..ffba3ae8 100644 --- a/winit-web/src/web_sys/mod.rs +++ b/winit-web/src/web_sys/mod.rs @@ -25,10 +25,6 @@ pub use self::resize_scaling::ResizeScaleHandle; pub use self::safe_area::SafeAreaHandle; pub use self::schedule::Schedule; -pub fn throw(msg: &str) { - wasm_bindgen::throw_str(msg); -} - pub struct PageTransitionEventHandle { _show_listener: event_handle::EventListenerHandle, _hide_listener: event_handle::EventListenerHandle, diff --git a/winit/src/changelog/unreleased.md b/winit/src/changelog/unreleased.md index f3a0f6d2..eacfa59a 100644 --- a/winit/src/changelog/unreleased.md +++ b/winit/src/changelog/unreleased.md @@ -39,3 +39,9 @@ The migration guide could reference other migration examples in the current changelog entry. ## Unreleased + +### Changed + +- On Web, avoid throwing an exception in `EventLoop::run_app`, instead preferring to return to the caller. + This requires passing a `'static` application to ensure that the application state will live as long as necessary. +- On Web, the event loop can now always be re-created once it has finished running. diff --git a/winit/src/event_loop.rs b/winit/src/event_loop.rs index fc3e15ad..2ac533f9 100644 --- a/winit/src/event_loop.rs +++ b/winit/src/event_loop.rs @@ -118,7 +118,7 @@ impl EventLoop { EventLoopBuilder { platform_specific: Default::default() } } - /// Run the application with the event loop on the calling thread. + /// Run the event loop with the given application on the calling thread. /// /// The `app` is dropped when the event loop is shut down. /// @@ -173,34 +173,70 @@ impl EventLoop { /// [`ControlFlow::WaitUntil`] and life-cycle methods like [`ApplicationHandler::resumed`], but /// it should give you an idea of how things fit together. /// + /// ## Returns + /// + /// The semantics of this function can be a bit confusing, because the way different platforms + /// control their event loop varies significantly. + /// + /// On most platforms (Android, X11, Wayland, Windows, macOS), this blocks the caller, runs the + /// event loop internally, and then returns once [`ActiveEventLoop::exit`] is called. + /// + /// On iOS, this will register the application handler, and then call [`UIApplicationMain`] + /// (which is the only way to run the system event loop), which never returns to the caller + /// (the process instead exits after the handler has been dropped). + /// + /// On the web, this works by registering the application handler, and then immediately + /// returning to the caller. This is necessary because WebAssembly (and JavaScript) is always + /// executed in the context of the browser's own (internal) event loop, and thus we need to + /// return to avoid blocking that and allow events to later be delivered asynchronously. + /// + /// If you call this function inside `fn main`, you usually do not need to think about these + /// details. + /// + /// [`UIApplicationMain`]: https://developer.apple.com/documentation/uikit/uiapplicationmain(_:_:_:_:)-1yub7?language=objc + /// + /// ## Static + /// + /// To alleviate the issues noted above, this function requires that you pass in a `'static` + /// handler, to ensure that any state your application uses will be alive as long as the + /// application is running. + /// + /// To be clear, you should avoid doing e.g. `event_loop.run_app(&mut app)?`, and prefer + /// `event_loop.run_app(app)?` instead. + /// + /// If this requirement is prohibitive for you, consider using + #[cfg_attr( + any( + windows_platform, + macos_platform, + android_platform, + x11_platform, + wayland_platform, + docsrs, + ), + doc = "[`EventLoopExtRunOnDemand::run_app_on_demand`](crate::event_loop::run_on_demand::EventLoopExtRunOnDemand::run_app_on_demand)" + )] + #[cfg_attr( + not(any( + windows_platform, + macos_platform, + android_platform, + x11_platform, + wayland_platform, + docsrs, + )), + doc = "`EventLoopExtRunOnDemand::run_app_on_demand`" + )] + /// instead (though note that this is not available on iOS and web). + /// /// ## Platform-specific /// - /// - **iOS:** Will never return to the caller and so values not passed to this function will - /// *not* be dropped before the process exits. - /// - **Web:** Will _act_ as if it never returns to the caller by throwing a Javascript - /// exception (that Rust doesn't see) that will also mean that the rest of the function is - /// never executed and any values not passed to this function will *not* be dropped. - /// - /// Web applications are recommended to use - #[cfg_attr( - web_platform, - doc = " [`EventLoopExtWeb::spawn_app()`][crate::platform::web::EventLoopExtWeb::spawn_app()]" - )] - #[cfg_attr(not(web_platform), doc = " `EventLoopExtWeb::spawn_app()`")] - /// [^1] instead of [`run_app()`] to avoid the need for the Javascript exception trick, and to - /// make it clearer that the event loop runs asynchronously (via the browser's own, - /// internal, event loop) and doesn't block the current thread of execution like it does - /// on other platforms. - /// - /// This function won't be available with `target_feature = "exception-handling"`. - /// - /// [^1]: `spawn_app()` is only available on the Web platform. - /// - /// [`set_control_flow()`]: ActiveEventLoop::set_control_flow() - /// [`run_app()`]: Self::run_app() + /// - **Web** Once your handler has been dropped, it's possible to reinitialize another event + /// loop by calling this function again. This can be useful if you want to recreate the event + /// loop while the WebAssembly module is still loaded. For example, this can be used to + /// recreate the event loop when switching between tabs on a single page application. #[inline] - #[cfg(not(all(web_platform, target_feature = "exception-handling")))] - pub fn run_app(self, app: A) -> Result<(), EventLoopError> { + pub fn run_app(self, app: A) -> Result<(), EventLoopError> { self.event_loop.run_app(app) } @@ -385,10 +421,6 @@ impl winit_wayland::EventLoopBuilderExtWayland for EventLoopBuilder { #[cfg(web_platform)] impl winit_web::EventLoopExtWeb for EventLoop { - fn spawn_app(self, app: A) { - self.event_loop.spawn_app(app); - } - fn set_poll_strategy(&self, strategy: winit_web::PollStrategy) { self.event_loop.set_poll_strategy(strategy); }