| | | 1 | | // SPDX-FileCopyrightText: 2026 Alper Çelik |
| | | 2 | | // SPDX-License-Identifier: MIT OR Apache-2.0 |
| | | 3 | | |
| | | 4 | | //! Runtime-level operations (owned by the runtime worker): raw window/display |
| | | 5 | | //! handles, the cross-thread event proxy and event-loop control. |
| | | 6 | | //! |
| | | 7 | | //! # Event flow |
| | | 8 | | //! |
| | | 9 | | //! `EventLoopProxy::send_event` boxes the event into a process-wide queue and |
| | | 10 | | //! wakes the Qt event loop. On the GUI thread the pump tick (`qt_on_pump_tick` |
| | | 11 | | //! in `crate::bridge::runtime`, driven by the `QTimer` installed by |
| | | 12 | | //! `start_pump()` and by the queued wake-ups) drains the queue into the sink |
| | | 13 | | //! installed with `set_event_sink()`. `request_exit()` stores the exit code; the |
| | | 14 | | //! tick delivers the events queued before it and then leaves the loop through |
| | | 15 | | //! `QCoreApplication::exit(code)`. |
| | | 16 | | //! |
| | | 17 | | //! # Raw handles |
| | | 18 | | //! |
| | | 19 | | //! What Qt can answer truthfully (verified against Qt 6.11 sources): |
| | | 20 | | //! |
| | | 21 | | //! * X11 (xcb plugin): `QWidget::winId()` is the window's XID and |
| | | 22 | | //! `QX11Application` hands out the `xcb_connection_t*` (`Display*` fallback). |
| | | 23 | | //! * Wayland: `QWidget::winId()` is the window's `wl_surface*` |
| | | 24 | | //! (`QWaylandWindow::winId()` returns `wlSurface()`, qtbase |
| | | 25 | | //! `src/plugins/platforms/wayland/qwaylandwindow.cpp`), and |
| | | 26 | | //! `QWaylandApplication` hands out the `wl_display*`. |
| | | 27 | | //! * Every other platform plugin (offscreen, minimal, eglfs, …) and a missing |
| | | 28 | | //! application: no window handle exists — the offscreen plugin even answers |
| | | 29 | | //! `QX11Application` with a GLX-only `Display*` and returns counter values |
| | | 30 | | //! from `winId()` — so `HandleError::Unavailable` is returned instead of a |
| | | 31 | | //! fabricated value. The plugin name gates the decision (`platformConnection` |
| | | 32 | | //! in `qt_runtime.cpp`). |
| | | 33 | | |
| | | 34 | | use std::any::Any; |
| | | 35 | | use std::collections::VecDeque; |
| | | 36 | | use std::ffi::c_void; |
| | | 37 | | use std::num::NonZeroU32; |
| | | 38 | | use std::os::raw::{c_int, c_ulong}; |
| | | 39 | | use std::ptr::NonNull; |
| | | 40 | | use std::sync::atomic::{AtomicI32, Ordering}; |
| | | 41 | | |
| | | 42 | | use parking_lot::Mutex; |
| | | 43 | | use raw_window_handle::{ |
| | | 44 | | DisplayHandle, HandleError, RawDisplayHandle, RawWindowHandle, WaylandDisplayHandle, |
| | | 45 | | WaylandWindowHandle, WindowHandle, XcbDisplayHandle, XcbWindowHandle, XlibDisplayHandle, |
| | | 46 | | XlibWindowHandle, |
| | | 47 | | }; |
| | | 48 | | use tauri_runtime::{Error, Result, UserEvent}; |
| | | 49 | | |
| | | 50 | | use crate::bridge::runtime::ffi; |
| | | 51 | | |
| | | 52 | | /// Platform kinds reported by `qt_runtime_platform_kind()` (see the bridge); |
| | | 53 | | /// anything else — including 0, "no application / unsupported platform" — maps |
| | | 54 | | /// to `NativePlatform::None`. |
| | | 55 | | const PLATFORM_XCB: i32 = 1; |
| | | 56 | | const PLATFORM_XLIB: i32 = 2; |
| | | 57 | | const PLATFORM_WAYLAND: i32 = 3; |
| | | 58 | | |
| | | 59 | | /// `EXIT_CODE` value meaning "no exit requested". |
| | | 60 | | const NO_EXIT: i32 = i32::MIN; |
| | | 61 | | |
| | | 62 | | /// X11 screen index reported in display handles. Qt's `QX11Application` exposes |
| | | 63 | | /// `display()` and `connection()` only — no screen number — and Qt connects to |
| | | 64 | | /// the connection's default screen, which is index 0. |
| | | 65 | | const X11_DEFAULT_SCREEN: c_int = 0; |
| | | 66 | | |
| | | 67 | | /// Events queued by `proxy_send_event`, drained on the Qt GUI thread. |
| | | 68 | | static EVENT_QUEUE: Mutex<VecDeque<Box<dyn Any + Send>>> = Mutex::new(VecDeque::new()); |
| | | 69 | | |
| | | 70 | | /// Where drained events go; installed by the integrator from `run()`. |
| | | 71 | | static EVENT_SINK: Mutex<Option<Box<dyn FnMut(Box<dyn Any + Send>) + Send>>> = Mutex::new(None); |
| | | 72 | | |
| | | 73 | | /// Pending exit code (see `request_exit`). |
| | | 74 | | static EXIT_CODE: AtomicI32 = AtomicI32::new(NO_EXIT); |
| | | 75 | | |
| | | 76 | | /// Queues `event` for delivery on the Qt GUI thread. |
| | | 77 | | /// |
| | | 78 | | /// Safe and non-blocking from any thread, and usable before the event loop |
| | | 79 | | /// runs (the queue is drained by the first pump tick). |
| | 0 | 80 | | pub(crate) fn proxy_send_event<T: UserEvent>(event: T) -> Result<()> { |
| | 0 | 81 | | enqueue(Box::new(event)); |
| | 0 | 82 | | Ok(()) |
| | 0 | 83 | | } |
| | | 84 | | |
| | | 85 | | /// Pushes a boxed event into the queue and schedules a drain. |
| | | 86 | | /// |
| | | 87 | | /// One wake-up is posted per event: the GUI thread processes the posted |
| | | 88 | | /// invocation after the event was pushed by this thread, so the drain it |
| | | 89 | | /// triggers always sees the event. (Coalescing the wake-ups would risk losing |
| | | 90 | | /// the last one; an extra queued invocation is cheap.) |
| | 0 | 91 | | fn enqueue(event: Box<dyn Any + Send>) { |
| | 0 | 92 | | EVENT_QUEUE.lock().push_back(event); |
| | 0 | 93 | | ffi::qt_runtime_wake(); |
| | 0 | 94 | | } |
| | | 95 | | |
| | | 96 | | /// Installs the sink that receives every drained event. |
| | | 97 | | /// |
| | | 98 | | /// The integrator calls this once from `run()`/`run_return()`; calling it again |
| | | 99 | | /// replaces the previous sink. The sink is invoked exclusively on the Qt GUI |
| | | 100 | | /// thread (from the pump tick) and must not call `set_event_sink` itself — use |
| | | 101 | | /// `main_thread_sink` when the closure is not `Send`. |
| | 21 | 102 | | pub fn set_event_sink(sink: Box<dyn FnMut(Box<dyn Any + Send>) + Send>) { |
| | 21 | 103 | | let sink = debug::wrap_sink(sink); |
| | 21 | 104 | | *EVENT_SINK.lock() = Some(sink); |
| | 21 | 105 | | } |
| | | 106 | | |
| | | 107 | | /// Closure wrapper that is only ever invoked on the Qt GUI thread but may be |
| | | 108 | | /// stored in a `Send` slot (see `main_thread_sink`). |
| | | 109 | | struct MainThreadOnly<F>(F); |
| | | 110 | | |
| | | 111 | | // SAFETY: the pump tick — see `main_thread_sink` — is the only caller and it |
| | | 112 | | // runs on the Qt GUI thread. |
| | | 113 | | unsafe impl<F> Send for MainThreadOnly<F> {} |
| | | 114 | | |
| | | 115 | | impl<F: FnMut(Box<dyn Any + Send>)> MainThreadOnly<F> { |
| | 0 | 116 | | fn call(&mut self, event: Box<dyn Any + Send>) { |
| | 0 | 117 | | (self.0)(event) |
| | 0 | 118 | | } |
| | | 119 | | } |
| | | 120 | | |
| | | 121 | | /// Wraps a closure that is only ever invoked on the Qt GUI thread so it can be |
| | | 122 | | /// handed to `set_event_sink`, which requires `Send` for its process-wide slot. |
| | | 123 | | /// |
| | | 124 | | /// The trait's `run`/`run_return` take `FnMut(RunEvent<T>)` without a `Send` |
| | | 125 | | /// bound, so the callback may capture `Rc`/`RefCell` state; the wrapper asserts |
| | | 126 | | /// that this never matters because the pump drains on the GUI thread only. |
| | 21 | 127 | | pub fn main_thread_sink<F>(sink: F) -> Box<dyn FnMut(Box<dyn Any + Send>) + Send> |
| | 21 | 128 | | where |
| | 21 | 129 | | F: FnMut(Box<dyn Any + Send>) + 'static, |
| | | 130 | | { |
| | | 131 | | // Calling through the wrapper (instead of `sink.0`) keeps the closure from |
| | | 132 | | // capturing the inner `F` by itself, which would require `F: Send`. |
| | 21 | 133 | | let mut sink = MainThreadOnly(sink); |
| | 21 | 134 | | Box::new(move |event| sink.call(event)) |
| | 21 | 135 | | } |
| | | 136 | | |
| | | 137 | | /// Installs the Qt-side drain timer; called once from `run()`/`run_return()` |
| | | 138 | | /// after `set_event_sink`. |
| | 21 | 139 | | pub fn start_pump() { |
| | 21 | 140 | | if !ffi::qt_runtime_start_pump() { |
| | 0 | 141 | | crate::qtrace!("[tauri-runtime-qt] start_pump: no QApplication yet, events stay queued"); |
| | 21 | 142 | | } |
| | 21 | 143 | | debug::fire_probe(); |
| | 21 | 144 | | } |
| | | 145 | | |
| | | 146 | | /// Requests an exit of the Qt event loop with `code`. |
| | | 147 | | /// |
| | | 148 | | /// Safe from any thread and non-blocking: the code is stored and the GUI thread |
| | | 149 | | /// exits on the next pump tick, after delivering the events that were queued |
| | | 150 | | /// before this call. Returns `Err(Error::EventLoopClosed)` when no |
| | | 151 | | /// `QCoreApplication` exists (nothing to exit); events queued before `run()` |
| | | 152 | | /// start are still delivered, because the first tick exits. |
| | 0 | 153 | | pub(crate) fn request_exit(code: i32) -> Result<()> { |
| | 0 | 154 | | if !ffi::qt_runtime_has_application() { |
| | 0 | 155 | | return Err(Error::EventLoopClosed); |
| | 0 | 156 | | } |
| | 0 | 157 | | EXIT_CODE.store(code, Ordering::SeqCst); |
| | 0 | 158 | | ffi::qt_runtime_wake(); |
| | 0 | 159 | | Ok(()) |
| | 0 | 160 | | } |
| | | 161 | | |
| | | 162 | | /// Runs one turn of the Qt event loop without blocking. |
| | 0 | 163 | | pub(crate) fn run_iteration() { |
| | 0 | 164 | | ffi::qt_runtime_process_events(); |
| | 0 | 165 | | } |
| | | 166 | | |
| | | 167 | | /// Pump tick; runs on the Qt GUI thread (see `bridge::runtime::qt_on_pump_tick`). |
| | 14370 | 168 | | pub(crate) fn pump_tick() { |
| | | 169 | | // Take the queue under the lock but feed the sink outside of it: the sink |
| | | 170 | | // may send further events through the proxy, and the queue mutex is not |
| | | 171 | | // reentrant. |
| | 14370 | 172 | | let drained: Vec<Box<dyn Any + Send>> = EVENT_QUEUE.lock().drain(..).collect(); |
| | | 173 | | |
| | 14370 | 174 | | if !drained.is_empty() { |
| | 0 | 175 | | let mut slot = EVENT_SINK.lock(); |
| | 0 | 176 | | match slot.as_mut() { |
| | 0 | 177 | | Some(sink) => { |
| | 0 | 178 | | for event in drained { |
| | 0 | 179 | | sink(event); |
| | 0 | 180 | | } |
| | | 181 | | } |
| | | 182 | | // No sink yet (`run()` has not started): hold the events for the |
| | | 183 | | // next tick instead of dropping them. |
| | | 184 | | None => { |
| | 0 | 185 | | let mut queue = EVENT_QUEUE.lock(); |
| | 0 | 186 | | for event in drained.into_iter().rev() { |
| | 0 | 187 | | queue.push_front(event); |
| | 0 | 188 | | } |
| | | 189 | | } |
| | | 190 | | } |
| | 14370 | 191 | | } |
| | | 192 | | |
| | | 193 | | // Exit last: everything queued before the exit request was delivered above. |
| | 14370 | 194 | | let code = EXIT_CODE.swap(NO_EXIT, Ordering::SeqCst); |
| | 14370 | 195 | | if code != NO_EXIT { |
| | 0 | 196 | | ffi::qt_runtime_exit(code); |
| | 14370 | 197 | | } |
| | 14370 | 198 | | } |
| | | 199 | | |
| | | 200 | | /// Native platform Qt is running on. |
| | | 201 | | #[derive(Debug, Clone, Copy, PartialEq, Eq)] |
| | | 202 | | enum NativePlatform { |
| | | 203 | | /// No QGuiApplication yet, or a plugin without native handles. |
| | | 204 | | None, |
| | | 205 | | /// X11 through the xcb plugin: XID + `xcb_connection_t*`. |
| | | 206 | | Xcb, |
| | | 207 | | /// X11 with only the Xlib `Display*` available. |
| | | 208 | | Xlib, |
| | | 209 | | /// Wayland: `wl_surface*` + `wl_display*`. |
| | | 210 | | Wayland, |
| | | 211 | | } |
| | | 212 | | |
| | | 213 | | /// Name of the platform plugin Qt is running on ("wayland", "xcb", …). |
| | 21 | 214 | | pub fn platform_name() -> String { |
| | 21 | 215 | | ffi::qt_runtime_platform_name() |
| | 21 | 216 | | } |
| | | 217 | | |
| | 0 | 218 | | fn native_platform() -> NativePlatform { |
| | 0 | 219 | | match ffi::qt_runtime_platform_kind() { |
| | 0 | 220 | | PLATFORM_XCB => NativePlatform::Xcb, |
| | 0 | 221 | | PLATFORM_XLIB => NativePlatform::Xlib, |
| | 0 | 222 | | PLATFORM_WAYLAND => NativePlatform::Wayland, |
| | 0 | 223 | | _ => NativePlatform::None, |
| | | 224 | | } |
| | 0 | 225 | | } |
| | | 226 | | |
| | | 227 | | /// Raw handle of the Qt window behind the runtime's `native` handle. |
| | | 228 | | /// |
| | | 229 | | /// X11: `QWidget::winId()` is the XID; the xcb variant is used when Qt has an |
| | | 230 | | /// xcb connection, the Xlib variant otherwise. Wayland: `QWidget::winId()` is |
| | | 231 | | /// the window's `wl_surface*`. Unknown handles, missing windows and platforms |
| | | 232 | | /// without native handles report `HandleError::Unavailable`. |
| | 0 | 233 | | pub(crate) fn window_handle( |
| | 0 | 234 | | native: u64, |
| | 0 | 235 | | ) -> std::result::Result<WindowHandle<'static>, HandleError> { |
| | 0 | 236 | | let platform = native_platform(); |
| | 0 | 237 | | let winid = ffi::qt_runtime_window_winid(native); |
| | | 238 | | |
| | 0 | 239 | | let raw = match platform { |
| | | 240 | | NativePlatform::Xcb => { |
| | 0 | 241 | | let window = u32::try_from(winid) |
| | 0 | 242 | | .ok() |
| | 0 | 243 | | .and_then(NonZeroU32::new) |
| | 0 | 244 | | .ok_or(HandleError::Unavailable)?; |
| | 0 | 245 | | RawWindowHandle::Xcb(XcbWindowHandle::new(window)) |
| | | 246 | | } |
| | | 247 | | NativePlatform::Xlib => { |
| | 0 | 248 | | if winid == 0 { |
| | 0 | 249 | | return Err(HandleError::Unavailable); |
| | 0 | 250 | | } |
| | 0 | 251 | | RawWindowHandle::Xlib(XlibWindowHandle::new(winid as c_ulong)) |
| | | 252 | | } |
| | | 253 | | NativePlatform::Wayland => { |
| | 0 | 254 | | let surface = NonNull::new(winid as *mut c_void).ok_or(HandleError::Unavailable)?; |
| | 0 | 255 | | RawWindowHandle::Wayland(WaylandWindowHandle::new(surface)) |
| | | 256 | | } |
| | 0 | 257 | | NativePlatform::None => return Err(HandleError::Unavailable), |
| | | 258 | | }; |
| | | 259 | | |
| | | 260 | | // SAFETY: the handle describes the live Qt platform resource. Qt keeps it |
| | | 261 | | // alive for at least as long as the window exists, and the returned borrow |
| | | 262 | | // never outlives the process-lifetime QGuiApplication that owns it. |
| | 0 | 263 | | Ok(unsafe { WindowHandle::borrow_raw(raw) }) |
| | 0 | 264 | | } |
| | | 265 | | |
| | | 266 | | /// Raw display handle of the Qt platform connection. |
| | | 267 | | /// |
| | | 268 | | /// The pointers come from the `QNativeInterface` the platform plugin |
| | | 269 | | /// implements (`QX11Application` / `QWaylandApplication`); platforms without |
| | | 270 | | /// one report `HandleError::Unavailable`. |
| | 0 | 271 | | pub(crate) fn display_handle() -> std::result::Result<DisplayHandle<'static>, HandleError> { |
| | 0 | 272 | | let platform = native_platform(); |
| | 0 | 273 | | let pointer = ffi::qt_runtime_display_pointer(); |
| | | 274 | | |
| | 0 | 275 | | let raw = match platform { |
| | | 276 | | NativePlatform::Xcb => { |
| | 0 | 277 | | let connection = |
| | 0 | 278 | | NonNull::new(pointer as *mut c_void).ok_or(HandleError::Unavailable)?; |
| | 0 | 279 | | RawDisplayHandle::Xcb(XcbDisplayHandle::new(Some(connection), X11_DEFAULT_SCREEN)) |
| | | 280 | | } |
| | | 281 | | NativePlatform::Xlib => { |
| | 0 | 282 | | let display = NonNull::new(pointer as *mut c_void).ok_or(HandleError::Unavailable)?; |
| | 0 | 283 | | RawDisplayHandle::Xlib(XlibDisplayHandle::new(Some(display), X11_DEFAULT_SCREEN)) |
| | | 284 | | } |
| | | 285 | | NativePlatform::Wayland => { |
| | 0 | 286 | | let display = NonNull::new(pointer as *mut c_void).ok_or(HandleError::Unavailable)?; |
| | 0 | 287 | | RawDisplayHandle::Wayland(WaylandDisplayHandle::new(display)) |
| | | 288 | | } |
| | 0 | 289 | | NativePlatform::None => return Err(HandleError::Unavailable), |
| | | 290 | | }; |
| | | 291 | | |
| | | 292 | | // SAFETY: the connection is owned by the Qt platform plugin and lives until |
| | | 293 | | // the process exits; the borrow does not outlive it. |
| | 0 | 294 | | Ok(unsafe { DisplayHandle::borrow_raw(raw) }) |
| | 0 | 295 | | } |
| | | 296 | | |
| | | 297 | | /// `TAURIQT_PROXY_DEBUG=1` end-to-end probe of the proxy path. It logs every |
| | | 298 | | /// event the sink receives, queues three probe events from a worker thread at |
| | | 299 | | /// pump start and then asks the loop to `exit(0)`. The whole module is dormant |
| | | 300 | | /// unless the environment variable is set. |
| | | 301 | | mod debug { |
| | | 302 | | use super::*; |
| | | 303 | | use std::sync::Once; |
| | | 304 | | use std::time::Duration; |
| | | 305 | | |
| | | 306 | | static PROBE: Once = Once::new(); |
| | | 307 | | |
| | 42 | 308 | | pub fn enabled() -> bool { |
| | 42 | 309 | | std::env::var_os("TAURIQT_PROXY_DEBUG").is_some_and(|value| value != "0") |
| | 42 | 310 | | } |
| | | 311 | | |
| | | 312 | | /// Logs each event on its way into the real sink. |
| | 21 | 313 | | pub fn wrap_sink( |
| | 21 | 314 | | mut sink: Box<dyn FnMut(Box<dyn Any + Send>) + Send>, |
| | 21 | 315 | | ) -> Box<dyn FnMut(Box<dyn Any + Send>) + Send> { |
| | 21 | 316 | | if !enabled() { |
| | 21 | 317 | | return sink; |
| | 0 | 318 | | } |
| | 0 | 319 | | let mut received = 0usize; |
| | 0 | 320 | | Box::new(move |event| { |
| | 0 | 321 | | received += 1; |
| | 0 | 322 | | match event.downcast_ref::<String>() { |
| | 0 | 323 | | Some(payload) => eprintln!( |
| | | 324 | | "[tauri-runtime-qt] proxy-debug: sink received event #{received}: {payload}" |
| | | 325 | | ), |
| | 0 | 326 | | None => eprintln!( |
| | | 327 | | "[tauri-runtime-qt] proxy-debug: sink received event #{received} (non-probe payload)" |
| | | 328 | | ), |
| | | 329 | | } |
| | 0 | 330 | | sink(event); |
| | 0 | 331 | | }) |
| | 21 | 332 | | } |
| | | 333 | | |
| | | 334 | | /// Queues three probe events from a worker thread, then requests exit(0). |
| | 21 | 335 | | pub fn fire_probe() { |
| | 21 | 336 | | if !enabled() { |
| | 21 | 337 | | return; |
| | 0 | 338 | | } |
| | 0 | 339 | | PROBE.call_once(|| { |
| | 0 | 340 | | eprintln!( |
| | | 341 | | "[tauri-runtime-qt] proxy-debug: native state: {}", |
| | 0 | 342 | | ffi::qt_runtime_debug_native_state() |
| | | 343 | | ); |
| | 0 | 344 | | eprintln!( |
| | | 345 | | "[tauri-runtime-qt] proxy-debug: display_handle() = {:?}", |
| | 0 | 346 | | display_handle() |
| | | 347 | | ); |
| | 0 | 348 | | eprintln!( |
| | | 349 | | "[tauri-runtime-qt] proxy-debug: window_handle(u64::MAX) = {:?}", |
| | 0 | 350 | | window_handle(u64::MAX) |
| | | 351 | | ); |
| | | 352 | | // The registry hands out 1-based handles; report the first real |
| | | 353 | | // window as well (unknown ids answer with `Unavailable`). |
| | 0 | 354 | | for id in 1..=3u64 { |
| | 0 | 355 | | if let Ok(handle) = window_handle(id) { |
| | 0 | 356 | | eprintln!("[tauri-runtime-qt] proxy-debug: window_handle({id}) = {handle:?}"); |
| | 0 | 357 | | break; |
| | 0 | 358 | | } |
| | | 359 | | } |
| | 0 | 360 | | eprintln!( |
| | | 361 | | "[tauri-runtime-qt] proxy-debug: queueing 3 probe events from a worker thread" |
| | | 362 | | ); |
| | 0 | 363 | | std::thread::spawn(|| { |
| | 0 | 364 | | for index in 1..=3 { |
| | 0 | 365 | | std::thread::sleep(Duration::from_millis(200)); |
| | 0 | 366 | | eprintln!("[tauri-runtime-qt] proxy-debug: worker queues probe #{index}"); |
| | 0 | 367 | | enqueue(Box::new(format!("proxy-probe-{index}"))); |
| | 0 | 368 | | } |
| | 0 | 369 | | std::thread::sleep(Duration::from_millis(400)); |
| | 0 | 370 | | eprintln!("[tauri-runtime-qt] proxy-debug: worker requests exit(0)"); |
| | 0 | 371 | | let _ = request_exit(0); |
| | 0 | 372 | | }); |
| | 0 | 373 | | }); |
| | 21 | 374 | | } |
| | | 375 | | } |