aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorFabian Kosmale <fabian.kosmale@qt.io>2024-10-31 14:30:25 +0100
committerQt Cherry-pick Bot <cherrypick_bot@qt-project.org>2024-11-01 21:55:40 +0000
commitb4ac921e5b7e1861a643ded10e9237265dd1567e (patch)
tree575fc9ee5a1a56d25bd57b7be7a8f14bd055c370
parent1d5182e97951042e3bc4b82ae8f6391bfdff5485 (diff)
Engine: Keep script context alive
A closure might need to access the imported scripts, which live in the context. The closure might however live longer than than the context is valid, and so far, the lookup would have failed. Solve that issue by not completely discarding the reference to the script, but instead downgrading it to a weak reference. Additionally, closurse will keep a reference to the scripts as an invalid property, so that the gc will keep them alive. The ignoreMessage call in importedScriptsAccessOnObjectWithInvalidContext is removed, as we keep the script value alive. Fixes: QTBUG-130575 Change-Id: Ieb3044c419271473a9fc9afcf9e88ab4b3f4b88a Reviewed-by: Ulf Hermann <ulf.hermann@qt.io> (cherry picked from commit aefdce82fd53537c7a2f4c3d63714d1e0d335551) Reviewed-by: Qt Cherry-pick Bot <cherrypick_bot@qt-project.org>
-rw-r--r--src/qml/jsruntime/qv4runtime.cpp23
-rw-r--r--src/qml/qml/qqmlcontextdata.cpp23
-rw-r--r--src/qml/qml/qqmlcontextdata_p.h27
-rw-r--r--tests/auto/qml/qqmlecmascript/data/scriptCapturingClosure/ClosureMaker.qml11
-rw-r--r--tests/auto/qml/qqmlecmascript/data/scriptCapturingClosure/myscript.js2
-rw-r--r--tests/auto/qml/qqmlecmascript/data/scriptCapturingClosure/useScriptCapturingClosure.qml18
-rw-r--r--tests/auto/qml/qqmlecmascript/tst_qqmlecmascript.cpp17
7 files changed, 112 insertions, 9 deletions
diff --git a/src/qml/jsruntime/qv4runtime.cpp b/src/qml/jsruntime/qv4runtime.cpp
index 2a6b97130e..11c0c48345 100644
--- a/src/qml/jsruntime/qv4runtime.cpp
+++ b/src/qml/jsruntime/qv4runtime.cpp
@@ -285,9 +285,28 @@ ReturnedValue Runtime::Closure::call(ExecutionEngine *engine, int functionId)
->runtimeFunctions[functionId];
Q_ASSERT(clos);
ExecutionContext *current = engine->currentContext();
+ Scope s(engine);
+ QV4::ScopedObject closure(s);
+
if (clos->isGenerator())
- return GeneratorFunction::create(current, clos)->asReturnedValue();
- return FunctionObject::createScriptFunction(current, clos)->asReturnedValue();
+ closure = GeneratorFunction::create(current, clos)->asReturnedValue();
+ else
+ closure = FunctionObject::createScriptFunction(current, clos)->asReturnedValue();
+ // ### TODO: only keep reference to scripts if actually needed; see QTBUG-130795
+ Scoped<QV4::QmlContext> callingQmlContext(s, s.engine->qmlContext());
+ if (callingQmlContext) {
+ // ### TODO: It would be more efficient to use custom Function prototypes instead of setting a property
+ // ==> QTBUG-130798
+ Scoped<QV4::QQmlContextWrapper> qmlContextWrapper(s, callingQmlContext->d()->qml());
+ const QV4::QQmlContextWrapper *resource = qmlContextWrapper;
+ QQmlRefPointer<QQmlContextData> context = resource->getContext();
+ if (!context->importedScripts().isNullOrUndefined()) {
+ QV4::ScopedString name(s, engine->newString(QLatin1StringView("$importedScripts")));
+ QV4::ScopedObject scripts(s, context->importedScripts());
+ closure->insertMember(name, scripts, Attr_Invalid);
+ }
+ }
+ return closure.asReturnedValue();
}
Bool Runtime::DeleteProperty_NoThrow::call(ExecutionEngine *engine, const Value &base, const Value &index)
diff --git a/src/qml/qml/qqmlcontextdata.cpp b/src/qml/qml/qqmlcontextdata.cpp
index 8667a1ce81..f3d80a2e26 100644
--- a/src/qml/qml/qqmlcontextdata.cpp
+++ b/src/qml/qml/qqmlcontextdata.cpp
@@ -99,7 +99,19 @@ void QQmlContextData::invalidate()
m_prevChild = nullptr;
}
- m_importedScripts.clear();
+ if (!m_hasWeakImportedScripts) { // invalidate might be called multiple times
+ if (!m_importedScripts.isNullOrUndefined()) {
+ QV4::Scope scope(m_engine->handle());
+ QV4::ScopedValue val(scope, m_importedScripts.value());
+ m_importedScripts.~PersistentValue();
+ new (&m_weakImportedScripts) QV4::WeakValue();
+ m_weakImportedScripts.set(m_engine->handle(), val);
+ m_hasWeakImportedScripts = true;
+ } else {
+ // clear even if the value is null/undefined, in case it was set to explicit null/undefined
+ m_importedScripts.clear();
+ }
+ }
m_engine = nullptr;
clearParent();
@@ -137,8 +149,17 @@ QQmlContextData::~QQmlContextData()
// avoid recursion
addref();
+ if (!m_hasWeakImportedScripts) {
+ // avoid busy work in invalidate – we don't want to construct a weak value
+ // just to throw it away afterwards
+ m_importedScripts.clear();
+ }
if (m_engine)
invalidate();
+ if (m_hasWeakImportedScripts)
+ m_weakImportedScripts.~WeakValue();
+ else
+ m_importedScripts.~PersistentValue();
m_linkedContext.reset();
Q_ASSERT(refCount() == 1);
diff --git a/src/qml/qml/qqmlcontextdata_p.h b/src/qml/qml/qqmlcontextdata_p.h
index c906a216a5..5e7198938e 100644
--- a/src/qml/qml/qqmlcontextdata_p.h
+++ b/src/qml/qml/qqmlcontextdata_p.h
@@ -274,8 +274,17 @@ public:
bool isRootObjectInCreation() const { return m_isRootObjectInCreation; }
void setRootObjectInCreation(bool rootInCreation) { m_isRootObjectInCreation = rootInCreation; }
- QV4::Value importedScripts() const { return m_importedScripts.value(); }
- void setImportedScripts(QV4::ExecutionEngine *engine, QV4::Value scripts) { m_importedScripts.set(engine, scripts); }
+ QV4::Value importedScripts() const {
+ if (m_hasWeakImportedScripts)
+ return m_weakImportedScripts.value();
+ else
+ return m_importedScripts.value();
+ }
+ void setImportedScripts(QV4::ExecutionEngine *engine, QV4::Value scripts) {
+ // setImportedScripts should not be called on an invalidated context
+ Q_ASSERT(!m_hasWeakImportedScripts);
+ m_importedScripts.set(engine, scripts);
+ }
QQmlRefPointer<QQmlContextData> linkedContext() const { return m_linkedContext; }
void setLinkedContext(const QQmlRefPointer<QQmlContextData> &context) { m_linkedContext = context; }
@@ -343,7 +352,7 @@ private:
m_unresolvedNames(false), m_hasEmittedDestruction(false), m_isRootObjectInCreation(false),
m_ownedByParent(ownership == OwnedByParent),
m_ownedByPublicContext(ownership == OwnedByPublicContext), m_hasExtraObject(false),
- m_dummy(0), m_publicContext(publicContext), m_incubator(nullptr)
+ m_hasWeakImportedScripts(false), m_dummy(0), m_publicContext(publicContext), m_incubator(nullptr)
{
Q_ASSERT(!m_ownedByParent || !m_ownedByPublicContext);
if (!m_parent)
@@ -388,7 +397,8 @@ private:
quint32 m_ownedByParent:1;
quint32 m_ownedByPublicContext:1;
quint32 m_hasExtraObject:1; // used in QQmlDelegateModelItem::dataForObject to find the corresponding QQmlDelegateModelItem of an object
- Q_DECL_UNUSED_MEMBER quint32 m_dummy:23;
+ quint32 m_hasWeakImportedScripts:1;
+ Q_DECL_UNUSED_MEMBER quint32 m_dummy:22;
QQmlContext *m_publicContext = nullptr;
union {
@@ -411,7 +421,14 @@ private:
QObject *m_contextObject = nullptr;
// Any script blocks that exist on this context
- QV4::PersistentValue m_importedScripts; // This is a JS Array
+ union {
+ /* an invalidated context transitions from a strong reference to the scripts
+ to a weak one, so that the context doesn't needlessly holds on to the scripts,
+ but closures can still access them if needed
+ */
+ QV4::PersistentValue m_importedScripts = {}; // This is a JS Array
+ QV4::WeakValue m_weakImportedScripts;
+ };
QUrl m_baseUrl;
QString m_baseUrlString;
diff --git a/tests/auto/qml/qqmlecmascript/data/scriptCapturingClosure/ClosureMaker.qml b/tests/auto/qml/qqmlecmascript/data/scriptCapturingClosure/ClosureMaker.qml
new file mode 100644
index 0000000000..a399e5c9a6
--- /dev/null
+++ b/tests/auto/qml/qqmlecmascript/data/scriptCapturingClosure/ClosureMaker.qml
@@ -0,0 +1,11 @@
+import "myscript.js" as JS
+import QtQml
+
+QtObject {
+ function getClosure() {
+ function inner() {
+ return JS.val1 === JS.val2 + 1;
+ }
+ return inner;
+ }
+}
diff --git a/tests/auto/qml/qqmlecmascript/data/scriptCapturingClosure/myscript.js b/tests/auto/qml/qqmlecmascript/data/scriptCapturingClosure/myscript.js
new file mode 100644
index 0000000000..5651186522
--- /dev/null
+++ b/tests/auto/qml/qqmlecmascript/data/scriptCapturingClosure/myscript.js
@@ -0,0 +1,2 @@
+const val1 = 42
+const val2 = 41
diff --git a/tests/auto/qml/qqmlecmascript/data/scriptCapturingClosure/useScriptCapturingClosure.qml b/tests/auto/qml/qqmlecmascript/data/scriptCapturingClosure/useScriptCapturingClosure.qml
new file mode 100644
index 0000000000..8f547917b1
--- /dev/null
+++ b/tests/auto/qml/qqmlecmascript/data/scriptCapturingClosure/useScriptCapturingClosure.qml
@@ -0,0 +1,18 @@
+import QtQml
+
+QtObject {
+ id: root
+ property var func
+ property int state: -1
+
+ function run() {
+ root.state = root.func() ? 1 : 0;
+ }
+
+ Component.onCompleted: {
+ const comp = Qt.createComponent("./ClosureMaker.qml");
+ let o = comp.createObject();
+ root.func = o.getClosure();
+ o.destroy();
+ }
+}
diff --git a/tests/auto/qml/qqmlecmascript/tst_qqmlecmascript.cpp b/tests/auto/qml/qqmlecmascript/tst_qqmlecmascript.cpp
index 14368b47ba..c9e9674282 100644
--- a/tests/auto/qml/qqmlecmascript/tst_qqmlecmascript.cpp
+++ b/tests/auto/qml/qqmlecmascript/tst_qqmlecmascript.cpp
@@ -168,6 +168,7 @@ private slots:
void importScripts_data();
void importScripts();
void importCreationContext();
+ void canAccsseScriptFromClosureAfterContextWasInvalidated();
void scarceResources();
void scarceResources_data();
void scarceResources_other();
@@ -5077,6 +5078,21 @@ void tst_qqmlecmascript::importCreationContext()
QVERIFY(success);
}
+void tst_qqmlecmascript::canAccsseScriptFromClosureAfterContextWasInvalidated()
+{
+ QQmlEngine engine;
+ QQmlComponent component(&engine, testFileUrl("scriptCapturingClosure/useScriptCapturingClosure.qml"));
+ QScopedPointer<QObject> object(component.create());
+ QVERIFY2(object, qPrintable(component.errorString()));
+ QCOMPARE(object->property("state").toInt(), -1);
+ gc(engine); // not only does the context store a weak reference, it also is kept alive
+ // the weak reference might have outlived one collection, especially if gc was already running (incrementally)
+ // run a full gc cycle again to make sure it's gone
+ gc(engine);
+ QMetaObject::invokeMethod(object.get(), "run");
+ QCOMPARE(object->property("state").toInt(), 1);
+}
+
void tst_qqmlecmascript::scarceResources_other()
{
/* These tests require knowledge of state, since we test values after
@@ -8716,7 +8732,6 @@ void tst_qqmlecmascript::importedScriptsAccessOnObjectWithInvalidContext()
{
QQmlEngine engine;
const QUrl url = testFileUrl("importedScriptsAccessOnObjectWithInvalidContext.qml");
- QTest::ignoreMessage(QtWarningMsg, qPrintable(url.toString() + ":29: TypeError: Cannot read property 'Foo' of null"));
QQmlComponent component(&engine, url);
QScopedPointer<QObject> obj(component.create());
QVERIFY2(obj, qPrintable(component.errorString()));