aboutsummaryrefslogtreecommitdiffstats
path: root/src/qml/jsruntime/qv4variantassociationobject.cpp
diff options
context:
space:
mode:
authorLuca Di Sera <[email protected]>2024-11-13 14:32:21 +0100
committerLuca Di Sera <[email protected]>2024-12-01 22:30:22 +0100
commitcca0b460f882aa3fce2e8aa7594bee974d9ed74d (patch)
tree90c7d04cd447cb586292d537f6eab7431e2c28ca /src/qml/jsruntime/qv4variantassociationobject.cpp
parenta6c8de2780b6dface7504b5423e064e026a6e564 (diff)
Implement read/write-backs for QVariantMap and QVariantHash
When QML receives something from the C++ side, it, sometimes, tries to set up a read/"write-back" mechanism to allow changes on the QML side to be reflected on the C++ side and changes to the original element being synchronized with the QML side. This can happen, for example, when accessing a property of a C++ object that was registered to QML directly from QML. Similarly, it can do so when nesting some of its internal representation of an object with C++ provenance. For example, a `QVariantList` that is passed to the QML side and converted to a `Sequence` type might require some of its stored element to perform a write-back when accessed and modified to ensure that mutations are permanent. For `QVariantMap` and `QVariantHash` this was currently not implemented, with `QVariantMap` being generally converted to a Javascript object and support for `QVariantHash` not necessarily entirely implemented. This can produce surprising results. For example, a `QVariantMap` that is stored in a `QVariantList`, where the list is passed over to the QML side from the C++ side, might incur into its mutations being lost when accessed as a member of the converted `QVariantList`. To ensure that this does not happen, `QVariantMap` and `QVariantHash` will now be converted to a representation that is a `ReferenceObject`, that is, it uses the general read/write-back mechanism in QML. Introduce a new QV4 Object, `VariantAssociationObject`, that can store either a `QVariantMap` or a `QVariantHash` and has read/write-back behavior. The prototype for the object is now registered by the engine and can be accessed through the `variantAssociationPrototype` method. A `QVariantMap`/`QVariantHash` that is being converted to a JS representation will now be converted to the newly introduced object instead of being mapped to a generic JS object. `variantMapToJS` and `variantToJs` in "qv4egnine.cpp", that were used during the conversion of `QVariantMap` to a Javascript object were removed as they are now unused. Some additional cases were added to support conversion from/to `QVariantHash` and conversion from the newly added object. The newly added object supports a small subset of an object functionality and is not intended, at least currently, to support the whole breadth of interactions that a Javascript object would. In particular it aims to support setting properties, retrieving properties, deleting properties, basic iteration and `Object.hasOwnProperty`. It further implements basic read/write-back behavior for those interactions and allows for recursive read/write-backs through the general `virtualMetacall` interface. Additionally, the code `QQmlVMEMetaObject::writeVarProperty` was modified to ensure that the new reference object is detached when assigned to a `var` property, so as to be consistent with the general behavior of the other reference objects. As a drive-by, a comment in the above code that stated that some of the code should be adjusted if a new case was added to it was modified to state that the code should be adjusted with no additional clause, as a new element was added but the adjustment will not be performed as part of this patch. Some general test cases were introduced in `tst_qqmllanguage` for the new object. In particular to test the most basic interactions of the above subset of an object interaction, some of the read/write-back behavior and the behavior of detached when being assigned to a var property. Fixes: QTBUG-129972 Change-Id: Ib655ba6001aef07a74ccf235d2e3223b74d7be59 Reviewed-by: Fabian Kosmale <[email protected]>
Diffstat (limited to 'src/qml/jsruntime/qv4variantassociationobject.cpp')
-rw-r--r--src/qml/jsruntime/qv4variantassociationobject.cpp377
1 files changed, 377 insertions, 0 deletions
diff --git a/src/qml/jsruntime/qv4variantassociationobject.cpp b/src/qml/jsruntime/qv4variantassociationobject.cpp
new file mode 100644
index 0000000000..29ce23cf13
--- /dev/null
+++ b/src/qml/jsruntime/qv4variantassociationobject.cpp
@@ -0,0 +1,377 @@
+// Copyright (C) 2024 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
+
+#include "qv4variantassociationobject_p.h"
+
+#include <private/qqmlengine_p.h>
+
+QT_BEGIN_NAMESPACE
+
+template<typename Return, typename MapCallable, typename HashCallable>
+Return visitVariantAssociation(
+ const QV4::Heap::VariantAssociationObject* association,
+ MapCallable&& mapCallable,
+ HashCallable&& hashCallable
+) {
+ switch (association->m_type) {
+ case QV4::Heap::VariantAssociationObject::AssociationType::VariantMap:
+ return std::invoke(
+ std::forward<MapCallable>(mapCallable),
+ reinterpret_cast<const QVariantMap *>(&association->m_variantAssociation));
+ case QV4::Heap::VariantAssociationObject::AssociationType::VariantHash:
+ return std::invoke(
+ std::forward<HashCallable>(hashCallable),
+ reinterpret_cast<const QVariantHash *>(&association->m_variantAssociation));
+ default: Q_UNREACHABLE();
+ };
+}
+
+template<typename Return, typename MapCallable, typename HashCallable>
+Return visitVariantAssociation(
+ QV4::Heap::VariantAssociationObject* association,
+ MapCallable&& mapCallable,
+ HashCallable&& hashCallable
+) {
+ switch (association->m_type) {
+ case QV4::Heap::VariantAssociationObject::AssociationType::VariantMap:
+ return std::invoke(
+ std::forward<MapCallable>(mapCallable),
+ reinterpret_cast<QVariantMap *>(&association->m_variantAssociation));
+ case QV4::Heap::VariantAssociationObject::AssociationType::VariantHash:
+ return std::invoke(
+ std::forward<HashCallable>(hashCallable),
+ reinterpret_cast<QVariantHash *>(&association->m_variantAssociation));
+ default: Q_UNREACHABLE();
+ };
+}
+
+template<typename Return, typename Callable>
+Return visitVariantAssociation(
+ const QV4::Heap::VariantAssociationObject* association,
+ Callable&& callable
+) {
+ return visitVariantAssociation<Return>(
+ association,
+ std::forward<Callable>(callable),
+ std::forward<Callable>(callable));
+}
+
+template<typename Return, typename Callable>
+Return visitVariantAssociation(
+ QV4::Heap::VariantAssociationObject* association,
+ Callable&& callable
+) {
+ return visitVariantAssociation<Return>(
+ association,
+ std::forward<Callable>(callable),
+ std::forward<Callable>(callable));
+}
+
+static void mapPropertyKey(std::vector<QString>& mapping, const QString& key) {
+ auto it = std::find(mapping.cbegin(), mapping.cend(), key);
+ if (it == mapping.cend())
+ mapping.emplace_back(key);
+}
+
+static int keyToIndex(const std::vector<QString>& mapping, const QString& key) {
+ auto it = std::find(mapping.cbegin(), mapping.cend(), key);
+ return (it == mapping.cend()) ? -1 : std::distance(mapping.cbegin(), it);
+}
+
+static const QString& indexToKey(const std::vector<QString>& mapping, int index) {
+ Q_ASSERT(index >= 0);
+ Q_ASSERT(index < static_cast<int>(mapping.size()));
+ return mapping[index];
+}
+
+namespace QV4 {
+
+ DEFINE_OBJECT_VTABLE(VariantAssociationObject);
+
+ ReturnedValue VariantAssociationPrototype::fromQVariantMap(
+ ExecutionEngine *engine,
+ const QVariantMap& variantMap,
+ QV4::Heap::Object* container,
+ int property, Heap::ReferenceObject::Flags flags)
+ {
+ return engine->memoryManager->allocate<VariantAssociationObject>(
+ variantMap, container, property, flags)->asReturnedValue();
+ }
+
+ ReturnedValue VariantAssociationPrototype::fromQVariantHash(
+ ExecutionEngine *engine,
+ const QVariantHash& variantHash,
+ QV4::Heap::Object* container,
+ int property, Heap::ReferenceObject::Flags flags)
+ {
+ return engine->memoryManager->allocate<VariantAssociationObject>(
+ variantHash, container, property, flags)->asReturnedValue();
+ }
+
+ namespace Heap {
+ void VariantAssociationObject::init(
+ const QVariantMap& variantMap,
+ QV4::Heap::Object* container,
+ int property, Heap::ReferenceObject::Flags flags)
+ {
+ ReferenceObject::init(container, property, flags);
+
+ new(m_variantAssociation) QVariantMap(variantMap);
+ m_type = AssociationType::VariantMap;
+
+ propertyIndexMapping = new std::vector<QString>(variantMap.keyBegin(), variantMap.keyEnd());
+ }
+
+ void VariantAssociationObject::init(
+ const QVariantHash& variantHash,
+ QV4::Heap::Object* container,
+ int property, Heap::ReferenceObject::Flags flags)
+ {
+ ReferenceObject::init(container, property, flags);
+
+ new(m_variantAssociation) QVariantHash(variantHash);
+ m_type = AssociationType::VariantHash;
+
+ propertyIndexMapping = new std::vector<QString>(variantHash.keyBegin(), variantHash.keyEnd());
+ }
+
+ void VariantAssociationObject::destroy() {
+ visitVariantAssociation<void>(
+ this,
+ std::destroy_at<QVariantMap>,
+ std::destroy_at<QVariantHash>);
+ delete propertyIndexMapping;
+ ReferenceObject::destroy();
+ }
+
+ QVariant VariantAssociationObject::toVariant() const
+ {
+ return visitVariantAssociation<QVariant>(
+ this, [](auto association){ return QVariant(*association); });
+ }
+
+ bool VariantAssociationObject::setVariant(const QVariant &variant)
+ {
+ auto metatypeId = variant.metaType().id();
+
+ if (metatypeId != QMetaType::QVariantMap && metatypeId != QMetaType::QVariantHash)
+ return false;
+
+ if (metatypeId == QMetaType::QVariantMap && m_type == AssociationType::VariantMap) {
+ *reinterpret_cast<QVariantMap *>(&m_variantAssociation) = variant.toMap();
+ } else if (metatypeId == QMetaType::QVariantMap && m_type == AssociationType::VariantHash) {
+ std::destroy_at(reinterpret_cast<QVariantHash *>(&m_variantAssociation));
+ new(m_variantAssociation) QVariantMap(variant.toMap());
+ m_type = AssociationType::VariantMap;
+ } else if (metatypeId == QMetaType::QVariantHash && m_type == AssociationType::VariantHash) {
+ *reinterpret_cast<QVariantHash *>(&m_variantAssociation) = variant.toHash();
+ } else if (metatypeId == QMetaType::QVariantHash && m_type == AssociationType::VariantMap) {
+ std::destroy_at(reinterpret_cast<QVariantMap *>(&m_variantAssociation));
+ new(m_variantAssociation) QVariantHash(variant.toHash());
+ m_type = AssociationType::VariantHash;
+ }
+
+ auto keys = visitVariantAssociation<QStringList>(
+ this, [](auto* association){ return association->keys(); });
+ for (const QString& key : keys)
+ mapPropertyKey(*propertyIndexMapping, key);
+
+ return true;
+ }
+
+ VariantAssociationObject *VariantAssociationObject::detached() const
+ {
+ return visitVariantAssociation<VariantAssociationObject*>(
+ this,
+ [engine = internalClass->engine](auto association){
+ return engine->memoryManager->allocate<QV4::VariantAssociationObject>(
+ *association, nullptr, -1, ReferenceObject::Flag::NoFlag);
+ }
+ );
+ }
+
+ } // namespace Heap
+
+ ReturnedValue VariantAssociationObject::virtualGet(const Managed *that, PropertyKey id, const Value *, bool * hasProperty)
+ {
+ QString key = id.toQString();
+ return static_cast<const VariantAssociationObject *>(that)->getElement(key, hasProperty);
+
+ }
+
+ bool VariantAssociationObject::virtualPut(Managed *that, PropertyKey id, const Value &value, Value *)
+ {
+ QString key = id.toQString();
+ return static_cast<VariantAssociationObject *>(that)->putElement(key, value);
+ }
+
+ bool VariantAssociationObject::virtualDeleteProperty(Managed *that, PropertyKey id)
+ {
+ QString key = id.toQString();
+ return static_cast<VariantAssociationObject *>(that)->deleteElement(key);
+ }
+
+ OwnPropertyKeyIterator *VariantAssociationObject::virtualOwnPropertyKeys(
+ const Object *m, Value *target
+ ) {
+ struct VariantAssociationOwnPropertyKeyIterator : ObjectOwnPropertyKeyIterator
+ {
+ QStringList keys;
+
+ ~VariantAssociationOwnPropertyKeyIterator() override = default;
+
+ PropertyKey next(const Object *o, Property *pd = nullptr, PropertyAttributes *attrs = nullptr) override
+ {
+ const VariantAssociationObject *variantAssociation =
+ static_cast<const VariantAssociationObject *>(o);
+
+ if (memberIndex == 0) {
+ keys = variantAssociation->keys();
+ keys.sort();
+ }
+
+ if (static_cast<qsizetype>(memberIndex) < keys.count()) {
+ Scope scope(variantAssociation->engine());
+ ScopedString propertyName(scope, scope.engine->newString(keys[memberIndex]));
+ ScopedPropertyKey id(scope, propertyName->toPropertyKey());
+
+ if (attrs)
+ *attrs = QV4::Attr_Data;
+ if (pd)
+ pd->value = variantAssociation->getElement(keys[memberIndex]);
+
+ ++memberIndex;
+
+ return id;
+ }
+
+ return PropertyKey::invalid();
+ }
+ };
+
+ QV4::ReferenceObject::readReference(static_cast<const VariantAssociationObject *>(m)->d());
+
+ *target = *m;
+ return new VariantAssociationOwnPropertyKeyIterator;
+ }
+
+ PropertyAttributes VariantAssociationObject::virtualGetOwnProperty(
+ const Managed *m, PropertyKey id, Property *p
+ ) {
+ auto variantAssociation = static_cast<const VariantAssociationObject *>(m);
+
+ bool hasElement = false;
+ Scope scope(variantAssociation->engine());
+ ScopedValue element(scope, variantAssociation->getElement(id.toQString(), &hasElement));
+
+ if (!hasElement)
+ return Attr_Invalid;
+
+ if (p)
+ p->value = element->asReturnedValue();
+
+ return Attr_Data;
+ }
+
+ int VariantAssociationObject::virtualMetacall(Object *object, QMetaObject::Call call, int index, void **a)
+ {
+ VariantAssociationObject *variantAssociation = static_cast<VariantAssociationObject *>(object);
+ Q_ASSERT(variantAssociation);
+
+ Heap::VariantAssociationObject *heapAssociation = variantAssociation->d();
+
+ switch (call) {
+ case QMetaObject::ReadProperty: {
+ QV4::ReferenceObject::readReference(heapAssociation);
+
+ if (index < 0 || index >= static_cast<int>(heapAssociation->propertyIndexMapping->size()))
+ return 0;
+
+ const QString& key = indexToKey(*heapAssociation->propertyIndexMapping, index);
+
+ if (!visitVariantAssociation<bool>(heapAssociation, [key](auto association) {
+ return association->contains(key);
+ })) {
+ return 0;
+ }
+
+ visitVariantAssociation<void>(heapAssociation, [a, key](auto association) {
+ *static_cast<QVariant*>(a[0]) = association->value(key);
+ });
+
+ break;
+ }
+ case QMetaObject::WriteProperty: {
+ if (index < 0 || index >= static_cast<int>(heapAssociation->propertyIndexMapping->size()))
+ return 0;
+
+ const QString& key = indexToKey(*heapAssociation->propertyIndexMapping, index);
+
+ visitVariantAssociation<void>(heapAssociation, [a, key](auto association){
+ if (association->contains(key))
+ association->insert(key, *static_cast<QVariant*>(a[0]));
+ });
+
+ QV4::ReferenceObject::writeBack(heapAssociation);
+
+ break;
+ }
+ default:
+ return 0; // not supported
+ }
+
+ return -1;
+ }
+
+ QV4::ReturnedValue VariantAssociationObject::getElement(const QString& key, bool *hasProperty) const {
+ QV4::ReferenceObject::readReference(d());
+
+ return visitVariantAssociation<QV4::ReturnedValue>(
+ d(),
+ [engine = engine(), this, key, hasProperty](auto* association) {
+ bool hasElement = association->contains(key);
+ if (hasProperty)
+ *hasProperty = hasElement;
+ return engine->fromVariant(
+ association->value(key),
+ d(), hasElement ? keyToIndex(*d()->propertyIndexMapping, key) : -1,
+ Heap::ReferenceObject::Flag::CanWriteBack |
+ Heap::ReferenceObject::Flag::IsVariant);
+ }
+ );
+ }
+
+ bool VariantAssociationObject::putElement(const QString& key, const Value& value) {
+ Heap::VariantAssociationObject *heapAssociation = d();
+
+ visitVariantAssociation<void>(heapAssociation, [engine = engine(), value, key](auto association){
+ association->insert(key, engine->toVariant(value, QMetaType{}, false));
+ });
+
+ mapPropertyKey(*heapAssociation->propertyIndexMapping, key);
+
+ QV4::ReferenceObject::writeBack(heapAssociation);
+ return true;
+ }
+
+ bool VariantAssociationObject::deleteElement(const QString& key) {
+ bool result = visitVariantAssociation<bool>(d(), [key](auto association) {
+ return association->remove(key);
+ });
+
+ if (result)
+ QV4::ReferenceObject::writeBack(d());
+
+ return result;
+ }
+
+ QStringList VariantAssociationObject::keys() const {
+ return visitVariantAssociation<QStringList>(d(), [](auto association){
+ return association->keys();
+ });
+ }
+} // namespace QV4
+
+QT_END_NAMESPACE
+
+#include "moc_qv4variantassociationobject_p.cpp"