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).
This commit is contained in:
Mads Marquart 2025-03-17 04:49:58 +01:00 committed by Kirill Chibisov
parent e8bccfff4f
commit f69b601abb
9 changed files with 81 additions and 102 deletions

View file

@ -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<A: ApplicationHandler>(&mut self, app: A) -> Result<(), EventLoopError>;

View file

@ -238,7 +238,8 @@ impl EventLoop {
})
}
pub fn run_app<A: ApplicationHandler>(self, app: A) -> ! {
// Require `'static` for correctness, we won't be able to `Drop` the user's state otherwise.
pub fn run_app<A: ApplicationHandler + 'static>(self, app: A) -> ! {
let application: Option<Retained<UIApplication>> =
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:
// <https://github.com/rust-windowing/winit/pull/4165#issuecomment-2760514167>
app_state::launch(self.mtm, app, || UIApplication::main(None, None, self.mtm))
}

View file

@ -39,32 +39,9 @@ impl EventLoop {
EVENT_LOOP_CREATED.store(false, Ordering::Relaxed);
}
pub fn run_app<A: ApplicationHandler>(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<dyn ApplicationHandler + '_>,
Box<dyn ApplicationHandler + 'static>,
>(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<A: ApplicationHandler + 'static>(self, app: A) {
self.elw.run(Box::new(app), true);
pub fn run_app<A: ApplicationHandler + 'static>(self, app: A) -> Result<(), EventLoopError> {
self.elw.run(Box::new(app));
Ok(())
}
pub fn window_target(&self) -> &dyn RootActiveEventLoop {

View file

@ -48,7 +48,6 @@ struct Execution {
exit: Cell<bool>,
runner: RefCell<RunnerEnum>,
suspended: Cell<bool>,
event_loop_recreation: Cell<bool>,
events: RefCell<VecDeque<Event>>,
id: Cell<usize>,
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,10 +770,8 @@ 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();
}
}
// Check if the event loop is currently closed
fn is_closed(&self) -> bool {
@ -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()
}

View file

@ -56,8 +56,7 @@ impl ActiveEventLoop {
Self { runner: runner::Shared::new(), modifiers: ModifiersShared::default() }
}
pub(crate) fn run(&self, app: Box<dyn ApplicationHandler>, event_loop_recreation: bool) {
self.runner.event_loop_recreation(event_loop_recreation);
pub(crate) fn run(&self, app: Box<dyn ApplicationHandler>) {
self.runner.start(app, self.clone());
}

View file

@ -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<A: ApplicationHandler + 'static>(self, app: A);
/// Sets the strategy for [`ControlFlow::Poll`].
///
/// See [`PollStrategy`].

View file

@ -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<dyn FnMut(PageTransitionEvent)>,
_hide_listener: event_handle::EventListenerHandle<dyn FnMut(PageTransitionEvent)>,

View file

@ -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.

View file

@ -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<A: ApplicationHandler>(self, app: A) -> Result<(), EventLoopError> {
pub fn run_app<A: ApplicationHandler + 'static>(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<A: ApplicationHandler + 'static>(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);
}