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(QQmlEngine *eng);
116
117 QQmlEngine *qmlengine;
118
119 QMutex m_lock;
120 QWaitCondition m_wait;
121
122 // ExecutionEngines are owned by the worker script and created and deleted
123 // in the worker thread. QQuickWorkerScript instances, however, belong to
124 // the main thread. They are only inserted as place holders when creating
125 // the worker script.
126 QHash<int, QBiPointer<QV4::ExecutionEngine, QQuickWorkerScript>> workers;
127
128 int m_nextId;
129
130 static QV4::ReturnedValue method_sendMessage(const QV4::FunctionObject *, const QV4::Value *thisObject, const QV4::Value *argv, int argc);
131 QV4::ExecutionEngine *workerEngine(int id);
132
133signals:
134 void stopThread();
135
136protected:
137 bool event(QEvent *) override;
138
139private:
140 void processMessage(int, const QByteArray &);
141 void processLoad(int, const QUrl &);
142 void reportScriptException(WorkerScript *, const QQmlError &error);
143};
144
145QQuickWorkerScriptEnginePrivate::QQuickWorkerScriptEnginePrivate(QQmlEngine *engine)
146: qmlengine(engine), m_nextId(0)
147{
148}
149
150QV4::ReturnedValue QQuickWorkerScriptEnginePrivate::method_sendMessage(const QV4::FunctionObject *b,
151 const QV4::Value *, const QV4::Value *argv, int argc)
152{
153 QV4::Scope scope(b);
154 const WorkerScript *script = workerScriptExtension(engine: scope.engine);
155 Q_ASSERT(script);
156
157 QV4::ScopedValue v(scope, argc > 0 ? argv[0] : QV4::Value::undefinedValue());
158 QByteArray data = QV4::Serialize::serialize(v, scope.engine);
159
160 QMutexLocker locker(&script->p->m_lock);
161 if (script->owner)
162 QCoreApplication::postEvent(receiver: script->owner, event: new WorkerDataEvent(0, data));
163
164 return QV4::Encode::undefined();
165}
166
167bool QQuickWorkerScriptEnginePrivate::event(QEvent *event)
168{
169 if (event->type() == (QEvent::Type)WorkerDataEvent::WorkerData) {
170 WorkerDataEvent *workerEvent = static_cast<WorkerDataEvent *>(event);
171 processMessage(workerEvent->workerId(), workerEvent->data());
172 return true;
173 } else if (event->type() == (QEvent::Type)WorkerLoadEvent::WorkerLoad) {
174 WorkerLoadEvent *workerEvent = static_cast<WorkerLoadEvent *>(event);
175 processLoad(workerEvent->workerId(), workerEvent->url());
176 return true;
177 } else if (event->type() == (QEvent::Type)WorkerDestroyEvent) {
178 emit stopThread();
179 return true;
180 } else if (event->type() == (QEvent::Type)WorkerRemoveEvent::WorkerRemove) {
181 QMutexLocker locker(&m_lock);
182 WorkerRemoveEvent *workerEvent = static_cast<WorkerRemoveEvent *>(event);
183 auto itr = workers.constFind(key: workerEvent->workerId());
184 if (itr != workers.cend()) {
185 if (itr->isT1())
186 delete itr->asT1();
187 workers.erase(it: itr);
188 }
189 return true;
190 } else {
191 return QObject::event(event);
192 }
193}
194
195QV4::ExecutionEngine *QQuickWorkerScriptEnginePrivate::workerEngine(int id)
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), d(new QQuickWorkerScriptEnginePrivate(parent))
346{
347 d->m_lock.lock();
348 connect(sender: d, SIGNAL(stopThread()), receiver: this, SLOT(quit()), Qt::DirectConnection);
349 start(QThread::LowestPriority);
350 d->m_wait.wait(lockedMutex: &d->m_lock);
351 d->moveToThread(thread: this);
352 d->m_lock.unlock();
353}
354
355QQuickWorkerScriptEngine::~QQuickWorkerScriptEngine()
356{
357 d->m_lock.lock();
358 QCoreApplication::postEvent(receiver: d, event: new QEvent((QEvent::Type)QQuickWorkerScriptEnginePrivate::WorkerDestroyEvent));
359 d->m_lock.unlock();
360
361 //We have to force to cleanup the main thread's event queue here
362 //to make sure the main GUI release all pending locks/wait conditions which
363 //some worker script/agent are waiting for (QQmlListModelWorkerAgent::sync() for example).
364 while (!isFinished()) {
365 // We can't simply wait here, because the worker thread will not terminate
366 // until the main thread processes the last data event it generates
367 QCoreApplication::processEvents();
368 yieldCurrentThread();
369 }
370
371 delete d;
372}
373
374
375WorkerScript::WorkerScript(QV4::ExecutionEngine *engine)
376{
377 engine->initQmlGlobalObject();
378
379 QV4::Scope scope(engine);
380 QV4::ScopedObject api(scope, engine->newObject());
381 QV4::ScopedString sendMessageName(scope, engine->newString(QStringLiteral("sendMessage")));
382 QV4::ScopedFunctionObject sendMessage(
383 scope, QV4::FunctionObject::createBuiltinFunction(
384 engine, nameOrSymbol: sendMessageName,
385 code: QQuickWorkerScriptEnginePrivate::method_sendMessage, argumentCount: 1));
386 api->put(name: sendMessageName, v: sendMessage);
387 QV4::ScopedString workerScriptName(scope, engine->newString(QStringLiteral("WorkerScript")));
388 engine->globalObject->put(name: workerScriptName, v: api);
389
390#if QT_CONFIG(qml_network)
391 engine->networkAccessManager = [](QV4::ExecutionEngine *engine) {
392 WorkerScript *workerScript = workerScriptExtension(engine);
393 if (workerScript->scriptLocalNAM)
394 return workerScript->scriptLocalNAM.get();
395 if (auto *namFactory = workerScript->p->qmlengine->networkAccessManagerFactory())
396 workerScript->scriptLocalNAM.reset(other: namFactory->create(parent: workerScript->p));
397 else
398 workerScript->scriptLocalNAM.reset(other: new QNetworkAccessManager(workerScript->p));
399 return workerScript->scriptLocalNAM.get();
400 };
401#endif // qml_network
402}
403
404int QQuickWorkerScriptEngine::registerWorkerScript(QQuickWorkerScript *owner)
405{
406 const int id = d->m_nextId++;
407
408 d->m_lock.lock();
409 d->workers.insert(key: id, value: owner);
410 d->m_lock.unlock();
411
412 return id;
413}
414
415void QQuickWorkerScriptEngine::removeWorkerScript(int id)
416{
417 const auto it = d->workers.constFind(key: id);
418 if (it == d->workers.cend())
419 return;
420
421 if (it->isT1()) {
422 QV4::ExecutionEngine *engine = it->asT1();
423 workerScriptExtension(engine)->owner = nullptr;
424 }
425 QCoreApplication::postEvent(receiver: d, event: new WorkerRemoveEvent(id));
426}
427
428void QQuickWorkerScriptEngine::executeUrl(int id, const QUrl &url)
429{
430 QCoreApplication::postEvent(receiver: d, event: new WorkerLoadEvent(id, url));
431}
432
433void QQuickWorkerScriptEngine::sendMessage(int id, const QByteArray &data)
434{
435 QCoreApplication::postEvent(receiver: d, event: new WorkerDataEvent(id, data));
436}
437
438void QQuickWorkerScriptEngine::run()
439{
440 d->m_lock.lock();
441 d->m_wait.wakeAll();
442 d->m_lock.unlock();
443
444 exec();
445
446 for (auto it = d->workers.begin(), end = d->workers.end(); it != end; ++it) {
447 if (it->isT1())
448 delete it->asT1();
449 }
450
451 d->workers.clear();
452}
453
454
455/*!
456 \qmltype WorkerScript
457 \nativetype QQuickWorkerScript
458 \ingroup qtquick-threading
459 \inqmlmodule QtQml.WorkerScript
460 \brief Enables the use of threads in a Qt Quick application.
461
462 Use WorkerScript to run operations in a new thread.
463 This is useful for running operations in the background so
464 that the main GUI thread is not blocked.
465
466 Messages can be passed between the new thread and the parent thread
467 using \l sendMessage() and the \c onMessage() handler.
468
469 An example:
470
471 \snippet qml/workerscript/workerscript.qml 0
472
473 The above worker script specifies a JavaScript file, "script.mjs", that handles
474 the operations to be performed in the new thread. Here is \c script.mjs:
475
476 \quotefile qml/workerscript/script.mjs
477
478 When the user clicks anywhere within the rectangle, \c sendMessage() is
479 called, triggering the \tt WorkerScript.onMessage() handler in
480 \tt script.mjs. This in turn sends a reply message that is then received
481 by the \tt onMessage() handler of \tt myWorker.
482
483 The example uses a script that is an ECMAScript module, because it has the ".mjs" extension.
484 It can use import statements to access functionality from other modules and it is run in JavaScript
485 strict mode.
486
487 If a worker script has the extension ".js" instead, then it is considered to contain plain JavaScript
488 statements and it is run in non-strict mode.
489
490 \note Each WorkerScript element will instantiate a separate JavaScript engine to ensure perfect
491 isolation and thread-safety. If the impact of that results in a memory consumption that is too
492 high for your environment, then consider sharing a WorkerScript element.
493
494 \section3 Restrictions
495
496 Since the \c WorkerScript.onMessage() function is run in a separate thread, the
497 JavaScript file is evaluated in a context separate from the main QML engine. This means
498 that unlike an ordinary JavaScript file that is imported into QML, the \c script.mjs
499 in the above example cannot access the properties, methods or other attributes
500 of the QML item, nor can it access any context properties set on the QML object
501 through QQmlContext.
502
503 Additionally, there are restrictions on the types of values that can be passed to and
504 from the worker script. See the sendMessage() documentation for details.
505
506 Worker scripts that are plain JavaScript sources can not use \l {qtqml-javascript-imports.html}{.import} syntax.
507 Scripts that are ECMAScript modules can freely use import and export statements.
508*/
509QQuickWorkerScript::QQuickWorkerScript(QObject *parent)
510: QObject(parent), m_engine(nullptr), m_scriptId(-1), m_componentComplete(true)
511{
512}
513
514QQuickWorkerScript::~QQuickWorkerScript()
515{
516 if (m_scriptId != -1) m_engine->removeWorkerScript(id: m_scriptId);
517}
518
519/*!
520 \qmlproperty url WorkerScript::source
521
522 This holds the url of the JavaScript file that implements the
523 \tt WorkerScript.onMessage() handler for threaded operations.
524
525 If the file name component of the url ends with ".mjs", then the script
526 is parsed as an ECMAScript module and run in strict mode. Otherwise it is considered to be
527 plain script.
528*/
529QUrl QQuickWorkerScript::source() const
530{
531 return m_source;
532}
533
534void QQuickWorkerScript::setSource(const QUrl &source)
535{
536 if (m_source == source)
537 return;
538
539 m_source = source;
540
541 if (engine()) {
542 const QQmlContext *context = qmlContext(this);
543 m_engine->executeUrl(id: m_scriptId, url: context ? context->resolvedUrl(m_source) : m_source);
544 }
545
546 emit sourceChanged();
547}
548
549/*!
550 \qmlproperty bool WorkerScript::ready
551
552 This holds whether the WorkerScript has been initialized and is ready
553 for receiving messages via \tt WorkerScript.sendMessage().
554*/
555bool QQuickWorkerScript::ready() const
556{
557 return m_engine != nullptr;
558}
559
560/*!
561 \qmlmethod WorkerScript::sendMessage(jsobject message)
562
563 Sends the given \a message to a worker script handler in another
564 thread. The other worker script handler can receive this message
565 through the onMessage() handler.
566
567 The \c message object may only contain values of the following
568 types:
569
570 \list
571 \li boolean, number, string
572 \li JavaScript objects and arrays
573 \li ListModel objects (any other type of QObject* is not allowed)
574 \endlist
575
576 All objects and arrays are copied to the \c message. With the exception
577 of ListModel objects, any modifications by the other thread to an object
578 passed in \c message will not be reflected in the original object.
579*/
580void QQuickWorkerScript::sendMessage(QQmlV4FunctionPtr args)
581{
582 if (!engine()) {
583 qWarning(msg: "QQuickWorkerScript: Attempt to send message before WorkerScript establishment");
584 return;
585 }
586
587 QV4::Scope scope(args->v4engine());
588 QV4::ScopedValue argument(scope, QV4::Value::undefinedValue());
589 if (args->length() != 0)
590 argument = (*args)[0];
591
592 m_engine->sendMessage(id: m_scriptId, data: QV4::Serialize::serialize(argument, scope.engine));
593}
594
595void QQuickWorkerScript::classBegin()
596{
597 m_componentComplete = false;
598}
599
600QQuickWorkerScriptEngine *QQuickWorkerScript::engine()
601{
602 if (m_engine) return m_engine;
603 if (m_componentComplete) {
604 const QQmlContext *context = qmlContext(this);
605 QQmlEngine *engine = context ? context->engine() : nullptr;
606 if (!engine) {
607 qWarning(msg: "QQuickWorkerScript: engine() called without qmlEngine() set");
608 return nullptr;
609 }
610
611 QQmlEnginePrivate *enginePrivate = QQmlEnginePrivate::get(e: engine);
612 if (enginePrivate->workerScriptEngine == nullptr)
613 enginePrivate->workerScriptEngine = new QQuickWorkerScriptEngine(engine);
614 m_engine = qobject_cast<QQuickWorkerScriptEngine *>(object: enginePrivate->workerScriptEngine);
615 Q_ASSERT(m_engine);
616 m_scriptId = m_engine->registerWorkerScript(owner: this);
617
618 if (m_source.isValid())
619 m_engine->executeUrl(id: m_scriptId, url: context->resolvedUrl(m_source));
620
621 emit readyChanged();
622
623 return m_engine;
624 }
625 return nullptr;
626}
627
628void QQuickWorkerScript::componentComplete()
629{
630 m_componentComplete = true;
631 engine(); // Get it started now.
632}
633
634/*!
635 \qmlsignal WorkerScript::message(jsobject msg)
636
637 This signal is emitted when a message \a msg is received from a worker
638 script in another thread through a call to sendMessage().
639*/
640
641bool QQuickWorkerScript::event(QEvent *event)
642{
643 if (event->type() == (QEvent::Type)WorkerDataEvent::WorkerData) {
644 if (QQmlEngine *engine = qmlEngine(this)) {
645 QV4::ExecutionEngine *v4 = engine->handle();
646 WorkerDataEvent *workerEvent = static_cast<WorkerDataEvent *>(event);
647 emit message(messageObject: QJSValuePrivate::fromReturnedValue(
648 d: QV4::Serialize::deserialize(workerEvent->data(), v4)));
649 }
650 return true;
651 } else if (event->type() == (QEvent::Type)WorkerErrorEvent::WorkerError) {
652 WorkerErrorEvent *workerEvent = static_cast<WorkerErrorEvent *>(event);
653 QQmlEnginePrivate::warning(qmlEngine(this), workerEvent->error());
654 return true;
655 } else {
656 return QObject::event(event);
657 }
658}
659
660QT_END_NAMESPACE
661
662#include <qquickworkerscript.moc>
663
664#include "moc_qquickworkerscript_p.cpp"
665

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