1// Copyright (C) 2020 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 "qqmlpropertybinding_p.h"
5
6#include <private/qqmlbinding_p.h>
7#include <private/qqmlglobal_p.h>
8#include <private/qqmlscriptstring_p.h>
9#include <private/qv4functionobject_p.h>
10#include <private/qv4jscall_p.h>
11#include <private/qv4qmlcontext_p.h>
12
13#include <QtQml/qqmlinfo.h>
14
15#include <QtCore/qloggingcategory.h>
16
17QT_BEGIN_NAMESPACE
18
19Q_LOGGING_CATEGORY(lcQQPropertyBinding, "qt.qml.propertybinding");
20
21QUntypedPropertyBinding QQmlPropertyBinding::create(const QQmlPropertyData *pd, QV4::Function *function,
22 QObject *obj, const QQmlRefPointer<QQmlContextData> &ctxt,
23 QV4::ExecutionContext *scope, QObject *target, QQmlPropertyIndex targetIndex)
24{
25 Q_ASSERT(pd);
26 return create(propertyType: pd->propType(), function, obj, ctxt, scope, target, targetIndex);
27}
28
29QUntypedPropertyBinding QQmlPropertyBinding::create(QMetaType propertyType, QV4::Function *function,
30 QObject *obj,
31 const QQmlRefPointer<QQmlContextData> &ctxt,
32 QV4::ExecutionContext *scope, QObject *target,
33 QQmlPropertyIndex targetIndex)
34{
35 auto buffer = new std::byte[QQmlPropertyBinding::getSizeEnsuringAlignment()
36 + sizeof(QQmlPropertyBindingJS)+jsExpressionOffsetLength()]; // QQmlPropertyBinding uses delete[]
37 auto binding = new (buffer) QQmlPropertyBinding(propertyType, target, targetIndex,
38 TargetData::WithoutBoundFunction);
39 auto js = new(buffer + QQmlPropertyBinding::getSizeEnsuringAlignment() + jsExpressionOffsetLength()) QQmlPropertyBindingJS();
40 Q_ASSERT(binding->jsExpression() == js);
41 Q_ASSERT(js->asBinding() == binding);
42 Q_UNUSED(js);
43 binding->jsExpression()->setNotifyOnValueChanged(true);
44 binding->jsExpression()->setContext(ctxt);
45 binding->jsExpression()->setScopeObject(obj);
46 binding->jsExpression()->setupFunction(qmlContext: scope, f: function);
47 return QUntypedPropertyBinding(static_cast<QPropertyBindingPrivate *>(QPropertyBindingPrivatePtr(binding).data()));
48}
49
50QUntypedPropertyBinding QQmlPropertyBinding::createFromCodeString(const QQmlPropertyData *pd, const QString& str, QObject *obj, const QQmlRefPointer<QQmlContextData> &ctxt, const QString &url, quint16 lineNumber, QObject *target, QQmlPropertyIndex targetIndex)
51{
52 auto buffer = new std::byte[QQmlPropertyBinding::getSizeEnsuringAlignment()
53 + sizeof(QQmlPropertyBindingJS)+jsExpressionOffsetLength()]; // QQmlPropertyBinding uses delete[]
54 auto binding = new(buffer) QQmlPropertyBinding(QMetaType(pd->propType()), target, targetIndex, TargetData::WithoutBoundFunction);
55 auto js = new(buffer + QQmlPropertyBinding::getSizeEnsuringAlignment() + jsExpressionOffsetLength()) QQmlPropertyBindingJS();
56 Q_ASSERT(binding->jsExpression() == js);
57 Q_ASSERT(js->asBinding() == binding);
58 Q_UNUSED(js);
59 binding->jsExpression()->setNotifyOnValueChanged(true);
60 binding->jsExpression()->setContext(ctxt);
61 binding->jsExpression()->createQmlBinding(ctxt, scope: obj, code: str, filename: url, line: lineNumber);
62 return QUntypedPropertyBinding(static_cast<QPropertyBindingPrivate *>(QPropertyBindingPrivatePtr(binding).data()));
63}
64
65QUntypedPropertyBinding QQmlPropertyBinding::createFromScriptString(const QQmlPropertyData *property, const QQmlScriptString &script, QObject *obj, QQmlContext *ctxt, QObject *target, QQmlPropertyIndex targetIndex)
66{
67 const QQmlScriptStringPrivate *scriptPrivate = script.d.data();
68 // without a valid context, we cannot create anything
69 if (!ctxt && (!scriptPrivate->context || !scriptPrivate->context->isValid())) {
70 return {};
71 }
72
73 auto scopeObject = obj ? obj : scriptPrivate->scope;
74
75 QV4::Function *runtimeFunction = nullptr;
76 QString url;
77 QQmlRefPointer<QQmlContextData> ctxtdata = QQmlContextData::get(context: scriptPrivate->context);
78 QQmlEnginePrivate *engine = QQmlEnginePrivate::get(e: scriptPrivate->context->engine());
79 if (engine && ctxtdata && !ctxtdata->urlString().isEmpty() && ctxtdata->typeCompilationUnit()) {
80 url = ctxtdata->urlString();
81 if (scriptPrivate->bindingId != QQmlBinding::Invalid)
82 runtimeFunction = ctxtdata->typeCompilationUnit()->runtimeFunctions.at(i: scriptPrivate->bindingId);
83 }
84 // Do we actually have a function in the script string? If not, this becomes createCodeFromString
85 if (!runtimeFunction)
86 return createFromCodeString(pd: property, str: scriptPrivate->script, obj, ctxt: ctxtdata, url, lineNumber: scriptPrivate->lineNumber, target, targetIndex);
87
88 auto buffer = new std::byte[QQmlPropertyBinding::getSizeEnsuringAlignment()
89 + sizeof(QQmlPropertyBindingJS)+jsExpressionOffsetLength()]; // QQmlPropertyBinding uses delete[]
90 auto binding = new(buffer) QQmlPropertyBinding(QMetaType(property->propType()), target, targetIndex, TargetData::WithoutBoundFunction);
91 auto js = new(buffer + QQmlPropertyBinding::getSizeEnsuringAlignment() + jsExpressionOffsetLength()) QQmlPropertyBindingJS();
92 Q_ASSERT(binding->jsExpression() == js);
93 Q_ASSERT(js->asBinding() == binding);
94 js->setContext(QQmlContextData::get(context: ctxt ? ctxt : scriptPrivate->context));
95
96 QV4::ExecutionEngine *v4 = engine->v4engine();
97 QV4::Scope scope(v4);
98 QV4::Scoped<QV4::QmlContext> qmlContext(scope, QV4::QmlContext::create(parent: v4->rootContext(), context: ctxtdata, scopeObject));
99 js->setupFunction(qmlContext, f: runtimeFunction);
100 return QUntypedPropertyBinding(static_cast<QPropertyBindingPrivate *>(QPropertyBindingPrivatePtr(binding).data()));
101}
102
103QUntypedPropertyBinding QQmlPropertyBinding::createFromBoundFunction(const QQmlPropertyData *pd, QV4::BoundFunction *function, QObject *obj, const QQmlRefPointer<QQmlContextData> &ctxt, QV4::ExecutionContext *scope, QObject *target, QQmlPropertyIndex targetIndex)
104{
105 auto buffer = new std::byte[QQmlPropertyBinding::getSizeEnsuringAlignment()
106 + sizeof(QQmlPropertyBindingJSForBoundFunction)+jsExpressionOffsetLength()]; // QQmlPropertyBinding uses delete[]
107 auto binding = new(buffer) QQmlPropertyBinding(QMetaType(pd->propType()), target, targetIndex, TargetData::HasBoundFunction);
108 auto js = new(buffer + QQmlPropertyBinding::getSizeEnsuringAlignment() + jsExpressionOffsetLength()) QQmlPropertyBindingJSForBoundFunction();
109 Q_ASSERT(binding->jsExpression() == js);
110 Q_ASSERT(js->asBinding() == binding);
111 Q_UNUSED(js);
112 binding->jsExpression()->setNotifyOnValueChanged(true);
113 binding->jsExpression()->setContext(ctxt);
114 binding->jsExpression()->setScopeObject(obj);
115 binding->jsExpression()->setupFunction(qmlContext: scope, f: function->function());
116 js->m_boundFunction.set(engine: function->engine(), value: *function);
117 return QUntypedPropertyBinding(static_cast<QPropertyBindingPrivate *>(QPropertyBindingPrivatePtr(binding).data()));
118}
119
120/*!
121 \fn bool QQmlPropertyBindingJS::hasDependencies()
122 \internal
123
124 Returns true if this binding has dependencies.
125 Dependencies can be either QProperty dependencies or dependencies of
126 the JS expression (aka activeGuards). Translations end up as a QProperty
127 dependency, so they do not need any special handling
128 Note that a QQmlPropertyBinding never stores qpropertyChangeTriggers.
129 */
130
131
132void QQmlPropertyBindingJS::expressionChanged()
133{
134 auto binding = asBinding();
135 if (!binding->propertyDataPtr)
136 return;
137 const auto currentTag = m_error.tag();
138 if (currentTag == InEvaluationLoop) {
139 QQmlError err;
140 auto location = QQmlJavaScriptExpression::sourceLocation();
141 err.setUrl(QUrl{location.sourceFile});
142 err.setLine(location.line);
143 err.setColumn(location.column);
144 const auto ctxt = context();
145 QQmlEngine *engine = ctxt ? ctxt->engine() : nullptr;
146 if (engine)
147 err.setDescription(asBinding()->createBindingLoopErrorDescription());
148 else
149 err.setDescription(QString::fromLatin1(ba: "Binding loop detected"));
150 err.setObject(asBinding()->target());
151 qmlWarning(me: this->scopeObject(), error: err);
152 return;
153 }
154 m_error.setTag(InEvaluationLoop);
155 PendingBindingObserverList bindingObservers;
156 binding->evaluateRecursive(bindingObservers);
157 binding->notifyNonRecursive(bindingObservers);
158 m_error.setTag(NoTag);
159}
160
161QQmlPropertyBinding::QQmlPropertyBinding(QMetaType mt, QObject *target, QQmlPropertyIndex targetIndex, TargetData::BoundFunction hasBoundFunction)
162 : QPropertyBindingPrivate(mt,
163 bindingFunctionVTableForQQmlPropertyBinding(type: mt),
164 QPropertyBindingSourceLocation(), true)
165{
166 static_assert (std::is_trivially_destructible_v<TargetData>);
167 static_assert (sizeof(TargetData) + sizeof(DeclarativeErrorCallback) <= sizeof(QPropertyBindingSourceLocation));
168 static_assert (alignof(TargetData) <= alignof(QPropertyBindingSourceLocation));
169 const auto state = hasBoundFunction ? TargetData::HasBoundFunction : TargetData::WithoutBoundFunction;
170 new (&declarativeExtraData) TargetData {target, targetIndex, state};
171 errorCallBack = bindingErrorCallback;
172}
173
174static QtPrivate::QPropertyBindingData *bindingDataFromPropertyData(QUntypedPropertyData *dataPtr, QMetaType type)
175{
176 // XXX Qt 7: We need a clean way to access the binding data
177 /* This function makes the (dangerous) assumption that if we could not get the binding data
178 from the binding storage, we must have been handed a QProperty.
179 This does hold for anything a user could write, as there the only ways of providing a bindable property
180 are to use the Q_X_BINDABLE macros, or to directly expose a QProperty.
181 As long as we can ensure that any "fancier" property we implement is not resettable, we should be fine.
182 We procede to calculate the address of the binding data pointer from the address of the data pointer
183 */
184 Q_ASSERT(dataPtr);
185 std::byte *qpropertyPointer = reinterpret_cast<std::byte *>(dataPtr);
186 qpropertyPointer += type.sizeOf();
187 constexpr auto alignment = alignof(QtPrivate::QPropertyBindingData *);
188 auto aligned = (quintptr(qpropertyPointer) + alignment - 1) & ~(alignment - 1); // ensure pointer alignment
189 return reinterpret_cast<QtPrivate::QPropertyBindingData *>(aligned);
190}
191
192void QQmlPropertyBinding::handleUndefinedAssignment(QQmlEnginePrivate *ep, void *dataPtr)
193{
194 const QQmlPropertyData *propertyData = nullptr;
195 QQmlPropertyData valueTypeData;
196 QQmlData *data = QQmlData::get(object: target(), create: false);
197 Q_ASSERT(data);
198 if (Q_UNLIKELY(!data->propertyCache))
199 data->propertyCache = QQmlMetaType::propertyCache(metaObject: target()->metaObject());
200
201 propertyData = data->propertyCache->property(index: targetIndex().coreIndex());
202 Q_ASSERT(propertyData);
203 Q_ASSERT(!targetIndex().hasValueTypeIndex());
204 QQmlProperty prop = QQmlPropertyPrivate::restore(target(), *propertyData, &valueTypeData, nullptr);
205 // helper function for writing back value into dataPtr
206 // this is necessary for QObjectCompatProperty, which doesn't give us the "real" dataPtr
207 // if we don't write the correct value, we would otherwise set the default constructed value
208 auto writeBackCurrentValue = [&](QVariant &&currentValue) {
209 if (currentValue.metaType() != valueMetaType())
210 currentValue.convert(type: valueMetaType());
211 auto metaType = valueMetaType();
212 metaType.destruct(data: dataPtr);
213 metaType.construct(where: dataPtr, copy: currentValue.constData());
214 };
215 if (prop.isResettable()) {
216 // Normally a reset would remove any existing binding; but now we need to keep the binding alive
217 // to handle the case where this binding becomes defined again
218 // We therefore detach the binding, call reset, and reattach again
219 const auto storage = qGetBindingStorage(o: target());
220 auto bindingData = storage->bindingData(data: propertyDataPtr);
221 if (!bindingData)
222 bindingData = bindingDataFromPropertyData(dataPtr: propertyDataPtr, type: propertyData->propType());
223 QPropertyBindingDataPointer bindingDataPointer{.ptr: bindingData};
224 auto firstObserver = takeObservers();
225 bindingData->d_ref() = 0;
226 if (firstObserver) {
227 bindingDataPointer.setObservers(firstObserver.ptr);
228 }
229 Q_ASSERT(!bindingData->hasBinding());
230 setIsUndefined(true);
231 //suspend binding evaluation state for reset and subsequent read
232 auto state = QtPrivate::suspendCurrentBindingStatus();
233 prop.reset(); // May re-allocate the bindingData
234 QVariant currentValue = QVariant(prop.propertyMetaType(), propertyDataPtr);
235 QtPrivate::restoreBindingStatus(status: state);
236 writeBackCurrentValue(std::move(currentValue));
237
238 // Re-fetch binding data
239 bindingData = storage->bindingData(data: propertyDataPtr);
240 if (!bindingData)
241 bindingData = bindingDataFromPropertyData(dataPtr: propertyDataPtr, type: propertyData->propType());
242 bindingDataPointer = QPropertyBindingDataPointer {.ptr: bindingData};
243
244 // reattach the binding (without causing a new notification)
245 if (Q_UNLIKELY(bindingData->d() & QtPrivate::QPropertyBindingData::BindingBit)) {
246 qCWarning(lcQQPropertyBinding) << "Resetting " << prop.name() << "due to the binding becoming undefined caused a new binding to be installed\n"
247 << "The old binding binding will be abandoned";
248 deref();
249 return;
250 }
251 // reset might have changed observers (?), so refresh firstObserver
252 firstObserver = bindingDataPointer.firstObserver();
253 bindingData->d_ref() = reinterpret_cast<quintptr>(this) | QtPrivate::QPropertyBindingData::BindingBit;
254 if (firstObserver)
255 prependObserver(observer: firstObserver);
256 } else {
257 QQmlError qmlError;
258 auto location = jsExpression()->sourceLocation();
259 qmlError.setColumn(location.column);
260 qmlError.setLine(location.line);
261 qmlError.setUrl(QUrl {location.sourceFile});
262 const QString description = QStringLiteral(R"(QML %1: Unable to assign [undefined] to "%2")").arg(args: QQmlMetaType::prettyTypeName(object: target()) , args: prop.name());
263 qmlError.setDescription(description);
264 qmlError.setObject(target());
265 ep->warning(qmlError);
266 }
267}
268
269QString QQmlPropertyBinding::createBindingLoopErrorDescription()
270{
271 const QQmlPropertyData *propertyData = nullptr;
272 QQmlPropertyData valueTypeData;
273 QQmlData *data = QQmlData::get(object: target(), create: false);
274 Q_ASSERT(data);
275 if (Q_UNLIKELY(!data->propertyCache))
276 data->propertyCache = QQmlMetaType::propertyCache(metaObject: target()->metaObject());
277
278 propertyData = data->propertyCache->property(index: targetIndex().coreIndex());
279 Q_ASSERT(propertyData);
280 Q_ASSERT(!targetIndex().hasValueTypeIndex());
281 QQmlProperty prop = QQmlPropertyPrivate::restore(target(), *propertyData, &valueTypeData, nullptr);
282 return QStringLiteral(R"(QML %1: Binding loop detected for property "%2")").arg(args: QQmlMetaType::prettyTypeName(object: target()) , args: prop.name());
283}
284
285void QQmlPropertyBinding::bindingErrorCallback(QPropertyBindingPrivate *that)
286{
287 auto This = static_cast<QQmlPropertyBinding *>(that);
288 auto target = This->target();
289 auto engine = qmlEngine(target);
290 if (!engine)
291 return;
292
293 auto error = This->bindingError();
294 QQmlError qmlError;
295 auto location = This->jsExpression()->sourceLocation();
296 qmlError.setColumn(location.column);
297 qmlError.setLine(location.line);
298 qmlError.setUrl(QUrl {location.sourceFile});
299 auto description = error.description();
300 if (error.type() == QPropertyBindingError::BindingLoop) {
301 description = This->createBindingLoopErrorDescription();
302 }
303 qmlError.setDescription(description);
304 qmlError.setObject(target);
305 QQmlEnginePrivate::get(e: engine)->warning(qmlError);
306}
307
308template<typename TranslateWithUnit>
309auto qQmlTranslationPropertyBindingCreateBinding(
310 const QQmlRefPointer<QV4::ExecutableCompilationUnit> &compilationUnit,
311 TranslateWithUnit &&translateWithUnit)
312{
313 return [compilationUnit, translateWithUnit](QMetaType metaType, void *dataPtr) -> bool {
314 // Create a dependency to the translationLanguage
315 QQmlEnginePrivate::get(e: compilationUnit->engine)->translationLanguage.value();
316
317 QVariant resultVariant(translateWithUnit(compilationUnit));
318 if (metaType != QMetaType::fromType<QString>())
319 resultVariant.convert(type: metaType);
320
321 const bool hasChanged = !metaType.equals(lhs: resultVariant.constData(), rhs: dataPtr);
322 metaType.destruct(data: dataPtr);
323 metaType.construct(where: dataPtr, copy: resultVariant.constData());
324 return hasChanged;
325 };
326}
327
328QUntypedPropertyBinding QQmlTranslationPropertyBinding::create(
329 const QQmlPropertyData *pd,
330 const QQmlRefPointer<QV4::ExecutableCompilationUnit> &compilationUnit,
331 const QV4::CompiledData::Binding *binding)
332{
333 auto translationBinding = qQmlTranslationPropertyBindingCreateBinding(
334 compilationUnit,
335 translateWithUnit: [binding](const QQmlRefPointer<QV4::ExecutableCompilationUnit> &compilationUnit) {
336 return compilationUnit->bindingValueAsString(binding);
337 });
338
339 return QUntypedPropertyBinding(QMetaType(pd->propType()), translationBinding,
340 QPropertyBindingSourceLocation());
341}
342
343QUntypedPropertyBinding QQmlTranslationPropertyBinding::create(
344 const QMetaType &propertyType,
345 const QQmlRefPointer<QV4::ExecutableCompilationUnit> &compilationUnit,
346 const QQmlTranslation &translationData)
347{
348 auto translationBinding = qQmlTranslationPropertyBindingCreateBinding(
349 compilationUnit,
350 translateWithUnit: [translationData](
351 const QQmlRefPointer<QV4::ExecutableCompilationUnit> &compilationUnit) {
352 Q_UNUSED(compilationUnit);
353 return translationData.translate();
354 });
355
356 return QUntypedPropertyBinding(propertyType, translationBinding,
357 QPropertyBindingSourceLocation());
358}
359
360QV4::ReturnedValue QQmlPropertyBindingJSForBoundFunction::evaluate(bool *isUndefined)
361{
362 QV4::ExecutionEngine *v4 = engine()->handle();
363 int argc = 0;
364 const QV4::Value *argv = nullptr;
365 const QV4::Value *thisObject = nullptr;
366 QV4::BoundFunction *b = nullptr;
367 if ((b = m_boundFunction.as<QV4::BoundFunction>())) {
368 QV4::Heap::MemberData *args = b->boundArgs();
369 if (args) {
370 argc = args->values.size;
371 argv = args->values.data();
372 }
373 thisObject = &b->d()->boundThis;
374 }
375 QV4::Scope scope(v4);
376 QV4::JSCallData jsCall(thisObject, argv, argc);
377
378 return QQmlJavaScriptExpression::evaluate(callData: jsCall.callData(scope), isUndefined);
379}
380
381QT_END_NAMESPACE
382

source code of qtdeclarative/src/qml/qml/qqmlpropertybinding.cpp