diff options
Diffstat (limited to 'src/qml/qml/qqmlpluginimporter.cpp')
-rw-r--r-- | src/qml/qml/qqmlpluginimporter.cpp | 612 |
1 files changed, 612 insertions, 0 deletions
diff --git a/src/qml/qml/qqmlpluginimporter.cpp b/src/qml/qml/qqmlpluginimporter.cpp new file mode 100644 index 0000000000..24f12891b9 --- /dev/null +++ b/src/qml/qml/qqmlpluginimporter.cpp @@ -0,0 +1,612 @@ +// Copyright (C) 2021 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 "qqmlpluginimporter_p.h" +#include "qqmlimport_p.h" + +#include <private/qqmlextensionplugin_p.h> +#include <private/qqmltypeloader_p.h> +#include <private/qqmlglobal_p.h> + +#include <QtCore/qobject.h> +#include <QtCore/qpluginloader.h> +#include <QtCore/qdir.h> +#include <QtCore/qloggingcategory.h> +#include <QtCore/qjsonarray.h> + +#include <unordered_map> + +QT_BEGIN_NAMESPACE + +Q_DECLARE_LOGGING_CATEGORY(lcQmlImport) + +struct QmlPlugin { + std::unique_ptr<QPluginLoader> loader; +}; + +class PluginMap +{ + Q_DISABLE_COPY_MOVE(PluginMap) +public: + PluginMap() = default; + ~PluginMap() = default; + + // This is a std::unordered_map because QHash cannot handle move-only types. + using Container = std::unordered_map<QString, QmlPlugin>; + +private: + QBasicMutex mutex; + Container plugins; + friend class PluginMapPtr; +}; + +class PluginMapPtr +{ + Q_DISABLE_COPY_MOVE(PluginMapPtr) +public: + PluginMapPtr(PluginMap *map) : map(map), locker(&map->mutex) {} + ~PluginMapPtr() = default; + + PluginMap::Container &operator*() { return map->plugins; } + const PluginMap::Container &operator*() const { return map->plugins; } + + PluginMap::Container *operator->() { return &map->plugins; } + const PluginMap::Container *operator->() const { return &map->plugins; } + +private: + PluginMap *map; + QMutexLocker<QBasicMutex> locker; +}; + +Q_GLOBAL_STATIC(PluginMap, qmlPluginsById); // stores the uri and the PluginLoaders + +static QVector<QStaticPlugin> makePlugins() +{ + QVector<QStaticPlugin> plugins; + // To avoid traversing all static plugins for all imports, we cut down + // the list the first time called to only contain QML plugins: + const auto staticPlugins = QPluginLoader::staticPlugins(); + for (const QStaticPlugin &plugin : staticPlugins) { + const QString iid = plugin.metaData().value(QLatin1String("IID")).toString(); + if (iid == QLatin1String(QQmlEngineExtensionInterface_iid) + || iid == QLatin1String(QQmlExtensionInterface_iid) + || iid == QLatin1String(QQmlExtensionInterface_iid_old)) { + if (Q_UNLIKELY(iid == QLatin1String(QQmlExtensionInterface_iid_old))) { + qWarning() + << "Found plugin with old IID, this will be unsupported in upcoming Qt releases:" + << plugin.metaData(); + } + plugins.append(plugin); + } + } + return plugins; +} + +/* + Returns the list of possible versioned URI combinations. For example, if \a uri is + QtQml.Models, \a vmaj is 2, and \a vmin is 0, this method returns the following: + [QtQml.Models.2.0, QtQml.2.0.Models, QtQml.Models.2, QtQml.2.Models, QtQml.Models] + */ +static QStringList versionUriList(const QString &uri, QTypeRevision version) +{ + QStringList result; + for (int mode = QQmlImports::FullyVersioned; mode <= QQmlImports::Unversioned; ++mode) { + int index = uri.size(); + do { + QString versionUri = uri; + versionUri.insert(index, QQmlImports::versionString( + version, QQmlImports::ImportVersion(mode))); + result += versionUri; + + index = uri.lastIndexOf(u'.', index - 1); + } while (index > 0 && mode != QQmlImports::Unversioned); + } + return result; +} + +static bool unloadPlugin(const std::pair<const QString, QmlPlugin> &plugin) +{ + const auto &loader = plugin.second.loader; + if (!loader) + return false; + +#if QT_CONFIG(library) + if (auto extensionPlugin = qobject_cast<QQmlExtensionPlugin *>(loader->instance())) + extensionPlugin->unregisterTypes(); + +# ifndef Q_OS_MACOS + if (!loader->unload()) { + qWarning("Unloading %s failed: %s", qPrintable(plugin.first), + qPrintable(loader->errorString())); + return false; + } +# endif +#endif + + return true; +} + +void qmlClearEnginePlugins() +{ + PluginMapPtr plugins(qmlPluginsById()); + for (const auto &plugin : std::as_const(*plugins)) + unloadPlugin(plugin); + plugins->clear(); +} + +bool QQmlPluginImporter::removePlugin(const QString &pluginId) +{ + PluginMapPtr plugins(qmlPluginsById()); + + auto it = plugins->find(pluginId); + if (it == plugins->end()) + return false; + + const bool success = unloadPlugin(*it); + + plugins->erase(it); + return success; +} + +QStringList QQmlPluginImporter::plugins() +{ + PluginMapPtr plugins(qmlPluginsById()); + QStringList results; + for (auto it = plugins->cbegin(), end = plugins->cend(); it != end; ++it) { + if (it->second.loader != nullptr) + results.append(it->first); + } + return results; +} + +QString QQmlPluginImporter::truncateToDirectory(const QString &qmldirFilePath) +{ + const int slash = qmldirFilePath.lastIndexOf(u'/'); + return slash > 0 ? qmldirFilePath.left(slash) : qmldirFilePath; +} + +void QQmlPluginImporter::finalizePlugin(QObject *instance, const QString &pluginId) { + // The plugin's per-engine initialization does not need lock protection, as this function is + // only called from the engine specific loader thread and importDynamicPlugin as well as + // importStaticPlugin are the only places of access. + + database->initializedPlugins.insert(pluginId); + if (auto *extensionIface = qobject_cast<QQmlExtensionInterface *>(instance)) + typeLoader->initializeEngine(extensionIface, uri.toUtf8().constData()); + else if (auto *engineIface = qobject_cast<QQmlEngineExtensionInterface *>(instance)) + typeLoader->initializeEngine(engineIface, uri.toUtf8().constData()); +} + +QTypeRevision QQmlPluginImporter::importStaticPlugin(QObject *instance, const QString &pluginId) { + // Dynamic plugins are differentiated by their filepath. For static plugins we + // don't have that information so we use their address as key instead. + QTypeRevision importVersion = version; + { + PluginMapPtr plugins(qmlPluginsById()); + + // Plugin types are global across all engines and should only be + // registered once. But each engine still needs to be initialized. + bool typesRegistered = plugins->find(pluginId) != plugins->end(); + + if (!typesRegistered) { + plugins->insert(std::make_pair(pluginId, QmlPlugin())); + if (QQmlMetaType::registerPluginTypes( + instance, QFileInfo(qmldirPath).absoluteFilePath(), uri, + qmldir->typeNamespace(), importVersion, errors) + == QQmlMetaType::RegistrationResult::Failure) { + return QTypeRevision(); + } + + importVersion = QQmlImportDatabase::lockModule( + uri, qmldir->typeNamespace(), importVersion, errors); + if (!importVersion.isValid()) + return QTypeRevision(); + } + + // Release the lock on plugins early as we're done with the global part. Releasing the lock + // also allows other QML loader threads to acquire the lock while this thread is blocking + // in the initializeEngine call to the gui thread (which in turn may be busy waiting for + // other QML loader threads and thus not process the initializeEngine call). + } + + if (!database->initializedPlugins.contains(pluginId)) + finalizePlugin(instance, pluginId); + + return QQmlImports::validVersion(importVersion); +} + +QTypeRevision QQmlPluginImporter::importDynamicPlugin( + const QString &filePath, const QString &pluginId, bool optional) +{ + QObject *instance = nullptr; + QTypeRevision importVersion = version; + + const bool engineInitialized = database->initializedPlugins.contains(pluginId); + { + PluginMapPtr plugins(qmlPluginsById()); + bool typesRegistered = plugins->find(pluginId) != plugins->end(); + + if (!engineInitialized || !typesRegistered) { + const QFileInfo fileInfo(filePath); + if (!typesRegistered && optional) { + switch (QQmlMetaType::registerPluginTypes( + nullptr, fileInfo.absolutePath(), uri, qmldir->typeNamespace(), + importVersion, errors)) { + case QQmlMetaType::RegistrationResult::NoRegistrationFunction: + // try again with plugin + break; + case QQmlMetaType::RegistrationResult::Success: + importVersion = QQmlImportDatabase::lockModule( + uri, qmldir->typeNamespace(), importVersion, errors); + if (!importVersion.isValid()) + return QTypeRevision(); + // instance and loader intentionally left at nullptr + plugins->insert(std::make_pair(pluginId, QmlPlugin())); + // Not calling initializeEngine with null instance + database->initializedPlugins.insert(pluginId); + return importVersion; + case QQmlMetaType::RegistrationResult::Failure: + return QTypeRevision(); + } + } + +#if QT_CONFIG(library) + if (!typesRegistered) { + + // Check original filePath. If that one is empty, not being able + // to load the plugin is not an error. We were just checking if + // the types are already available. absoluteFilePath can still be + // empty if filePath is not. + if (filePath.isEmpty()) + return QTypeRevision(); + + const QString absoluteFilePath = fileInfo.absoluteFilePath(); + if (!QQml_isFileCaseCorrect(absoluteFilePath)) { + if (errors) { + QQmlError error; + error.setDescription( + QQmlImportDatabase::tr("File name case mismatch for \"%1\"") + .arg(absoluteFilePath)); + errors->prepend(error); + } + return QTypeRevision(); + } + + QmlPlugin plugin; + plugin.loader = std::make_unique<QPluginLoader>(absoluteFilePath); + if (!plugin.loader->load()) { + if (errors) { + QQmlError error; + error.setDescription(plugin.loader->errorString()); + errors->prepend(error); + } + return QTypeRevision(); + } + + instance = plugin.loader->instance(); + plugins->insert(std::make_pair(pluginId, std::move(plugin))); + + // Continue with shared code path for dynamic and static plugins: + if (QQmlMetaType::registerPluginTypes( + instance, fileInfo.absolutePath(), uri, qmldir->typeNamespace(), + importVersion, errors) + == QQmlMetaType::RegistrationResult::Failure) { + return QTypeRevision(); + } + + importVersion = QQmlImportDatabase::lockModule( + uri, qmldir->typeNamespace(), importVersion, errors); + if (!importVersion.isValid()) + return QTypeRevision(); + } else { + auto it = plugins->find(pluginId); + if (it != plugins->end() && it->second.loader) + instance = it->second.loader->instance(); + } +#else + // Here plugin is not optional and NOT QT_CONFIG(library) + // Cannot finalize such plugin and return valid, because no types are registered. + // Just return invalid. + if (!optional) + return QTypeRevision(); +#endif // QT_CONFIG(library) + } + + // Release the lock on plugins early as we're done with the global part. Releasing the lock + // also allows other QML loader threads to acquire the lock while this thread is blocking + // in the initializeEngine call to the gui thread (which in turn may be busy waiting for + // other QML loader threads and thus not process the initializeEngine call). + } + + if (!engineInitialized) + finalizePlugin(instance, pluginId); + + return QQmlImports::validVersion(importVersion); +} + +/*! + \internal + + Searches for a plugin called \a baseName in \a qmldirPluginPath, taking the + path of the qmldir file itself, and the plugin paths of the QQmlImportDatabase + into account. + + The baseName is amended with a platform-dependent prefix and suffix to + construct the final plugin file name: + + \table + \header \li Platform \li Prefix \li Valid suffixes + \row \li Windows \li \li \c .dll, \c .d.dll + \row \li Unix/Linux \li lib \li \c .so + \row \li \macos \li lib \li \c .dylib, \c _debug.dylib \c .bundle, \c .so + \row \li Android \li lib \li \c .so, \c _<ABI>.so + \endtable + + If the \a qmldirPluginPath is absolute, it is searched first. Then each of the + filePluginPath entries in the QQmlImportDatabase is checked in turn. If the + entry is relative, it is resolved on top of the path of the qmldir file, + otherwise it is taken verbatim. If a "." is found in the filePluginPath, and + \a qmldirPluginPath is relative, then \a qmldirPluginPath is used in its + place. + + TODO: Document the android special casing. + + TODO: The above paragraph, as well as the code implementing it makes very + little sense and is mostly here for backwards compatibility. + */ +QString QQmlPluginImporter::resolvePlugin(const QString &qmldirPluginPath, const QString &baseName) +{ +#if defined(Q_OS_WIN) + static const QString prefix; + static const QStringList suffixes = { + # ifdef QT_DEBUG + QLatin1String("d.dll"), // try a qmake-style debug build first + QLatin1String(".dll") + #else + QLatin1String(".dll"), + QLatin1String("d.dll") // try a qmake-style debug build after + # endif + }; +#elif defined(Q_OS_DARWIN) + static const QString prefix = QLatin1String("lib"); + static const QStringList suffixes = { + # ifdef QT_DEBUG + QLatin1String("_debug.dylib"), // try a qmake-style debug build first + QLatin1String(".dylib"), + # else + QLatin1String(".dylib"), + QLatin1String("_debug.dylib"), // try a qmake-style debug build after + # endif + QLatin1String(".so"), + QLatin1String(".bundle") + }; +#else // Unix + static const QString prefix = QLatin1String("lib"); + static const QStringList suffixes = { + # if defined(Q_OS_ANDROID) + QStringLiteral(LIBS_SUFFIX), + # endif + QLatin1String(".so") + }; +#endif + + QStringList searchPaths = database->filePluginPath; + bool qmldirPluginPathIsRelative = QDir::isRelativePath(qmldirPluginPath); + if (!qmldirPluginPathIsRelative) + searchPaths.prepend(qmldirPluginPath); + + for (const QString &pluginPath : std::as_const(searchPaths)) { + QString resolvedBasePath; + if (pluginPath == QLatin1String(".")) { + if (qmldirPluginPathIsRelative && !qmldirPluginPath.isEmpty() + && qmldirPluginPath != QLatin1String(".")) { + resolvedBasePath = QDir::cleanPath(qmldirPath + u'/' + qmldirPluginPath); + } else { + resolvedBasePath = qmldirPath; + } + } else { + if (QDir::isRelativePath(pluginPath)) + resolvedBasePath = QDir::cleanPath(qmldirPath + u'/' + pluginPath); + else + resolvedBasePath = pluginPath; + } + + // hack for resources, should probably go away + if (resolvedBasePath.startsWith(u':')) + resolvedBasePath = QCoreApplication::applicationDirPath(); + + if (!resolvedBasePath.endsWith(u'/')) + resolvedBasePath += u'/'; + + QString resolvedPath = resolvedBasePath + prefix + baseName; + for (const QString &suffix : suffixes) { + QString absolutePath = typeLoader->absoluteFilePath(resolvedPath + suffix); + if (!absolutePath.isEmpty()) + return absolutePath; + } + +#if defined(Q_OS_ANDROID) + if (qmldirPath.size() > 25 && qmldirPath.at(0) == QLatin1Char(':') + && qmldirPath.at(1) == QLatin1Char('/') + && qmldirPath.startsWith(QStringLiteral(":/android_rcc_bundle/qml/"), + Qt::CaseInsensitive)) { + QString pluginName = qmldirPath.mid(21) + u'/' + baseName; + pluginName.replace(QLatin1Char('/'), QLatin1Char('_')); + QString bundledPath = resolvedBasePath + QLatin1String("lib") + pluginName; + for (const QString &suffix : suffixes) { + const QString absolutePath = typeLoader->absoluteFilePath(bundledPath + suffix); + if (!absolutePath.isEmpty()) { + qWarning("The implicit resolving of Qml plugin locations using the URI " + "embedded in the filename has been deprecated. Please use the " + "modern CMake API to create QML modules or set the name of " + "QML plugin in qmldir file, that matches the name of plugin " + "on file system. The correct plugin name is '%s'.", + qPrintable(pluginName)); + return absolutePath; + } + } + } +#endif + } + + qCDebug(lcQmlImport) << "resolvePlugin" << "Could not resolve dynamic plugin with base name" + << baseName << "in" << qmldirPath + << " file does not exist"; + + return QString(); +} + +/* + Get all static plugins that are QML plugins and has a meta data URI that matches with one of + \a versionUris, which is a list of all possible versioned URI combinations - see versionUriList() + above. + */ +bool QQmlPluginImporter::populatePluginDataVector(QVector<StaticPluginData> &result, const QStringList &versionUris) +{ + static const QVector<QStaticPlugin> plugins = makePlugins(); + for (const QStaticPlugin &plugin : plugins) { + // Since a module can list more than one plugin, + // we keep iterating even after we found a match. + QObject *instance = plugin.instance(); + if (qobject_cast<QQmlEngineExtensionPlugin *>(instance) + || qobject_cast<QQmlExtensionPlugin *>(instance)) { + const QJsonArray metaTagsUriList = plugin.metaData().value( + QStringLiteral("uri")).toArray(); + if (metaTagsUriList.isEmpty()) { + if (errors) { + QQmlError error; + error.setDescription(QQmlImportDatabase::tr( + "static plugin for module \"%1\" with name \"%2\" " + "has no metadata URI") + .arg(uri, QString::fromUtf8( + instance->metaObject()->className()))); + error.setUrl(QUrl::fromLocalFile(qmldir->qmldirLocation())); + errors->prepend(error); + } + return false; + } + // A plugin can be set up to handle multiple URIs, so go through the list: + for (const QJsonValueConstRef metaTagUri : metaTagsUriList) { + if (versionUris.contains(metaTagUri.toString())) { + result.append({ plugin, metaTagsUriList }); + break; + } + } + } + } + return true; +} + +QTypeRevision QQmlPluginImporter::importPlugins() { + const auto qmldirPlugins = qmldir->plugins(); + const int qmldirPluginCount = qmldirPlugins.size(); + QTypeRevision importVersion = version; + + // If the path contains a version marker or if we have more than one plugin, + // we need to use paths. In that case we cannot fall back to other instances + // of the same module if a qmldir is rejected. However, as we don't generate + // such modules, it shouldn't be a problem. + const bool canUseUris = qmldirPluginCount == 1 + && qmldirPath.endsWith(u'/' + QString(uri).replace(u'.', u'/')); + const QString moduleId = canUseUris ? uri : qmldir->qmldirLocation(); + + if (!database->modulesForWhichPluginsHaveBeenLoaded.contains(moduleId)) { + // First search for listed qmldir plugins dynamically. If we cannot resolve them all, we + // continue searching static plugins that has correct metadata uri. Note that since we + // only know the uri for a static plugin, and not the filename, we cannot know which + // static plugin belongs to which listed plugin inside qmldir. And for this reason, + // mixing dynamic and static plugins inside a single module is not recommended. + + int dynamicPluginsFound = 0; + int staticPluginsFound = 0; + + for (const QQmlDirParser::Plugin &plugin : qmldirPlugins) { + const QString resolvedFilePath = resolvePlugin(plugin.path, plugin.name); + + if (!canUseUris && resolvedFilePath.isEmpty()) + continue; + + importVersion = importDynamicPlugin( + resolvedFilePath, + canUseUris ? uri : QFileInfo(resolvedFilePath).absoluteFilePath(), + plugin.optional); + if (importVersion.isValid()) + ++dynamicPluginsFound; + else if (!resolvedFilePath.isEmpty()) + return QTypeRevision(); + } + + if (dynamicPluginsFound < qmldirPluginCount) { + // Check if the missing plugins can be resolved statically. We do this by looking at + // the URIs embedded in a plugins meta data. Since those URIs can be anything from + // fully versioned to unversioned, we need to compare with differnt version strings. + // If a module has several plugins, they must all have the same version. Start by + // populating pluginPairs with relevant plugins to cut the list short early on: + const QStringList versionUris = versionUriList(uri, importVersion); + QVector<StaticPluginData> pluginPairs; + if (!populatePluginDataVector(pluginPairs, versionUris)) + return QTypeRevision(); + + for (const QString &versionUri : versionUris) { + for (const StaticPluginData &pair : std::as_const(pluginPairs)) { + for (const QJsonValueConstRef metaTagUri : pair.uriList) { + if (versionUri == metaTagUri.toString()) { + staticPluginsFound++; + QObject *instance = pair.plugin.instance(); + importVersion = importStaticPlugin( + instance, + canUseUris ? uri : QString::asprintf("%p", instance)); + if (!importVersion.isValid()){ + if (errors) { + Q_ASSERT(!errors->isEmpty()); + const QQmlError poppedError = errors->takeFirst(); + QQmlError error; + error.setDescription( + QQmlImportDatabase::tr( + "static plugin for module \"%1\" with " + "name \"%2\" cannot be loaded: %3") + .arg(uri, QString::fromUtf8( + instance->metaObject()->className()), + poppedError.description())); + error.setUrl(QUrl::fromLocalFile(qmldir->qmldirLocation())); + errors->prepend(error); + } + return QTypeRevision(); + } + + qCDebug(lcQmlImport) + << "importExtension" << "loaded static plugin " << versionUri; + + break; + } + } + } + if (staticPluginsFound > 0) + break; + } + } + + if ((dynamicPluginsFound + staticPluginsFound) < qmldirPluginCount) { + if (errors) { + QQmlError error; + if (qmldirPluginCount > 1 && staticPluginsFound > 0) { + error.setDescription(QQmlImportDatabase::tr( + "could not resolve all plugins for module \"%1\"") + .arg(uri)); + } else { + error.setDescription(QQmlImportDatabase::tr( + "module \"%1\" plugin \"%2\" not found") + .arg(uri, qmldirPlugins[dynamicPluginsFound].name)); + } + error.setUrl(QUrl::fromLocalFile(qmldir->qmldirLocation())); + errors->prepend(error); + } + return QTypeRevision(); + } + + database->modulesForWhichPluginsHaveBeenLoaded.insert(moduleId); + } + return QQmlImports::validVersion(importVersion); +} + +QT_END_NAMESPACE |