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:
parent
e8bccfff4f
commit
f69b601abb
9 changed files with 81 additions and 102 deletions
|
|
@ -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>;
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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`].
|
||||
|
|
|
|||
|
|
@ -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)>,
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue