diff options
Diffstat (limited to 'src/corelib')
28 files changed, 1196 insertions, 150 deletions
diff --git a/src/corelib/CMakeLists.txt b/src/corelib/CMakeLists.txt index 55d375f0350..32b70a1f288 100644 --- a/src/corelib/CMakeLists.txt +++ b/src/corelib/CMakeLists.txt @@ -701,6 +701,7 @@ qt_internal_extend_target(Core CONDITION APPLE kernel/qcore_mac.mm kernel/qcore_mac_p.h kernel/qcoreapplication_mac.cpp kernel/qeventdispatcher_cf.mm kernel/qeventdispatcher_cf_p.h + platform/darwin/qdarwinsecurityscopedfileengine.mm platform/darwin/qdarwinsecurityscopedfileengine_p.h LIBRARIES ${FWCoreFoundation} ${FWFoundation} @@ -1510,6 +1511,7 @@ endif() qt_internal_extend_target(Core CONDITION WASM SOURCES + platform/wasm/qwasmanimationdriver.cpp platform/wasm/qwasmanimationdriver_p.h platform/wasm/qwasmglobal.cpp platform/wasm/qwasmglobal_p.h platform/wasm/qstdweb.cpp platform/wasm/qstdweb_p.h platform/wasm/qwasmsocket.cpp platform/wasm/qwasmsocket_p.h diff --git a/src/corelib/Qt6AndroidMacros.cmake b/src/corelib/Qt6AndroidMacros.cmake index 6a83e947146..be362ba1925 100644 --- a/src/corelib/Qt6AndroidMacros.cmake +++ b/src/corelib/Qt6AndroidMacros.cmake @@ -106,6 +106,27 @@ function(qt6_add_android_dynamic_features target) endif() endfunction() + +function(qt_add_android_dynamic_feature_java_source_dirs) + qt6_add_android_dynamic_feature_java_source_dirs(${ARGV}) +endfunction() + +# Add java source directories for dynamic feature. Intermediate solution until java library +# support exists. +function(qt6_add_android_dynamic_feature_java_source_dirs target) + + set(opt_args "") + set(single_args "") + set(multi_args + SOURCE_DIRS + ) + cmake_parse_arguments(PARSE_ARGV 1 arg "${opt_args}" "${single_args}" "${multi_args}") + if(arg_SOURCE_DIRS) + set_property(TARGET ${target} APPEND PROPERTY + _qt_android_gradle_java_source_dirs ${arg_SOURCE_DIRS}) + endif() +endfunction() + # Generate the deployment settings json file for a cmake target. function(qt6_android_generate_deployment_settings target) # Information extracted from mkspecs/features/android/android_deployment_settings.prf diff --git a/src/corelib/Qt6CoreDeploySupport.cmake b/src/corelib/Qt6CoreDeploySupport.cmake index 632d7a0f6ba..175a82ff095 100644 --- a/src/corelib/Qt6CoreDeploySupport.cmake +++ b/src/corelib/Qt6CoreDeploySupport.cmake @@ -771,6 +771,8 @@ function(qt6_deploy_runtime_dependencies) # Specify path to target Qt's qtpaths .exe or .bat file, so windeployqt deploys the correct # libraries when cross-compiling from x86_64 to arm64 windows. + # We need to point to a qtpaths that will give info about the target platform, but which + # can run on the host. if(__QT_DEPLOY_TARGET_QT_PATHS_PATH AND EXISTS "${__QT_DEPLOY_TARGET_QT_PATHS_PATH}") list(APPEND tool_options --qtpaths "${__QT_DEPLOY_TARGET_QT_PATHS_PATH}") else() diff --git a/src/corelib/Qt6CoreMacros.cmake b/src/corelib/Qt6CoreMacros.cmake index 507beca4380..6f3a9fd05f5 100644 --- a/src/corelib/Qt6CoreMacros.cmake +++ b/src/corelib/Qt6CoreMacros.cmake @@ -3212,41 +3212,63 @@ function(_qt_internal_setup_deploy_support) endif() endif() - # Generate path to the target (not host) qtpaths file. Needed for windeployqt when - # cross-compiling from an x86_64 host to an arm64 target, so it knows which architecture - # libraries should be deployed. - if(CMAKE_HOST_WIN32) - if(CMAKE_CROSSCOMPILING) - set(qt_paths_ext ".bat") + # Generate path to the qtpaths executable or script, that will give info about the target + # platform, but which can run on the host. Needed for windeployqt when cross-compiling from + # an x86_64 host to an arm64 target, so it knows which architecture libraries should be + # deployed. + set(base_name "qtpaths") + set(base_names "") + + get_property(qt_major_version TARGET "${target}" PROPERTY INTERFACE_QT_MAJOR_VERSION) + if(qt_major_version) + list(APPEND base_names "${base_name}${qt_major_version}") + endif() + list(APPEND base_names "${base_name}") + + set(qtpaths_name_candidates "") + foreach(base_name IN LISTS base_names) + if(CMAKE_HOST_WIN32) + if(CMAKE_CROSSCOMPILING) + set(qt_paths_ext ".bat") + # Depending on whether QT_FORCE_BUILD_TOOLS was set when building Qt, a 'host-' + # prefix is prepended to the created qtpaths wrapper, not to collide with the + # cross-compiled excutable. + # Rather than exporting that QT_FORCE_BUILD_TOOLS to be available during user + # project configuration, search for both, with the bare one searched first. + list(APPEND qtpaths_name_candidates "${base_name}${qt_paths_ext}") + list(APPEND qtpaths_name_candidates "host-${base_name}${qt_paths_ext}") + else() + set(qt_paths_ext ".exe") + list(APPEND qtpaths_name_candidates "${base_name}${qt_paths_ext}") + endif() else() - set(qt_paths_ext ".exe") + list(APPEND qtpaths_name_candidates "${base_name}") endif() - else() - set(qt_paths_ext "") - endif() + endforeach() + set(qtpaths_prefix "${QT6_INSTALL_PREFIX}/${QT6_INSTALL_BINS}") + set(candidate_paths "") + foreach(qtpaths_name_candidate IN LISTS qtpaths_name_candidates) + set(candidate_path "${qtpaths_prefix}/${qtpaths_name_candidate}") + list(APPEND candidate_paths "${candidate_path}") + endforeach() set(target_qtpaths_path "") - set(qtpaths_prefix "${QT6_INSTALL_PREFIX}/${QT6_INSTALL_BINS}") - get_property(qt_major_version TARGET "${target}" PROPERTY INTERFACE_QT_MAJOR_VERSION) - if(qt_major_version) - set(target_qtpaths_with_major_version_path - "${qtpaths_prefix}/qtpaths${qt_major_version}${qt_paths_ext}") - if(EXISTS "${target_qtpaths_with_major_version_path}") - set(target_qtpaths_path "${target_qtpaths_with_major_version_path}") + foreach(candidate_path IN LISTS candidate_paths) + if(EXISTS "${candidate_path}") + set(target_qtpaths_path "${candidate_path}") + break() endif() - endif() + endforeach() - if(NOT target_qtpaths_path) - set(target_qtpaths_path_without_version "${qtpaths_prefix}/qtpaths${qt_paths_ext}") - if(EXISTS "${target_qtpaths_path_without_version}") - set(target_qtpaths_path "${target_qtpaths_path_without_version}") - endif() - endif() + list(JOIN candidate_paths "\n " candidate_paths_joined) - if(NOT target_qtpaths_path) - message(DEBUG "No qtpaths executable found for deployment purposes.") + if(NOT QT_NO_QTPATHS_DEPLOYMENT_WARNING AND NOT target_qtpaths_path) + message(WARNING + "No qtpaths executable found for deployment purposes. Candidates searched: \n " + "${candidate_paths_joined}" + ) endif() file(GENERATE OUTPUT "${QT_DEPLOY_SUPPORT}" CONTENT diff --git a/src/corelib/animation/qabstractanimation.cpp b/src/corelib/animation/qabstractanimation.cpp index 17814c6756a..c3e1ba4010f 100644 --- a/src/corelib/animation/qabstractanimation.cpp +++ b/src/corelib/animation/qabstractanimation.cpp @@ -113,6 +113,10 @@ #include "qabstractanimation_p.h" +#if defined(Q_OS_WASM) +#include <QtCore/private/qwasmanimationdriver_p.h> +#endif + #include <QtCore/qmath.h> #include <QtCore/qcoreevent.h> #include <QtCore/qpointer.h> diff --git a/src/corelib/animation/qabstractanimation_p.h b/src/corelib/animation/qabstractanimation_p.h index b4f462071a7..1eaa475f613 100644 --- a/src/corelib/animation/qabstractanimation_p.h +++ b/src/corelib/animation/qabstractanimation_p.h @@ -23,6 +23,10 @@ #include <private/qproperty_p.h> #include <qabstractanimation.h> +#if defined(Q_OS_WASM) +#include <QtCore/private/qwasmanimationdriver_p.h> +#endif + QT_REQUIRE_CONFIG(animation); QT_BEGIN_NAMESPACE @@ -184,7 +188,11 @@ private: friend class QAnimationDriver; QAnimationDriver *driver; +#if defined(Q_OS_WASM) + QWasmAnimationDriver defaultDriver; +#else QDefaultAnimationDriver defaultDriver; +#endif QBasicTimer pauseTimer; diff --git a/src/corelib/doc/qtcore.qdocconf b/src/corelib/doc/qtcore.qdocconf index d2b386373a0..b3e4e9d30a9 100644 --- a/src/corelib/doc/qtcore.qdocconf +++ b/src/corelib/doc/qtcore.qdocconf @@ -21,7 +21,7 @@ qhp.QtCore.virtualFolder = qtcore qhp.QtCore.indexTitle = Qt Core qhp.QtCore.indexRoot = -qhp.QtCore.subprojects = manual classes +qhp.QtCore.subprojects = manual examples classes qhp.QtCore.subprojects.manual.title = Qt Core qhp.QtCore.subprojects.manual.indexTitle = Qt Core module topics qhp.QtCore.subprojects.manual.type = manual @@ -31,6 +31,11 @@ qhp.QtCore.subprojects.classes.indexTitle = Qt Core C++ Classes qhp.QtCore.subprojects.classes.selectors = class fake:headerfile qhp.QtCore.subprojects.classes.sortPages = true +qhp.QtCore.subprojects.examples.title = Examples +qhp.QtCore.subprojects.examples.indexTitle = Qt Core Examples +qhp.QtCore.subprojects.examples.selectors = example +qhp.QtCore.subprojects.examples.sortPages = true + tagfile = ../../../doc/qtcore/qtcore.tags # Make QtCore depend on all doc modules; this ensures complete inheritance diff --git a/src/corelib/doc/snippets/CMakeLists.txt b/src/corelib/doc/snippets/CMakeLists.txt index 5521bf1f651..55db84ccbea 100644 --- a/src/corelib/doc/snippets/CMakeLists.txt +++ b/src/corelib/doc/snippets/CMakeLists.txt @@ -66,6 +66,7 @@ if(QT_FEATURE_gui) buffer/buffer.cpp qdebug/qdebugsnippet.cpp ) + add_subdirectory(eventfilters) endif() set_target_properties(corelib_snippets PROPERTIES COMPILE_OPTIONS "-w") @@ -76,7 +77,6 @@ endif() set_target_properties(corelib_snippets PROPERTIES UNITY_BUILD OFF) -add_subdirectory(eventfilters) add_subdirectory(qmetaobject-invokable) add_subdirectory(qmetaobject-revision) if(QT_FEATURE_process) diff --git a/src/corelib/doc/src/cmake/qt_add_android_dynamic_feature_java_source_dirs.qdoc b/src/corelib/doc/src/cmake/qt_add_android_dynamic_feature_java_source_dirs.qdoc new file mode 100644 index 00000000000..cf670110cab --- /dev/null +++ b/src/corelib/doc/src/cmake/qt_add_android_dynamic_feature_java_source_dirs.qdoc @@ -0,0 +1,30 @@ +// Copyright (C) 2024 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GFDL-1.3-no-invariants-only + +/*! +\page qt_add_android_dynamic_feature_java_source_dirs.html +\ingroup cmake-commands-qtcore + +\title qt_add_android_dynamic_feature_java_source_dirs +\keyword qt6_add_android_dynamic_feature_java_source_dirs + +\summary {Adds Java source directories to a dynamic feature build.} + +\include cmake-find-package-core.qdocinc + +\cmakecommandsince 6.11 + +\section1 Synopsis + +\badcode +qt_add_android_dynamic_feature_java_source_dirs(target [SOURCE_DIRS <directory1> <directory2> ...]) +\endcode + +\versionlessCMakeCommandsNote qt6_add_android_dynamic_feature_java_source_dirs() + +\section1 Description + +The command adds extra Java/Kotlin source directories to the \c {target} +executable when building the executable app with dynamic feature functionality. +To be used in conjunction with qt6_add_android_dynamic_features(). +*/ diff --git a/src/corelib/doc/src/qtcore.qdoc b/src/corelib/doc/src/qtcore.qdoc index ec5fa564639..fbcd02aeea5 100644 --- a/src/corelib/doc/src/qtcore.qdoc +++ b/src/corelib/doc/src/qtcore.qdoc @@ -31,3 +31,12 @@ target_link_libraries(mytarget PRIVATE Qt6::CorePrivate) \endcode */ + +/*! + \group corelib_examples + \title Qt Core Examples + + \brief Examples for the Qt Core. + + To learn how to use features of the Qt Core module, see examples: +*/ diff --git a/src/corelib/io/qfile.cpp b/src/corelib/io/qfile.cpp index e1fc043a0ff..0184fd838aa 100644 --- a/src/corelib/io/qfile.cpp +++ b/src/corelib/io/qfile.cpp @@ -592,6 +592,10 @@ QFile::rename(const QString &newName) return false; } + // Keep engine for target alive during the operation + // FIXME: Involve the target engine in the operation + auto targetEngine = QFileSystemEngine::createLegacyEngine(newName); + // If the file exists and it is a case-changing rename ("foo" -> "Foo"), // compare Ids to make sure it really is a different file. // Note: this does not take file engines into account. @@ -738,6 +742,11 @@ QFile::link(const QString &linkName) qWarning("QFile::link: Empty or null file name"); return false; } + + // Keep engine for target alive during the operation + // FIXME: Involve the target engine in the operation + auto targetEngine = QFileSystemEngine::createLegacyEngine(linkName); + QFileInfo fi(linkName); if (d->engine()->link(fi.absoluteFilePath())) { unsetError(); @@ -771,6 +780,10 @@ bool QFilePrivate::copy(const QString &newName) Q_ASSERT(error == QFile::NoError); Q_ASSERT(!q->isOpen()); + // Keep engine for target alive during the operation + // FIXME: Involve the target engine in the operation + auto targetEngine = QFileSystemEngine::createLegacyEngine(newName); + // Some file engines can perform this copy more efficiently (e.g., Windows // calling CopyFile). if (engine()->copy(newName)) diff --git a/src/corelib/io/qfilesystemengine.cpp b/src/corelib/io/qfilesystemengine.cpp index 03da2331e05..46d4cb709e2 100644 --- a/src/corelib/io/qfilesystemengine.cpp +++ b/src/corelib/io/qfilesystemengine.cpp @@ -190,6 +190,14 @@ QFileSystemEngine::createLegacyEngine(QFileSystemEntry &entry, QFileSystemMetaDa return engine; } +std::unique_ptr<QAbstractFileEngine> +QFileSystemEngine::createLegacyEngine(const QString &fileName) +{ + QFileSystemEntry entry(fileName); + QFileSystemMetaData metaData; + return createLegacyEngine(entry, metaData); +} + //static QString QFileSystemEngine::resolveUserName(const QFileSystemEntry &entry, QFileSystemMetaData &metaData) { diff --git a/src/corelib/io/qfilesystemengine_p.h b/src/corelib/io/qfilesystemengine_p.h index ee70ccc1e1b..46eeeda569e 100644 --- a/src/corelib/io/qfilesystemengine_p.h +++ b/src/corelib/io/qfilesystemengine_p.h @@ -161,6 +161,8 @@ public: static std::unique_ptr<QAbstractFileEngine> createLegacyEngine(QFileSystemEntry &entry, QFileSystemMetaData &data); + static std::unique_ptr<QAbstractFileEngine> + createLegacyEngine(const QString &fileName); private: static QString slowCanonicalized(const QString &path); diff --git a/src/corelib/io/qfsfileengine_p.h b/src/corelib/io/qfsfileengine_p.h index 2de6cb0cb73..8ad673bf0bf 100644 --- a/src/corelib/io/qfsfileengine_p.h +++ b/src/corelib/io/qfsfileengine_p.h @@ -82,7 +82,7 @@ public: bool setFileTime(const QDateTime &newDate, QFile::FileTime time) override; QDateTime fileTime(QFile::FileTime time) const override; void setFileName(const QString &file) override; - void setFileEntry(QFileSystemEntry &&entry); + virtual void setFileEntry(QFileSystemEntry &&entry); int handle() const override; #ifndef QT_NO_FILESYSTEMITERATOR diff --git a/src/corelib/itemmodels/qrangemodel_impl.h b/src/corelib/itemmodels/qrangemodel_impl.h index 70552cbfe05..88bd6cf444e 100644 --- a/src/corelib/itemmodels/qrangemodel_impl.h +++ b/src/corelib/itemmodels/qrangemodel_impl.h @@ -28,7 +28,7 @@ #include <functional> #include <iterator> #include <type_traits> -#include <QtCore/q20type_traits.h> +#include <QtCore/qxptype_traits.h> #include <tuple> #include <QtCore/q23utility.h> @@ -562,35 +562,30 @@ namespace QRangeModelDetails } }; - template <typename P, typename R, typename = void> - struct protocol_parentRow : std::false_type {}; template <typename P, typename R> - struct protocol_parentRow<P, R, - std::void_t<decltype(std::declval<P&>().parentRow(std::declval<wrapped_t<R>&>()))>> - : std::true_type {}; + using protocol_parentRow_test = decltype(std::declval<P&>() + .parentRow(std::declval<QRangeModelDetails::wrapped_t<R>&>())); + template <typename P, typename R> + using protocol_parentRow = qxp::is_detected<protocol_parentRow_test, P, R>; - template <typename P, typename R, typename = void> - struct protocol_childRows : std::false_type {}; template <typename P, typename R> - struct protocol_childRows<P, R, - std::void_t<decltype(std::declval<P&>().childRows(std::declval<wrapped_t<R>&>()))>> - : std::true_type {}; + using protocol_childRows_test = decltype(std::declval<P&>() + .childRows(std::declval<QRangeModelDetails::wrapped_t<R>&>())); + template <typename P, typename R> + using protocol_childRows = qxp::is_detected<protocol_childRows_test, P, R>; - template <typename P, typename R, typename = void> - struct protocol_setParentRow : std::false_type {}; template <typename P, typename R> - struct protocol_setParentRow<P, R, - std::void_t<decltype(std::declval<P&>().setParentRow(std::declval<wrapped_t<R>&>(), - std::declval<wrapped_t<R>*>()))>> - : std::true_type {}; + using protocol_setParentRow_test = decltype(std::declval<P&>() + .setParentRow(std::declval<QRangeModelDetails::wrapped_t<R>&>(), + std::declval<QRangeModelDetails::wrapped_t<R>*>())); + template <typename P, typename R> + using protocol_setParentRow = qxp::is_detected<protocol_setParentRow_test, P, R>; - template <typename P, typename R, typename = void> - struct protocol_mutable_childRows : std::false_type {}; template <typename P, typename R> - struct protocol_mutable_childRows<P, R, - std::void_t<decltype(refTo(std::declval<P&>().childRows(std::declval<wrapped_t<R>&>())) - = {}) >> - : std::true_type {}; + using protocol_mutable_childRows_test = decltype(refTo(std::declval<P&>() + .childRows(std::declval<wrapped_t<R>&>())) = {}); + template <typename P, typename R> + using protocol_mutable_childRows = qxp::is_detected<protocol_mutable_childRows_test, P, R>; template <typename P, typename = void> struct protocol_newRow : std::false_type {}; diff --git a/src/corelib/kernel/qmetacontainer.cpp b/src/corelib/kernel/qmetacontainer.cpp index 4b4ea06d8b9..6173198a972 100644 --- a/src/corelib/kernel/qmetacontainer.cpp +++ b/src/corelib/kernel/qmetacontainer.cpp @@ -210,7 +210,7 @@ void QMetaContainer::destroyIterator(const void *iterator) const */ bool QMetaContainer::compareIterator(const void *i, const void *j) const { - return hasIterator() ? d_ptr->compareIteratorFn(i, j) : false; + return i == j || (hasIterator() && d_ptr->compareIteratorFn(i, j)); } /*! @@ -249,7 +249,7 @@ void QMetaContainer::advanceIterator(void *iterator, qsizetype step) const */ qsizetype QMetaContainer::diffIterator(const void *i, const void *j) const { - return hasIterator() ? d_ptr->diffIteratorFn(i, j) : 0; + return (i != j && hasIterator()) ? d_ptr->diffIteratorFn(i, j) : 0; } /*! @@ -327,7 +327,7 @@ void QMetaContainer::destroyConstIterator(const void *iterator) const */ bool QMetaContainer::compareConstIterator(const void *i, const void *j) const { - return hasConstIterator() ? d_ptr->compareConstIteratorFn(i, j) : false; + return i == j || (hasConstIterator() && d_ptr->compareConstIteratorFn(i, j)); } /*! @@ -366,7 +366,7 @@ void QMetaContainer::advanceConstIterator(void *iterator, qsizetype step) const */ qsizetype QMetaContainer::diffConstIterator(const void *i, const void *j) const { - return hasConstIterator() ? d_ptr->diffConstIteratorFn(i, j) : 0; + return (i != j && hasConstIterator()) ? d_ptr->diffConstIteratorFn(i, j) : 0; } QT_END_NAMESPACE diff --git a/src/corelib/kernel/qobject.cpp b/src/corelib/kernel/qobject.cpp index 02c9f00f301..607dc23f56c 100644 --- a/src/corelib/kernel/qobject.cpp +++ b/src/corelib/kernel/qobject.cpp @@ -2696,23 +2696,38 @@ static void err_method_notfound(const QObject *object, case QSIGNAL_CODE: type = "signal"; break; } const char *loc = extract_location(method); + const char *err; if (strchr(method, ')') == nullptr) // common typing mistake - qCWarning(lcConnect, "QObject::%s: Parentheses expected, %s %s::%s%s%s", func, type, - object->metaObject()->className(), method + 1, loc ? " in " : "", loc ? loc : ""); + err = "Parentheses expected,"; else - qCWarning(lcConnect, "QObject::%s: No such %s %s::%s%s%s", func, type, - object->metaObject()->className(), method + 1, loc ? " in " : "", loc ? loc : ""); + err = "No such"; + qCWarning(lcConnect, "QObject::%s: %s %s %s::%s%s%s", func, err, type, + object->metaObject()->className(), method + 1, loc ? " in " : "", loc ? loc : ""); +} + +enum class ConnectionEnd : bool { Sender, Receiver }; +Q_DECL_COLD_FUNCTION +static void err_info_about_object(const char *func, const QObject *o, ConnectionEnd end) +{ + if (!o) + return; + const QString name = o->objectName(); + if (name.isEmpty()) + return; + const bool sender = end == ConnectionEnd::Sender; + qCWarning(lcConnect, "QObject::%s: (%s name:%*s'%ls')", + func, + sender ? "sender" : "receiver", + sender ? 3 : 1, // ← length of generated whitespace + "", + qUtf16Printable(name)); } Q_DECL_COLD_FUNCTION static void err_info_about_objects(const char *func, const QObject *sender, const QObject *receiver) { - QString a = sender ? sender->objectName() : QString(); - QString b = receiver ? receiver->objectName() : QString(); - if (!a.isEmpty()) - qCWarning(lcConnect, "QObject::%s: (sender name: '%s')", func, a.toLocal8Bit().data()); - if (!b.isEmpty()) - qCWarning(lcConnect, "QObject::%s: (receiver name: '%s')", func, b.toLocal8Bit().data()); + err_info_about_object(func, sender, ConnectionEnd::Sender); + err_info_about_object(func, receiver, ConnectionEnd::Receiver); } Q_DECL_COLD_FUNCTION diff --git a/src/corelib/kernel/qobject_impl.h b/src/corelib/kernel/qobject_impl.h index b57d7e50ccd..34e6bd84f3f 100644 --- a/src/corelib/kernel/qobject_impl.h +++ b/src/corelib/kernel/qobject_impl.h @@ -1,8 +1,6 @@ // Copyright (C) 2016 The Qt Company Ltd. // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only -#ifndef Q_QDOC - #ifndef QOBJECT_H #error Do not include qobject_impl.h directly #endif @@ -41,5 +39,3 @@ namespace QtPrivate { QT_END_NAMESPACE - -#endif diff --git a/src/corelib/platform/darwin/qdarwinsecurityscopedfileengine.mm b/src/corelib/platform/darwin/qdarwinsecurityscopedfileengine.mm new file mode 100644 index 00000000000..cb38445f4fe --- /dev/null +++ b/src/corelib/platform/darwin/qdarwinsecurityscopedfileengine.mm @@ -0,0 +1,552 @@ +// Copyright (C) 2025 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only +// Qt-Security score:significant reason:default + +#include "qdarwinsecurityscopedfileengine_p.h" + +#include <QtCore/qloggingcategory.h> +#include <QtCore/qstandardpaths.h> +#include <QtCore/qreadwritelock.h> +#include <QtCore/qscopedvaluerollback.h> + +#include <QtCore/private/qcore_mac_p.h> +#include <QtCore/private/qfsfileengine_p.h> +#include <QtCore/private/qfilesystemengine_p.h> + +#include <thread> +#include <mutex> + +#include <CoreFoundation/CoreFoundation.h> +#include <Foundation/NSURL.h> + +QT_BEGIN_NAMESPACE + +using namespace Qt::StringLiterals; + +Q_STATIC_LOGGING_CATEGORY(lcSecEngine, "qt.core.io.security-scoped-fileengine", QtCriticalMsg) + +template<typename T> class BackgroundLoader; + +/* + File engine handler for security scoped file paths. + + Installs itself as soon as QtCore is loaded if the application + is sandboxed (optionally on macOS, and always on iOS and friends). +*/ +class SecurityScopedFileEngineHandler : public QAbstractFileEngineHandler +{ +public: + SecurityScopedFileEngineHandler(); + ~SecurityScopedFileEngineHandler(); + + void registerPossiblySecurityScopedURL(NSURL *url); + + std::unique_ptr<QAbstractFileEngine> create(const QString &fileName) const override; + + static BackgroundLoader<SecurityScopedFileEngineHandler>& get(); + +private: + Q_DISABLE_COPY_MOVE(SecurityScopedFileEngineHandler) + + void saveBookmark(NSURL *url); + void saveBookmarks(); + + NSURL *bookmarksFile() const; + + static NSString *cacheKeyForUrl(NSURL *url); + static NSString *cacheKeyForPath(const QString &url); + + NSMutableDictionary *m_bookmarks = nullptr; + mutable QReadWriteLock m_bookmarkLock; + + friend class SecurityScopedFileEngine; +}; + +/* + Helper class for asynchronous instantiation of types. +*/ +template<typename T> +class BackgroundLoader +{ +public: + explicit BackgroundLoader(bool shouldLoad) { + if (shouldLoad) { + m_thread = std::thread([this]() { + m_instance = std::make_unique<T>(); + }); + } + } + + ~BackgroundLoader() + { + std::scoped_lock lock(m_mutex); + if (m_thread.joinable()) + m_thread.join(); + } + + T* operator->() const + { + std::scoped_lock lock(m_mutex); + if (m_thread.joinable()) + m_thread.join(); + return m_instance.get(); + } + + explicit operator bool() const + { + std::scoped_lock lock(m_mutex); + return m_thread.joinable() || m_instance; + } + +private: + mutable std::mutex m_mutex; + mutable std::thread m_thread; + std::unique_ptr<T> m_instance; +}; + +/* + Thread-safe background-loading of optional security scoped handler, + with the ability to kick off instantiation early during program load. +*/ +BackgroundLoader<SecurityScopedFileEngineHandler>& SecurityScopedFileEngineHandler::get() +{ + using Handler = BackgroundLoader<SecurityScopedFileEngineHandler>; + static Handler handler = []() -> Handler { + if (!qt_apple_isSandboxed()) + return Handler{false}; + + qCInfo(lcSecEngine) << "Application sandbox is active. Registering security-scoped file engine."; + return Handler{true}; + }(); + return handler; +} + +static void initializeSecurityScopedFileEngineHandler() +{ + // Kick off loading of bookmarks early in the background + std::ignore = SecurityScopedFileEngineHandler::get(); +} +Q_CONSTRUCTOR_FUNCTION(initializeSecurityScopedFileEngineHandler); + +/* + Registration function for possibly security scoped URLs. + + Entry points that might provide security scoped URLs such as file + dialogs or drag-and-drop should use this function to ensure that + the security scoped file engine handler knows about the URL. +*/ +QUrl qt_apple_urlFromPossiblySecurityScopedURL(NSURL *url) +{ + if (auto &handler = SecurityScopedFileEngineHandler::get()) + handler->registerPossiblySecurityScopedURL(url); + + // Note: The URL itself doesn't encode any of the bookmark data, + // neither in the scheme or as fragments or query parameters, + // as it's all handled by the bookmark cache in the file engine. + return QUrl(QString::fromNSString(url.absoluteString) + .normalized(QString::NormalizationForm_C)); +} + +static bool checkIfResourceIsReachable(NSURL *url) +{ + NSError *error = nullptr; + if ([url checkResourceIsReachableAndReturnError:&error]) + return true; + + // Our goal is to check whether the file exists or not, and if + // not, defer creating a bookmark for it. If we get any other + // error we want to know. + if (![error.domain isEqualToString:NSCocoaErrorDomain] || error.code != NSFileReadNoSuchFileError) { + qCWarning(lcSecEngine) << "Unexpected" << error + << "when resolving reachability for" << url; + } + + return false; +} + +/* + File engine for maintaining access lifetime of security-scoped + resources on sandboxed Apple platforms. + + Note that there isn't necessarily a 1:1 relationship between + the file being operated on by the QFSFileEngine and the security + scoped resource that allows access to it, for example in the + case of a folder giving access to all files (and sub-folders) + within it. +*/ +class SecurityScopedFileEngine : public QFSFileEngine +{ + Q_DECLARE_PRIVATE(QFSFileEngine) +public: + SecurityScopedFileEngine(const QString &fileName, NSURL *securityScopedUrl) + : QFSFileEngine(fileName) + , m_securityScopedUrl([securityScopedUrl retain]) + { + startAccessingSecurityScopedResource(); + } + + ~SecurityScopedFileEngine() + { + stopAccessingSecurityScopedResource(); + [m_securityScopedUrl release]; + } + + void setFileName(const QString &fileName) override + { + QFileSystemEntry entry(fileName); + setFileEntry(std::move(entry)); + } + + void setFileEntry(QFileSystemEntry &&entry) override + { + // We can't rely on the new entry being accessible under the same + // security scope as the original path, or even that the new path + // is a security scoped resource, so stop access here, and start + // access for the new resource below if needed. + stopAccessingSecurityScopedResource(); + [m_securityScopedUrl release]; + m_securityScopedUrl = nil; + + const QString fileName = entry.filePath(); + QFSFileEngine::setFileEntry(std::move(entry)); + + // The new path may not be a security scoped resource, but if it is + // we need to establish access to it. The only way to do that is to + // actually create an engine for it, including resolving bookmarks. + auto newEngine = SecurityScopedFileEngineHandler::get()->create(fileName); + if (auto *engine = dynamic_cast<SecurityScopedFileEngine*>(newEngine.get())) { + m_securityScopedUrl = [engine->m_securityScopedUrl retain]; + startAccessingSecurityScopedResource(); + } + } + +private: + void startAccessingSecurityScopedResource() + { + if ([m_securityScopedUrl startAccessingSecurityScopedResource]) { + qCDebug(lcSecEngine) << "Started accessing" << m_securityScopedUrl.path + << "on behalf of" << fileName(DefaultName); + + m_securityScopeWasReachable = securityScopeIsReachable(); + } else { + qCWarning(lcSecEngine) << "Unexpectedly using security scoped" + << "file engine for" << m_securityScopedUrl.path + << "on behalf of" << fileName(DefaultName) + << "without needing scoped access"; + } + } + + void stopAccessingSecurityScopedResource() + { + if (!m_securityScopeWasReachable && securityScopeIsReachable()) { + // The security scoped URL didn't exist when we first started + // accessing it, but it does now, so persist a bookmark for it. + qCDebug(lcSecEngine) << "Security scoped resource has been created. Saving bookmark."; + SecurityScopedFileEngineHandler::get()->saveBookmark(m_securityScopedUrl); + } + + // Note: Stopping access is a no-op if we didn't have access + [m_securityScopedUrl stopAccessingSecurityScopedResource]; + qCDebug(lcSecEngine) << "Stopped accessing" << m_securityScopedUrl.path + << "on behalf of" << fileName(DefaultName); + } + + bool securityScopeIsReachable() const + { + return checkIfResourceIsReachable(m_securityScopedUrl); + } + + // See note above about relationship to fileName + NSURL *m_securityScopedUrl = nullptr; + bool m_securityScopeWasReachable = false; +}; + +// ---------------------------------------------------------------------- + +SecurityScopedFileEngineHandler::SecurityScopedFileEngineHandler() +{ + QMacAutoReleasePool pool; + + NSURL *savedBookmarks = bookmarksFile(); + if ([NSFileManager.defaultManager fileExistsAtPath:savedBookmarks.path]) { + NSError *error = nullptr; + m_bookmarks = [[NSDictionary dictionaryWithContentsOfURL:savedBookmarks + error:&error] mutableCopy]; + + if (error) { + qCWarning(lcSecEngine) << "Failed to load bookmarks from" + << savedBookmarks << ":" << error; + } else { + qCInfo(lcSecEngine) << "Loaded existing bookmarks for" << m_bookmarks.allKeys; + } + } + + if (!m_bookmarks) + m_bookmarks = [NSMutableDictionary new]; +} + +SecurityScopedFileEngineHandler::~SecurityScopedFileEngineHandler() +{ + [m_bookmarks release]; +} + +void SecurityScopedFileEngineHandler::registerPossiblySecurityScopedURL(NSURL *url) +{ + QMacAutoReleasePool pool; + + // Start accessing the resource, to check if it's security scoped, + // and allow us to create a bookmark for it on both macOS and iOS. + if (![url startAccessingSecurityScopedResource]) + return; // All good, not security scoped + + if (checkIfResourceIsReachable(url)) { + // We can access the resource, which means it exists, so we can + // create a persistent bookmark for it right away. We want to do + // this as soon as possible, so that if the app is terminated the + // user can continue working on the file without the app needing + // to ask for access again via a file dialog. + saveBookmark(url); + } else { + // The file isn't accessible, likely because it doesn't exist. + // As we can only create security scoped bookmarks for files + // that exist we store the URL itself for now, and save it to + // a bookmark later when we detect that the file has been created. + qCInfo(lcSecEngine) << "Resource is not reachable." + << "Registering URL" << url << "instead"; + QWriteLocker locker(&m_bookmarkLock); + m_bookmarks[cacheKeyForUrl(url)] = url; + } + + // Balance access from above + [url stopAccessingSecurityScopedResource]; + +#if defined(Q_OS_MACOS) + // On macOS, unlike iOS, URLs from file dialogs, etc, come with implicit + // access already, and we are expected to balance this access with an + // explicit stopAccessingSecurityScopedResource. We release the last + // access here to unify the behavior between macOS and iOS, and then + // leave it up to the SecurityScopedFileEngine to regain access, where + // we know the lifetime of resource use, and when to release access. + [url stopAccessingSecurityScopedResource]; +#endif +} + +std::unique_ptr<QAbstractFileEngine> SecurityScopedFileEngineHandler::create(const QString &fileName) const +{ + QMacAutoReleasePool pool; + + static thread_local bool recursionGuard = false; + if (recursionGuard) + return nullptr; + + if (fileName.isEmpty()) + return nullptr; + + QFileSystemEntry fileSystemEntry(fileName); + QFileSystemMetaData metaData; + + { + // Check if there's another engine that claims to handle the given file name. + // This covers non-QFSFileEngines like QTemporaryFileEngine, and QResourceFileEngine. + // If there isn't one, we'll get nullptr back, and know that we can access the + // file via our special QFSFileEngine. + QScopedValueRollback<bool> rollback(recursionGuard, true); + if (auto engine = QFileSystemEngine::createLegacyEngine(fileSystemEntry, metaData)) { + // Shortcut the logic of the createLegacyEngine call we're in by + // just returning this engine now. + qCDebug(lcSecEngine) << "Preferring non-QFSFileEngine engine" + << engine.get() << "for" << fileName; + return engine; + } + } + + // We're mapping the file name to existing bookmarks below, so make sure + // we use as close as we can get to the canonical path. For files that + // do not exist we fall back to the cleaned absolute path. + auto canonicalEntry = QFileSystemEngine::canonicalName(fileSystemEntry, metaData); + if (canonicalEntry.isEmpty()) + canonicalEntry = QFileSystemEngine::absoluteName(fileSystemEntry); + + if (canonicalEntry.isRelative()) { + // We try to map relative paths to absolute above, but doing so requires + // knowledge of the current working directory, which we only have if the + // working directory has already started access through other means. We + // can't explicitly start access of the working directory here, as doing + // so requires its name, which we can't get from getcwd() without access. + // Fortunately all of the entry points of security scoped URLs such as + // file dialogs or drag-and-drop give us absolute paths, and APIs like + // QDir::filePath() will construct absolute URLs without needing the + // current working directory. + qCWarning(lcSecEngine) << "Could not resolve" << fileSystemEntry.filePath() + << "against current working working directory"; + return nullptr; + } + + // Clean the path as well, to remove any trailing slashes for directories + QString filePath = QDir::cleanPath(canonicalEntry.filePath()); + + // Files inside the sandbox container can always be accessed directly + static const QString sandboxRoot = QString::fromNSString(NSHomeDirectory()); + if (filePath.startsWith(sandboxRoot)) + return nullptr; + + // The same applies to files inside the application's own bundle + static const QString bundleRoot = QString::fromNSString(NSBundle.mainBundle.bundlePath); + if (filePath.startsWith(bundleRoot)) + return nullptr; + + qCDebug(lcSecEngine) << "Looking up bookmark for" << filePath << "based on incoming fileName" << fileName; + + // Check if we have a persisted bookmark for this fileName, or + // any of its containing directories (which will give us access + // to the file). + QReadLocker locker(&m_bookmarkLock); + auto *cacheKey = cacheKeyForPath(filePath); + NSObject *bookmarkData = nullptr; + while (cacheKey.length > 1) { + bookmarkData = m_bookmarks[cacheKey]; + if (bookmarkData) + break; + cacheKey = [cacheKey stringByDeletingLastPathComponent]; + } + + // We didn't find a bookmark, so there's no point in trying to manage + // this file via a SecurityScopedFileEngine. + if (!bookmarkData) { + qCDebug(lcSecEngine) << "No bookmark found. Falling back to QFSFileEngine."; + return nullptr; + } + + NSURL *securityScopedUrl = nullptr; + if ([bookmarkData isKindOfClass:NSURL.class]) { + securityScopedUrl = static_cast<NSURL*>(bookmarkData); + } else { + NSError *error = nullptr; + BOOL bookmarkDataIsStale = NO; + securityScopedUrl = [NSURL URLByResolvingBookmarkData:static_cast<NSData*>(bookmarkData) + options: + #if defined(Q_OS_MACOS) + NSURLBookmarkResolutionWithSecurityScope + #else + // iOS bookmarks are always security scoped, and we + // don't need or want any of the other options. + NSURLBookmarkResolutionOptions(0) + #endif + relativeToURL:nil /* app-scoped bookmark */ + bookmarkDataIsStale:&bookmarkDataIsStale + error:&error]; + + if (!securityScopedUrl || error) { + qCWarning(lcSecEngine) << "Failed to resolve bookmark data for" + << fileName << ":" << error; + return nullptr; + } + + if (bookmarkDataIsStale) { + // This occurs when for example the file has been renamed, moved, + // or deleted. Normally this would be the place to update the + // bookmark to point to the new location, but Qt clients may not + // be prepared for QFiles changing their file-names under their + // feet so we treat it as a missing file. + qCDebug(lcSecEngine) << "Bookmark for" << cacheKey << "was stale"; + locker.unlock(); + QWriteLocker writeLocker(&m_bookmarkLock); + [m_bookmarks removeObjectForKey:cacheKey]; + auto *mutableThis = const_cast<SecurityScopedFileEngineHandler*>(this); + mutableThis->saveBookmarks(); + return nullptr; + } + } + + qCInfo(lcSecEngine) << "Resolved security scope" << securityScopedUrl + << "for path" << filePath; + return std::make_unique<SecurityScopedFileEngine>(fileName, securityScopedUrl); +} + +/* + Create an app-scoped bookmark, and store it in our persistent cache. + + We do this so that the user can continue accessing the file even after + application restarts. + + Storing the bookmarks to disk (inside the sandbox) is safe, as only the + app that created the app-scoped bookmarks can obtain access to the file + system resource that the URL points to. Specifically, a bookmark created + with security scope fails to resolve if the caller does not have the same + code signing identity as the caller that created the bookmark. +*/ +void SecurityScopedFileEngineHandler::saveBookmark(NSURL *url) +{ + NSError *error = nullptr; + NSData *bookmarkData = [url bookmarkDataWithOptions: + #if defined(Q_OS_MACOS) + NSURLBookmarkCreationWithSecurityScope + #else + // iOS bookmarks are always security scoped, and we + // don't need or want any of the other options. + NSURLBookmarkCreationOptions(0) + #endif + includingResourceValuesForKeys:nil + relativeToURL:nil /* app-scoped bookmark */ + error:&error]; + + if (bookmarkData) { + QWriteLocker locker(&m_bookmarkLock); + NSString *cacheKey = cacheKeyForUrl(url); + qCInfo(lcSecEngine) + << (m_bookmarks[cacheKey] ? "Updating" : "Registering") + << "bookmark for" << cacheKey; + m_bookmarks[cacheKey] = bookmarkData; + saveBookmarks(); + } else { + qCWarning(lcSecEngine) << "Failed to create bookmark data for" << url << error; + } +} + +/* + Saves the bookmarks cache to disk. + + We do this preemptively whenever we create a bookmark, to ensure + the file can be accessed later on even if the app crashes. +*/ +void SecurityScopedFileEngineHandler::saveBookmarks() +{ + QMacAutoReleasePool pool; + + NSError *error = nullptr; + NSURL *bookmarksFilePath = bookmarksFile(); + [NSFileManager.defaultManager + createDirectoryAtURL:[bookmarksFilePath URLByDeletingLastPathComponent] + withIntermediateDirectories:YES attributes:nil error:&error]; + if (error) { + qCWarning(lcSecEngine) << "Failed to create bookmarks path:" << error; + return; + } + [m_bookmarks writeToURL:bookmarksFile() error:&error]; + if (error) { + qCWarning(lcSecEngine) << "Failed to save bookmarks to" + << bookmarksFile() << ":" << error; + } +} + +NSURL *SecurityScopedFileEngineHandler::bookmarksFile() const +{ + NSURL *appSupportDir = [[NSFileManager.defaultManager URLsForDirectory: + NSApplicationSupportDirectory inDomains:NSUserDomainMask] firstObject]; + return [appSupportDir URLByAppendingPathComponent:@"SecurityScopedBookmarks.plist"]; +} + +NSString *SecurityScopedFileEngineHandler::cacheKeyForUrl(NSURL *url) +{ + return cacheKeyForPath(QString::fromNSString(url.path)); +} + +NSString *SecurityScopedFileEngineHandler::cacheKeyForPath(const QString &path) +{ + auto normalized = path.normalized(QString::NormalizationForm_D); + // We assume the file paths we get via file dialogs and similar + // are already canonical, but clean it just in case. + return QDir::cleanPath(normalized).toNSString(); +} + +QT_END_NAMESPACE diff --git a/src/corelib/platform/darwin/qdarwinsecurityscopedfileengine_p.h b/src/corelib/platform/darwin/qdarwinsecurityscopedfileengine_p.h new file mode 100644 index 00000000000..f6098fa977d --- /dev/null +++ b/src/corelib/platform/darwin/qdarwinsecurityscopedfileengine_p.h @@ -0,0 +1,29 @@ +// Copyright (C) 2025 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only +// Qt-Security score:significant reason:default + +#ifndef QDARWINSECURITYSCOPEDFILEENGINE_H +#define QDARWINSECURITYSCOPEDFILEENGINE_H + +// +// W A R N I N G +// ------------- +// +// This file is part of the QPA API and is not meant to be used +// in applications. Usage of this API may make your code +// source and binary incompatible with future versions of Qt. +// +// We mean it. +// + +#include <QtCore/qurl.h> + +Q_FORWARD_DECLARE_OBJC_CLASS(NSURL); + +QT_BEGIN_NAMESPACE + +Q_CORE_EXPORT QUrl qt_apple_urlFromPossiblySecurityScopedURL(NSURL *url); + +QT_END_NAMESPACE + +#endif // QDARWINSECURITYSCOPEDFILEENGINE_H diff --git a/src/corelib/platform/wasm/qstdweb.cpp b/src/corelib/platform/wasm/qstdweb.cpp index 287138bb915..4f3ecc4c6d9 100644 --- a/src/corelib/platform/wasm/qstdweb.cpp +++ b/src/corelib/platform/wasm/qstdweb.cpp @@ -178,12 +178,17 @@ Blob Blob::slice(uint32_t begin, uint32_t end) const ArrayBuffer Blob::arrayBuffer_sync() const { emscripten::val buffer; - uint32_t handlerIndex = qstdweb::Promise::make(m_blob, QStringLiteral("arrayBuffer"), { - .thenFunc = [&buffer](emscripten::val arrayBuffer) { - buffer = arrayBuffer; + QList<uint32_t> handlers; + qstdweb::Promise::make( + handlers, + m_blob, + QStringLiteral("arrayBuffer"), + { + .thenFunc = [&buffer](emscripten::val arrayBuffer) { + buffer = arrayBuffer; } }); - Promise::suspendExclusive(handlerIndex); + Promise::suspendExclusive(handlers); return ArrayBuffer(buffer); } @@ -441,7 +446,7 @@ EventCallback::EventCallback(emscripten::val element, const std::string &name, } -uint32_t Promise::adoptPromise(emscripten::val promise, PromiseCallbacks callbacks) +uint32_t Promise::adoptPromise(emscripten::val promise, PromiseCallbacks callbacks, QList<uint32_t> *handlers) { Q_ASSERT_X(!!callbacks.catchFunc || !!callbacks.finallyFunc || !!callbacks.thenFunc, "Promise::adoptPromise", "must provide at least one callback function"); @@ -498,14 +503,21 @@ uint32_t Promise::adoptPromise(emscripten::val promise, PromiseCallbacks callbac promise = promise.call<emscripten::val>("finally", suspendResume->jsEventHandlerAt(*finallyIndex)); + if (handlers) { + if (thenIndex) + handlers->push_back(*thenIndex); + if (catchIndex) + handlers->push_back(*catchIndex); + handlers->push_back(*finallyIndex); + } return *finallyIndex; } -void Promise::suspendExclusive(uint32_t handlerIndex) +void Promise::suspendExclusive(QList<uint32_t> handlerIndices) { QWasmSuspendResumeControl *suspendResume = QWasmSuspendResumeControl::get(); Q_ASSERT(suspendResume); - suspendResume->suspendExclusive(handlerIndex); + suspendResume->suspendExclusive(handlerIndices); suspendResume->sendPendingEvents(); } @@ -657,11 +669,12 @@ void FileSystemWritableFileStreamIODevice::close() return; } - uint32_t handlerIndex = Promise::make(m_stream.val(), QStringLiteral("close"), { + QList<uint32_t> handlers; + Promise::make(handlers, m_stream.val(), QStringLiteral("close"), { .thenFunc = [](emscripten::val) { } }); - Promise::suspendExclusive(handlerIndex); + Promise::suspendExclusive(handlers); QIODevice::close(); } @@ -683,14 +696,15 @@ bool FileSystemWritableFileStreamIODevice::seek(qint64 pos) emscripten::val seekParams = emscripten::val::object(); seekParams.set("type", std::string("seek")); seekParams.set("position", static_cast<double>(pos)); - uint32_t handlerIndex = Promise::make(m_stream.val(), QStringLiteral("write"), { + QList<uint32_t> handlers; + Promise::make(handlers, m_stream.val(), QStringLiteral("write"), { .thenFunc = [&success](emscripten::val) { success = true; }, .catchFunc = [](emscripten::val) { } }, seekParams); - Promise::suspendExclusive(handlerIndex); + Promise::suspendExclusive(handlers); if (!success) return false; @@ -708,14 +722,15 @@ qint64 FileSystemWritableFileStreamIODevice::writeData(const char *data, qint64 bool success = false; Uint8Array array = Uint8Array::copyFrom(data, size); - uint32_t handlerIndex = Promise::make(m_stream.val(), QStringLiteral("write"), { + QList<uint32_t> handlers; + Promise::make(handlers, m_stream.val(), QStringLiteral("write"), { .thenFunc = [&success](emscripten::val) { success = true; }, .catchFunc = [](emscripten::val) { } }, array.val()); - Promise::suspendExclusive(handlerIndex); + Promise::suspendExclusive(handlers); if (success) { qint64 newPos = pos() + size; @@ -770,7 +785,8 @@ bool FileSystemFileIODevice::open(QIODevice::OpenMode mode) File file; bool success = false; - uint32_t handlerIndex = Promise::make(m_fileHandle.val(), QStringLiteral("getFile"), { + QList<uint32_t> handlers; + Promise::make(handlers, m_fileHandle.val(), QStringLiteral("getFile"), { .thenFunc = [&file, &success](emscripten::val fileVal) { file = File(fileVal); success = true; @@ -778,7 +794,7 @@ bool FileSystemFileIODevice::open(QIODevice::OpenMode mode) .catchFunc = [](emscripten::val) { } }); - Promise::suspendExclusive(handlerIndex); + Promise::suspendExclusive(handlers); if (success) { m_blobDevice = std::make_unique<BlobIODevice>(file.slice(0, file.size())); @@ -796,7 +812,8 @@ bool FileSystemFileIODevice::open(QIODevice::OpenMode mode) FileSystemWritableFileStream writableStream; bool success = false; - uint32_t handlerIndex = Promise::make(m_fileHandle.val(), QStringLiteral("createWritable"), { + QList<uint32_t> handlers; + Promise::make(handlers, m_fileHandle.val(), QStringLiteral("createWritable"), { .thenFunc = [&writableStream, &success](emscripten::val writable) { writableStream = FileSystemWritableFileStream(writable); success = true; @@ -804,7 +821,7 @@ bool FileSystemFileIODevice::open(QIODevice::OpenMode mode) .catchFunc = [](emscripten::val) { } }); - Promise::suspendExclusive(handlerIndex); + Promise::suspendExclusive(handlers); if (success) { m_writableDevice = std::make_unique<FileSystemWritableFileStreamIODevice>(writableStream); diff --git a/src/corelib/platform/wasm/qstdweb_p.h b/src/corelib/platform/wasm/qstdweb_p.h index 9a97370448e..b14d9e4012f 100644 --- a/src/corelib/platform/wasm/qstdweb_p.h +++ b/src/corelib/platform/wasm/qstdweb_p.h @@ -238,7 +238,7 @@ namespace qstdweb { }; namespace Promise { - uint32_t Q_CORE_EXPORT adoptPromise(emscripten::val promise, PromiseCallbacks callbacks); + uint32_t Q_CORE_EXPORT adoptPromise(emscripten::val promise, PromiseCallbacks callbacks, QList<uint32_t> *handlers = nullptr); template<typename... Args> uint32_t make(emscripten::val target, @@ -255,7 +255,24 @@ namespace qstdweb { return adoptPromise(std::move(promiseObject), std::move(callbacks)); } - void Q_CORE_EXPORT suspendExclusive(uint32_t handlerIndex); + template<typename... Args> + void make( + QList<uint32_t> &handlers, + emscripten::val target, + QString methodName, + PromiseCallbacks callbacks, + Args... args) + { + emscripten::val promiseObject = target.call<emscripten::val>( + methodName.toStdString().c_str(), std::forward<Args>(args)...); + if (promiseObject.isUndefined() || promiseObject["constructor"]["name"].as<std::string>() != "Promise") { + qFatal("This function did not return a promise"); + } + + adoptPromise(std::move(promiseObject), std::move(callbacks), &handlers); + } + + void Q_CORE_EXPORT suspendExclusive(QList<uint32_t> handlerIndices); void Q_CORE_EXPORT all(std::vector<emscripten::val> promises, PromiseCallbacks callbacks); }; diff --git a/src/corelib/platform/wasm/qwasmanimationdriver.cpp b/src/corelib/platform/wasm/qwasmanimationdriver.cpp new file mode 100644 index 00000000000..ab0c8240dd1 --- /dev/null +++ b/src/corelib/platform/wasm/qwasmanimationdriver.cpp @@ -0,0 +1,129 @@ +// Copyright (C) 2025 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#include "qwasmanimationdriver_p.h" +#include "qwasmsuspendresumecontrol_p.h" + +#include <emscripten/val.h> + +QT_BEGIN_NAMESPACE + + +// QWasmAnimationDriver drives animations using requestAnimationFrame(). This +// ensures that animations are advanced in sync with frame update calls, which +// again are synced to the screen refresh rate. + +namespace { + constexpr int FallbackTimerInterval = 500; +} + +QWasmAnimationDriver::QWasmAnimationDriver(QUnifiedTimer *) + : QAnimationDriver(nullptr) +{ + connect(this, &QAnimationDriver::started, this, &QWasmAnimationDriver::start); + connect(this, &QAnimationDriver::stopped, this, &QWasmAnimationDriver::stop); +} + +QWasmAnimationDriver::~QWasmAnimationDriver() +{ + disconnect(this, &QAnimationDriver::started, this, &QWasmAnimationDriver::start); + disconnect(this, &QAnimationDriver::stopped, this, &QWasmAnimationDriver::stop); + + if (m_animateCallbackHandle != 0) + QWasmAnimationFrameMultiHandler::instance()->unregisterAnimateCallback(m_animateCallbackHandle); +} + +qint64 QWasmAnimationDriver::elapsed() const +{ + return isRunning() ? qint64(m_currentTimestamp - m_startTimestamp) : 0; +} + +double QWasmAnimationDriver::getCurrentTimeFromTimeline() const +{ + // Get the current timeline time, which is an equivalent time source to the + // animation frame time source. According to the documentation this API + // may be unavailable in various cases; check for null before accessing. + emscripten::val document = emscripten::val::global("document"); + emscripten::val timeline = document["timeline"]; + if (!timeline.isNull() && !timeline.isUndefined()) { + emscripten::val currentTime = timeline["currentTime"]; + if (!currentTime.isNull() && !currentTime.isUndefined()) + return currentTime.as<double>(); + } + return 0; +} + +void QWasmAnimationDriver::handleFallbackTimeout() +{ + if (!isRunning()) + return; + + // Get the current time from a timing source equivalent to the animation frame time + double currentTime = getCurrentTimeFromTimeline(); + if (currentTime == 0) + currentTime = m_currentTimestamp + FallbackTimerInterval; + const double timeSinceLastFrame = currentTime - m_currentTimestamp; + + // Advance animations if animations are active but there has been no rcent animation + // frame callback. + if (timeSinceLastFrame > FallbackTimerInterval * 0.8) { + m_currentTimestamp = currentTime; + advance(); + } +} + +void QWasmAnimationDriver::start() +{ + if (isRunning()) + return; + + // Set start timestamp to document.timeline.currentTime() + m_startTimestamp = getCurrentTimeFromTimeline(); + m_currentTimestamp = m_startTimestamp; + + // Register animate callback + m_animateCallbackHandle = QWasmAnimationFrameMultiHandler::instance()->registerAnimateCallback( + [this](double timestamp) { handleAnimationFrame(timestamp); }); + + // Start fallback timer to ensure animations advance even if animaton frame callbacks stop coming + fallbackTimer.setInterval(FallbackTimerInterval); + connect(&fallbackTimer, &QTimer::timeout, this, &QWasmAnimationDriver::handleFallbackTimeout); + fallbackTimer.start(); + + QAnimationDriver::start(); +} + +void QWasmAnimationDriver::stop() +{ + m_startTimestamp = 0; + m_currentTimestamp = 0; + + // Stop and disconnect the fallback timer + fallbackTimer.stop(); + disconnect(&fallbackTimer, &QTimer::timeout, this, &QWasmAnimationDriver::handleFallbackTimeout); + + // Deregister the animation frame callback + if (m_animateCallbackHandle != 0) { + QWasmAnimationFrameMultiHandler::instance()->unregisterAnimateCallback(m_animateCallbackHandle); + m_animateCallbackHandle = 0; + } + + QAnimationDriver::stop(); +} + +void QWasmAnimationDriver::handleAnimationFrame(double timestamp) +{ + if (!isRunning()) + return; + + m_currentTimestamp = timestamp; + + // Fall back to setting m_startTimestamp here in cases where currentTime + // was not available in start() (gives 0 elapsed time for the first frame) + if (m_startTimestamp == 0) + m_startTimestamp = timestamp; + + advance(); +} + +QT_END_NAMESPACE diff --git a/src/corelib/platform/wasm/qwasmanimationdriver_p.h b/src/corelib/platform/wasm/qwasmanimationdriver_p.h new file mode 100644 index 00000000000..f8435c17a9a --- /dev/null +++ b/src/corelib/platform/wasm/qwasmanimationdriver_p.h @@ -0,0 +1,54 @@ +// Copyright (C) 2025 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#ifndef QWASMANIMATIONDRIVER_P_H +#define QWASMANIMATIONDRIVER_P_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include <QtCore/private/qglobal_p.h> +#include <QtCore/qabstractanimation.h> +#include <QtCore/qtimer.h> + +#include <memory> + +QT_BEGIN_NAMESPACE + +class QUnifiedTimer; + +class Q_CORE_EXPORT QWasmAnimationDriver : public QAnimationDriver +{ + Q_OBJECT +public: + QWasmAnimationDriver(QUnifiedTimer *unifiedTimer); + ~QWasmAnimationDriver() override; + + qint64 elapsed() const override; + +protected: + void start() override; + void stop() override; + +private: + void handleAnimationFrame(double timestamp); + void handleFallbackTimeout(); + double getCurrentTimeFromTimeline() const; + + QTimer fallbackTimer; + uint32_t m_animateCallbackHandle = 0; + double m_startTimestamp = 0; + double m_currentTimestamp = 0; +}; + +QT_END_NAMESPACE + +#endif // QWASMANIMATIONDRIVER_P_H diff --git a/src/corelib/platform/wasm/qwasmsuspendresumecontrol.cpp b/src/corelib/platform/wasm/qwasmsuspendresumecontrol.cpp index 093898c520a..a4bc7843380 100644 --- a/src/corelib/platform/wasm/qwasmsuspendresumecontrol.cpp +++ b/src/corelib/platform/wasm/qwasmsuspendresumecontrol.cpp @@ -4,6 +4,8 @@ #include "qwasmsuspendresumecontrol_p.h" #include "qstdweb_p.h" +#include <QtCore/qapplicationstatic.h> + #include <emscripten.h> #include <emscripten/val.h> #include <emscripten/bind.h> @@ -75,32 +77,20 @@ void qtRegisterEventHandlerJs(int index) { }[name]; } - function deepShallowClone(parent, obj, depth) { + function deepShallowClone(obj) { if (obj === null) return obj; - if (typeof obj === 'function') { - if (obj.name !== "") - return createNamedFunction(obj.name, parent, obj); - } - - if (depth >= 1) - return obj; - - if (typeof obj !== 'object') + if (!(obj instanceof Event)) return obj; - if (Array.isArray(obj)) { - const arrCopy = []; - for (let i = 0; i < obj.length; i++) - arrCopy[i] = deepShallowClone(obj, obj[i], depth + 1); - - return arrCopy; - } - const objCopy = {}; - for (const key in obj) - objCopy[key] = deepShallowClone(obj, obj[key], depth + 1); + for (const key in obj) { + if (typeof obj[key] === 'function') + objCopy[key] = createNamedFunction(obj[key].name, obj, obj[key]); + else + objCopy[key] = obj[key]; + } return objCopy; } @@ -110,7 +100,7 @@ void qtRegisterEventHandlerJs(int index) { let handler = (arg) => { // Copy the top level object, alias the rest. // functions are copied by creating new forwarding functions. - arg = deepShallowClone(arg, arg, 0); + arg = deepShallowClone(arg); // Add event to event queue control.pendingEvents.push({ @@ -206,9 +196,13 @@ void QWasmSuspendResumeControl::suspend() qtSuspendJs(); } -void QWasmSuspendResumeControl::suspendExclusive(uint32_t eventHandlerIndex) +void QWasmSuspendResumeControl::suspendExclusive(QList<uint32_t> eventHandlerIndices) { - suspendResumeControlJs().set("exclusiveEventHandler", eventHandlerIndex); + m_eventFilter = [eventHandlerIndices](int handler) { + return eventHandlerIndices.contains(handler); + }; + + suspendResumeControlJs().set("exclusiveEventHandler", eventHandlerIndices.back()); qtSuspendJs(); } @@ -221,37 +215,27 @@ int QWasmSuspendResumeControl::sendPendingEvents() emscripten::val control = suspendResumeControlJs(); emscripten::val pendingEvents = control["pendingEvents"]; - if (control["exclusiveEventHandler"].as<int>() > 0) - return sendPendingExclusiveEvent(); - - if (pendingEvents["length"].as<int>() == 0) - return 0; - int count = 0; - while (pendingEvents["length"].as<int>() > 0) { // Make sure it is reentrant - // Grab one event (handler and arg), and call it - emscripten::val event = pendingEvents.call<val>("shift"); - auto it = m_eventHandlers.find(event["index"].as<int>()); - if (it != m_eventHandlers.end()) - it->second(event["arg"]); - ++count; + for (int i = 0; i < pendingEvents["length"].as<int>();) { + if (!m_eventFilter(pendingEvents[i]["index"].as<int>())) { + ++i; + } else { + // Grab one event (handler and arg), and call it + emscripten::val event = pendingEvents[i]; + pendingEvents.call<void>("splice", i, 1); + + auto it = m_eventHandlers.find(event["index"].as<int>()); + if (it != m_eventHandlers.end()) + it->second(event["arg"]); + ++count; + } } - return count; -} -// Sends the pending exclusive event, and resets the "exclusive" state -int QWasmSuspendResumeControl::sendPendingExclusiveEvent() -{ - emscripten::val control = suspendResumeControlJs(); - int exclusiveHandlerIndex = control["exclusiveEventHandler"].as<int>(); - control.set("exclusiveEventHandler", 0); - emscripten::val event = control["pendingEvents"].call<val>("pop"); - int eventHandlerIndex = event["index"].as<int>(); - Q_ASSERT(exclusiveHandlerIndex == eventHandlerIndex); - auto it = m_eventHandlers.find(eventHandlerIndex); - Q_ASSERT(it != m_eventHandlers.end()); - it->second(event["arg"]); - return 1; + if (control["exclusiveEventHandler"].as<int>() > 0) { + control.set("exclusiveEventHandler", 0); + m_eventFilter = [](int) { return true;}; + } + return count; } void qtSendPendingEvents() @@ -357,3 +341,103 @@ void QWasmTimer::clearTimeout() val::global("window").call<void>("clearTimeout", double(m_timerId)); m_timerId = 0; } + +// +// QWasmAnimationFrameMultiHandler +// +// Multiplexes multiple animate and draw callbacks to a single native requestAnimationFrame call. +// Animate callbacks are called before draw callbacks to ensure animations are advanced before drawing. +// +QWasmAnimationFrameMultiHandler::QWasmAnimationFrameMultiHandler() +{ + auto wrapper = [this](val arg) { + handleAnimationFrame(arg.as<double>()); + }; + m_handlerIndex = QWasmSuspendResumeControl::get()->registerEventHandler(wrapper); +} + +QWasmAnimationFrameMultiHandler::~QWasmAnimationFrameMultiHandler() +{ + cancelAnimationFrameRequest(); + QWasmSuspendResumeControl::get()->removeEventHandler(m_handlerIndex); +} + +Q_APPLICATION_STATIC(QWasmAnimationFrameMultiHandler, s_animationFrameHandler); +QWasmAnimationFrameMultiHandler *QWasmAnimationFrameMultiHandler::instance() +{ + return s_animationFrameHandler(); +} + +// Registers a permanent animation callback. Call unregisterAnimateCallback() to unregister +uint32_t QWasmAnimationFrameMultiHandler::registerAnimateCallback(Callback callback) +{ + uint32_t handle = ++m_nextAnimateHandle; + m_animateCallbacks[handle] = std::move(callback); + ensureAnimationFrameRequested(); + return handle; +} + +// Registers a single-shot draw callback. +uint32_t QWasmAnimationFrameMultiHandler::registerDrawCallback(Callback callback) +{ + uint32_t handle = ++m_nextDrawHandle; + m_drawCallbacks[handle] = std::move(callback); + ensureAnimationFrameRequested(); + return handle; +} + +void QWasmAnimationFrameMultiHandler::unregisterAnimateCallback(uint32_t handle) +{ + m_animateCallbacks.erase(handle); + if (m_animateCallbacks.empty() && m_drawCallbacks.empty()) + cancelAnimationFrameRequest(); +} + +void QWasmAnimationFrameMultiHandler::unregisterDrawCallback(uint32_t handle) +{ + m_drawCallbacks.erase(handle); + if (m_animateCallbacks.empty() && m_drawCallbacks.empty()) + cancelAnimationFrameRequest(); +} + +void QWasmAnimationFrameMultiHandler::handleAnimationFrame(double timestamp) +{ + m_requestId = -1; + + // Advance animations. Copy the callbacks list in case callbacks are + // unregistered during iteration + auto animateCallbacksCopy = m_animateCallbacks; + for (const auto &pair : animateCallbacksCopy) + pair.second(timestamp); + + // Draw the frame. Note that draw callbacks are cleared after each + // frame, matching QWindow::requestUpdate() behavior. Copy the callbacks + // list in case new callbacks are registered while drawing the frame + auto drawCallbacksCopy = m_drawCallbacks; + m_drawCallbacks.clear(); + for (const auto &pair : drawCallbacksCopy) + pair.second(timestamp); + + // Request next frame if there are still callbacks registered + if (!m_animateCallbacks.empty() || !m_drawCallbacks.empty()) + ensureAnimationFrameRequested(); +} + +void QWasmAnimationFrameMultiHandler::ensureAnimationFrameRequested() +{ + if (m_requestId != -1) + return; + + using ReturnType = double; + val handler = QWasmSuspendResumeControl::get()->jsEventHandlerAt(m_handlerIndex); + m_requestId = int64_t(val::global("window").call<ReturnType>("requestAnimationFrame", handler)); +} + +void QWasmAnimationFrameMultiHandler::cancelAnimationFrameRequest() +{ + if (m_requestId == -1) + return; + + val::global("window").call<void>("cancelAnimationFrame", double(m_requestId)); + m_requestId = -1; +} diff --git a/src/corelib/platform/wasm/qwasmsuspendresumecontrol_p.h b/src/corelib/platform/wasm/qwasmsuspendresumecontrol_p.h index 37c71ed8123..ff97ff3d7ea 100644 --- a/src/corelib/platform/wasm/qwasmsuspendresumecontrol_p.h +++ b/src/corelib/platform/wasm/qwasmsuspendresumecontrol_p.h @@ -38,15 +38,16 @@ public: static emscripten::val suspendResumeControlJs(); void suspend(); - void suspendExclusive(uint32_t eventHandlerIndex); + // Accept events for all handlers, start to process events on last handler in list + void suspendExclusive(QList<uint32_t> eventHandlerIndices); int sendPendingEvents(); - int sendPendingExclusiveEvent(); private: friend void qtSendPendingEvents(); static QWasmSuspendResumeControl *s_suspendResumeControl; std::map<int, std::function<void(emscripten::val)>> m_eventHandlers; + std::function<bool(int)> m_eventFilter = [](int) { return true; }; }; class Q_CORE_EXPORT QWasmEventHandler @@ -83,4 +84,37 @@ private: uint64_t m_timerId = 0; }; +class Q_CORE_EXPORT QWasmAnimationFrameMultiHandler +{ +public: + using Callback = std::function<void(double)>; + + static QWasmAnimationFrameMultiHandler *instance(); + + uint32_t registerAnimateCallback(Callback callback); + uint32_t registerDrawCallback(Callback callback); + + void unregisterAnimateCallback(uint32_t handle); + void unregisterDrawCallback(uint32_t handle); + + QWasmAnimationFrameMultiHandler(); + ~QWasmAnimationFrameMultiHandler(); + QWasmAnimationFrameMultiHandler(const QWasmAnimationFrameMultiHandler&) = delete; + QWasmAnimationFrameMultiHandler& operator=(const QWasmAnimationFrameMultiHandler&) = delete; + +private: + void handleAnimationFrame(double timestamp); + void ensureAnimationFrameRequested(); + void cancelAnimationFrameRequest(); + + static QWasmAnimationFrameMultiHandler *s_instance; + + std::map<uint32_t, Callback> m_animateCallbacks; + std::map<uint32_t, Callback> m_drawCallbacks; + uint32_t m_nextAnimateHandle = 0; + uint32_t m_nextDrawHandle = 0; + uint32_t m_handlerIndex = 0; + int64_t m_requestId = -1; +}; + #endif diff --git a/src/corelib/text/qlocale_icu.cpp b/src/corelib/text/qlocale_icu.cpp index a10ae1c84b2..7e1dba5ee92 100644 --- a/src/corelib/text/qlocale_icu.cpp +++ b/src/corelib/text/qlocale_icu.cpp @@ -17,10 +17,10 @@ static_assert(std::is_same_v<UChar, char16_t>, namespace QtIcuPrivate { -enum class CaseConversion : bool { Upper, Lower }; +enum class IcuCaseConversion : bool { Upper, Lower }; static bool qt_u_strToCase(const QString &str, QString *out, const char *localeID, - CaseConversion conv) + IcuCaseConversion conv) { Q_ASSERT(out); @@ -34,9 +34,9 @@ static bool qt_u_strToCase(const QString &str, QString *out, const char *localeI // try to be a completely transparent wrapper: using R [[maybe_unused]] = decltype(u_strToUpper(std::forward<decltype(args)>(args)...)); switch (conv) { - case CaseConversion::Upper: + case IcuCaseConversion::Upper: return u_strToUpper(std::forward<decltype(args)>(args)...); - case CaseConversion::Lower: + case IcuCaseConversion::Lower: return u_strToLower(std::forward<decltype(args)>(args)...); }; Q_UNREACHABLE_RETURN(R{0}); @@ -79,7 +79,7 @@ QString QLocalePrivate::toUpper(const QString &str, bool *ok) const Q_ASSERT(ok); using namespace QtIcuPrivate; QString out; - *ok = qt_u_strToCase(str, &out, bcp47Name('_'), CaseConversion::Upper); + *ok = qt_u_strToCase(str, &out, bcp47Name('_'), IcuCaseConversion::Upper); return out; } @@ -88,7 +88,7 @@ QString QLocalePrivate::toLower(const QString &str, bool *ok) const Q_ASSERT(ok); using namespace QtIcuPrivate; QString out; - *ok = qt_u_strToCase(str, &out, bcp47Name('_'), CaseConversion::Lower); + *ok = qt_u_strToCase(str, &out, bcp47Name('_'), IcuCaseConversion::Lower); return out; } diff --git a/src/corelib/tools/qcryptographichash.cpp b/src/corelib/tools/qcryptographichash.cpp index 092ff46b084..53cca38ba7b 100644 --- a/src/corelib/tools/qcryptographichash.cpp +++ b/src/corelib/tools/qcryptographichash.cpp @@ -101,8 +101,6 @@ static inline int SHA384_512AddLength(SHA512Context *context, unsigned int lengt } #endif // !QT_CONFIG(opensslv30) -#include "qtcore-config_p.h" - #if QT_CONFIG(system_libb2) #include <blake2.h> #else |
