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.find(key: workerEvent->workerId());
184 if (itr != workers.end()) {
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 auto module = engine->loadModule(url: url);
257 if (module.compiled) {
258 if (module.compiled->instantiate(engine))
259 module.compiled->evaluate();
260 } else if (module.native) {
261 // Nothing to do. There is no global code in a native module.
262 } else {
263 engine->throwError(QStringLiteral("Could not load module file"));
264 }
265 } else {
266 QString error;
267 QV4::Scope scope(engine);
268 QScopedPointer<QV4::Script> program;
269 program.reset(other: QV4::Script::createFromFileOrCache(
270 engine, /*qmlContext*/nullptr, fileName, originalUrl: url, error: &error));
271 if (program.isNull()) {
272 if (!error.isEmpty())
273 qWarning().nospace() << error;
274 return;
275 }
276
277 if (!engine->hasException)
278 program->run();
279 }
280
281 if (engine->hasException)
282 reportScriptException(script, error: engine->catchExceptionAsQmlError());
283}
284
285void QQuickWorkerScriptEnginePrivate::reportScriptException(WorkerScript *script,
286 const QQmlError &error)
287{
288 QMutexLocker locker(&script->p->m_lock);
289 if (script->owner)
290 QCoreApplication::postEvent(receiver: script->owner, event: new WorkerErrorEvent(error));
291}
292
293WorkerDataEvent::WorkerDataEvent(int workerId, const QByteArray &data)
294: QEvent((QEvent::Type)WorkerData), m_id(workerId), m_data(data)
295{
296}
297
298WorkerDataEvent::~WorkerDataEvent()
299{
300}
301
302int WorkerDataEvent::workerId() const
303{
304 return m_id;
305}
306
307QByteArray WorkerDataEvent::data() const
308{
309 return m_data;
310}
311
312WorkerLoadEvent::WorkerLoadEvent(int workerId, const QUrl &url)
313: QEvent((QEvent::Type)WorkerLoad), m_id(workerId), m_url(url)
314{
315}
316
317int WorkerLoadEvent::workerId() const
318{
319 return m_id;
320}
321
322QUrl WorkerLoadEvent::url() const
323{
324 return m_url;
325}
326
327WorkerRemoveEvent::WorkerRemoveEvent(int workerId)
328: QEvent((QEvent::Type)WorkerRemove), m_id(workerId)
329{
330}
331
332int WorkerRemoveEvent::workerId() const
333{
334 return m_id;
335}
336
337WorkerErrorEvent::WorkerErrorEvent(const QQmlError &error)
338: QEvent((QEvent::Type)WorkerError), m_error(error)
339{
340}
341
342QQmlError WorkerErrorEvent::error() const
343{
344 return m_error;
345}
346
347QQuickWorkerScriptEngine::QQuickWorkerScriptEngine(QQmlEngine *parent)
348: QThread(parent), d(new QQuickWorkerScriptEnginePrivate(parent))
349{
350 d->m_lock.lock();
351 connect(sender: d, SIGNAL(stopThread()), receiver: this, SLOT(quit()), Qt::DirectConnection);
352 start(QThread::LowestPriority);
353 d->m_wait.wait(lockedMutex: &d->m_lock);
354 d->moveToThread(thread: this);
355 d->m_lock.unlock();
356}
357
358QQuickWorkerScriptEngine::~QQuickWorkerScriptEngine()
359{
360 d->m_lock.lock();
361 QCoreApplication::postEvent(receiver: d, event: new QEvent((QEvent::Type)QQuickWorkerScriptEnginePrivate::WorkerDestroyEvent));
362 d->m_lock.unlock();
363
364 //We have to force to cleanup the main thread's event queue here
365 //to make sure the main GUI release all pending locks/wait conditions which
366 //some worker script/agent are waiting for (QQmlListModelWorkerAgent::sync() for example).
367 while (!isFinished()) {
368 // We can't simply wait here, because the worker thread will not terminate
369 // until the main thread processes the last data event it generates
370 QCoreApplication::processEvents();
371 yieldCurrentThread();
372 }
373
374 delete d;
375}
376
377
378WorkerScript::WorkerScript(QV4::ExecutionEngine *engine)
379{
380 engine->initQmlGlobalObject();
381
382 QV4::Scope scope(engine);
383 QV4::ScopedObject api(scope, engine->newObject());
384 QV4::ScopedString sendMessageName(scope, engine->newString(QStringLiteral("sendMessage")));
385 QV4::ScopedFunctionObject sendMessage(
386 scope, QV4::FunctionObject::createBuiltinFunction(
387 engine, nameOrSymbol: sendMessageName,
388 code: QQuickWorkerScriptEnginePrivate::method_sendMessage, argumentCount: 1));
389 api->put(name: sendMessageName, v: sendMessage);
390 QV4::ScopedString workerScriptName(scope, engine->newString(QStringLiteral("WorkerScript")));
391 engine->globalObject->put(name: workerScriptName, v: api);
392
393#if QT_CONFIG(qml_network)
394 engine->networkAccessManager = [](QV4::ExecutionEngine *engine) {
395 WorkerScript *workerScript = workerScriptExtension(engine);
396 if (workerScript->scriptLocalNAM)
397 return workerScript->scriptLocalNAM.get();
398 if (auto *namFactory = workerScript->p->qmlengine->networkAccessManagerFactory())
399 workerScript->scriptLocalNAM.reset(other: namFactory->create(parent: workerScript->p));
400 else
401 workerScript->scriptLocalNAM.reset(other: new QNetworkAccessManager(workerScript->p));
402 return workerScript->scriptLocalNAM.get();
403 };
404#endif // qml_network
405}
406
407int QQuickWorkerScriptEngine::registerWorkerScript(QQuickWorkerScript *owner)
408{
409 const int id = d->m_nextId++;
410
411 d->m_lock.lock();
412 d->workers.insert(key: id, value: owner);
413 d->m_lock.unlock();
414
415 return id;
416}
417
418void QQuickWorkerScriptEngine::removeWorkerScript(int id)
419{
420 const auto it = d->workers.find(key: id);
421 if (it == d->workers.end())
422 return;
423
424 if (it->isT1()) {
425 QV4::ExecutionEngine *engine = it->asT1();
426 workerScriptExtension(engine)->owner = nullptr;
427 }
428 QCoreApplication::postEvent(receiver: d, event: new WorkerRemoveEvent(id));
429}
430
431void QQuickWorkerScriptEngine::executeUrl(int id, const QUrl &url)
432{
433 QCoreApplication::postEvent(receiver: d, event: new WorkerLoadEvent(id, url));
434}
435
436void QQuickWorkerScriptEngine::sendMessage(int id, const QByteArray &data)
437{
438 QCoreApplication::postEvent(receiver: d, event: new WorkerDataEvent(id, data));
439}
440
441void QQuickWorkerScriptEngine::run()
442{
443 d->m_lock.lock();
444 d->m_wait.wakeAll();
445 d->m_lock.unlock();
446
447 exec();
448
449 for (auto it = d->workers.begin(), end = d->workers.end(); it != end; ++it) {
450 if (it->isT1())
451 delete it->asT1();
452 }
453
454 d->workers.clear();
455}
456
457
458/*!
459 \qmltype WorkerScript
460 \instantiates QQuickWorkerScript
461 \ingroup qtquick-threading
462 \inqmlmodule QtQml.WorkerScript
463 \brief Enables the use of threads in a Qt Quick application.
464
465 Use WorkerScript to run operations in a new thread.
466 This is useful for running operations in the background so
467 that the main GUI thread is not blocked.
468
469 Messages can be passed between the new thread and the parent thread
470 using \l sendMessage() and the \c onMessage() handler.
471
472 An example:
473
474 \snippet qml/workerscript/workerscript.qml 0
475
476 The above worker script specifies a JavaScript file, "script.mjs", that handles
477 the operations to be performed in the new thread. Here is \c script.mjs:
478
479 \quotefile qml/workerscript/script.mjs
480
481 When the user clicks anywhere within the rectangle, \c sendMessage() is
482 called, triggering the \tt WorkerScript.onMessage() handler in
483 \tt script.mjs. This in turn sends a reply message that is then received
484 by the \tt onMessage() handler of \tt myWorker.
485
486 The example uses a script that is an ECMAScript module, because it has the ".mjs" extension.
487 It can use import statements to access functionality from other modules and it is run in JavaScript
488 strict mode.
489
490 If a worker script has the extension ".js" instead, then it is considered to contain plain JavaScript
491 statements and it is run in non-strict mode.
492
493 \note Each WorkerScript element will instantiate a separate JavaScript engine to ensure perfect
494 isolation and thread-safety. If the impact of that results in a memory consumption that is too
495 high for your environment, then consider sharing a WorkerScript element.
496
497 \section3 Restrictions
498
499 Since the \c WorkerScript.onMessage() function is run in a separate thread, the
500 JavaScript file is evaluated in a context separate from the main QML engine. This means
501 that unlike an ordinary JavaScript file that is imported into QML, the \c script.mjs
502 in the above example cannot access the properties, methods or other attributes
503 of the QML item, nor can it access any context properties set on the QML object
504 through QQmlContext.
505
506 Additionally, there are restrictions on the types of values that can be passed to and
507 from the worker script. See the sendMessage() documentation for details.
508
509 Worker scripts that are plain JavaScript sources can not use \l {qtqml-javascript-imports.html}{.import} syntax.
510 Scripts that are ECMAScript modules can freely use import and export statements.
511*/
512QQuickWorkerScript::QQuickWorkerScript(QObject *parent)
513: QObject(parent), m_engine(nullptr), m_scriptId(-1), m_componentComplete(true)
514{
515}
516
517QQuickWorkerScript::~QQuickWorkerScript()
518{
519 if (m_scriptId != -1) m_engine->removeWorkerScript(id: m_scriptId);
520}
521
522/*!
523 \qmlproperty url WorkerScript::source
524
525 This holds the url of the JavaScript file that implements the
526 \tt WorkerScript.onMessage() handler for threaded operations.
527
528 If the file name component of the url ends with ".mjs", then the script
529 is parsed as an ECMAScript module and run in strict mode. Otherwise it is considered to be
530 plain script.
531*/
532QUrl QQuickWorkerScript::source() const
533{
534 return m_source;
535}
536
537void QQuickWorkerScript::setSource(const QUrl &source)
538{
539 if (m_source == source)
540 return;
541
542 m_source = source;
543
544 if (engine()) {
545 const QQmlContext *context = qmlContext(this);
546 m_engine->executeUrl(id: m_scriptId, url: context ? context->resolvedUrl(m_source) : m_source);
547 }
548
549 emit sourceChanged();
550}
551
552/*!
553 \qmlproperty bool WorkerScript::ready
554
555 This holds whether the WorkerScript has been initialized and is ready
556 for receiving messages via \tt WorkerScript.sendMessage().
557*/
558bool QQuickWorkerScript::ready() const
559{
560 return m_engine != nullptr;
561}
562
563/*!
564 \qmlmethod WorkerScript::sendMessage(jsobject message)
565
566 Sends the given \a message to a worker script handler in another
567 thread. The other worker script handler can receive this message
568 through the onMessage() handler.
569
570 The \c message object may only contain values of the following
571 types:
572
573 \list
574 \li boolean, number, string
575 \li JavaScript objects and arrays
576 \li ListModel objects (any other type of QObject* is not allowed)
577 \endlist
578
579 All objects and arrays are copied to the \c message. With the exception
580 of ListModel objects, any modifications by the other thread to an object
581 passed in \c message will not be reflected in the original object.
582*/
583void QQuickWorkerScript::sendMessage(QQmlV4Function *args)
584{
585 if (!engine()) {
586 qWarning(msg: "QQuickWorkerScript: Attempt to send message before WorkerScript establishment");
587 return;
588 }
589
590 QV4::Scope scope(args->v4engine());
591 QV4::ScopedValue argument(scope, QV4::Value::undefinedValue());
592 if (args->length() != 0)
593 argument = (*args)[0];
594
595 m_engine->sendMessage(id: m_scriptId, data: QV4::Serialize::serialize(argument, scope.engine));
596}
597
598void QQuickWorkerScript::classBegin()
599{
600 m_componentComplete = false;
601}
602
603QQuickWorkerScriptEngine *QQuickWorkerScript::engine()
604{
605 if (m_engine) return m_engine;
606 if (m_componentComplete) {
607 const QQmlContext *context = qmlContext(this);
608 QQmlEngine *engine = context ? context->engine() : nullptr;
609 if (!engine) {
610 qWarning(msg: "QQuickWorkerScript: engine() called without qmlEngine() set");
611 return nullptr;
612 }
613
614 QQmlEnginePrivate *enginePrivate = QQmlEnginePrivate::get(e: engine);
615 if (enginePrivate->workerScriptEngine == nullptr)
616 enginePrivate->workerScriptEngine = new QQuickWorkerScriptEngine(engine);
617 m_engine = qobject_cast<QQuickWorkerScriptEngine *>(object: enginePrivate->workerScriptEngine);
618 Q_ASSERT(m_engine);
619 m_scriptId = m_engine->registerWorkerScript(owner: this);
620
621 if (m_source.isValid())
622 m_engine->executeUrl(id: m_scriptId, url: context->resolvedUrl(m_source));
623
624 emit readyChanged();
625
626 return m_engine;
627 }
628 return nullptr;
629}
630
631void QQuickWorkerScript::componentComplete()
632{
633 m_componentComplete = true;
634 engine(); // Get it started now.
635}
636
637/*!
638 \qmlsignal WorkerScript::message(jsobject msg)
639
640 This signal is emitted when a message \a msg is received from a worker
641 script in another thread through a call to sendMessage().
642*/
643
644bool QQuickWorkerScript::event(QEvent *event)
645{
646 if (event->type() == (QEvent::Type)WorkerDataEvent::WorkerData) {
647 if (QQmlEngine *engine = qmlEngine(this)) {
648 QV4::ExecutionEngine *v4 = engine->handle();
649 WorkerDataEvent *workerEvent = static_cast<WorkerDataEvent *>(event);
650 emit message(messageObject: QJSValuePrivate::fromReturnedValue(
651 d: QV4::Serialize::deserialize(workerEvent->data(), v4)));
652 }
653 return true;
654 } else if (event->type() == (QEvent::Type)WorkerErrorEvent::WorkerError) {
655 WorkerErrorEvent *workerEvent = static_cast<WorkerErrorEvent *>(event);
656 QQmlEnginePrivate::warning(qmlEngine(this), workerEvent->error());
657 return true;
658 } else {
659 return QObject::event(event);
660 }
661}
662
663QT_END_NAMESPACE
664
665#include <qquickworkerscript.moc>
666
667#include "moc_qquickworkerscript_p.cpp"
668

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