QML Engine Internals, Part 2: Bindings

This blog post is part of an ongoing series about the internals of the QML engine.

In the last blog post, we covered how the QML engine loads QML files. To recap, QML files are parsed and then C++ objects are created for all elements in the QML file. For example, we saw that when the QML file contains a Text element, the engine creates an instance of the C++ QQuickText class.

Loading files is actually pretty much all the QML engine does. After that, it does not get involved much anymore at runtime. Things like event handling and painting are done in the C++ classes. For example, a TextInput element does input event handling in e.g. QQuickTextInput::keyPressEvent() and painting in QQuickTextInput::updatePaintNode(), no QML engine involved at all.

There are two important things in which the QML engine is still involved at runtime: Bound signal handlers and property binding updates. Bound signal handlers are things like an onClicked handler for a MouseArea. What we will look into in this post are bindings. Consider the following example:

import QtQuick 2.0 Rectangle { width: 300 height: 300 color: "lightsteelblue" Text { anchors.centerIn: parent text: "Window Area: " + (parent.width * parent.height) } }

The example contains two kinds of assignments to properties:

Simple value assignments, like assigning 300 to the width property of QQuickRectangle. In this case that would be the VME instruction STORE_DOUBLE, which is executed when the component is created. The VME simply calls QMetaObject::metacall(QMetaObject::WriteProperty, …), which will end up in QQuickRectangle::setWidth(). After the initial assignment, the QML engine doesn’t touch the width property anymore. Binding assignments, like assigning the binding “Window Area: ” + (parent.width * parent.height) to the text property or assigning the binding parent to the centerIn property. Thanks to the magic of bindings, the text property is automatically updated whenever the width property or the height property of the rectangle change. How does that work? Actually no magic involved, read on to find out.

Creating the Binding

Looking at the VME instructions with QML_COMPILER_DUMP=1, we see that both bindings are created with the STORE_COMPILED_BINDING instruction:

... 9 STORE_COMPILED_BINDING 43 1 0 10 FETCH 19 11 STORE_COMPILED_BINDING 17 0 1 ...

Compiled bindings are an optimization, we’ll first look into normal bindings, which are created with the STORE_BINDING instruction. Looking into the code in QQmlVME::run(), we see that the code creates a QQmlBinding object that gets the string “function $text() { return “Window Area: ” + (parent.width * parent.height) }” as its expression. That’s right, each binding is a JavaScript function! The “function $text()” part was added by the QML compiler, since v8, the JavaScript engine used in QML, can only evaluate complete functions. The function string is then compiled to a v8::Function object by the v8 compiler. The v8 engine produces native machine code, as it has a Just-in-Time (JIT) compiler. The v8::Function object is not executed yet, but kept around.

To sum up what happens when a binding is created with the STORE_BINDING instruction: A QQmlBinding object is created, which compiles a v8::Function from the function string it gets passed.

Running the Binding

At some point, the binding needs to be run, which means letting the v8 engine evaluate the binding function and writing the result of that to the target property. This is done at the very end of the creation phase, QQmlVME::complete() calls an update() function for each binding, in our case QQmlBinding::update(). update() simply executes the v8::Function object and writes the return value to the target property, which in our case is the text property of the rectangle.

But wait, how does v8 know the values of parent.width and parent.height? Actually, how does it know about the parent object at all? The answer to that is: It doesn’t, the v8 engine has no clue about which QObjects exists under which name in the QML file, and what its properties are. When the v8 engine encounters an unknown object or an unknown property, it asks an object wrapper in the QML engine, and the wrapper finds the correct object or property and hands it back to the v8 engine. Let’s see how the width property of QQuickItem is accessed by looking at a backtrace:

