1/****************************************************************************
2**
3** Copyright (C) 2016 The Qt Company Ltd.
4** Contact: https://www.qt.io/licensing/
5**
6** This file is part of the QtQml module of the Qt Toolkit.
7**
8** $QT_BEGIN_LICENSE:LGPL$
9** Commercial License Usage
10** Licensees holding valid commercial Qt licenses may use this file in
11** accordance with the commercial license agreement provided with the
12** Software or, alternatively, in accordance with the terms contained in
13** a written agreement between you and The Qt Company. For licensing terms
14** and conditions see https://www.qt.io/terms-conditions. For further
15** information use the contact form at https://www.qt.io/contact-us.
16**
17** GNU Lesser General Public License Usage
18** Alternatively, this file may be used under the terms of the GNU Lesser
19** General Public License version 3 as published by the Free Software
20** Foundation and appearing in the file LICENSE.LGPL3 included in the
21** packaging of this file. Please review the following information to
22** ensure the GNU Lesser General Public License version 3 requirements
23** will be met: https://www.gnu.org/licenses/lgpl-3.0.html.
24**
25** GNU General Public License Usage
26** Alternatively, this file may be used under the terms of the GNU
27** General Public License version 2.0 or (at your option) the GNU General
28** Public license version 3 or any later version approved by the KDE Free
29** Qt Foundation. The licenses are as published by the Free Software
30** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3
31** included in the packaging of this file. Please review the following
32** information to ensure the GNU General Public License requirements will
33** be met: https://www.gnu.org/licenses/gpl-2.0.html and
34** https://www.gnu.org/licenses/gpl-3.0.html.
35**
36** $QT_END_LICENSE$
37**
38****************************************************************************/
39
40#include "qtqmlworkerscriptglobal_p.h"
41#include "qquickworkerscript_p.h"
42#include <private/qqmlengine_p.h>
43#include <private/qqmlexpression_p.h>
44
45#include <QtCore/qcoreevent.h>
46#include <QtCore/qcoreapplication.h>
47#include <QtCore/qdebug.h>
48#include <QtQml/qjsengine.h>
49#include <QtCore/qmutex.h>
50#include <QtCore/qwaitcondition.h>
51#include <QtCore/qfile.h>
52#include <QtCore/qdatetime.h>
53#include <QtQml/qqmlinfo.h>
54#include <QtQml/qqmlfile.h>
55#if QT_CONFIG(qml_network)
56#include <QtNetwork/qnetworkaccessmanager.h>
57#include "qqmlnetworkaccessmanagerfactory.h"
58#endif
59
60#include <private/qv4serialize_p.h>
61
62#include <private/qv4value_p.h>
63#include <private/qv4functionobject_p.h>
64#include <private/qv4script_p.h>
65#include <private/qv4scopedvalue_p.h>
66#include <private/qv4jscall_p.h>
67
68QT_BEGIN_NAMESPACE
69
70class WorkerDataEvent : public QEvent
71{
72public:
73 enum Type { WorkerData = QEvent::User };
74
75 WorkerDataEvent(int workerId, const QByteArray &data);
76 virtual ~WorkerDataEvent();
77
78 int workerId() const;
79 QByteArray data() const;
80
81private:
82 int m_id;
83 QByteArray m_data;
84};
85
86class WorkerLoadEvent : public QEvent
87{
88public:
89 enum Type { WorkerLoad = WorkerDataEvent::WorkerData + 1 };
90
91 WorkerLoadEvent(int workerId, const QUrl &url);
92
93 int workerId() const;
94 QUrl url() const;
95
96private:
97 int m_id;
98 QUrl m_url;
99};
100
101class WorkerRemoveEvent : public QEvent
102{
103public:
104 enum Type { WorkerRemove = WorkerLoadEvent::WorkerLoad + 1 };
105
106 WorkerRemoveEvent(int workerId);
107
108 int workerId() const;
109
110private:
111 int m_id;
112};
113
114class WorkerErrorEvent : public QEvent
115{
116public:
117 enum Type { WorkerError = WorkerRemoveEvent::WorkerRemove + 1 };
118
119 WorkerErrorEvent(const QQmlError &error);
120
121 QQmlError error() const;
122
123private:
124 QQmlError m_error;
125};
126
127struct WorkerScript : public QV4::ExecutionEngine::Deletable
128{
129 WorkerScript(QV4::ExecutionEngine *);
130 ~WorkerScript() = default;
131
132 QQuickWorkerScriptEnginePrivate *p = nullptr;
133 QUrl source;
134 QQuickWorkerScript *owner = nullptr;
135#if QT_CONFIG(qml_network)
136 QScopedPointer<QNetworkAccessManager> scriptLocalNAM;
137#endif
138};
139
140V4_DEFINE_EXTENSION(WorkerScript, workerScriptExtension);
141
142class QQuickWorkerScriptEnginePrivate : public QObject
143{
144 Q_OBJECT
145public:
146 enum WorkerEventTypes {
147 WorkerDestroyEvent = QEvent::User + 100
148 };
149
150 QQuickWorkerScriptEnginePrivate(QQmlEngine *eng);
151
152 QQmlEngine *qmlengine;
153
154 QMutex m_lock;
155 QWaitCondition m_wait;
156
157 QHash<int, QV4::ExecutionEngine *> workers;
158
159 int m_nextId;
160
161 static QV4::ReturnedValue method_sendMessage(const QV4::FunctionObject *, const QV4::Value *thisObject, const QV4::Value *argv, int argc);
162
163signals:
164 void stopThread();
165
166protected:
167 bool event(QEvent *) override;
168
169private:
170 void processMessage(int, const QByteArray &);
171 void processLoad(int, const QUrl &);
172 void reportScriptException(WorkerScript *, const QQmlError &error);
173};
174
175QQuickWorkerScriptEnginePrivate::QQuickWorkerScriptEnginePrivate(QQmlEngine *engine)
176: qmlengine(engine), m_nextId(0)
177{
178}
179
180QV4::ReturnedValue QQuickWorkerScriptEnginePrivate::method_sendMessage(const QV4::FunctionObject *b,
181 const QV4::Value *, const QV4::Value *argv, int argc)
182{
183 QV4::Scope scope(b);
184 const WorkerScript *script = workerScriptExtension(engine: scope.engine);
185 Q_ASSERT(script);
186
187 QV4::ScopedValue v(scope, argc > 0 ? argv[0] : QV4::Value::undefinedValue());
188 QByteArray data = QV4::Serialize::serialize(v, scope.engine);
189
190 QMutexLocker locker(&script->p->m_lock);
191 if (script->owner)
192 QCoreApplication::postEvent(receiver: script->owner, event: new WorkerDataEvent(0, data));
193
194 return QV4::Encode::undefined();
195}
196
197bool QQuickWorkerScriptEnginePrivate::event(QEvent *event)
198{
199 if (event->type() == (QEvent::Type)WorkerDataEvent::WorkerData) {
200 WorkerDataEvent *workerEvent = static_cast<WorkerDataEvent *>(event);
201 processMessage(workerEvent->workerId(), workerEvent->data());
202 return true;
203 } else if (event->type() == (QEvent::Type)WorkerLoadEvent::WorkerLoad) {
204 WorkerLoadEvent *workerEvent = static_cast<WorkerLoadEvent *>(event);
205 processLoad(workerEvent->workerId(), workerEvent->url());
206 return true;
207 } else if (event->type() == (QEvent::Type)WorkerDestroyEvent) {
208 emit stopThread();
209 return true;
210 } else if (event->type() == (QEvent::Type)WorkerRemoveEvent::WorkerRemove) {
211 QMutexLocker locker(&m_lock);
212 WorkerRemoveEvent *workerEvent = static_cast<WorkerRemoveEvent *>(event);
213 auto itr = workers.find(akey: workerEvent->workerId());
214 if (itr != workers.end()) {
215 delete itr.value();
216 workers.erase(it: itr);
217 }
218 return true;
219 } else {
220 return QObject::event(event);
221 }
222}
223
224void QQuickWorkerScriptEnginePrivate::processMessage(int id, const QByteArray &data)
225{
226 QV4::ExecutionEngine *engine = workers.value(akey: id);
227 if (!engine)
228 return;
229
230 QV4::Scope scope(engine);
231 QV4::ScopedString v(scope, engine->newString(QStringLiteral("WorkerScript")));
232 QV4::ScopedObject worker(scope, engine->globalObject->get(name: v));
233 QV4::ScopedFunctionObject onmessage(scope);
234 if (worker)
235 onmessage = worker->get(name: (v = engine->newString(QStringLiteral("onMessage"))));
236
237 if (!onmessage)
238 return;
239
240 QV4::ScopedValue value(scope, QV4::Serialize::deserialize(data, engine));
241
242 QV4::JSCallData jsCallData(scope, 1);
243 *jsCallData->thisObject = engine->global();
244 jsCallData->args[0] = value;
245 onmessage->call(data: jsCallData);
246 if (scope.hasException()) {
247 QQmlError error = scope.engine->catchExceptionAsQmlError();
248 WorkerScript *script = workerScriptExtension(engine);
249 reportScriptException(script, error);
250 }
251}
252
253void QQuickWorkerScriptEnginePrivate::processLoad(int id, const QUrl &url)
254{
255 if (url.isRelative())
256 return;
257
258 QString fileName = QQmlFile::urlToLocalFileOrQrc(url);
259
260 QV4::ExecutionEngine *engine = workers.value(akey: id);
261 if (!engine)
262 return;
263
264 WorkerScript *script = workerScriptExtension(engine);
265 script->source = url;
266
267 if (fileName.endsWith(s: QLatin1String(".mjs"))) {
268 auto moduleUnit = engine->loadModule(url: url);
269 if (moduleUnit) {
270 if (moduleUnit->instantiate(engine))
271 moduleUnit->evaluate();
272 } else {
273 engine->throwError(QStringLiteral("Could not load module file"));
274 }
275 } else {
276 QString error;
277 QV4::Scope scope(engine);
278 QScopedPointer<QV4::Script> program;
279 program.reset(other: QV4::Script::createFromFileOrCache(
280 engine, /*qmlContext*/nullptr, fileName, originalUrl: url, error: &error));
281 if (program.isNull()) {
282 if (!error.isEmpty())
283 qWarning().nospace() << error;
284 return;
285 }
286
287 if (!engine->hasException)
288 program->run();
289 }
290
291 if (engine->hasException)
292 reportScriptException(script, error: engine->catchExceptionAsQmlError());
293}
294
295void QQuickWorkerScriptEnginePrivate::reportScriptException(WorkerScript *script,
296 const QQmlError &error)
297{
298 QMutexLocker locker(&script->p->m_lock);
299 if (script->owner)
300 QCoreApplication::postEvent(receiver: script->owner, event: new WorkerErrorEvent(error));
301}
302
303WorkerDataEvent::WorkerDataEvent(int workerId, const QByteArray &data)
304: QEvent((QEvent::Type)WorkerData), m_id(workerId), m_data(data)
305{
306}
307
308WorkerDataEvent::~WorkerDataEvent()
309{
310}
311
312int WorkerDataEvent::workerId() const
313{
314 return m_id;
315}
316
317QByteArray WorkerDataEvent::data() const
318{
319 return m_data;
320}
321
322WorkerLoadEvent::WorkerLoadEvent(int workerId, const QUrl &url)
323: QEvent((QEvent::Type)WorkerLoad), m_id(workerId), m_url(url)
324{
325}
326
327int WorkerLoadEvent::workerId() const
328{
329 return m_id;
330}
331
332QUrl WorkerLoadEvent::url() const
333{
334 return m_url;
335}
336
337WorkerRemoveEvent::WorkerRemoveEvent(int workerId)
338: QEvent((QEvent::Type)WorkerRemove), m_id(workerId)
339{
340}
341
342int WorkerRemoveEvent::workerId() const
343{
344 return m_id;
345}
346
347WorkerErrorEvent::WorkerErrorEvent(const QQmlError &error)
348: QEvent((QEvent::Type)WorkerError), m_error(error)
349{
350}
351
352QQmlError WorkerErrorEvent::error() const
353{
354 return m_error;
355}
356
357QQuickWorkerScriptEngine::QQuickWorkerScriptEngine(QQmlEngine *parent)
358: QThread(parent), d(new QQuickWorkerScriptEnginePrivate(parent))
359{
360 d->m_lock.lock();
361 connect(sender: d, SIGNAL(stopThread()), receiver: this, SLOT(quit()), Qt::DirectConnection);
362 start(QThread::LowestPriority);
363 d->m_wait.wait(lockedMutex: &d->m_lock);
364 d->moveToThread(thread: this);
365 d->m_lock.unlock();
366}
367
368QQuickWorkerScriptEngine::~QQuickWorkerScriptEngine()
369{
370 d->m_lock.lock();
371 QCoreApplication::postEvent(receiver: d, event: new QEvent((QEvent::Type)QQuickWorkerScriptEnginePrivate::WorkerDestroyEvent));
372 d->m_lock.unlock();
373
374 //We have to force to cleanup the main thread's event queue here
375 //to make sure the main GUI release all pending locks/wait conditions which
376 //some worker script/agent are waiting for (QQmlListModelWorkerAgent::sync() for example).
377 while (!isFinished()) {
378 // We can't simply wait here, because the worker thread will not terminate
379 // until the main thread processes the last data event it generates
380 QCoreApplication::processEvents();
381 yieldCurrentThread();
382 }
383
384 delete d;
385}
386
387
388WorkerScript::WorkerScript(QV4::ExecutionEngine *engine)
389{
390 engine->initQmlGlobalObject();
391
392 QV4::Scope scope(engine);
393 QV4::ScopedObject api(scope, engine->newObject());
394 QV4::ScopedString sendMessageName(scope, engine->newString(QStringLiteral("sendMessage")));
395 QV4::ScopedFunctionObject sendMessage(
396 scope, QV4::FunctionObject::createBuiltinFunction(
397 engine, nameOrSymbol: sendMessageName,
398 code: QQuickWorkerScriptEnginePrivate::method_sendMessage, argumentCount: 1));
399 api->put(name: sendMessageName, v: sendMessage);
400 QV4::ScopedString workerScriptName(scope, engine->newString(QStringLiteral("WorkerScript")));
401 engine->globalObject->put(name: workerScriptName, v: api);
402
403#if QT_CONFIG(qml_network)
404 engine->networkAccessManager = [](QV4::ExecutionEngine *engine) {
405 WorkerScript *workerScript = workerScriptExtension(engine);
406 if (workerScript->scriptLocalNAM)
407 return workerScript->scriptLocalNAM.get();
408 if (auto *namFactory = workerScript->p->qmlengine->networkAccessManagerFactory())
409 workerScript->scriptLocalNAM.reset(other: namFactory->create(parent: workerScript->p));
410 else
411 workerScript->scriptLocalNAM.reset(other: new QNetworkAccessManager(workerScript->p));
412 return workerScript->scriptLocalNAM.get();
413 };
414#endif // qml_network
415}
416
417int QQuickWorkerScriptEngine::registerWorkerScript(QQuickWorkerScript *owner)
418{
419 const int id = d->m_nextId++;
420 auto *engine = new QV4::ExecutionEngine;
421
422 d->m_lock.lock();
423 d->workers.insert(akey: id, avalue: engine);
424 d->m_lock.unlock();
425
426 WorkerScript *script = workerScriptExtension(engine);
427 script->owner = owner;
428 script->p = d;
429
430 return id;
431}
432
433void QQuickWorkerScriptEngine::removeWorkerScript(int id)
434{
435 if (QV4::ExecutionEngine *engine = d->workers.value(akey: id)) {
436 workerScriptExtension(engine)->owner = nullptr;
437 QCoreApplication::postEvent(receiver: d, event: new WorkerRemoveEvent(id));
438 }
439}
440
441void QQuickWorkerScriptEngine::executeUrl(int id, const QUrl &url)
442{
443 QCoreApplication::postEvent(receiver: d, event: new WorkerLoadEvent(id, url));
444}
445
446void QQuickWorkerScriptEngine::sendMessage(int id, const QByteArray &data)
447{
448 QCoreApplication::postEvent(receiver: d, event: new WorkerDataEvent(id, data));
449}
450
451void QQuickWorkerScriptEngine::run()
452{
453 d->m_lock.lock();
454
455 d->m_wait.wakeAll();
456
457 d->m_lock.unlock();
458
459 exec();
460
461 qDeleteAll(c: d->workers);
462 d->workers.clear();
463}
464
465
466/*!
467 \qmltype WorkerScript
468 \instantiates QQuickWorkerScript
469 \ingroup qtquick-threading
470 \inqmlmodule QtQml.WorkerScript
471 \brief Enables the use of threads in a Qt Quick application.
472
473 Use WorkerScript to run operations in a new thread.
474 This is useful for running operations in the background so
475 that the main GUI thread is not blocked.
476
477 Messages can be passed between the new thread and the parent thread
478 using \l sendMessage() and the \c onMessage() handler.
479
480 An example:
481
482 \snippet qml/workerscript/workerscript.qml 0
483
484 The above worker script specifies a JavaScript file, "script.mjs", that handles
485 the operations to be performed in the new thread. Here is \c script.mjs:
486
487 \quotefile qml/workerscript/script.mjs
488
489 When the user clicks anywhere within the rectangle, \c sendMessage() is
490 called, triggering the \tt WorkerScript.onMessage() handler in
491 \tt script.mjs. This in turn sends a reply message that is then received
492 by the \tt onMessage() handler of \tt myWorker.
493
494 The example uses a script that is an ECMAScript module, because it has the ".mjs" extension.
495 It can use import statements to access functionality from other modules and it is run in JavaScript
496 strict mode.
497
498 If a worker script has the extension ".js" instead, then it is considered to contain plain JavaScript
499 statements and it is run in non-strict mode.
500
501 \note Each WorkerScript element will instantiate a separate JavaScript engine to ensure perfect
502 isolation and thread-safety. If the impact of that results in a memory consumption that is too
503 high for your environment, then consider sharing a WorkerScript element.
504
505 \section3 Restrictions
506
507 Since the \c WorkerScript.onMessage() function is run in a separate thread, the
508 JavaScript file is evaluated in a context separate from the main QML engine. This means
509 that unlike an ordinary JavaScript file that is imported into QML, the \c script.mjs
510 in the above example cannot access the properties, methods or other attributes
511 of the QML item, nor can it access any context properties set on the QML object
512 through QQmlContext.
513
514 Additionally, there are restrictions on the types of values that can be passed to and
515 from the worker script. See the sendMessage() documentation for details.
516
517 Worker scripts that are plain JavaScript sources can not use \l {qtqml-javascript-imports.html}{.import} syntax.
518 Scripts that are ECMAScript modules can freely use import and export statements.
519
520 \sa {Qt Quick Examples - Threading},
521 {Threaded ListModel Example}
522*/
523QQuickWorkerScript::QQuickWorkerScript(QObject *parent)
524: QObject(parent), m_engine(nullptr), m_scriptId(-1), m_componentComplete(true)
525{
526}
527
528QQuickWorkerScript::~QQuickWorkerScript()
529{
530 if (m_scriptId != -1) m_engine->removeWorkerScript(id: m_scriptId);
531}
532
533/*!
534 \qmlproperty url WorkerScript::source
535
536 This holds the url of the JavaScript file that implements the
537 \tt WorkerScript.onMessage() handler for threaded operations.
538
539 If the file name component of the url ends with ".mjs", then the script
540 is parsed as an ECMAScript module and run in strict mode. Otherwise it is considered to be
541 plain script.
542*/
543QUrl QQuickWorkerScript::source() const
544{
545 return m_source;
546}
547
548void QQuickWorkerScript::setSource(const QUrl &source)
549{
550 if (m_source == source)
551 return;
552
553 m_source = source;
554
555 if (engine())
556 m_engine->executeUrl(id: m_scriptId, url: m_source);
557
558 emit sourceChanged();
559}
560
561/*!
562 \qmlproperty bool WorkerScript::ready
563
564 This holds whether the WorkerScript has been initialized and is ready
565 for receiving messages via \tt WorkerScript.sendMessage().
566*/
567bool QQuickWorkerScript::ready() const
568{
569 return m_engine != nullptr;
570}
571
572/*!
573 \qmlmethod WorkerScript::sendMessage(jsobject message)
574
575 Sends the given \a message to a worker script handler in another
576 thread. The other worker script handler can receive this message
577 through the onMessage() handler.
578
579 The \c message object may only contain values of the following
580 types:
581
582 \list
583 \li boolean, number, string
584 \li JavaScript objects and arrays
585 \li ListModel objects (any other type of QObject* is not allowed)
586 \endlist
587
588 All objects and arrays are copied to the \c message. With the exception
589 of ListModel objects, any modifications by the other thread to an object
590 passed in \c message will not be reflected in the original object.
591*/
592void QQuickWorkerScript::sendMessage(QQmlV4Function *args)
593{
594 if (!engine()) {
595 qWarning(msg: "QQuickWorkerScript: Attempt to send message before WorkerScript establishment");
596 return;
597 }
598
599 QV4::Scope scope(args->v4engine());
600 QV4::ScopedValue argument(scope, QV4::Value::undefinedValue());
601 if (args->length() != 0)
602 argument = (*args)[0];
603
604 m_engine->sendMessage(id: m_scriptId, data: QV4::Serialize::serialize(argument, scope.engine));
605}
606
607void QQuickWorkerScript::classBegin()
608{
609 m_componentComplete = false;
610}
611
612QQuickWorkerScriptEngine *QQuickWorkerScript::engine()
613{
614 if (m_engine) return m_engine;
615 if (m_componentComplete) {
616 QQmlEngine *engine = qmlEngine(this);
617 if (!engine) {
618 qWarning(msg: "QQuickWorkerScript: engine() called without qmlEngine() set");
619 return nullptr;
620 }
621
622 QQmlEnginePrivate *enginePrivate = QQmlEnginePrivate::get(e: engine);
623 if (enginePrivate->workerScriptEngine == nullptr)
624 enginePrivate->workerScriptEngine = new QQuickWorkerScriptEngine(engine);
625 m_engine = qobject_cast<QQuickWorkerScriptEngine *>(object: enginePrivate->workerScriptEngine);
626 Q_ASSERT(m_engine);
627 m_scriptId = m_engine->registerWorkerScript(owner: this);
628
629 if (m_source.isValid())
630 m_engine->executeUrl(id: m_scriptId, url: m_source);
631
632 emit readyChanged();
633
634 return m_engine;
635 }
636 return nullptr;
637}
638
639void QQuickWorkerScript::componentComplete()
640{
641 m_componentComplete = true;
642 engine(); // Get it started now.
643}
644
645/*!
646 \qmlsignal WorkerScript::message(jsobject msg)
647
648 This signal is emitted when a message \a msg is received from a worker
649 script in another thread through a call to sendMessage().
650*/
651
652bool QQuickWorkerScript::event(QEvent *event)
653{
654 if (event->type() == (QEvent::Type)WorkerDataEvent::WorkerData) {
655 if (QQmlEngine *engine = qmlEngine(this)) {
656 QV4::ExecutionEngine *v4 = engine->handle();
657 WorkerDataEvent *workerEvent = static_cast<WorkerDataEvent *>(event);
658 emit message(messageObject: QJSValue(v4, QV4::Serialize::deserialize(workerEvent->data(), v4)));
659 }
660 return true;
661 } else if (event->type() == (QEvent::Type)WorkerErrorEvent::WorkerError) {
662 WorkerErrorEvent *workerEvent = static_cast<WorkerErrorEvent *>(event);
663 QQmlEnginePrivate::warning(qmlEngine(this), workerEvent->error());
664 return true;
665 } else {
666 return QObject::event(event);
667 }
668}
669
670QT_END_NAMESPACE
671
672#include <qquickworkerscript.moc>
673
674#include "moc_qquickworkerscript_p.cpp"
675

source code of qtdeclarative/src/qmlworkerscript/qquickworkerscript.cpp