Qt
Internal/Contributor docs for the Qt SDK. Note: These are NOT official API docs; those are found at https://doc.qt.io/
Loading...
Searching...
No Matches
qqmlpluginimporter.cpp
Go to the documentation of this file.
1// Copyright (C) 2021 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
3
5#include "qqmlimport_p.h"
6
7#include <private/qqmlextensionplugin_p.h>
8#include <private/qqmltypeloader_p.h>
9#include <private/qqmlglobal_p.h>
10
11#include <QtCore/qobject.h>
12#include <QtCore/qpluginloader.h>
13#include <QtCore/qdir.h>
14#include <QtCore/qloggingcategory.h>
15#include <QtCore/qjsonarray.h>
16
17#include <unordered_map>
18
19QT_BEGIN_NAMESPACE
20
21struct QmlPlugin {
22 std::unique_ptr<QPluginLoader> loader;
23};
24
26{
28public:
29 PluginMap() = default;
30 ~PluginMap() = default;
31
32 // This is a std::unordered_map because QHash cannot handle move-only types.
34
35private:
36 QBasicMutex mutex;
37 Container plugins;
38 friend class PluginMapPtr;
39};
40
42{
44public:
46 ~PluginMapPtr() = default;
47
48 PluginMap::Container &operator*() { return map->plugins; }
49 const PluginMap::Container &operator*() const { return map->plugins; }
50
51 PluginMap::Container *operator->() { return &map->plugins; }
52 const PluginMap::Container *operator->() const { return &map->plugins; }
53
54private:
55 PluginMap *map;
56 QMutexLocker<QBasicMutex> locker;
57};
58
59Q_GLOBAL_STATIC(PluginMap, qmlPluginsById); // stores the uri and the PluginLoaders
60
66
72
73/*
74 * This function verifies if staticPlugin is valid QML plugin by
75 * verifying the IID member of Metadata and
76 * checks whether the plugin instance has the correct parent plugin extension class.
77 * Checks the presence of URI member of Metadata and returns QJsonArray,
78 * which in majority cases contains only 1 element.
79 */
80static QJsonArray tryExtractQmlPluginURIs(const QStaticPlugin &plugin)
81{
82 const auto isQmlPlugin = [](const QStaticPlugin &plugin, auto &&pluginMetadata) -> bool {
83 const QString iid = pluginMetadata.value(QLatin1String("IID")).toString();
84 const bool isQmlExtensionIID = iid == QLatin1String(QQmlEngineExtensionInterface_iid)
85 || iid == QLatin1String(QQmlExtensionInterface_iid)
86 || iid == QLatin1String(QQmlExtensionInterface_iid_old);
87 if (Q_UNLIKELY(iid == QLatin1String(QQmlExtensionInterface_iid_old))) {
88 qWarning() << QQmlImports::tr(
89 "Found plugin with old IID, this will be unsupported in upcoming Qt "
90 "releases:")
91 << pluginMetadata;
92 }
93 if (!isQmlExtensionIID) {
94 return false;
95 }
96
97 // plugin.instance must not be called earlier to avoid instantiating
98 // non-QML plugins, potentially from an unexpected thread(typeloader vs
99 // main thread)
100 const auto *pluginInstance = plugin.instance();
101 return qobject_cast<const QQmlEngineExtensionPlugin *>(pluginInstance)
102 || qobject_cast<const QQmlExtensionPlugin *>(pluginInstance);
103 };
104
105 const auto &pluginMetadata = plugin.metaData();
106 if (!isQmlPlugin(plugin, pluginMetadata)) {
107 return {};
108 }
109
110 const QJsonArray metadataUriList = pluginMetadata.value(QStringLiteral("uri")).toArray();
111 if (metadataUriList.isEmpty()) {
112 qWarning() << QQmlImports::tr("qml static plugin with name \"%2\" has no metadata URI")
113 .arg(plugin.instance()->metaObject()->className())
114 << pluginMetadata;
115 return {};
116 }
117 return metadataUriList;
118}
119
121{
122 QVector<StaticPluginMapping> qmlPlugins;
123 const auto staticPlugins = QPluginLoader::staticPlugins();
124 qmlPlugins.reserve(staticPlugins.size());
125
126 for (const auto &plugin : staticPlugins) {
127 // Filter out static plugins which are not Qml plugins
128 for (const QJsonValueConstRef &pluginURI : tryExtractQmlPluginURIs(plugin)) {
129 qmlPlugins.append({ plugin, pluginURI.toString() });
130 }
131 }
132 return qmlPlugins;
133}
134
135/*
136 Returns the list of possible versioned URI combinations. For example, if \a uri is
137 (id = QtQml.Models, (vmaj = 2, vmin = 0)), this method returns the following:
138 [QtQml.Models.2.0, QtQml.2.0.Models, QtQml.Models.2, QtQml.2.Models, QtQml.Models]
139 */
141{
142 QStringList result;
143 for (int mode = QQmlImports::FullyVersioned; mode <= QQmlImports::Unversioned; ++mode) {
144 int index = uri.id.size();
145 do {
146 QString versionUri = uri.id;
147 versionUri.insert(
148 index,
149 QQmlImports::versionString(uri.version, QQmlImports::ImportVersion(mode)));
150 result += versionUri;
151
152 index = uri.id.lastIndexOf(u'.', index - 1);
153 } while (index > 0 && mode != QQmlImports::Unversioned);
154 }
155 return result;
156}
157
158/*
159 Get all static plugins that are QML plugins and has a meta data URI that matches with one of
160 \a versionUris, which is a list of all possible versioned URI combinations - see
161 versionUriList() above.
162 */
164{
165 static const auto qmlPlugins = staticQmlPlugins();
166
167 // Since plugin metadata URIs can be anything from
168 // fully versioned to unversioned, we need to compare with differnt version strings.
169 // If a module has several plugins, they must all have the same version. Start by
170 // populating pluginPairs with relevant plugins to cut the list short early on:
171 const QStringList versionedURIs = versionUriList(uri);
172 QVector<StaticPluginMapping> matches;
173 std::copy_if(qmlPlugins.begin(), qmlPlugins.end(), std::back_inserter(matches),
174 [&](const auto &pluginMapping) {
175 return versionedURIs.contains(pluginMapping.metadataURI);
176 });
177 return matches;
178}
179
180static bool unloadPlugin(const std::pair<const QString, QmlPlugin> &plugin)
181{
182 const auto &loader = plugin.second.loader;
183 if (!loader)
184 return false;
185
186#if QT_CONFIG(library)
187 if (auto extensionPlugin = qobject_cast<QQmlExtensionPlugin *>(loader->instance()))
188 extensionPlugin->unregisterTypes();
189
190# ifndef Q_OS_MACOS
191 if (!loader->unload()) {
192 qWarning("Unloading %s failed: %s", qPrintable(plugin.first),
193 qPrintable(loader->errorString()));
194 return false;
195 }
196# endif
197#endif
198
199 return true;
200}
201
202/*!
203 \internal
204*/
205static QTypeRevision lockModule(const QString &uri, const QString &typeNamespace,
206 QTypeRevision version, QList<QQmlError> *errors)
207{
208 if (!version.hasMajorVersion()) {
209 version = QQmlMetaType::latestModuleVersion(uri);
210 if (!version.isValid())
211 errors->prepend(QQmlImports::moduleNotFoundError(uri, version));
212 }
213 if (version.hasMajorVersion() && !typeNamespace.isEmpty()
214 && !QQmlMetaType::protectModule(uri, version, true)) {
215 // Not being able to protect the module means there are not types registered for it,
216 // means the plugin we loaded didn't provide any, means we didn't find the module.
217 // We output the generic error message as depending on the load order of imports we may
218 // hit this path or another one that only figures "plugin is already loaded but module
219 // unavailable" and doesn't try to protect it anymore.
220 errors->prepend(QQmlImports::moduleNotFoundError(uri, version));
221 return QTypeRevision();
222 }
223
224 return version;
225}
226
227
229{
230 PluginMapPtr plugins(qmlPluginsById());
231 for (const auto &plugin : std::as_const(*plugins))
232 unloadPlugin(plugin);
233 plugins->clear();
234}
235
236bool QQmlPluginImporter::removePlugin(const QString &pluginId)
237{
238 PluginMapPtr plugins(qmlPluginsById());
239
240 auto it = plugins->find(pluginId);
241 if (it == plugins->end())
242 return false;
243
244 const bool success = unloadPlugin(*it);
245
246 plugins->erase(it);
247 return success;
248}
249
250QStringList QQmlPluginImporter::plugins()
251{
252 PluginMapPtr plugins(qmlPluginsById());
253 QStringList results;
254 for (auto it = plugins->cbegin(), end = plugins->cend(); it != end; ++it) {
255 if (it->second.loader != nullptr)
256 results.append(it->first);
257 }
258 return results;
259}
260
261QString QQmlPluginImporter::truncateToDirectory(const QString &qmldirFilePath)
262{
263 const int slash = qmldirFilePath.lastIndexOf(u'/');
264 return slash > 0 ? qmldirFilePath.left(slash) : qmldirFilePath;
265}
266
267void QQmlPluginImporter::finalizePlugin(QObject *instance, const QString &pluginId) {
268 // The plugin's per-engine initialization does not need lock protection, as this function is
269 // only called from the engine specific loader thread and importDynamicPlugin as well as
270 // importStaticPlugin are the only places of access.
271
272 typeLoader->setPluginInitialized(pluginId);
273 if (auto *extensionIface = qobject_cast<QQmlExtensionInterface *>(instance))
274 typeLoader->initializeEngine(extensionIface, uri.toUtf8().constData());
275 else if (auto *engineIface = qobject_cast<QQmlEngineExtensionInterface *>(instance))
276 typeLoader->initializeEngine(engineIface, uri.toUtf8().constData());
277}
278
279QTypeRevision QQmlPluginImporter::importStaticPlugin(QObject *instance, const QString &pluginId) {
280 // Dynamic plugins are differentiated by their filepath. For static plugins we
281 // don't have that information so we use their address as key instead.
282 QTypeRevision importVersion = version;
283 {
284 PluginMapPtr plugins(qmlPluginsById());
285
286 // Plugin types are global across all engines and should only be
287 // registered once. But each engine still needs to be initialized.
288 bool typesRegistered = plugins->find(pluginId) != plugins->end();
289
290 if (!typesRegistered) {
291 plugins->insert(std::make_pair(pluginId, QmlPlugin()));
292 if (QQmlMetaType::registerPluginTypes(
293 instance, QFileInfo(qmldirPath).absoluteFilePath(), uri,
294 qmldir->typeNamespace(), importVersion, errors)
295 == QQmlMetaType::RegistrationResult::Failure) {
296 return QTypeRevision();
297 }
298
299 importVersion = lockModule(uri, qmldir->typeNamespace(), importVersion, errors);
300 if (!importVersion.isValid())
301 return QTypeRevision();
302 }
303
304 // Release the lock on plugins early as we're done with the global part. Releasing the lock
305 // also allows other QML loader threads to acquire the lock while this thread is blocking
306 // in the initializeEngine call to the gui thread (which in turn may be busy waiting for
307 // other QML loader threads and thus not process the initializeEngine call).
308 }
309
310 if (!typeLoader->isPluginInitialized(pluginId))
311 finalizePlugin(instance, pluginId);
312
313 return QQmlImports::validVersion(importVersion);
314}
315
316QTypeRevision QQmlPluginImporter::importDynamicPlugin(
317 const QString &filePath, const QString &pluginId, bool optional)
318{
319 QObject *instance = nullptr;
320 QTypeRevision importVersion = version;
321
322 const bool engineInitialized = typeLoader->isPluginInitialized(pluginId);
323 {
324 PluginMapPtr plugins(qmlPluginsById());
325 const auto plugin = plugins->find(pluginId);
326 bool typesRegistered = plugin != plugins->end();
327
328 if (!engineInitialized || !typesRegistered) {
329 const QFileInfo fileInfo(filePath);
330 if (!typesRegistered && optional) {
331 switch (QQmlMetaType::registerPluginTypes(
332 nullptr, fileInfo.absolutePath(), uri, qmldir->typeNamespace(),
333 importVersion, errors)) {
334 case QQmlMetaType::RegistrationResult::NoRegistrationFunction:
335 // try again with plugin
336 break;
337 case QQmlMetaType::RegistrationResult::Success:
338 importVersion = lockModule(uri, qmldir->typeNamespace(), importVersion, errors);
339 if (!importVersion.isValid())
340 return QTypeRevision();
341 // instance and loader intentionally left at nullptr
342 plugins->insert(std::make_pair(pluginId, QmlPlugin()));
343 // Not calling initializeEngine with null instance
344 typeLoader->setPluginInitialized(pluginId);
345 return importVersion;
346 case QQmlMetaType::RegistrationResult::Failure:
347 return QTypeRevision();
348 }
349 }
350
351#if QT_CONFIG(library)
352 if (!typesRegistered) {
353
354 // Check original filePath. If that one is empty, not being able
355 // to load the plugin is not an error. We were just checking if
356 // the types are already available. absoluteFilePath can still be
357 // empty if filePath is not.
358 if (filePath.isEmpty())
359 return QTypeRevision();
360
361 QmlPlugin plugin;
362 plugin.loader = std::make_unique<QPluginLoader>(fileInfo.absoluteFilePath());
363 if (!plugin.loader->load()) {
364 if (errors) {
365 QQmlError error;
366 error.setDescription(plugin.loader->errorString());
367 errors->prepend(error);
368 }
369 return QTypeRevision();
370 }
371
372 instance = plugin.loader->instance();
373 plugins->insert(std::make_pair(pluginId, std::move(plugin)));
374
375 // Continue with shared code path for dynamic and static plugins:
376 if (QQmlMetaType::registerPluginTypes(
377 instance, fileInfo.absolutePath(), uri, qmldir->typeNamespace(),
378 importVersion, errors)
379 == QQmlMetaType::RegistrationResult::Failure) {
380 return QTypeRevision();
381 }
382
383 importVersion = lockModule(uri, qmldir->typeNamespace(), importVersion, errors);
384 if (!importVersion.isValid())
385 return QTypeRevision();
386 } else {
387 Q_ASSERT(plugin != plugins->end());
388 if (const auto &loader = plugin->second.loader) {
389 instance = loader->instance();
390 } else if (!optional) {
391 // If the plugin is not optional, we absolutely need to have a loader.
392 // Not having a loader here can mean that the plugin was loaded statically
393 // before. Return an invalid result to have the caller try that option.
394 return QTypeRevision();
395 }
396 }
397#else
398 // Here plugin is not optional and NOT QT_CONFIG(library)
399 // Cannot finalize such plugin and return valid, because no types are registered.
400 // Just return invalid.
401 if (!optional)
402 return QTypeRevision();
403#endif // QT_CONFIG(library)
404 }
405
406 // Release the lock on plugins early as we're done with the global part. Releasing the lock
407 // also allows other QML loader threads to acquire the lock while this thread is blocking
408 // in the initializeEngine call to the gui thread (which in turn may be busy waiting for
409 // other QML loader threads and thus not process the initializeEngine call).
410 }
411
412 if (!engineInitialized)
413 finalizePlugin(instance, pluginId);
414
415 return QQmlImports::validVersion(importVersion);
416}
417
418/*!
419 \internal
420
421 Searches for a plugin called \a baseName in \a qmldirPluginPath, taking the
422 path of the qmldir file itself, and the plugin paths of the QQmlTypeLoader
423 into account.
424
425 The baseName is amended with a platform-dependent prefix and suffix to
426 construct the final plugin file name:
427
428 \table
429 \header \li Platform \li Prefix \li Valid suffixes
430 \row \li Windows \li \li \c .dll, \c .d.dll
431 \row \li Unix/Linux \li lib \li \c .so
432 \row \li \macos \li lib \li \c .dylib, \c _debug.dylib \c .bundle, \c .so
433 \row \li Android \li lib \li \c .so, \c _<ABI>.so
434 \endtable
435
436 If the \a qmldirPluginPath is absolute, it is searched first. Then each of the
437 filePluginPath entries in the QQmlTypeLoader is checked in turn. If the
438 entry is relative, it is resolved on top of the path of the qmldir file,
439 otherwise it is taken verbatim. If a "." is found in the filePluginPath, and
440 \a qmldirPluginPath is relative, then \a qmldirPluginPath is used in its
441 place.
442
443 TODO: Document the android special casing.
444
445 TODO: The above paragraph, as well as the code implementing it makes very
446 little sense and is mostly here for backwards compatibility.
447 */
448QString QQmlPluginImporter::resolvePlugin(const QString &qmldirPluginPath, const QString &baseName)
449{
450#if defined(Q_OS_WIN)
451 static const QString prefix;
452 static const QStringList suffixes = {
453 # ifdef QT_DEBUG
454 QLatin1String("d.dll"), // try a qmake-style debug build first
455 QLatin1String(".dll")
456 #else
457 QLatin1String(".dll"),
458 QLatin1String("d.dll") // try a qmake-style debug build after
459 # endif
460 };
461#elif defined(Q_OS_DARWIN)
462 static const QString prefix = QLatin1String("lib");
463 static const QStringList suffixes = {
464 # ifdef QT_DEBUG
465 QLatin1String("_debug.dylib"), // try a qmake-style debug build first
466 QLatin1String(".dylib"),
467 # else
468 QLatin1String(".dylib"),
469 QLatin1String("_debug.dylib"), // try a qmake-style debug build after
470 # endif
471 QLatin1String(".so"),
472 QLatin1String(".bundle")
473 };
474#else // Unix
475 static const QString prefix = QLatin1String("lib");
476 static const QStringList suffixes = {
477 # if defined(Q_OS_ANDROID)
478 QStringLiteral(LIBS_SUFFIX),
479 # endif
480 QLatin1String(".so")
481 };
482#endif
483
484 QStringList searchPaths = typeLoader->pluginPathList();
485 bool qmldirPluginPathIsRelative = QDir::isRelativePath(qmldirPluginPath);
486 if (!qmldirPluginPathIsRelative)
487 searchPaths.prepend(qmldirPluginPath);
488
489 for (const QString &pluginPath : std::as_const(searchPaths)) {
490 QString resolvedBasePath;
491 if (pluginPath == QLatin1String(".")) {
492 if (qmldirPluginPathIsRelative && !qmldirPluginPath.isEmpty()
493 && qmldirPluginPath != QLatin1String(".")) {
494 resolvedBasePath = QDir::cleanPath(qmldirPath + u'/' + qmldirPluginPath);
495 } else {
496 resolvedBasePath = qmldirPath;
497 }
498 } else {
499 if (QDir::isRelativePath(pluginPath))
500 resolvedBasePath = QDir::cleanPath(qmldirPath + u'/' + pluginPath);
501 else
502 resolvedBasePath = pluginPath;
503 }
504
505 // hack for resources, should probably go away
506 if (resolvedBasePath.startsWith(u':'))
507 resolvedBasePath = QCoreApplication::applicationDirPath();
508
509 if (!resolvedBasePath.endsWith(u'/'))
510 resolvedBasePath += u'/';
511
512 QString resolvedPath = resolvedBasePath + prefix + baseName;
513 for (const QString &suffix : suffixes) {
514 QString absolutePath = typeLoader->absoluteFilePath(resolvedPath + suffix);
515 if (!absolutePath.isEmpty())
516 return absolutePath;
517 }
518
519#if defined(Q_OS_ANDROID)
520 if (qmldirPath.size() > 25 && qmldirPath.at(0) == QLatin1Char(':')
521 && qmldirPath.at(1) == QLatin1Char('/')
522 && qmldirPath.startsWith(QStringLiteral(":/android_rcc_bundle/qml/"),
523 Qt::CaseInsensitive)) {
524 QString pluginName = qmldirPath.mid(21) + u'/' + baseName;
525 pluginName.replace(QLatin1Char('/'), QLatin1Char('_'));
526 QString bundledPath = resolvedBasePath + QLatin1String("lib") + pluginName;
527 for (const QString &suffix : suffixes) {
528 const QString absolutePath = typeLoader->absoluteFilePath(bundledPath + suffix);
529 if (!absolutePath.isEmpty()) {
530 qWarning("The implicit resolving of Qml plugin locations using the URI "
531 "embedded in the filename has been deprecated. Please use the "
532 "modern CMake API to create QML modules or set the name of "
533 "QML plugin in qmldir file, that matches the name of plugin "
534 "on file system. The correct plugin name is '%s'.",
535 qPrintable(pluginName));
536 return absolutePath;
537 }
538 }
539 }
540#endif
541 }
542
543 qCDebug(lcQmlImport) << "resolvePlugin" << "Could not resolve dynamic plugin with base name"
544 << baseName << "in" << qmldirPath
545 << " file does not exist";
546
547 return QString();
548}
549
550QTypeRevision QQmlPluginImporter::importPlugins() {
551 const auto qmldirPlugins = qmldir->plugins();
552 const int qmldirPluginCount = qmldirPlugins.size();
553 QTypeRevision importVersion = version;
554
555 // If the path contains a version marker or if we have more than one plugin,
556 // we need to use paths. In that case we cannot fall back to other instances
557 // of the same module if a qmldir is rejected. However, as we don't generate
558 // such modules, it shouldn't be a problem.
559 const bool canUseUris = qmldirPluginCount == 1
560 && qmldirPath.endsWith(u'/' + QString(uri).replace(u'.', u'/'));
561 const QString moduleId = canUseUris ? uri : qmldir->qmldirLocation();
562
563 if (typeLoader->isModulePluginProcessingDone(moduleId)) {
564 return QQmlImports::validVersion(importVersion);
565 }
566
567 // First search for listed qmldir plugins dynamically. If we cannot resolve them all, we
568 // continue searching static plugins that has correct metadata uri. Note that since we
569 // only know the uri for a static plugin, and not the filename, we cannot know which
570 // static plugin belongs to which listed plugin inside qmldir. And for this reason,
571 // mixing dynamic and static plugins inside a single module is not recommended.
572
573 int dynamicPluginsFound = 0;
574 int staticPluginsLoaded = 0;
575
576 for (const QQmlDirParser::Plugin &plugin : qmldirPlugins) {
577 const QString resolvedFilePath = resolvePlugin(plugin.path, plugin.name);
578
579 if (!canUseUris && resolvedFilePath.isEmpty())
580 continue;
581
582 importVersion = importDynamicPlugin(
583 resolvedFilePath, canUseUris ? uri : QFileInfo(resolvedFilePath).absoluteFilePath(),
584 plugin.optional);
585 if (importVersion.isValid())
586 ++dynamicPluginsFound;
587 else if (!resolvedFilePath.isEmpty())
588 return QTypeRevision();
589 }
590
591 if (dynamicPluginsFound < qmldirPluginCount) {
592 // Check if the missing plugins can be resolved statically. We do this by looking at
593 // the URIs embedded in a plugins meta data.
594 const auto pluginPairs = staticQmlPluginsMatchingURI({ uri, importVersion });
595
596 for (const auto &pluginWithURI : std::as_const(pluginPairs)) {
597 QObject *instance = pluginWithURI.plugin.instance();
598 importVersion = importStaticPlugin(
599 instance, canUseUris ? uri : QString::asprintf("%p", instance));
600 if (!importVersion.isValid()) {
601 continue;
602 }
603 staticPluginsLoaded++;
604 qCDebug(lcQmlImport) << "importExtension" << "loaded static plugin "
605 << pluginWithURI.metadataURI;
606 }
607 if (!pluginPairs.empty() && staticPluginsLoaded == 0) {
608 // found matched plugins, but failed to load, early exit to preserve the error
609 return QTypeRevision();
610 }
611 }
612
613 if ((dynamicPluginsFound + staticPluginsLoaded) < qmldirPluginCount) {
614 if (errors) {
615 QQmlError error;
616 if (qmldirPluginCount > 1 && staticPluginsLoaded > 0) {
617 error.setDescription(
618 QQmlImports::tr("could not resolve all plugins for module \"%1\"")
619 .arg(uri));
620 } else {
621 error.setDescription(QQmlImports::tr("module \"%1\" plugin \"%2\" not found")
622 .arg(uri, qmldirPlugins[dynamicPluginsFound].name));
623 }
624 error.setUrl(QUrl::fromLocalFile(qmldir->qmldirLocation()));
625 errors->prepend(error);
626 }
627 return QTypeRevision();
628 }
629
630 typeLoader->setModulePluginProcessingDone(moduleId);
631
632 return QQmlImports::validVersion(importVersion);
633}
634
635QT_END_NAMESPACE
const PluginMap::Container & operator*() const
PluginMap::Container * operator->()
const PluginMap::Container * operator->() const
PluginMap::Container & operator*()
~PluginMap()=default
\inmodule QtCore
void qmlClearEnginePlugins()
static QStringList versionUriList(const VersionedURI &uri)
static QVector< StaticPluginMapping > staticQmlPlugins()
static QTypeRevision lockModule(const QString &uri, const QString &typeNamespace, QTypeRevision version, QList< QQmlError > *errors)
static QJsonArray tryExtractQmlPluginURIs(const QStaticPlugin &plugin)
static QVector< StaticPluginMapping > staticQmlPluginsMatchingURI(const VersionedURI &uri)
Q_GLOBAL_STATIC(PluginMap, qmlPluginsById)
static bool unloadPlugin(const std::pair< const QString, QmlPlugin > &plugin)
QTypeRevision version