#0 QQuickItem::width (this=0x6d8580) at items/qquickitem.cpp:4711 #1 0x00007ffff78e592d in QQuickItem::qt_metacall (this=0x6d8580, _c=QMetaObject::ReadProperty, _id=8, _a=0x7fffffffc270) at .moc/debug-shared/moc_qquickitem.cpp:675 #2 0x00007ffff7a61689 in QQuickRectangle::qt_metacall (this=0x6d8580, _c=QMetaObject::ReadProperty, _id=9, _a=0x7fffffffc270) at .moc/debug-shared/moc_qquickrectangle_p.cpp:526 #3 0x00007ffff7406dc3 in ReadAccessor::Direct (object=0x6d8580, property=..., output=0x7fffffffc2c8, n=0x0) at qml/v8/qv8qobjectwrapper.cpp:243 #4 0x00007ffff7406330 in GenericValueGetter (info=...) at qml/v8/qv8qobjectwrapper.cpp:296 #5 0x00007ffff49bf16a in v8::internal::JSObject::GetPropertyWithCallback (this=0x363c64f4ccb1, receiver=0x363c64f4ccb1, structure=0x1311a45651a9, name=0x3c3c6811b7f9) at ../3rdparty/v8/src/objects.cc:198 #6 0x00007ffff49c11c3 in v8::internal::Object::GetProperty (this=0x363c64f4ccb1, receiver=0x363c64f4ccb1, result=0x7fffffffc570, name=0x3c3c6811b7f9, attributes=0x7fffffffc5e8) at ../3rdparty/v8/src/objects.cc:627 #7 0x00007ffff495c0f1 in v8::internal::LoadIC::Load (this=0x7fffffffc660, state=v8::internal::UNINITIALIZED, object=..., name=...) at ../3rdparty/v8/src/ic.cc:933 #8 0x00007ffff4960ff5 in v8::internal::LoadIC_Miss (args=..., isolate=0x603070) at ../3rdparty/v8/src/ic.cc:2001 #9 0x000034b88ae0618e in ?? () ... [more ?? frames from the JIT'ed v8::Function code] ... #1 0x00007ffff481c3ef in v8::Function::Call (this=0x694fe0, recv=..., argc=0, argv=0x0) at ../3rdparty/v8/src/api.cc:3709 #2 0x00007ffff7379afd in QQmlJavaScriptExpression::evaluate (this=0x6d7430, context=0x6d8440, function=..., isUndefined=0x7fffffffcd23) at qml/qqmljavascriptexpression.cpp:171 #3 0x00007ffff72b7b85 in QQmlBinding::update (this=0x6d7410, flags=...) at qml/qqmlbinding.cpp:285 #4 0x00007ffff72b8237 in QQmlBinding::setEnabled (this=0x6d7410, e=true, flags=...) at qml/qqmlbinding.cpp:389 #5 0x00007ffff72b8173 in QQmlBinding::setEnabled (This=0x6d7448, e=true, f=...) at qml/qqmlbinding.cpp:370 #6 0x00007ffff72c15fb in QQmlAbstractBinding::setEnabled (this=0x6d7448, e=true, f=...) a /../../qtbase/include/QtQml/5.0.0/QtQml/private/../../../../../../qtdeclarative/src/qml/qml/qqmlabstractbinding_p.h:98 #7 0x00007ffff72dcb14 in QQmlVME::complete (this=0x698930, interrupt=...) at qml/qqmlvme.cpp:1292 #8 0x00007ffff72c72ae in QQmlComponentPrivate::complete (enginePriv=0x650560, state=0x698930) at qml/qqmlcomponent.cpp:919 #9 0x00007ffff72c739b in QQmlComponentPrivate::completeCreate (this=0x698890) at qml/qqmlcomponent.cpp:954 #10 0x00007ffff72c734c in QQmlComponent::completeCreate (this=0x698750) at qml/qqmlcomponent.cpp:947 #11 0x00007ffff72c6b2f in QQmlComponent::create (this=0x698750, context=0x68ea30) at qml/qqmlcomponent.cpp:781 #12 0x00007ffff79d4dce in QQuickView::continueExecute (this=0x7fffffffd2f0) at items/qquickview.cpp:445 #13 0x00007ffff79d3fca in QQuickViewPrivate::execute (this=0x64dc10) at items/qquickview.cpp:106 #14 0x00007ffff79d4400 in QQuickView::setSource (this=0x7fffffffd2f0 at items/qquickview.cpp:243 #15 0x0000000000400d70 in main ()

We can see that the wrapper is in qv8qobjectwrapper.cpp and ends up calling QObject::qt_metacall(QMetaObject::ReadProperty, …) to get the property value. The wrapper was called from v8 code, which itself was called by generated machine code of our v8::Function object. The generated machine code doesn’t have stack frames, and therefore GDB is unable to show the backtrace after the ??. I cheated a bit and pieced together this backtrace from two separate backtraces, which explains the inconsistent frame numbering.

So the v8 engine involves an object wrapper to get property values. In the same vein, it involves a context wrapper to find objects themselves, for example the parent object that is accessed during binding evaluation.

To sum up: A binding is evaluated by running the compiled v8::Function code. The v8 engine access unknown objects and properties by calling out to wrappers in Qt. The result returned by the v8::Function is then written to the target property.

Updating the Binding

Ok, now we know how the text property got its initial value. But how do binding updates work? How does the QML engine know that it needs to re-run the binding when the width or height properties change?

The answer to that question lies in the object wrapper, which, as you remember, is called from the v8 engine whenever it needs to access a property. The object wrapper does more than just returning property values: It captures all properties that are accessed. Essentially, when a property is accessed, the object wrapper calls the capture function of the binding that is currently run, which in our example is QQmlJavaScriptExpression::GuardCapture::captureProperty() (QQmlBinding is a subclass of QQmlJavaScriptExpression).

In the capture function, the binding simply connects to the NOTIFY signal of the captured property. When the NOTIFY signal is emitted, a connected slot in the binding is called that re-runs the binding. If you haven’t heard about NOTIFY signals yet, no worries, it is quite simple: When a property is declared with Q_PROPERTY, it is possible to declare a NOTIFY signal there. This signal is emitted by the object whenever the property changes in any way.

For example, the declaration for the width property in QQuickItem looks like this: Q_PROPERTY(qreal width READ width WRITE setWidth NOTIFY widthChanged) In our scenario, when the width property is accessed while the binding is being run for the first time, the property capture code connects to the widthChanged() signal. Whenever QQuickItem later emits widthChanged(), the connected slot in the binding is called and the binding is re-evaluated.

This is why it is important to have NOTIFY signals and to emit them whenever your property changes. If you forget to do so, the binding is not re-evaluated, and essentially the property binding doesn’t work correctly. On the other hand, if you emit a NOTIFY signal even though the property hasn’t really changed, the binding would be re-evaluated needlessly.

To sum up: When accessing properties, the object wrapper calls a capture function from the binding, which connects to the NOTIFY signal of that property to be able to re-evaluate itself whenever the property changes.

Conclusion

In this blog post, we’ve looked at how bindings work. The very short summary is that each binding is a compiled JavaScript function that is executed whenever one of the referenced properties changes.

I hope you enjoyed reading about this, I certainly found it very interesting to examine the guts of property bindings.

In the next blog post of the series, we’ll have a look at the different binding types. Right now, we only looked at the most basic binding, QQmlBinding, but we know already that more binding types, like compiled bindings, exist. The mystery of those will get unraveled soon, stay tuned!

Read Part 3…