From 5032ff5c2b48d53e4580bb7d50b1f6de081263b0 Mon Sep 17 00:00:00 2001 From: Ulf Hermann Date: Thu, 4 Apr 2019 15:41:18 +0200 Subject: Move workerscript to its own module Change-Id: I778cfe842ddf1c600a837d8f2061a338887eed95 Reviewed-by: Lars Knoll --- src/qmlworkerscript/qquickworkerscript.cpp | 673 +++++++++++++++++++++++++++++ 1 file changed, 673 insertions(+) create mode 100644 src/qmlworkerscript/qquickworkerscript.cpp (limited to 'src/qmlworkerscript/qquickworkerscript.cpp') diff --git a/src/qmlworkerscript/qquickworkerscript.cpp b/src/qmlworkerscript/qquickworkerscript.cpp new file mode 100644 index 0000000000..273ec2a7a6 --- /dev/null +++ b/src/qmlworkerscript/qquickworkerscript.cpp @@ -0,0 +1,673 @@ +/**************************************************************************** +** +** Copyright (C) 2016 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the QtQml module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 3 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL3 included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 3 requirements +** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 2.0 or (at your option) the GNU General +** Public license version 3 or any later version approved by the KDE Free +** Qt Foundation. The licenses are as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-2.0.html and +** https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include "qtqmlworkerscriptglobal_p.h" +#include "qquickworkerscript_p.h" +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#if QT_CONFIG(qml_network) +#include +#include "qqmlnetworkaccessmanagerfactory.h" +#endif + +#include +#include + +#include +#include +#include +#include +#include + +QT_BEGIN_NAMESPACE + +class WorkerDataEvent : public QEvent +{ +public: + enum Type { WorkerData = QEvent::User }; + + WorkerDataEvent(int workerId, const QByteArray &data); + virtual ~WorkerDataEvent(); + + int workerId() const; + QByteArray data() const; + +private: + int m_id; + QByteArray m_data; +}; + +class WorkerLoadEvent : public QEvent +{ +public: + enum Type { WorkerLoad = WorkerDataEvent::WorkerData + 1 }; + + WorkerLoadEvent(int workerId, const QUrl &url); + + int workerId() const; + QUrl url() const; + +private: + int m_id; + QUrl m_url; +}; + +class WorkerRemoveEvent : public QEvent +{ +public: + enum Type { WorkerRemove = WorkerLoadEvent::WorkerLoad + 1 }; + + WorkerRemoveEvent(int workerId); + + int workerId() const; + +private: + int m_id; +}; + +class WorkerErrorEvent : public QEvent +{ +public: + enum Type { WorkerError = WorkerRemoveEvent::WorkerRemove + 1 }; + + WorkerErrorEvent(const QQmlError &error); + + QQmlError error() const; + +private: + QQmlError m_error; +}; + +class QQuickWorkerScriptEnginePrivate : public QObject +{ + Q_OBJECT +public: + enum WorkerEventTypes { + WorkerDestroyEvent = QEvent::User + 100 + }; + + QQuickWorkerScriptEnginePrivate(QQmlEngine *eng); + + QQmlEngine *qmlengine; + + QMutex m_lock; + QWaitCondition m_wait; + + struct WorkerScript : public QV8Engine { + WorkerScript(int id, QQuickWorkerScriptEnginePrivate *parent); + ~WorkerScript() override; + +#if QT_CONFIG(qml_network) + QNetworkAccessManager *networkAccessManager() override; +#endif + + QQuickWorkerScriptEnginePrivate *p = nullptr; + QUrl source; + QQuickWorkerScript *owner = nullptr; +#if QT_CONFIG(qml_network) + QScopedPointer accessManager; +#endif + int id = -1; + }; + + QHash workers; + QV4::ReturnedValue getWorker(WorkerScript *); + + int m_nextId; + + static QV4::ReturnedValue method_sendMessage(const QV4::FunctionObject *, const QV4::Value *thisObject, const QV4::Value *argv, int argc); + +signals: + void stopThread(); + +protected: + bool event(QEvent *) override; + +private: + void processMessage(int, const QByteArray &); + void processLoad(int, const QUrl &); + void reportScriptException(WorkerScript *, const QQmlError &error); +}; + +QQuickWorkerScriptEnginePrivate::QQuickWorkerScriptEnginePrivate(QQmlEngine *engine) +: qmlengine(engine), m_nextId(0) +{ +} + +QV4::ReturnedValue QQuickWorkerScriptEnginePrivate::method_sendMessage(const QV4::FunctionObject *b, + const QV4::Value *, const QV4::Value *argv, int argc) +{ + QV4::Scope scope(b); + WorkerScript *script = static_cast(scope.engine->v8Engine); + + QV4::ScopedValue v(scope, argc > 0 ? argv[0] : QV4::Value::undefinedValue()); + QByteArray data = QV4::Serialize::serialize(v, scope.engine); + + QMutexLocker locker(&script->p->m_lock); + if (script && script->owner) + QCoreApplication::postEvent(script->owner, new WorkerDataEvent(0, data)); + + return QV4::Encode::undefined(); +} + +bool QQuickWorkerScriptEnginePrivate::event(QEvent *event) +{ + if (event->type() == (QEvent::Type)WorkerDataEvent::WorkerData) { + WorkerDataEvent *workerEvent = static_cast(event); + processMessage(workerEvent->workerId(), workerEvent->data()); + return true; + } else if (event->type() == (QEvent::Type)WorkerLoadEvent::WorkerLoad) { + WorkerLoadEvent *workerEvent = static_cast(event); + processLoad(workerEvent->workerId(), workerEvent->url()); + return true; + } else if (event->type() == (QEvent::Type)WorkerDestroyEvent) { + emit stopThread(); + return true; + } else if (event->type() == (QEvent::Type)WorkerRemoveEvent::WorkerRemove) { + QMutexLocker locker(&m_lock); + WorkerRemoveEvent *workerEvent = static_cast(event); + QHash::iterator itr = workers.find(workerEvent->workerId()); + if (itr != workers.end()) { + delete itr.value(); + workers.erase(itr); + } + return true; + } else { + return QObject::event(event); + } +} + +void QQuickWorkerScriptEnginePrivate::processMessage(int id, const QByteArray &data) +{ + WorkerScript *script = workers.value(id); + if (!script) + return; + + QV4::ExecutionEngine *v4 = QV8Engine::getV4(script); + QV4::Scope scope(v4); + QV4::ScopedString v(scope); + QV4::ScopedObject worker(scope, v4->globalObject->get((v = v4->newString(QStringLiteral("WorkerScript"))))); + QV4::ScopedFunctionObject onmessage(scope); + if (worker) + onmessage = worker->get((v = v4->newString(QStringLiteral("onMessage")))); + + if (!onmessage) + return; + + QV4::ScopedValue value(scope, QV4::Serialize::deserialize(data, v4)); + + QV4::JSCallData jsCallData(scope, 1); + *jsCallData->thisObject = v4->global(); + jsCallData->args[0] = value; + onmessage->call(jsCallData); + if (scope.hasException()) { + QQmlError error = scope.engine->catchExceptionAsQmlError(); + reportScriptException(script, error); + } +} + +void QQuickWorkerScriptEnginePrivate::processLoad(int id, const QUrl &url) +{ + if (url.isRelative()) + return; + + QString fileName = QQmlFile::urlToLocalFileOrQrc(url); + + WorkerScript *script = workers.value(id); + if (!script) + return; + + QV4::ExecutionEngine *v4 = QV8Engine::getV4(script); + + script->source = url; + + if (fileName.endsWith(QLatin1String(".mjs"))) { + auto moduleUnit = v4->loadModule(url); + if (moduleUnit) { + if (moduleUnit->instantiate(v4)) + moduleUnit->evaluate(); + } else { + v4->throwError(QStringLiteral("Could not load module file")); + } + } else { + QString error; + QV4::Scope scope(v4); + QScopedPointer program; + program.reset(QV4::Script::createFromFileOrCache(v4, /*qmlContext*/nullptr, fileName, url, &error)); + if (program.isNull()) { + if (!error.isEmpty()) + qWarning().nospace() << error; + return; + } + + if (!v4->hasException) + program->run(); + } + + if (v4->hasException) { + QQmlError error = v4->catchExceptionAsQmlError(); + reportScriptException(script, error); + } +} + +void QQuickWorkerScriptEnginePrivate::reportScriptException(WorkerScript *script, + const QQmlError &error) +{ + QMutexLocker locker(&script->p->m_lock); + if (script->owner) + QCoreApplication::postEvent(script->owner, new WorkerErrorEvent(error)); +} + +WorkerDataEvent::WorkerDataEvent(int workerId, const QByteArray &data) +: QEvent((QEvent::Type)WorkerData), m_id(workerId), m_data(data) +{ +} + +WorkerDataEvent::~WorkerDataEvent() +{ +} + +int WorkerDataEvent::workerId() const +{ + return m_id; +} + +QByteArray WorkerDataEvent::data() const +{ + return m_data; +} + +WorkerLoadEvent::WorkerLoadEvent(int workerId, const QUrl &url) +: QEvent((QEvent::Type)WorkerLoad), m_id(workerId), m_url(url) +{ +} + +int WorkerLoadEvent::workerId() const +{ + return m_id; +} + +QUrl WorkerLoadEvent::url() const +{ + return m_url; +} + +WorkerRemoveEvent::WorkerRemoveEvent(int workerId) +: QEvent((QEvent::Type)WorkerRemove), m_id(workerId) +{ +} + +int WorkerRemoveEvent::workerId() const +{ + return m_id; +} + +WorkerErrorEvent::WorkerErrorEvent(const QQmlError &error) +: QEvent((QEvent::Type)WorkerError), m_error(error) +{ +} + +QQmlError WorkerErrorEvent::error() const +{ + return m_error; +} + +QQuickWorkerScriptEngine::QQuickWorkerScriptEngine(QQmlEngine *parent) +: QThread(parent), d(new QQuickWorkerScriptEnginePrivate(parent)) +{ + d->m_lock.lock(); + connect(d, SIGNAL(stopThread()), this, SLOT(quit()), Qt::DirectConnection); + start(QThread::LowestPriority); + d->m_wait.wait(&d->m_lock); + d->moveToThread(this); + d->m_lock.unlock(); +} + +QQuickWorkerScriptEngine::~QQuickWorkerScriptEngine() +{ + d->m_lock.lock(); + QCoreApplication::postEvent(d, new QEvent((QEvent::Type)QQuickWorkerScriptEnginePrivate::WorkerDestroyEvent)); + d->m_lock.unlock(); + + //We have to force to cleanup the main thread's event queue here + //to make sure the main GUI release all pending locks/wait conditions which + //some worker script/agent are waiting for (QQmlListModelWorkerAgent::sync() for example). + while (!isFinished()) { + // We can't simply wait here, because the worker thread will not terminate + // until the main thread processes the last data event it generates + QCoreApplication::processEvents(); + yieldCurrentThread(); + } + + d->deleteLater(); +} + +QQuickWorkerScriptEnginePrivate::WorkerScript::WorkerScript(int id, QQuickWorkerScriptEnginePrivate *parent) + : QV8Engine(new QV4::ExecutionEngine) + , p(parent) + , id(id) +{ + m_v4Engine->v8Engine = this; + + initQmlGlobalObject(); + + QV4::Scope scope(m_v4Engine); + QV4::ScopedObject api(scope, scope.engine->newObject()); + QV4::ScopedString name(scope, m_v4Engine->newString(QStringLiteral("sendMessage"))); + QV4::ScopedValue sendMessage(scope, QV4::FunctionObject::createBuiltinFunction(m_v4Engine, name, method_sendMessage, 1)); + api->put(QV4::ScopedString(scope, scope.engine->newString(QStringLiteral("sendMessage"))), sendMessage); + m_v4Engine->globalObject->put(QV4::ScopedString(scope, scope.engine->newString(QStringLiteral("WorkerScript"))), api); +} + +QQuickWorkerScriptEnginePrivate::WorkerScript::~WorkerScript() +{ + delete m_v4Engine; +} + +#if QT_CONFIG(qml_network) +QNetworkAccessManager *QQuickWorkerScriptEnginePrivate::WorkerScript::networkAccessManager() +{ + if (!accessManager) { + if (p->qmlengine && p->qmlengine->networkAccessManagerFactory()) { + accessManager.reset(p->qmlengine->networkAccessManagerFactory()->create(p)); + } else { + accessManager.reset(new QNetworkAccessManager(p)); + } + } + return accessManager.data(); +} +#endif + +int QQuickWorkerScriptEngine::registerWorkerScript(QQuickWorkerScript *owner) +{ + typedef QQuickWorkerScriptEnginePrivate::WorkerScript WorkerScript; + WorkerScript *script = new WorkerScript(d->m_nextId++, d); + + script->owner = owner; + + d->m_lock.lock(); + d->workers.insert(script->id, script); + d->m_lock.unlock(); + + return script->id; +} + +void QQuickWorkerScriptEngine::removeWorkerScript(int id) +{ + QQuickWorkerScriptEnginePrivate::WorkerScript* script = d->workers.value(id); + if (script) { + script->owner = nullptr; + QCoreApplication::postEvent(d, new WorkerRemoveEvent(id)); + } +} + +void QQuickWorkerScriptEngine::executeUrl(int id, const QUrl &url) +{ + QCoreApplication::postEvent(d, new WorkerLoadEvent(id, url)); +} + +void QQuickWorkerScriptEngine::sendMessage(int id, const QByteArray &data) +{ + QCoreApplication::postEvent(d, new WorkerDataEvent(id, data)); +} + +void QQuickWorkerScriptEngine::run() +{ + d->m_lock.lock(); + + d->m_wait.wakeAll(); + + d->m_lock.unlock(); + + exec(); + + qDeleteAll(d->workers); + d->workers.clear(); +} + + +/*! + \qmltype WorkerScript + \instantiates QQuickWorkerScript + \ingroup qtquick-threading + \inqmlmodule QtQml + \brief Enables the use of threads in a Qt Quick application. + + Use WorkerScript to run operations in a new thread. + This is useful for running operations in the background so + that the main GUI thread is not blocked. + + Messages can be passed between the new thread and the parent thread + using \l sendMessage() and the \c onMessage() handler. + + An example: + + \snippet qml/workerscript/workerscript.qml 0 + + The above worker script specifies a JavaScript file, "script.mjs", that handles + the operations to be performed in the new thread. Here is \c script.mjs: + + \quotefile qml/workerscript/script.mjs + + When the user clicks anywhere within the rectangle, \c sendMessage() is + called, triggering the \tt WorkerScript.onMessage() handler in + \tt script.mjs. This in turn sends a reply message that is then received + by the \tt onMessage() handler of \tt myWorker. + + The example uses a script that is an ECMAScript module, because it has the ".mjs" extension. + It can use import statements to access functionality from other modules and it is run in JavaScript + strict mode. + + If a worker script has the extension ".js" instead, then it is considered to contain plain JavaScript + statements and it is run in non-strict mode. + + \note Each WorkerScript element will instantiate a separate JavaScript engine to ensure perfect + isolation and thread-safety. If the impact of that results in a memory consumption that is too + high for your environment, then consider sharing a WorkerScript element. + + \section3 Restrictions + + Since the \c WorkerScript.onMessage() function is run in a separate thread, the + JavaScript file is evaluated in a context separate from the main QML engine. This means + that unlike an ordinary JavaScript file that is imported into QML, the \c script.mjs + in the above example cannot access the properties, methods or other attributes + of the QML item, nor can it access any context properties set on the QML object + through QQmlContext. + + Additionally, there are restrictions on the types of values that can be passed to and + from the worker script. See the sendMessage() documentation for details. + + Worker scripts that are plain JavaScript sources can not use \l {qtqml-javascript-imports.html}{.import} syntax. + Scripts that are ECMAScript modules can freely use import and export statements. + + \sa {Qt Quick Examples - Threading}, + {Threaded ListModel Example} +*/ +QQuickWorkerScript::QQuickWorkerScript(QObject *parent) +: QObject(parent), m_engine(nullptr), m_scriptId(-1), m_componentComplete(true) +{ +} + +QQuickWorkerScript::~QQuickWorkerScript() +{ + if (m_scriptId != -1) m_engine->removeWorkerScript(m_scriptId); +} + +/*! + \qmlproperty url WorkerScript::source + + This holds the url of the JavaScript file that implements the + \tt WorkerScript.onMessage() handler for threaded operations. + + If the file name component of the url ends with ".mjs", then the script + is parsed as an ECMAScript module and run in strict mode. Otherwise it is considered to be + plain script. +*/ +QUrl QQuickWorkerScript::source() const +{ + return m_source; +} + +void QQuickWorkerScript::setSource(const QUrl &source) +{ + if (m_source == source) + return; + + m_source = source; + + if (engine()) + m_engine->executeUrl(m_scriptId, m_source); + + emit sourceChanged(); +} + +/*! + \qmlmethod WorkerScript::sendMessage(jsobject message) + + Sends the given \a message to a worker script handler in another + thread. The other worker script handler can receive this message + through the onMessage() handler. + + The \c message object may only contain values of the following + types: + + \list + \li boolean, number, string + \li JavaScript objects and arrays + \li ListModel objects (any other type of QObject* is not allowed) + \endlist + + All objects and arrays are copied to the \c message. With the exception + of ListModel objects, any modifications by the other thread to an object + passed in \c message will not be reflected in the original object. +*/ +void QQuickWorkerScript::sendMessage(QQmlV4Function *args) +{ + if (!engine()) { + qWarning("QQuickWorkerScript: Attempt to send message before WorkerScript establishment"); + return; + } + + QV4::Scope scope(args->v4engine()); + QV4::ScopedValue argument(scope, QV4::Value::undefinedValue()); + if (args->length() != 0) + argument = (*args)[0]; + + m_engine->sendMessage(m_scriptId, QV4::Serialize::serialize(argument, scope.engine)); +} + +void QQuickWorkerScript::classBegin() +{ + m_componentComplete = false; +} + +QQuickWorkerScriptEngine *QQuickWorkerScript::engine() +{ + if (m_engine) return m_engine; + if (m_componentComplete) { + QQmlEngine *engine = qmlEngine(this); + if (!engine) { + qWarning("QQuickWorkerScript: engine() called without qmlEngine() set"); + return nullptr; + } + + QQmlEnginePrivate *enginePrivate = QQmlEnginePrivate::get(engine); + if (enginePrivate->workerScriptEngine == nullptr) + enginePrivate->workerScriptEngine = new QQuickWorkerScriptEngine(engine); + m_engine = qobject_cast(enginePrivate->workerScriptEngine); + Q_ASSERT(m_engine); + m_scriptId = m_engine->registerWorkerScript(this); + + if (m_source.isValid()) + m_engine->executeUrl(m_scriptId, m_source); + + return m_engine; + } + return nullptr; +} + +void QQuickWorkerScript::componentComplete() +{ + m_componentComplete = true; + engine(); // Get it started now. +} + +/*! + \qmlsignal WorkerScript::message(jsobject msg) + + This signal is emitted when a message \a msg is received from a worker + script in another thread through a call to sendMessage(). + + The corresponding handler is \c onMessage. +*/ + +bool QQuickWorkerScript::event(QEvent *event) +{ + if (event->type() == (QEvent::Type)WorkerDataEvent::WorkerData) { + if (QQmlEngine *engine = qmlEngine(this)) { + QV4::ExecutionEngine *v4 = engine->handle(); + WorkerDataEvent *workerEvent = static_cast(event); + emit message(QJSValue(v4, QV4::Serialize::deserialize(workerEvent->data(), v4))); + } + return true; + } else if (event->type() == (QEvent::Type)WorkerErrorEvent::WorkerError) { + WorkerErrorEvent *workerEvent = static_cast(event); + QQmlEnginePrivate::warning(qmlEngine(this), workerEvent->error()); + return true; + } else { + return QObject::event(event); + } +} + +QT_END_NAMESPACE + +#include + +#include "moc_qquickworkerscript_p.cpp" -- cgit v1.2.3