< Summary

Information
Class: /home/runner/work/TauriQt/TauriQt/tauri-runtime-qt/src/qt_runtime.cpp
Assembly: Default
File(s): /home/runner/work/TauriQt/TauriQt/tauri-runtime-qt/src/qt_runtime.cpp
Line coverage
30%
Covered lines: 60
Uncovered lines: 137
Coverable lines: 197
Total lines: 342
Line coverage: 30.4%
Branch coverage
21%
Covered branches: 14
Total branches: 66
Branch coverage: 21.2%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

File(s)

/home/runner/work/TauriQt/TauriQt/tauri-runtime-qt/src/qt_runtime.cpp

#LineLine coverage
 1// SPDX-FileCopyrightText: 2026 Alper Çelik
 2// SPDX-License-Identifier: MIT OR Apache-2.0
 3
 4// Runtime-level Qt glue: native platform handles and the event-loop pump that
 5// drains Rust's user-event queue into the sink installed by `run()`
 6// (see src/ops/runtime.rs for the Rust half and src/qt_runtime.hpp for the
 7// threading contract).
 8
 9#include "qt_runtime.hpp"
 10
 11#include "qt_window.hpp"  // qt_window_winid: the widget-registry lookup
 12
 13// cxx-generated header of the bridge in `src/bridge/runtime.rs`; declares
 14// `qt_on_pump_tick` and pulls in `qt_runtime.hpp`.
 15#include "tauri-runtime-qt/src/bridge/runtime.rs.h"
 16
 17#include <QtCore/QByteArray>
 18#include <QtCore/QCoreApplication>
 19#include <QtCore/QMetaObject>
 20#include <QtCore/QPointer>
 21#include <QtCore/QSemaphore>
 22#include <QtCore/QString>
 23#include <QtCore/QThread>
 24#include <QtCore/QTimer>
 25#include <QtGui/QGuiApplication>
 26#include <QtGui/qguiapplication_platform.h>
 27#include <QtWidgets/QApplication>
 28#include <QtWidgets/QWidget>
 29
 30#include <csignal>
 31#include <cstddef>
 32#include <cstdint>
 33#include <cstdio>
 34#include <memory>
 35
 36namespace {
 37
 38/// How long an off-GUI-thread read waits for the GUI thread before reporting
 39/// failure (callers turn 0 into `HandleError::Unavailable`). The GUI thread
 40/// runs the event loop, so it answers in microseconds unless the application is
 41/// wedged.
 42constexpr int kGuiThreadTimeoutMs = 5000;
 43
 44/// Pump cadence. Wake-ups posted by `qt_runtime_wake()` drain immediately; the
 45/// timer is the safety net for events whose wake-up predates the
 46/// `QCoreApplication` (they stay queued until the first tick).
 47constexpr int kPumpIntervalMs = 16;
 48
 49QPointer<QTimer> g_pumpTimer;
 50
 4251QCoreApplication* application() {
 4252    return QCoreApplication::instance();
 4253}
 54
 55#ifdef TAURIQT_COVERAGE
 56// Provided by the LLVM profile runtime rustc links for `-Cinstrument-coverage`
 57// — the same runtime instance the clang-instrumented C++ translation units
 58// resolve against (see build.rs), so one call flushes Rust and C++ alike.
 59// Declared by hand: compiler-rt's InstrProfiling.h is not on the include path
 60// of an ordinary Qt build.
 61extern "C" int __llvm_profile_write_file(void);
 62
 63/// Flushes every instrumented translation unit and then dies of the signal with
 64/// its default disposition, so the exit status still says "killed".
 65///
 66/// Coverage runs are harness-driven: the harness stops the app (or the session
 67/// it lives in) the moment it sees its verdict, and a signal taking the default
 68/// action would skip the atexit hook the profile runtime installs, losing
 69/// everything the run accumulated. Every signal that can end such a run is
 70/// therefore armed with this flush: `SIGTERM`/`SIGINT` for an explicit stop,
 71/// `SIGHUP`/`SIGQUIT`/`SIGPIPE` for a dying compositor or display connection,
 72/// `SIGABRT` for a `qFatal()` in Qt. Coverage builds only.
 073void flushProfilesAndReraise(int signal_number) {
 074    std::fprintf(stderr, "[tauri-runtime-qt] coverage: flushing the profile (signal %d)\n",
 075                 signal_number);
 076    (void)__llvm_profile_write_file();
 077    std::signal(signal_number, SIG_DFL);
 078    std::raise(signal_number);
 079}
 80#endif
 81
 82/// `TAURIQT_EXIT_AFTER_MS=<n>`: leave the event loop through
 83/// `QCoreApplication::exit(0)` n milliseconds after the pump starts, so a
 84/// harness-driven run ends by itself and the profile runtime's atexit flush
 85/// gets to run (`timeout`-killing the app would discard the profile). In a
 86/// coverage build, arm the profile-flushing signal handlers as well. GUI thread
 87/// only; unset or unparsable value: no timer, no handlers.
 2188void installExitHooks(QCoreApplication* app) {
 2189    const QByteArray value = qgetenv("TAURIQT_EXIT_AFTER_MS");
 2190    if (!value.isEmpty()) {
 2191        bool parsed = false;
 2192        const int after_ms = value.toInt(&parsed);
 2193        if (parsed && after_ms > 0) {
 2194            QTimer::singleShot(after_ms, app, []() {
 195                std::fprintf(stderr,
 196                             "[tauri-runtime-qt] exit-after-ms: leaving the event loop\n");
 197                QCoreApplication::exit(0);
 198            });
 2199        }
 21100    }
 21101#ifdef TAURIQT_COVERAGE
 21102    const int flush_signals[] = {SIGTERM, SIGINT, SIGHUP, SIGQUIT, SIGABRT, SIGPIPE};
 126103    for (const int signal_number : flush_signals) {
 126104        std::signal(signal_number, flushProfilesAndReraise);
 126105    }
 21106#endif
 21107}
 108
 109#ifdef TAURIQT_COVERAGE
 110/// Default period of the dump installed by `installPeriodicFlush`.
 111constexpr int kProfileFlushMs = 1000;
 112
 113/// Dumps the profile of the running process every `TAURIQT_PROFILE_FLUSH_MS`
 114/// (default `kProfileFlushMs`, `0` disables it).
 115///
 116/// Not every run ends where it should: a harness can stop the app's session
 117/// under it, and a process killed with `SIGKILL` — or reached before one of the
 118/// handlers above — never runs the atexit hook the profile runtime installs, so
 119/// nothing would be written at all. The dump is cumulative and merges into
 120/// `LLVM_PROFILE_FILE`, so the last dump before the kill is what gets reported;
 121/// the interval therefore bounds how much of a killed run can be missing.
 122/// GUI thread only (called from `installPumpTimer`).
 21123void installPeriodicFlush(QCoreApplication* app) {
 21124    const QByteArray value = qgetenv("TAURIQT_PROFILE_FLUSH_MS");
 21125    bool parsed = false;
 21126    const int interval_ms = value.isEmpty() ? kProfileFlushMs : value.toInt(&parsed);
 21127    if ((!value.isEmpty() && !parsed) || interval_ms <= 0) {
 0128        return;
 0129    }
 21130    auto* timer = new QTimer(app);
 21131    timer->setTimerType(Qt::CoarseTimer);
 21132    timer->setInterval(interval_ms);
 230133    QObject::connect(timer, &QTimer::timeout, app, []() { (void)__llvm_profile_write_file(); });
 21134    timer->start();
 21135}
 136#endif
 137
 138/// Installs the drain timer; GUI thread only.
 21139void installPumpTimer() {
 21140    QCoreApplication* app = application();
 21141    if (app == nullptr || !g_pumpTimer.isNull()) {
 0142        return;
 0143    }
 21144    installExitHooks(app);
 21145#ifdef TAURIQT_COVERAGE
 21146    installPeriodicFlush(app);
 21147#endif
 21148    auto* timer = new QTimer(app);
 21149    timer->setTimerType(Qt::CoarseTimer);
 21150    timer->setInterval(kPumpIntervalMs);
 14370151    QObject::connect(timer, &QTimer::timeout, app, []() { qt_on_pump_tick(); });
 21152    timer->start();
 21153    g_pumpTimer = timer;
 21154}
 155
 156/// Runs `fn` on the GUI thread and waits for it; false when the GUI thread did
 157/// not answer in time. The semaphore is kept alive by the shared pointer even
 158/// when the wait times out, so a late call is still safe.
 159template <typename Fn>
 0160bool runOnGuiThreadAndWait(QCoreApplication* app, Fn fn) {
 0161    if (QThread::currentThread() == app->thread()) {
 0162        fn();
 0163        return true;
 0164    }
 0165    const auto done = std::make_shared<QSemaphore>();
 0166    QMetaObject::invokeMethod(app, [done, fn]() mutable {
 0167        fn();
 0168        done->release();
 0169    }, Qt::QueuedConnection);
 0170    return done->tryAcquire(1, kGuiThreadTimeoutMs);
 0171}
 172
 173/// The platform connection Qt opened: `kind` matches `qt_runtime_platform_kind()`,
 174/// `pointer` is the `xcb_connection_t*` / `Display*` / `wl_display*`.
 175struct PlatformConnection {
 176    std::int32_t kind = 0;
 177    std::uint64_t pointer = 0;
 178};
 179
 180/// Reads the platform connection Qt opened.
 181///
 182/// The plugin name decides whether the handles are real: only the `xcb`
 183/// (X11) and `wayland` plugins own a window system connection whose windows
 184/// can be described as raw handles. Other plugins answer the same native
 185/// interfaces with unrelated objects — the `offscreen` plugin, for instance,
 186/// implements `QX11Application` with the private `Display*` it opens for GLX
 187/// and returns counter values (1, 2, …) from `QWidget::winId()` — so they
 188/// report "unknown" instead of handing out values that do not describe a
 189/// window.
 190///
 191/// The platform integration and the connection it returns are created once by
 192/// the `QGuiApplication` constructor and never replaced, so this only reads
 193/// immutable state: `resolveInterface()` (which backs `nativeInterface<T>()`)
 194/// is a `dynamic_cast` against `platformNativeInterface()`. No widget or
 195/// event-loop state is touched, so the read is safe from any thread, including
 196/// before the event loop starts.
 0197PlatformConnection platformConnection() {
 0198    PlatformConnection connection;
 0199    auto* app = qobject_cast<QGuiApplication*>(application());
 0200    if (app == nullptr) {
 0201        return connection;
 0202    }
 203
 0204    const QString platformName = QGuiApplication::platformName();
 0205    if (platformName == QStringLiteral("wayland")) {
 0206#if QT_CONFIG(wayland)
 0207        if (auto* wayland = app->nativeInterface<QNativeInterface::QWaylandApplication>()) {
 0208            if (wayland->display() != nullptr) {
 0209                connection.kind = 3;
 0210                connection.pointer = reinterpret_cast<std::uint64_t>(wayland->display());
 0211            }
 0212        }
 0213#endif
 0214        return connection;
 0215    }
 216
 0217    if (platformName == QStringLiteral("xcb")) {
 0218#if QT_CONFIG(xcb)
 0219        if (auto* x11 = app->nativeInterface<QNativeInterface::QX11Application>()) {
 0220            if (x11->connection() != nullptr) {
 0221                connection.kind = 1;
 0222                connection.pointer = reinterpret_cast<std::uint64_t>(x11->connection());
 0223            } else if (x11->display() != nullptr) {
 0224                connection.kind = 2;
 0225                connection.pointer = reinterpret_cast<std::uint64_t>(x11->display());
 0226            }
 0227        }
 0228#endif
 0229        return connection;
 0230    }
 231
 232    // Any other plugin (offscreen, minimal, vnc, eglfs, linuxfb, …): no
 233    // window-system handles.
 0234    return connection;
 0235}
 236
 237}  // namespace
 238
 0239std::int32_t qt_runtime_platform_kind() {
 0240    return platformConnection().kind;
 0241}
 242
 21243rust::String qt_runtime_platform_name() {
 21244    if (QGuiApplication::instance() == nullptr) {
 0245        return rust::String();
 0246    }
 21247    return rust::String(QGuiApplication::platformName().toUtf8().constData());
 21248}
 249
 0250std::uint64_t qt_runtime_display_pointer() {
 0251    return platformConnection().pointer;
 0252}
 253
 0254std::uint64_t qt_runtime_window_winid(std::uint64_t id) {
 0255    QCoreApplication* app = application();
 0256    if (app == nullptr) {
 0257        return 0;
 0258    }
 0259    const auto winid = std::make_shared<std::uint64_t>(0);
 0260    const bool answered = runOnGuiThreadAndWait(app, [id, winid]() {
 0261        *winid = qt_window_winid(id);
 0262    });
 0263    return answered ? *winid : 0;
 0264}
 265
 0266bool qt_runtime_has_application() {
 0267    return application() != nullptr;
 0268}
 269
 0270void qt_runtime_wake() {
 0271    QCoreApplication* app = application();
 0272    if (app == nullptr) {
 0273        return;  // no event loop to wake; the first pump tick drains the queue
 0274    }
 0275    QMetaObject::invokeMethod(app, []() { qt_on_pump_tick(); }, Qt::QueuedConnection);
 0276}
 277
 21278bool qt_runtime_start_pump() {
 21279    QCoreApplication* app = application();
 21280    if (app == nullptr) {
 0281        return false;
 0282    }
 21283    if (QThread::currentThread() == app->thread()) {
 21284        installPumpTimer();
 21285    } else {
 0286        QMetaObject::invokeMethod(app, []() { installPumpTimer(); }, Qt::QueuedConnection);
 0287    }
 21288    return true;
 21289}
 290
 0291void qt_runtime_exit(std::int32_t code) {
 0292    QCoreApplication* app = application();
 0293    if (app == nullptr) {
 0294        return;
 0295    }
 0296    if (QThread::currentThread() == app->thread()) {
 0297        QCoreApplication::exit(code);
 0298        return;
 0299    }
 0300    QMetaObject::invokeMethod(app, [code]() { QCoreApplication::exit(code); },
 0301                              Qt::QueuedConnection);
 0302}
 303
 0304void qt_runtime_process_events() {
 0305    QCoreApplication* app = application();
 0306    if (app == nullptr) {
 0307        return;
 0308    }
 0309    if (QThread::currentThread() == app->thread()) {
 0310        QCoreApplication::processEvents();
 0311        return;
 0312    }
 0313    QMetaObject::invokeMethod(app, []() { QCoreApplication::processEvents(); },
 0314                              Qt::QueuedConnection);
 0315}
 316
 0317rust::String qt_runtime_debug_native_state() {
 0318    const PlatformConnection connection = platformConnection();
 319
 0320    const QString platformName =
 0321        qGuiApp != nullptr ? QGuiApplication::platformName() : QStringLiteral("<no application>");
 0322    QString text = QStringLiteral("platform=%1 kind=%2 display=0x%3")
 0323                       .arg(platformName)
 0324                       .arg(connection.kind)
 0325                       .arg(connection.pointer, 0, 16);
 326
 0327    if (qobject_cast<QApplication*>(application()) != nullptr) {
 0328        const QWidgetList windows = QApplication::topLevelWidgets();
 0329        text += QStringLiteral(" topLevelWidgets=%1").arg(windows.size());
 0330        for (QWidget* window : windows) {
 0331            text += QStringLiteral(" [winId=0x%1 visible=%2 title=\"%3\"]")
 0332                        .arg(static_cast<quintptr>(window->winId()), 0, 16)
 0333                        .arg(window->isVisible() ? 1 : 0)
 0334                        .arg(window->windowTitle());
 0335        }
 0336    } else {
 0337        text += QStringLiteral(" (no QApplication)");
 0338    }
 339
 0340    const QByteArray utf8 = text.toUtf8();
 0341    return rust::String(utf8.constData(), static_cast<std::size_t>(utf8.size()));
 0342}