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