diff options
-rw-r--r-- | src/qml/compiler/qv4bytecodegenerator_p.h | 6 | ||||
-rw-r--r-- | src/qml/compiler/qv4codegen.cpp | 7 | ||||
-rw-r--r-- | src/qml/compiler/qv4instr_moth.cpp | 3 | ||||
-rw-r--r-- | src/qml/compiler/qv4instr_moth_p.h | 2 | ||||
-rw-r--r-- | src/qml/jit/qv4assemblercommon_p.h | 5 | ||||
-rw-r--r-- | src/qml/jit/qv4baselinejit.cpp | 5 | ||||
-rw-r--r-- | src/qml/jit/qv4baselinejit_p.h | 1 | ||||
-rw-r--r-- | src/qml/jsapi/qjsengine.cpp | 36 | ||||
-rw-r--r-- | src/qml/jsapi/qjsengine.h | 3 | ||||
-rw-r--r-- | src/qml/jsapi/qjsvalue.cpp | 6 | ||||
-rw-r--r-- | src/qml/jsruntime/qv4enginebase_p.h | 20 | ||||
-rw-r--r-- | src/qml/jsruntime/qv4vme_moth.cpp | 11 | ||||
-rw-r--r-- | tests/auto/qml/qjsengine/tst_qjsengine.cpp | 79 |
13 files changed, 180 insertions, 4 deletions
diff --git a/src/qml/compiler/qv4bytecodegenerator_p.h b/src/qml/compiler/qv4bytecodegenerator_p.h index ab8661dbe3..acd4aa62ea 100644 --- a/src/qml/compiler/qv4bytecodegenerator_p.h +++ b/src/qml/compiler/qv4bytecodegenerator_p.h @@ -209,6 +209,12 @@ QT_WARNING_POP addJumpInstruction(Instruction::JumpTrue()).link(target); } + void checkException() + { + Instruction::CheckException chk; + addInstruction(chk); + } + void setUnwindHandler(ExceptionHandler *handler) { currentExceptionHandler = handler; diff --git a/src/qml/compiler/qv4codegen.cpp b/src/qml/compiler/qv4codegen.cpp index 1537ce408d..1bf0e7147d 100644 --- a/src/qml/compiler/qv4codegen.cpp +++ b/src/qml/compiler/qv4codegen.cpp @@ -1201,6 +1201,7 @@ bool Codegen::visit(ArrayPattern *ast) lhsValue.loadInAccumulator(); pushAccumulator(); + bytecodeGenerator->checkException(); bytecodeGenerator->jump().link(in); end.link(); } @@ -3201,11 +3202,13 @@ bool Codegen::visit(DoWhileStatement *ast) cond.link(); if (AST::cast<TrueLiteral *>(ast->expression)) { // do {} while (true) -> just jump back to the loop body, no need to generate a condition + bytecodeGenerator->checkException(); bytecodeGenerator->jump().link(body); } else if (AST::cast<FalseLiteral *>(ast->expression)) { // do {} while (false) -> fall through, no need to generate a condition } else { TailCallBlocker blockTailCalls(this); + bytecodeGenerator->checkException(); condition(ast->expression, &body, &end, false); } @@ -3322,6 +3325,7 @@ bool Codegen::visit(ForEachStatement *ast) setJumpOutLocation(bytecodeGenerator, ast->statement, ast->forToken); } + bytecodeGenerator->checkException(); bytecodeGenerator->jump().link(in); error: @@ -3370,6 +3374,7 @@ bool Codegen::visit(ForStatement *ast) bytecodeGenerator->addInstruction(clone); } statement(ast->expression); + bytecodeGenerator->checkException(); bytecodeGenerator->jump().link(cond); end.link(); @@ -3652,6 +3657,8 @@ bool Codegen::visit(WhileStatement *ast) ControlFlowLoop flow(this, &end, &cond); bytecodeGenerator->addLoopStart(cond); + bytecodeGenerator->checkException(); + if (!AST::cast<TrueLiteral *>(ast->expression)) { TailCallBlocker blockTailCalls(this); condition(ast->expression, &start, &end, true); diff --git a/src/qml/compiler/qv4instr_moth.cpp b/src/qml/compiler/qv4instr_moth.cpp index e022d14264..5148154a6a 100644 --- a/src/qml/compiler/qv4instr_moth.cpp +++ b/src/qml/compiler/qv4instr_moth.cpp @@ -539,6 +539,9 @@ void dumpBytecode(const char *code, int len, int nLocals, int nFormals, int /*st d << ABSOLUTE_OFFSET(); MOTH_END_INSTR(JumpNoException) + MOTH_BEGIN_INSTR(CheckException) + MOTH_END_INSTR(CheckException) + MOTH_BEGIN_INSTR(CmpEqNull) MOTH_END_INSTR(CmpEqNull) diff --git a/src/qml/compiler/qv4instr_moth_p.h b/src/qml/compiler/qv4instr_moth_p.h index 6421fc9d67..35a5fdfba5 100644 --- a/src/qml/compiler/qv4instr_moth_p.h +++ b/src/qml/compiler/qv4instr_moth_p.h @@ -152,6 +152,7 @@ QT_BEGIN_NAMESPACE #define INSTR_JumpFalse(op) INSTRUCTION(op, JumpFalse, 1, offset) #define INSTR_JumpNotUndefined(op) INSTRUCTION(op, JumpNotUndefined, 1, offset) #define INSTR_JumpNoException(op) INSTRUCTION(op, JumpNoException, 1, offset) +#define INSTR_CheckException(op) INSTRUCTION(op, CheckException, 0) #define INSTR_CmpEqNull(op) INSTRUCTION(op, CmpEqNull, 0) #define INSTR_CmpNeNull(op) INSTRUCTION(op, CmpNeNull, 0) #define INSTR_CmpEqInt(op) INSTRUCTION(op, CmpEqInt, 1, lhs) @@ -241,6 +242,7 @@ QT_BEGIN_NAMESPACE F(JumpFalse) \ F(JumpNoException) \ F(JumpNotUndefined) \ + F(CheckException) \ F(CmpEqNull) \ F(CmpNeNull) \ F(CmpEqInt) \ diff --git a/src/qml/jit/qv4assemblercommon_p.h b/src/qml/jit/qv4assemblercommon_p.h index e5c2aff1a7..f305213ce2 100644 --- a/src/qml/jit/qv4assemblercommon_p.h +++ b/src/qml/jit/qv4assemblercommon_p.h @@ -619,6 +619,9 @@ public: for (Jump j : catchyJumps) j.link(this); + // We don't need to check for isInterrupted here because if that is set, + // then the first checkException() in any exception handler will find another "exception" + // and jump out of the exception handler. loadPtr(exceptionHandlerAddress(), ScratchRegister); Jump exitFunction = branchPtr(Equal, ScratchRegister, TrustedImmPtr(0)); jump(ScratchRegister); @@ -633,6 +636,8 @@ public: void checkException() { + // This actually reads 4 bytes, starting at hasException. + // Therefore, it also reads the isInterrupted flag, and triggers an exception on that. addCatchyJump( branch32(NotEqual, Address(EngineRegister, offsetof(EngineBase, hasException)), diff --git a/src/qml/jit/qv4baselinejit.cpp b/src/qml/jit/qv4baselinejit.cpp index 517f0940e5..f4807f1917 100644 --- a/src/qml/jit/qv4baselinejit.cpp +++ b/src/qml/jit/qv4baselinejit.cpp @@ -794,6 +794,11 @@ void BaselineJIT::generate_JumpNotUndefined(int offset) labels.insert(as->jumpNotUndefined(absoluteOffset(offset))); } +void BaselineJIT::generate_CheckException() +{ + as->checkException(); +} + void BaselineJIT::generate_CmpEqNull() { as->cmpeqNull(); } void BaselineJIT::generate_CmpNeNull() { as->cmpneNull(); } void BaselineJIT::generate_CmpEqInt(int lhs) { as->cmpeqInt(lhs); } diff --git a/src/qml/jit/qv4baselinejit_p.h b/src/qml/jit/qv4baselinejit_p.h index 46622d29e6..284faf0ff0 100644 --- a/src/qml/jit/qv4baselinejit_p.h +++ b/src/qml/jit/qv4baselinejit_p.h @@ -163,6 +163,7 @@ public: void generate_JumpFalse(int offset) override; void generate_JumpNoException(int offset) override; void generate_JumpNotUndefined(int offset) override; + void generate_CheckException() override; void generate_CmpEqNull() override; void generate_CmpNeNull() override; void generate_CmpEqInt(int lhs) override; diff --git a/src/qml/jsapi/qjsengine.cpp b/src/qml/jsapi/qjsengine.cpp index aab72f8b2d..45ea79d31a 100644 --- a/src/qml/jsapi/qjsengine.cpp +++ b/src/qml/jsapi/qjsengine.cpp @@ -470,6 +470,33 @@ void QJSEngine::installExtensions(QJSEngine::Extensions extensions, const QJSVal QV4::GlobalExtensions::init(obj, extensions); } +/*! + \since 5.14 + Interrupts or re-enables JavaScript execution. + + If \a interrupted is \c true, any JavaScript executed by this engine + immediately aborts and returns an error object until this function is + called again with a value of \c false for \a interrupted. + + This function is thread safe. You may call it from a different thread + in order to interrupt, for example, an infinite loop in JavaScript. +*/ +void QJSEngine::setInterrupted(bool interrupted) +{ + m_v4Engine->isInterrupted = interrupted; +} + +/*! + \since 5.14 + Returns whether JavaScript execution is currently interrupted. + + \sa setInterrupted() +*/ +bool QJSEngine::isInterrupted() const +{ + return m_v4Engine->isInterrupted; +} + static QUrl urlForFileName(const QString &fileName) { if (!fileName.startsWith(QLatin1Char(':'))) @@ -527,6 +554,8 @@ QJSValue QJSEngine::evaluate(const QString& program, const QString& fileName, in result = script.run(); if (scope.engine->hasException) result = v4->catchException(); + if (v4->isInterrupted) + result = v4->newErrorObject(QStringLiteral("Interrupted")); QJSValue retval(v4, result->asReturnedValue()); @@ -565,7 +594,12 @@ QJSValue QJSEngine::importModule(const QString &fileName) if (m_v4Engine->hasException) return QJSValue(m_v4Engine, m_v4Engine->catchException()); moduleUnit->evaluate(); - return QJSValue(m_v4Engine, moduleNamespace->asReturnedValue()); + if (!m_v4Engine->isInterrupted) + return QJSValue(m_v4Engine, moduleNamespace->asReturnedValue()); + + return QJSValue( + m_v4Engine, + m_v4Engine->newErrorObject(QStringLiteral("Interrupted"))->asReturnedValue()); } /*! diff --git a/src/qml/jsapi/qjsengine.h b/src/qml/jsapi/qjsengine.h index 6300842341..31a4d68baa 100644 --- a/src/qml/jsapi/qjsengine.h +++ b/src/qml/jsapi/qjsengine.h @@ -113,6 +113,9 @@ public: void installExtensions(Extensions extensions, const QJSValue &object = QJSValue()); + void setInterrupted(bool interrupted); + bool isInterrupted() const; + QV4::ExecutionEngine *handle() const { return m_v4Engine; } void throwError(const QString &message); diff --git a/src/qml/jsapi/qjsvalue.cpp b/src/qml/jsapi/qjsvalue.cpp index e0bd986920..92eaf1d8ee 100644 --- a/src/qml/jsapi/qjsvalue.cpp +++ b/src/qml/jsapi/qjsvalue.cpp @@ -769,6 +769,8 @@ QJSValue QJSValue::call(const QJSValueList &args) ScopedValue result(scope, f->call(jsCallData)); if (engine->hasException) result = engine->catchException(); + if (engine->isInterrupted) + result = engine->newErrorObject(QStringLiteral("Interrupted")); return QJSValue(engine, result->asReturnedValue()); } @@ -825,6 +827,8 @@ QJSValue QJSValue::callWithInstance(const QJSValue &instance, const QJSValueList ScopedValue result(scope, f->call(jsCallData)); if (engine->hasException) result = engine->catchException(); + if (engine->isInterrupted) + result = engine->newErrorObject(QStringLiteral("Interrupted")); return QJSValue(engine, result->asReturnedValue()); } @@ -873,6 +877,8 @@ QJSValue QJSValue::callAsConstructor(const QJSValueList &args) ScopedValue result(scope, f->callAsConstructor(jsCallData)); if (engine->hasException) result = engine->catchException(); + if (engine->isInterrupted) + result = engine->newErrorObject(QStringLiteral("Interrupted")); return QJSValue(engine, result->asReturnedValue()); } diff --git a/src/qml/jsruntime/qv4enginebase_p.h b/src/qml/jsruntime/qv4enginebase_p.h index b5cfea8863..82eccd9f3c 100644 --- a/src/qml/jsruntime/qv4enginebase_p.h +++ b/src/qml/jsruntime/qv4enginebase_p.h @@ -69,9 +69,23 @@ struct Q_QML_EXPORT EngineBase { CppStackFrame *currentStackFrame = nullptr; Value *jsStackTop = nullptr; + + // The JIT expects hasException and isInterrupted to be in the same 32bit word in memory. quint8 hasException = false; - quint8 writeBarrierActive = false; + // isInterrupted is expected to be set from a different thread +#if defined(Q_ATOMIC_INT8_IS_SUPPORTED) + QAtomicInteger<quint8> isInterrupted = false; quint16 unused = 0; +#elif defined(Q_ATOMIC_INT16_IS_SUPPORTED) + quint8 unused = 0; + QAtomicInteger<quint16> isInterrupted = false; +#elif defined(V4_BOOTSTRAP) + // We don't need the isInterrupted flag when bootstrapping. + quint8 unused[3]; +#else +# error V4 needs either 8bit or 16bit atomics. +#endif + quint8 isExecutingInRegExpJIT = false; quint8 padding[3]; MemoryManager *memoryManager = nullptr; @@ -137,6 +151,10 @@ Q_STATIC_ASSERT(offsetof(EngineBase, hasException) == offsetof(EngineBase, jsSta Q_STATIC_ASSERT(offsetof(EngineBase, memoryManager) == offsetof(EngineBase, hasException) + 8); Q_STATIC_ASSERT(offsetof(EngineBase, runtime) == offsetof(EngineBase, memoryManager) + QT_POINTER_SIZE); +#ifndef V4_BOOTSTRAP +Q_STATIC_ASSERT(offsetof(EngineBase, isInterrupted) + sizeof(EngineBase::isInterrupted) <= offsetof(EngineBase, hasException) + 4); +#endif + } QT_END_NAMESPACE diff --git a/src/qml/jsruntime/qv4vme_moth.cpp b/src/qml/jsruntime/qv4vme_moth.cpp index 4c292d429a..ec44f42933 100644 --- a/src/qml/jsruntime/qv4vme_moth.cpp +++ b/src/qml/jsruntime/qv4vme_moth.cpp @@ -347,7 +347,7 @@ static struct InstrCount { #undef CHECK_EXCEPTION #endif #define CHECK_EXCEPTION \ - if (engine->hasException) \ + if (engine->hasException || engine->isInterrupted) \ goto handleUnwind static inline Heap::CallContext *getScope(QV4::Value *stack, int level) @@ -1013,6 +1013,10 @@ QV4::ReturnedValue VME::interpret(CppStackFrame *frame, ExecutionEngine *engine, code += offset; MOTH_END_INSTR(JumpNotUndefined) + MOTH_BEGIN_INSTR(CheckException) + CHECK_EXCEPTION; + MOTH_END_INSTR(CheckException) + MOTH_BEGIN_INSTR(CmpEqNull) acc = Encode(ACC.isNullOrUndefined()); MOTH_END_INSTR(CmpEqNull) @@ -1363,7 +1367,10 @@ QV4::ReturnedValue VME::interpret(CppStackFrame *frame, ExecutionEngine *engine, MOTH_END_INSTR(Debug) handleUnwind: - Q_ASSERT(engine->hasException || frame->unwindLevel); + // We do start the exception handler in case of isInterrupted. The exception handler will + // immediately abort, due to the same isInterrupted. We don't skip the exception handler + // because the current behavior is easier to implement in the JIT. + Q_ASSERT(engine->hasException || engine->isInterrupted || frame->unwindLevel); if (!frame->unwindHandler) { acc = Encode::undefined(); return acc; diff --git a/tests/auto/qml/qjsengine/tst_qjsengine.cpp b/tests/auto/qml/qjsengine/tst_qjsengine.cpp index 9c3316e39f..6ca2663f30 100644 --- a/tests/auto/qml/qjsengine/tst_qjsengine.cpp +++ b/tests/auto/qml/qjsengine/tst_qjsengine.cpp @@ -245,6 +245,9 @@ private slots: void equality(); void aggressiveGc(); + void interrupt_data(); + void interrupt(); + public: Q_INVOKABLE QJSValue throwingCppMethod1(); Q_INVOKABLE void throwingCppMethod2(); @@ -4839,6 +4842,82 @@ void tst_QJSEngine::aggressiveGc() qputenv("QV4_MM_AGGRESSIVE_GC", origAggressiveGc); } +void tst_QJSEngine::interrupt_data() +{ + QTest::addColumn<int>("jitThreshold"); + QTest::addColumn<QString>("code"); + + const int big = (1 << 24); + for (int i = 0; i <= big; i += big) { + const char *mode = i ? "interpret" : "jit"; + QTest::addRow("for with content / %s", mode) << i << "var a = 0; for (;;) { a += 2; }"; + QTest::addRow("for empty / %s", mode) << i << "for (;;) {}"; + QTest::addRow("for continue / %s", mode) << i << "for (;;) { continue; }"; + QTest::addRow("while with content / %s", mode) << i << "var a = 0; while (true) { a += 2; }"; + QTest::addRow("while empty / %s", mode) << i << "while (true) {}"; + QTest::addRow("while continue / %s", mode) << i << "while (true) { continue; }"; + QTest::addRow("do with content / %s", mode) << i << "var a = 0; do { a += 2; } while (true);"; + QTest::addRow("do empty / %s", mode) << i << "do {} while (true);"; + QTest::addRow("do continue / %s", mode) << i << "do { continue; } while (true);"; + QTest::addRow("nested loops / %s", mode) << i << "while (true) { for (;;) {} }"; + QTest::addRow("labeled continue / %s", mode) << i << "a: while (true) { for (;;) { continue a; } }"; + QTest::addRow("labeled break / %s", mode) << i << "while (true) { a: for (;;) { break a; } }"; + QTest::addRow("tail call / %s", mode) << i << "'use strict';\nfunction x() { return x(); }; x();"; + } +} + +class TemporaryJitThreshold +{ + Q_DISABLE_COPY_MOVE(TemporaryJitThreshold) +public: + TemporaryJitThreshold(int threshold) { + m_wasSet = qEnvironmentVariableIsSet(m_envVar); + m_value = qgetenv(m_envVar); + qputenv(m_envVar, QByteArray::number(threshold)); + } + + ~TemporaryJitThreshold() + { + if (m_wasSet) + qputenv(m_envVar, m_value); + else + qunsetenv(m_envVar); + } + +private: + const char *m_envVar = "QV4_JIT_CALL_THRESHOLD"; + bool m_wasSet = false; + QByteArray m_value; +}; + +void tst_QJSEngine::interrupt() +{ + QFETCH(int, jitThreshold); + QFETCH(QString, code); + + TemporaryJitThreshold threshold(jitThreshold); + Q_UNUSED(threshold); + + QJSEngine *engineInThread = nullptr; + QScopedPointer<QThread> worker(QThread::create([&engineInThread, &code, jitThreshold](){ + QJSEngine jsEngine; + engineInThread = &jsEngine; + QJSValue result = jsEngine.evaluate(code); + QVERIFY(jsEngine.isInterrupted()); + QVERIFY(result.isError()); + QCOMPARE(result.toString(), QString::fromLatin1("Error: Interrupted")); + engineInThread = nullptr; + })); + worker->start(); + + QTRY_VERIFY(engineInThread); + + engineInThread->setInterrupted(true); + + QVERIFY(worker->wait()); + QVERIFY(!engineInThread); +} + QTEST_MAIN(tst_QJSEngine) #include "tst_qjsengine.moc" |