1// Copyright (C) 2024 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
4#include "qv4variantassociationobject_p.h"
5
6#include <private/qqmlengine_p.h>
7
8QT_BEGIN_NAMESPACE
9
10/*!
11 * \class QV4::VariantAssociationObject
12 * \internal
13 *
14 * A VariantAssociationObject stores the contents of a QVariantMap or QVariantHash
15 * and makes them acccessible to the JavaScript engine. It behaves mostly like a
16 * regular JavaScript object. The entries of the QVariantMap or QVariantHash are
17 * exposed as properties.
18 *
19 * VariantAssociationObject is a ReferenceObject. Therefore it writes back its contents
20 * to the property it was retrieved from whenever it changes. It also re-reads
21 * the property whenever that one changes.
22 *
23 * As long as a VariantAssociationObject is attached to a property this way, it is the
24 * responsibility of the property's surrounding (C++) object to keep the contents valid.
25 * It has to, for example, track pointers to QObjects potentially deleted in other places
26 * so that they don't become dangling.
27 *
28 * However, the VariantAssociation can also be detached. This happens predominantly by
29 * assigning it to a QML-declared property. In that case, it becomes the
30 * VariantAssociationObject's responsibility to track its contents. To do so, it does not
31 * keep an actual QVariantMap or QVariantHash in this case, but rather stores its contents
32 * as actual JavaScript object properties. This includes QObjectWrappers for all QObject
33 * pointers it may contain. The contents are then marked like all JavaScript properties
34 * when the garbage collector runs, and QObjectWrapper also guards against external
35 * deletion. There is no property to read or write back in this case, and neither
36 * does the internal QVariantMap or QVariantHash need to be updated. Therefore, the
37 * objects stored in the individual properties are also created detached and won't
38 * read or write back.
39 */
40
41template<typename Return, typename MapCallable, typename HashCallable>
42Return visitVariantAssociation(
43 const QV4::Heap::VariantAssociationObject* association,
44 MapCallable&& mapCallable,
45 HashCallable&& hashCallable
46) {
47 switch (association->m_type) {
48 case QV4::Heap::VariantAssociationObject::AssociationType::VariantMap:
49 return std::invoke(
50 std::forward<MapCallable>(mapCallable),
51 reinterpret_cast<const QVariantMap *>(&association->m_variantAssociation));
52 case QV4::Heap::VariantAssociationObject::AssociationType::VariantHash:
53 return std::invoke(
54 std::forward<HashCallable>(hashCallable),
55 reinterpret_cast<const QVariantHash *>(&association->m_variantAssociation));
56 default: Q_UNREACHABLE();
57 };
58}
59
60template<typename Return, typename MapCallable, typename HashCallable>
61Return visitVariantAssociation(
62 QV4::Heap::VariantAssociationObject* association,
63 MapCallable&& mapCallable,
64 HashCallable&& hashCallable
65) {
66 switch (association->m_type) {
67 case QV4::Heap::VariantAssociationObject::AssociationType::VariantMap:
68 return std::invoke(
69 std::forward<MapCallable>(mapCallable),
70 reinterpret_cast<QVariantMap *>(&association->m_variantAssociation));
71 case QV4::Heap::VariantAssociationObject::AssociationType::VariantHash:
72 return std::invoke(
73 std::forward<HashCallable>(hashCallable),
74 reinterpret_cast<QVariantHash *>(&association->m_variantAssociation));
75 default: Q_UNREACHABLE();
76 };
77}
78
79template<typename Return, typename Callable>
80Return visitVariantAssociation(
81 const QV4::Heap::VariantAssociationObject* association,
82 Callable&& callable
83) {
84 return visitVariantAssociation<Return>(association, callable, callable);
85}
86
87template<typename Return, typename Callable>
88Return visitVariantAssociation(
89 QV4::Heap::VariantAssociationObject* association,
90 Callable&& callable
91) {
92 return visitVariantAssociation<Return>(association, callable, callable);
93}
94
95namespace QV4 {
96
97 DEFINE_OBJECT_VTABLE(VariantAssociationObject);
98
99 ReturnedValue VariantAssociationPrototype::fromQVariantMap(
100 ExecutionEngine *engine,
101 const QVariantMap& variantMap,
102 QV4::Heap::Object* container,
103 int property, Heap::ReferenceObject::Flags flags)
104 {
105 return engine->memoryManager->allocate<VariantAssociationObject>(
106 args: variantMap, args&: container, args&: property, args&: flags)->asReturnedValue();
107 }
108
109 ReturnedValue VariantAssociationPrototype::fromQVariantHash(
110 ExecutionEngine *engine,
111 const QVariantHash& variantHash,
112 QV4::Heap::Object* container,
113 int property, Heap::ReferenceObject::Flags flags)
114 {
115 return engine->memoryManager->allocate<VariantAssociationObject>(
116 args: variantHash, args&: container, args&: property, args&: flags)->asReturnedValue();
117 }
118
119 namespace Heap {
120 void VariantAssociationObject::init(
121 const QVariantMap& variantMap,
122 QV4::Heap::Object* container,
123 int property, Heap::ReferenceObject::Flags flags)
124 {
125 ReferenceObject::init(object: container, property, flags);
126 m_type = AssociationType::VariantMap;
127
128 if (container)
129 new (m_variantAssociation) QVariantMap(variantMap);
130 else
131 createElementWrappers(association: variantMap);
132 }
133
134 void VariantAssociationObject::init(
135 const QVariantHash& variantHash,
136 QV4::Heap::Object* container,
137 int property, Heap::ReferenceObject::Flags flags)
138 {
139 ReferenceObject::init(object: container, property, flags);
140 m_type = AssociationType::VariantHash;
141
142 if (container)
143 new (m_variantAssociation) QVariantHash(variantHash);
144 else
145 createElementWrappers(association: variantHash);
146 }
147
148 void VariantAssociationObject::destroy() {
149 if (object()) {
150 visitVariantAssociation<void>(
151 association: this, mapCallable&: std::destroy_at<QVariantMap>, hashCallable&: std::destroy_at<QVariantHash>);
152 }
153 ReferenceObject::destroy();
154 }
155
156 QVariant VariantAssociationObject::toVariant()
157 {
158 if (object()) {
159 return visitVariantAssociation<QVariant>(
160 association: this, callable: [](const auto *association){ return QVariant(*association); });
161 }
162
163 QV4::Scope scope(internalClass->engine);
164 QV4::ScopedObject self(scope, this);
165
166 switch (m_type) {
167 case AssociationType::VariantMap:
168 return QV4::ExecutionEngine::variantMapFromJS(o: self);
169 case AssociationType::VariantHash:
170 return QV4::ExecutionEngine::variantHashFromJS(o: self);
171 default:
172 break;
173 }
174
175 Q_UNREACHABLE_RETURN(QVariant());
176 }
177
178 bool VariantAssociationObject::setVariant(const QVariant &variant)
179 {
180 // Should only happen from readReference(). Therefore we are attached.
181 Q_ASSERT(object());
182
183 auto metatypeId = variant.metaType().id();
184
185 if (metatypeId != QMetaType::QVariantMap && metatypeId != QMetaType::QVariantHash)
186 return false;
187
188 if (metatypeId == QMetaType::QVariantMap && m_type == AssociationType::VariantMap) {
189 *reinterpret_cast<QVariantMap *>(&m_variantAssociation) = variant.toMap();
190 } else if (metatypeId == QMetaType::QVariantMap && m_type == AssociationType::VariantHash) {
191 std::destroy_at(location: reinterpret_cast<QVariantHash *>(&m_variantAssociation));
192 new(m_variantAssociation) QVariantMap(variant.toMap());
193 m_type = AssociationType::VariantMap;
194 } else if (metatypeId == QMetaType::QVariantHash && m_type == AssociationType::VariantHash) {
195 *reinterpret_cast<QVariantHash *>(&m_variantAssociation) = variant.toHash();
196 } else if (metatypeId == QMetaType::QVariantHash && m_type == AssociationType::VariantMap) {
197 std::destroy_at(location: reinterpret_cast<QVariantMap *>(&m_variantAssociation));
198 new(m_variantAssociation) QVariantHash(variant.toHash());
199 m_type = AssociationType::VariantHash;
200 }
201
202 return true;
203 }
204
205 template<typename Association>
206 VariantAssociationObject *createDetached(
207 QV4::ExecutionEngine *engine, const Association &association)
208 {
209 return engine->memoryManager->allocate<QV4::VariantAssociationObject>(
210 association, nullptr, -1, ReferenceObject::Flag::NoFlag);
211 }
212
213 VariantAssociationObject *VariantAssociationObject::detached()
214 {
215 if (object()) {
216 return visitVariantAssociation<VariantAssociationObject *>(
217 association: this, callable: [this](const auto *association) {
218 return createDetached(internalClass->engine, *association);
219 });
220 }
221
222 QV4::Scope scope(internalClass->engine);
223 QV4::ScopedObject self(scope, this);
224
225 switch (m_type) {
226 case AssociationType::VariantMap:
227 return createDetached(engine: scope.engine, association: QV4::ExecutionEngine::variantMapFromJS(o: self));
228 case AssociationType::VariantHash:
229 return createDetached(engine: scope.engine, association: QV4::ExecutionEngine::variantHashFromJS(o: self));
230 default:
231 break;
232 }
233
234 Q_UNREACHABLE_RETURN(nullptr);
235 }
236
237 QV4::ReturnedValue VariantAssociationObject::getElement(
238 const QString &key, bool *hasProperty)
239 {
240 // Must be attached. Otherwise we should use memberData/arrayData
241 Q_ASSERT(object());
242 Q_ASSERT(hasProperty);
243
244 QV4::ReferenceObject::readReference(ref: this);
245
246 return visitVariantAssociation<QV4::ReturnedValue>(association: this, callable: [&](const auto *association) {
247 auto it = association->constFind(key);
248 if (it == association->constEnd()) {
249 *hasProperty = false;
250 return Encode::undefined();
251 }
252
253 *hasProperty = true;
254
255 Scope scope(internalClass->engine);
256 ScopedString scopedKey(scope, scope.engine->newString(s: key));
257
258 uint i = 0;
259 if (propertyIndexMapping) {
260 Scoped<QV4::ArrayData> arrayData(scope, propertyIndexMapping->arrayData);
261 const uint end = arrayData->length();
262 for (; i < end; ++i) {
263 QV4::ScopedString value(scope, arrayData->get(i));
264 Q_ASSERT(value);
265 if (value->equals(other: scopedKey))
266 break;
267 }
268
269 if (i == end) {
270 ScopedArrayObject mapping(scope, propertyIndexMapping);
271 mapping->push_back(v: scopedKey);
272 }
273 } else {
274 propertyIndexMapping.set(
275 e: scope.engine, newVal: scope.engine->newArrayObject(values: scopedKey, length: 1));
276 }
277
278 return scope.engine->fromVariant(
279 *it, this, i,
280 ReferenceObject::Flag::CanWriteBack | ReferenceObject::Flag::IsVariant);
281 });
282 }
283
284 } // namespace Heap
285
286 ReturnedValue VariantAssociationObject::virtualGet(const Managed *that, PropertyKey id, const Value *, bool * hasProperty)
287 {
288 Heap::VariantAssociationObject *heapAssociation
289 = static_cast<const VariantAssociationObject *>(that)->d();
290 // If this is detached we rely on the element wrappers to hold the value
291 if (!heapAssociation->object())
292 return ReferenceObject::virtualGet(m: that, id, receiver: that, hasProperty);
293
294 bool found = false;
295 if (ReturnedValue result = heapAssociation->getElement(key: id.toQString(), hasProperty: &found); found) {
296 if (hasProperty)
297 *hasProperty = true;
298 return result;
299 }
300
301 return ReferenceObject::virtualGet(m: that, id, receiver: that, hasProperty);
302 }
303
304 bool VariantAssociationObject::virtualPut(Managed *that, PropertyKey id, const Value &value, Value *)
305 {
306 Heap::VariantAssociationObject *heapAssociation
307 = static_cast<VariantAssociationObject *>(that)->d();
308
309 if (!heapAssociation->object())
310 return ReferenceObject::virtualPut(m: that, id, value, receiver: that);
311
312 visitVariantAssociation<void>(association: heapAssociation, callable: [&](auto *association) {
313 association->insert(
314 id.toQString(),
315 heapAssociation->internalClass->engine->toVariant(value, typeHint: QMetaType(), createJSValueForObjectsAndSymbols: false));
316 });
317
318 QV4::ReferenceObject::writeBack(ref: heapAssociation);
319 return true;
320 }
321
322 bool VariantAssociationObject::virtualDeleteProperty(Managed *that, PropertyKey id)
323 {
324 Heap::VariantAssociationObject *heapAssociation
325 = static_cast<VariantAssociationObject *>(that)->d();
326
327 if (!heapAssociation->object())
328 return ReferenceObject::virtualDeleteProperty(m: that, id);
329
330 if (!visitVariantAssociation<bool>(association: heapAssociation, callable: [&](auto *association) {
331 return association->remove(id.toQString());
332 })) {
333 return false;
334 }
335
336 QV4::ReferenceObject::writeBack(ref: heapAssociation);
337 return true;
338 }
339
340 OwnPropertyKeyIterator *VariantAssociationObject::virtualOwnPropertyKeys(
341 const Object *m, Value *target
342 ) {
343 Heap::VariantAssociationObject *heapAssociation
344 = static_cast<const VariantAssociationObject *>(m)->d();
345
346 if (!heapAssociation->object())
347 return ReferenceObject::virtualOwnPropertyKeys(m, target);
348
349 struct VariantAssociationOwnPropertyKeyIterator : ObjectOwnPropertyKeyIterator
350 {
351 QStringList keys;
352
353 ~VariantAssociationOwnPropertyKeyIterator() override = default;
354
355 PropertyKey next(const Object *o, Property *pd = nullptr, PropertyAttributes *attrs = nullptr) override
356 {
357 Heap::VariantAssociationObject *heapAssociation
358 = static_cast<const VariantAssociationObject *>(o)->d();
359
360 if (memberIndex == 0) {
361 keys = visitVariantAssociation<QStringList>(
362 association: heapAssociation, callable: [](const auto *association) {
363 return association->keys();
364 });
365 keys.sort();
366 }
367
368 if (static_cast<qsizetype>(memberIndex) < keys.count()) {
369 Scope scope(heapAssociation->internalClass->engine);
370 ScopedString propertyName(scope, scope.engine->newString(s: keys[memberIndex]));
371 ScopedPropertyKey id(scope, propertyName->toPropertyKey());
372
373 if (attrs)
374 *attrs = QV4::Attr_Data;
375 if (pd) {
376 bool found = false;
377 pd->value = heapAssociation->getElement(key: keys[memberIndex], hasProperty: &found);
378 Q_ASSERT(found);
379 }
380
381 ++memberIndex;
382
383 return id;
384 }
385
386 return PropertyKey::invalid();
387 }
388 };
389
390 QV4::ReferenceObject::readReference(ref: heapAssociation);
391
392 *target = *m;
393 return new VariantAssociationOwnPropertyKeyIterator;
394 }
395
396 PropertyAttributes VariantAssociationObject::virtualGetOwnProperty(
397 const Managed *m, PropertyKey id, Property *p
398 ) {
399 Heap::VariantAssociationObject *heapAssociation
400 = static_cast<const VariantAssociationObject *>(m)->d();
401
402 if (!heapAssociation->object())
403 return ReferenceObject::virtualGetOwnProperty(m, id, p);
404
405 bool hasElement = false;
406 Scope scope(heapAssociation->internalClass->engine);
407 ScopedValue element(scope, heapAssociation->getElement(key: id.toQString(), hasProperty: &hasElement));
408
409 if (!hasElement)
410 return Attr_Invalid;
411
412 if (p)
413 p->value = element->asReturnedValue();
414
415 return Attr_Data;
416 }
417
418 int VariantAssociationObject::virtualMetacall(Object *object, QMetaObject::Call call, int index, void **a)
419 {
420 Heap::VariantAssociationObject *heapAssociation
421 = static_cast<VariantAssociationObject *>(object)->d();
422
423 // We only create attached wrappers if this variant association is itself attached.
424 // When detaching, we re-create everything. Therefore, we can't get a metaCall if
425 // we are detached.
426 Q_ASSERT(heapAssociation->object());
427
428 Q_ASSERT(heapAssociation->propertyIndexMapping);
429
430 Scope scope(heapAssociation->internalClass->engine);
431 Scoped<QV4::ArrayData> arrayData(scope, heapAssociation->propertyIndexMapping->arrayData);
432 if (index < 0 || uint(index) >= arrayData->length())
433 return 0;
434
435 ScopedString scopedKey(scope, arrayData->get(i: index));
436
437 switch (call) {
438 case QMetaObject::ReadProperty: {
439 QV4::ReferenceObject::readReference(ref: heapAssociation);
440
441 if (!visitVariantAssociation<bool>(association: heapAssociation, callable: [&](const auto *association) {
442 const auto it = association->constFind(scopedKey->toQString());
443 if (it == association->constEnd())
444 return false;
445
446 *static_cast<QVariant *>(a[0]) = *it;
447 return true;
448 })) {
449 return 0;
450 }
451
452 break;
453 }
454 case QMetaObject::WriteProperty: {
455 if (!visitVariantAssociation<bool>(association: heapAssociation, callable: [&](auto *association) {
456 const auto it = association->find(scopedKey->toQString());
457 if (it == association->end())
458 return false;
459 *it = *static_cast<const QVariant *>(a[0]);
460 return true;
461 })) {
462 return 0;
463 }
464
465 QV4::ReferenceObject::writeBack(ref: heapAssociation);
466 break;
467 }
468 default:
469 return 0; // not supported
470 }
471
472 return -1;
473 }
474} // namespace QV4
475
476QT_END_NAMESPACE
477

source code of qtdeclarative/src/qml/jsruntime/qv4variantassociationobject.cpp