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 | |
17 | QT_BEGIN_NAMESPACE |
18 | |
19 | Q_LOGGING_CATEGORY(lcQQPropertyBinding, "qt.qml.propertybinding" ); |
20 | |
21 | QUntypedPropertyBinding 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 | |
29 | QUntypedPropertyBinding 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 | |
50 | QUntypedPropertyBinding 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 | |
65 | QUntypedPropertyBinding 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 | |
103 | QUntypedPropertyBinding 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 | |
132 | void 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 | |
161 | QQmlPropertyBinding::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 | |
174 | static 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 | |
192 | void 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 &¤tValue) { |
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 | |
269 | QString 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 | |
285 | void 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 | |
308 | template<typename TranslateWithUnit> |
309 | auto 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 | |
328 | QUntypedPropertyBinding 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 | |
343 | QUntypedPropertyBinding 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 | |
360 | QV4::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 | |
381 | QT_END_NAMESPACE |
382 | |