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

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