Skip to content

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:

  1. 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.
  2. 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…

 

Categories: KDAB Blogs / KDAB on Qt / QML

12 thoughts on “QML Engine Internals, Part 2: Bindings”

  1. Is there an advantage of detecting bindings at run time as opposed to parse time?

    i.e for an expression, y: x + 20. What’s the advantage of having a “capturing” phase for a binding as opposed to just detecting at parse time time y depends on x ?

    Is it because it’s too hard for the parser to determine this dependency?

    1. Thomas McGuire

      Good question. In the general case, it is actually impossible for the parser to determine the dependency – think of an object which was exported from C++ to QML with QQmlContext::setContextProperty(). This could happen after the parsing is already done, so it can not be determined by the parser automatically.

      There are other ways, like Qt.createComponent(), to add new objects, or even remove objects, at runtime. Because of that, the scope is too dynamic to determine those captured properties at parse time.

      Oh, and another case: Bindings can contain conditionals, i.e. if-statements, so not all properties that are contained in the functions are necessarily used, depending on which branch of the if-statement was taken.

      1. In the case of conditionals… what if the property is “if a then b else c”, and in the first run, only a and b is used (and registered for notification). Now a changes. Will then a future change of c trigger an update?

        1. Thomas McGuire

          In the first run, since a is false, only a and b are captured, c is not captured. That means when c changes, the binding is not updated.

          Now, if a changes to false, the binding is updated. in that run, a and c are captured.

          That means a future change of c will now trigger an update, since it has been captured in the last run of the binding. OTOH, b will not trigger an update, as it hasn’t been captured in the last run.

          To sum up: Only the properties captured in the last binding run are subscribed to and trigger updates.

  2. Really good writing style, good details here. Keep them coming!

    QUESTION: Is there any “handling” of “circular-bindings”, such as when one `widthChanged()` notifies a binding in a (parent-) object, which triggers a later update back to that (child-) object to “change-the-width” again? For example, I could implementation-detail options like:

    – (a) (maybe) Circular bindings are detected-and-removed

    – (b) (0r) Circular bindings are allowed, but possible (they repeat in a non-expensive “heart-beat” manner, such as until the algorithm “calms-down” to no more updates being triggered)

    – (c) (or) Circular bindings are prohibitively expensive (e.g., rather than periodic-execution, they “thrash-the-system” unless the algorithm “calms-down” to no more updates)

    I’d assume (and “vote-for”) option (b), … any thoughts on this?

    1. Thomas McGuire

      The way the QML engine handles circular bindings (called “binding loops” there) is similar to a).

      When, at runtime, the same binding is binding is run recursively, the second run is not executed and an error message like this is printed:
      “Binding loop detected for property “width””

  3. Another interesting note is that in many cases bindings are reevaluated needlessly (for example, a hidden tab may have some text edits whose values are bound to … some property which is changing. As that property changes, all of those text edits will have their text property re-evaluated despite the fact that none of them are visible). This is because QML currently uses a “push” notification system rather than a “pull” is-dirty-requires-reevaluation system, and that’s one possible area of research for greatly improved performance, in the future.

    1. Thomas McGuire

      Ah yes, that is exactly the issue I was seeing with a customer project. We had a lot of binding updates on invisible pages, triggered by C++ state changes.

      I tried to disable bindings for invisible items, but that didn’t work so well. For one, bindings on the “visible” and the “opacity” property needed to be whitelisted and not disabled. And then, those properties might depend on other properties that have bindings…

      A generic “pull” mechanism sounds like a good idea.

      1. A pull system would also great in the case where your binding depends on several other properties that are changing altogether. In this case, the binding is re-evaluated several times while you only need the final evaluation. Is there a pattern that we can use in this case?

        1. Thomas McGuire

          There is no such pull system, there is no way that I know off to reduce the several re-evaluations.

          FYI: When first loading the component, the QML engine will try to evaluate bindings in an order that minimizes binding reevaluations. This is however a bit orthogonal to the issue here.

Leave a Reply

Your email address will not be published. Required fields are marked *