diff options
-rw-r--r-- | src/qml/qml/qqmlengine.cpp | 6 | ||||
-rw-r--r-- | src/qml/qml/qqmlengine_p.h | 2 | ||||
-rw-r--r-- | src/quick/util/qquickimageprovider_p.h | 2 | ||||
-rw-r--r-- | src/quick/util/qquickpixmapcache.cpp | 34 | ||||
-rw-r--r-- | tests/auto/quick/qquickimageprovider/tst_qquickimageprovider.cpp | 181 |
5 files changed, 192 insertions, 33 deletions
diff --git a/src/qml/qml/qqmlengine.cpp b/src/qml/qml/qqmlengine.cpp index b94f449a39..5e06ff283e 100644 --- a/src/qml/qml/qqmlengine.cpp +++ b/src/qml/qml/qqmlengine.cpp @@ -1169,6 +1169,12 @@ void QQmlEnginePrivate::registerFinalizeCallback(QObject *obj, int index) } } +QSharedPointer<QQmlImageProviderBase> QQmlEnginePrivate::imageProvider(const QString &providerId) const +{ + QMutexLocker locker(&mutex); + return imageProviders.value(providerId.toLower()); +} + #if QT_CONFIG(qml_network) /*! Sets the \a factory to use for creating QNetworkAccessManager(s). diff --git a/src/qml/qml/qqmlengine_p.h b/src/qml/qml/qqmlengine_p.h index 3b716683fd..e58ff554a7 100644 --- a/src/qml/qml/qqmlengine_p.h +++ b/src/qml/qml/qqmlengine_p.h @@ -167,6 +167,8 @@ public: mutable QQmlNetworkAccessManagerFactory *networkAccessManagerFactory; #endif QHash<QString,QSharedPointer<QQmlImageProviderBase> > imageProviders; + QSharedPointer<QQmlImageProviderBase> imageProvider(const QString &providerId) const; + QQmlAbstractUrlInterceptor* urlInterceptor; diff --git a/src/quick/util/qquickimageprovider_p.h b/src/quick/util/qquickimageprovider_p.h index b5baf79319..67e17010d4 100644 --- a/src/quick/util/qquickimageprovider_p.h +++ b/src/quick/util/qquickimageprovider_p.h @@ -61,7 +61,7 @@ class QQuickImageResponsePrivate : public QObjectPrivate { Q_DECLARE_PUBLIC(QQuickImageResponse) public: - bool finished = false; + QAtomicInteger<qint32> finished = false; void _q_finished() { finished = true; } }; diff --git a/src/quick/util/qquickpixmapcache.cpp b/src/quick/util/qquickpixmapcache.cpp index ced0acd9ab..5f31376599 100644 --- a/src/quick/util/qquickpixmapcache.cpp +++ b/src/quick/util/qquickpixmapcache.cpp @@ -207,7 +207,7 @@ protected: private: friend class QQuickPixmapReaderThreadObject; void processJobs(); - void processJob(QQuickPixmapReply *, const QUrl &, const QString &, QQuickImageProvider::ImageType, QQuickImageProvider *); + void processJob(QQuickPixmapReply *, const QUrl &, const QString &, QQuickImageProvider::ImageType, const QSharedPointer<QQuickImageProvider> &); #if QT_CONFIG(qml_network) void networkRequestDone(QNetworkReply *); #endif @@ -683,10 +683,11 @@ void QQuickPixmapReader::processJobs() const QUrl url = job->url; QString localFile; QQuickImageProvider::ImageType imageType = QQuickImageProvider::Invalid; - QQuickImageProvider *provider = nullptr; + QSharedPointer<QQuickImageProvider> provider; if (url.scheme() == QLatin1String("image")) { - provider = static_cast<QQuickImageProvider *>(engine->imageProvider(imageProviderId(url))); + QQmlEnginePrivate *enginePrivate = QQmlEnginePrivate::get(engine); + provider = enginePrivate->imageProvider(imageProviderId(url)).staticCast<QQuickImageProvider>(); if (provider) imageType = provider->imageType(); @@ -721,7 +722,7 @@ void QQuickPixmapReader::processJobs() } void QQuickPixmapReader::processJob(QQuickPixmapReply *runningJob, const QUrl &url, const QString &localFile, - QQuickImageProvider::ImageType imageType, QQuickImageProvider *provider) + QQuickImageProvider::ImageType imageType, const QSharedPointer<QQuickImageProvider> &provider) { // fetch if (url.scheme() == QLatin1String("image")) { @@ -737,7 +738,8 @@ void QQuickPixmapReader::processJob(QQuickPixmapReply *runningJob, const QUrl &u return; } - QQuickImageProviderWithOptions *providerV2 = QQuickImageProviderWithOptions::checkedCast(provider); + // This is safe because we ensure that provider does outlive providerV2 and it does not escape the function + QQuickImageProviderWithOptions *providerV2 = QQuickImageProviderWithOptions::checkedCast(provider.get()); switch (imageType) { case QQuickImageProvider::Invalid: @@ -817,17 +819,24 @@ void QQuickPixmapReader::processJob(QQuickPixmapReply *runningJob, const QUrl &u if (providerV2) { response = providerV2->requestImageResponse(imageId(url), runningJob->requestSize, runningJob->providerOptions); } else { - QQuickAsyncImageProvider *asyncProvider = static_cast<QQuickAsyncImageProvider*>(provider); + QQuickAsyncImageProvider *asyncProvider = static_cast<QQuickAsyncImageProvider*>(provider.get()); response = asyncProvider->requestImageResponse(imageId(url), runningJob->requestSize); } + { + QObject::connect(response, SIGNAL(finished()), threadObject, SLOT(asyncResponseFinished())); + // as the response object can outlive the provider QSharedPointer, we have to extend the pointee's lifetime by that of the response + // we do this by capturing a copy of the QSharedPointer in a lambda, and dropping it once the lambda has been called + auto provider_copy = provider; // capturing provider would capture it as a const reference, and copy capture with initializer is only available in C++14 + QObject::connect(response, &QQuickImageResponse::destroyed, response, [provider_copy]() { + // provider_copy will be deleted when the connection gets deleted + }); + } // Might be that the async provider was so quick it emitted the signal before we // could connect to it. if (static_cast<QQuickImageResponsePrivate*>(QObjectPrivate::get(response))->finished) { QMetaObject::invokeMethod(threadObject, "asyncResponseFinished", Qt::QueuedConnection, Q_ARG(QQuickImageResponse*, response)); - } else { - QObject::connect(response, SIGNAL(finished()), threadObject, SLOT(asyncResponseFinished())); } asyncResponses.insert(response, runningJob); @@ -1270,8 +1279,10 @@ static QQuickPixmapData* createPixmapDataSync(QQuickPixmap *declarativePixmap, Q QSize readSize; QQuickImageProvider::ImageType imageType = QQuickImageProvider::Invalid; - QQuickImageProvider *provider = static_cast<QQuickImageProvider *>(engine->imageProvider(imageProviderId(url))); - QQuickImageProviderWithOptions *providerV2 = QQuickImageProviderWithOptions::checkedCast(provider); + QQmlEnginePrivate *enginePrivate = QQmlEnginePrivate::get(engine); + QSharedPointer<QQuickImageProvider> provider = enginePrivate->imageProvider(imageProviderId(url)).dynamicCast<QQuickImageProvider>(); + // it is safe to use get() as providerV2 does not escape and is outlived by provider + QQuickImageProviderWithOptions *providerV2 = QQuickImageProviderWithOptions::checkedCast(provider.get()); if (provider) imageType = provider->imageType(); @@ -1570,7 +1581,8 @@ void QQuickPixmap::load(QQmlEngine *engine, const QUrl &url, const QSize &reques if (iter == store->m_cache.end()) { if (url.scheme() == QLatin1String("image")) { - if (QQuickImageProvider *provider = static_cast<QQuickImageProvider *>(engine->imageProvider(imageProviderId(url)))) { + QQmlEnginePrivate *enginePrivate = QQmlEnginePrivate::get(engine); + if (auto provider = enginePrivate->imageProvider(imageProviderId(url)).staticCast<QQuickImageProvider>()) { const bool threadedPixmaps = QGuiApplicationPrivate::platformIntegration()->hasCapability(QPlatformIntegration::ThreadedPixmaps); if (!threadedPixmaps && provider->imageType() == QQuickImageProvider::Pixmap) { // pixmaps can only be loaded synchronously diff --git a/tests/auto/quick/qquickimageprovider/tst_qquickimageprovider.cpp b/tests/auto/quick/qquickimageprovider/tst_qquickimageprovider.cpp index 4b75a7e008..c79e665d94 100644 --- a/tests/auto/quick/qquickimageprovider/tst_qquickimageprovider.cpp +++ b/tests/auto/quick/qquickimageprovider/tst_qquickimageprovider.cpp @@ -33,6 +33,7 @@ #include <QImageReader> #include <QWaitCondition> #include <QThreadPool> +#include <private/qqmlengine_p.h> Q_DECLARE_METATYPE(QQuickImageProvider*); @@ -67,6 +68,8 @@ private slots: void asyncTextureTest(); void instantAsyncTextureTest(); + void asyncImageThreadSafety(); + private: QString newImageFileName() const; void fillRequestTestsData(const QString &id); @@ -448,21 +451,56 @@ void tst_qquickimageprovider::threadTest() foreach (QQuickImage *img, images) { QCOMPARE(img->status(), QQuickImage::Loading); } - provider->ok = true; - provider->cond.wakeAll(); + { + QMutexLocker lock(&provider->mutex); + provider->ok = true; + provider->cond.wakeAll(); + } QTest::qWait(250); foreach (QQuickImage *img, images) { QTRY_COMPARE(img->status(), QQuickImage::Ready); } } -class TestImageResponse : public QQuickImageResponse, public QRunnable +class TestImageResponseRunner : public QObject, public QRunnable { + + Q_OBJECT + +public: + Q_SIGNAL void finished(QQuickTextureFactory *texture); + TestImageResponseRunner(QMutex *lock, QWaitCondition *condition, bool *ok, const QString &id, const QSize &requestedSize) + : m_lock(lock), m_condition(condition), m_ok(ok), m_id(id), m_requestedSize(requestedSize) {} + void run() + { + m_lock->lock(); + if (!(*m_ok)) { + m_condition->wait(m_lock); + } + m_lock->unlock(); + QImage image(50, 50, QImage::Format_RGB32); + image.fill(QColor(m_id).rgb()); + if (m_requestedSize.isValid()) + image = image.scaled(m_requestedSize); + emit finished(QQuickTextureFactory::textureFactoryForImage(image)); + } + +private: + QMutex *m_lock; + QWaitCondition *m_condition; + bool *m_ok; + QString m_id; + QSize m_requestedSize; +}; + +class TestImageResponse : public QQuickImageResponse { public: - TestImageResponse(QMutex *lock, QWaitCondition *condition, bool *ok, const QString &id, const QSize &requestedSize) + TestImageResponse(QMutex *lock, QWaitCondition *condition, bool *ok, const QString &id, const QSize &requestedSize, QThreadPool *pool) : m_lock(lock), m_condition(condition), m_ok(ok), m_id(id), m_requestedSize(requestedSize), m_texture(nullptr) { - setAutoDelete(false); + auto runnable = new TestImageResponseRunner(m_lock, m_condition, m_ok, m_id, m_requestedSize); + QObject::connect(runnable, &TestImageResponseRunner::finished, this, &TestImageResponse::handleResponse); + pool->start(runnable); } QQuickTextureFactory *textureFactory() const @@ -470,18 +508,8 @@ class TestImageResponse : public QQuickImageResponse, public QRunnable return m_texture; } - void run() - { - m_lock->lock(); - if (!(*m_ok)) { - m_condition->wait(m_lock); - } - m_lock->unlock(); - QImage image(50, 50, QImage::Format_RGB32); - image.fill(QColor(m_id).rgb()); - if (m_requestedSize.isValid()) - image = image.scaled(m_requestedSize); - m_texture = QQuickTextureFactory::textureFactoryForImage(image); + void handleResponse(QQuickTextureFactory *factory) { + this->m_texture = factory; emit finished(); } @@ -505,8 +533,7 @@ class TestAsyncProvider : public QQuickAsyncImageProvider QQuickImageResponse *requestImageResponse(const QString &id, const QSize &requestedSize) { - TestImageResponse *response = new TestImageResponse(&lock, &condition, &ok, id, requestedSize); - pool.start(response); + TestImageResponse *response = new TestImageResponse(&lock, &condition, &ok, id, requestedSize, &pool); return response; } @@ -544,8 +571,11 @@ void tst_qquickimageprovider::asyncTextureTest() foreach (QQuickImage *img, images) { QTRY_COMPARE(img->status(), QQuickImage::Loading); } - provider->ok = true; - provider->condition.wakeAll(); + { + QMutexLocker lock(&provider->lock); + provider->ok = true; + provider->condition.wakeAll(); + } foreach (QQuickImage *img, images) { QTRY_COMPARE(img->status(), QQuickImage::Ready); } @@ -616,6 +646,115 @@ void tst_qquickimageprovider::instantAsyncTextureTest() } +class WaitingAsyncImageResponse : public QQuickImageResponse, public QRunnable +{ +public: + WaitingAsyncImageResponse(QMutex *providerRemovedMutex, QWaitCondition *providerRemovedCond, bool *providerRemoved, QMutex *imageRequestedMutex, QWaitCondition *imageRequestedCond, bool *imageRequested) + : m_providerRemovedMutex(providerRemovedMutex), m_providerRemovedCond(providerRemovedCond), m_providerRemoved(providerRemoved), + m_imageRequestedMutex(imageRequestedMutex), m_imageRequestedCondition(imageRequestedCond), m_imageRequested(imageRequested) + { + setAutoDelete(false); + } + + void run() override + { + m_imageRequestedMutex->lock(); + *m_imageRequested = true; + m_imageRequestedCondition->wakeAll(); + m_imageRequestedMutex->unlock(); + m_providerRemovedMutex->lock(); + while (!*m_providerRemoved) + m_providerRemovedCond->wait(m_providerRemovedMutex); + m_providerRemovedMutex->unlock(); + emit finished(); + } + + QQuickTextureFactory *textureFactory() const + { + QImage image(50, 50, QImage::Format_RGB32); + auto texture = QQuickTextureFactory::textureFactoryForImage(image); + return texture; + } + + QMutex *m_providerRemovedMutex; + QWaitCondition *m_providerRemovedCond; + bool *m_providerRemoved; + QMutex *m_imageRequestedMutex; + QWaitCondition *m_imageRequestedCondition; + bool *m_imageRequested; + +}; + +class WaitingAsyncProvider : public QQuickAsyncImageProvider +{ +public: + WaitingAsyncProvider(QMutex *providerRemovedMutex, QWaitCondition *providerRemovedCond, bool *providerRemoved, QMutex *imageRequestedMutex, QWaitCondition *imageRequestedCond, bool *imageRequested) + : m_providerRemovedMutex(providerRemovedMutex), m_providerRemovedCond(providerRemovedCond), m_providerRemoved(providerRemoved), + m_imageRequestedMutex(imageRequestedMutex), m_imageRequestedCondition(imageRequestedCond), m_imageRequested(imageRequested) + { + } + + ~WaitingAsyncProvider() {} + + QQuickImageResponse *requestImageResponse(const QString &id, const QSize &requestedSize) + { + auto response = new WaitingAsyncImageResponse(m_providerRemovedMutex, m_providerRemovedCond, m_providerRemoved, m_imageRequestedMutex, m_imageRequestedCondition, m_imageRequested); + pool.start(response); + return response; + } + + QMutex *m_providerRemovedMutex; + QWaitCondition *m_providerRemovedCond; + bool *m_providerRemoved; + QMutex *m_imageRequestedMutex; + QWaitCondition *m_imageRequestedCondition; + bool *m_imageRequested; + QThreadPool pool; +}; + + +// QTBUG-76527 +void tst_qquickimageprovider::asyncImageThreadSafety() +{ + QQmlEngine engine; + QMutex providerRemovedMutex; + bool providerRemoved = false; + QWaitCondition providerRemovedCond; + QMutex imageRequestedMutex; + bool imageRequested = false; + QWaitCondition imageRequestedCond; + auto imageProvider = new WaitingAsyncProvider(&providerRemovedMutex, &providerRemovedCond, &providerRemoved, &imageRequestedMutex, &imageRequestedCond, &imageRequested); + engine.addImageProvider("test_waiting", imageProvider); + QVERIFY(engine.imageProvider("test_waiting") != nullptr); + auto privateEngine = QQmlEnginePrivate::get(&engine); + + QString componentStr = "import QtQuick 2.0\nItem { \n" + "Image { source: \"image://test_waiting/blue\"; }\n" + " }"; + QQmlComponent component(&engine); + component.setData(componentStr.toLatin1(), QUrl::fromLocalFile("")); + QScopedPointer<QObject> obj(component.create()); + QVERIFY(!obj.isNull()); + QWeakPointer<QQmlImageProviderBase> observer = privateEngine->imageProvider("test_waiting").toWeakRef(); + QVERIFY(!observer.isNull()); // engine still own the object + imageRequestedMutex.lock(); + while (!imageRequested) + imageRequestedCond.wait(&imageRequestedMutex); + imageRequestedMutex.unlock(); + engine.removeImageProvider("test_waiting"); + + QVERIFY(engine.imageProvider("test_waiting") == nullptr); + QVERIFY(!observer.isNull()); // lifetime has been extended + + providerRemovedMutex.lock(); + providerRemoved = true; + providerRemovedCond.wakeAll(); + providerRemovedMutex.unlock(); + + QTRY_VERIFY(observer.isNull()); // once the reply has finished, the imageprovider gets deleted +} + + QTEST_MAIN(tst_qquickimageprovider) #include "tst_qquickimageprovider.moc" |