diff options
author | Luca Di Sera <[email protected]> | 2024-11-13 14:32:21 +0100 |
---|---|---|
committer | Luca Di Sera <[email protected]> | 2024-12-01 22:30:22 +0100 |
commit | cca0b460f882aa3fce2e8aa7594bee974d9ed74d (patch) | |
tree | 90c7d04cd447cb586292d537f6eab7431e2c28ca /src/qml/jsruntime/qv4variantassociationobject.cpp | |
parent | a6c8de2780b6dface7504b5423e064e026a6e564 (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.cpp | 377 |
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" |