The internals of the QML engine. Part 2: Bindings

Original author: Thomas McGuire
  • Transfer
  • Tutorial
This post is a continuation of this ( translated ) article.

In a previous post, we looked at how the QML engine loads files. Let me remind you that first the QML files are parsed, then compiled into an intermediate byte code, and finally following the byte code instruction, a C ++ object is created for each event in each QML file. For example, we saw that when a QML file contains a Text element, the engine creates a C ++ instance of the QQuickText class.

In fact, downloading files is almost everything the QML engine does. After downloading, it stops interfering with the application process. Event handling and rendering elements in Runtime rests entirely with C ++. For example, a TextInput element handles events likeQQuickTextInput :: keyPressEvent () and updates QQuickTextInput :: updatePaintNode () , without the participation of the QML engine.

But there are two important things that QML engine still affects runtime: Related signal handlers ( Bound signal the handlers ) and update the binding properties ( property a binding ). Related signal handlers are things like the onClicked handler for MouseArea . Today we will look at bindings.

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

This example contains two types of property assignment:
  1. A simple property assignment, such as assigning a value of 300 to the width property of a Rectangle element of the C ++ class QQuickRectangle. In this case, VME simply executes the STORE_DOUBLE bytecode instruction when creating this component. VME just calls the function QMetaObject :: metacall (QMetaObject :: WriteProperty, ...) which ultimately ends with a call to QQuickRectangle :: SetWidth () . After this assignment. The QML engine just forgets about this assignment.
  2. A binding assignment, such as in the example above, “Window Area:” + (parent.width * parent.height) to the text property or parent to the centerIn property . Thanks to the magic of bindings, the text property will automatically update whenever the height or width of the Rectangle element changes. How it works? No magic at all, read on to find the answer.

Create binding


If we set the parameter QML_COMPILER_DUMP = 1, then we will see that the binders are created by the STORE_COMPILED_BINDING statement:

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


Compiled bindings, this is actually an optimized option. Let's start by taking a look at the normal bindings created by the STORE_BINDING statement. The QQmlVME :: run () function creates a QQmlBinding object , to which it passes a string of the form
function $text() { return “Window Area: ” + (parent.width * parent.height) }
which is a JS expression.

That's right, every binding is a JavaScript function. Part of the function “function $ text ()” is added by the QML compiler, so since V8 , which is the JML engine of QML, can, as you might guess, only execute JS code. The string function is then compiled into a v8 :: Function object, by the v8 compiler. The v8 engine, due to the built-in JIT compiler , creates native code. It does not execute the created v8 :: Function object, but saves it for the future.

Total: When executing the STORE_BINDING instruction, a QQmlBinding object is created, which compiles the function passed to it as a string into a v8 :: Function object using the built-in V8 JS engine for this.

Bind


At some point, our binding will need to be performed, which means that V8 will have to perform the function passed to it with the binding body and return the result of the assignment to assign it to the target property. This call occurs primarily at the end of the creation phase, the QQmlVME :: complete () function sequentially calls the update () method for each binding. In our case, the QQmlBinding :: update () function is called . The update () method simply executes the contents of the v8 :: Function object and writes the result of the execution to the text property of our rectangle.

But hey, how does V8 learn about the meaning of the variables parent.width and parent.height? Indeed, how does he learn about the parent? Answer: V8 has no idea about any objects of the QObject type represented in the QML file, nor about the composition of their properties. When a V8 encounters an object unknown to it or an unknown property of an object, it asks a question to the wrapper object of the QML engine, which finds a suitable object or property and passes it (or its value) back to V8. Let's see how the width property of the QQuickItem object is accessed using this dump as an example:
#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 see that the wrapper in the qv8qobjectwrapper.cpp file calls the QObject :: qt_metacall (QMetaObject :: ReadProperty, ...) function to get the value of the required property. Wrapper (Wrapper Object) was called from compiled V8 code stored in V8 :: Function. Unfortunately, the generated machine code does not have a call stack, so GDB is not able to show what’s going on for ?? . I deceived you a little and showed above two different call stacks, which explains a little the gap in line numbering.

Once again: V8 uses an object wrapper to get property values. In the same way, it uses a context wrapper to search for the objects themselves. for example, the parent object that we are referring to during the binding.

So, the binding is done by running the V8 :: Function code. The V8 engine gains access to unknown objects and properties by calling wrappers from Qt. The result of V8 :: Function is written to the target property.

Binding Update


Well, we now know how the text property got its original meaning. What about updating it? How does the QML engine know that this property needs to be updated when the height or width of the parent rectangle changes?

The answer to this question lives in the same object wrapper, which, as you recall, is called when V8 needs to access the property. Our wrapper does a little more than just return the value of the property: it records all the properties that were requested to access. Essentially, when accessing a property occurs, the wrapper calls the snap-capture function that is currently running. In our example, this is QQmlJavaScriptExpression :: GuardCapture :: captureProperty () (QQmlBinding is a subclass of the QQmlJavaScriptExpression class).

In the capture function, the binding is simply simply attached to the signal of the NOTIFY type of the property that was requested. Now, when a NOTIFY signal is called, the binding slot connected to it will be called and the binding itself will be restarted. If you have never heard of a NOTIFY signal, do not worry, its logic is simple: When a property is defined using the Q_PROPERTY macro, then a NOTIFY signal will always be present at this place, which will always be sent when this property changes.

For example, here is the declaration of the width property in the QQuickItem class:
Q_PROPERTY(qreal width READ width WRITE setWidth NOTIFY widthChanged)

In our scenario: when the width property is accessed for the first time, during the first launch of our binding, the property capture function will connect the widthChanged () signal and the launch slot of our binding. Now, when the QQuickItem object emits a widthChanged () signal while the application is running, all the bindings attached to it will be called and restarted.

It is very important to have a NOTIFY signal in self-defined properties and always send it when our property changes. If you forget to do this, the binding will not be restarted, and accordingly, the binding of the property will not work correctly. On the other hand, if a NOTIFY signal is sent at the moment when the property has not been changed, the binding will work idle.

To summarize: When accessing a property, the wrapper object calls the capture function from the binding, which attaches this binding to the NOTIFY signal of the property, so that when the property changes, the binding is performed again ...

Conclusion


Today we looked at how bindings work in QML. Briefly: binding is a JS function that is executed every time the properties involved in it change.

I hope this article was interesting to you, for example, it was very interesting for me to study the internal logic of the bindings.

In our next article, we will consider various types of bindings. All that we examined today is a basic binding, or QQmlBinding, but we know that there are other types, such as copied bindings. We will solve their secret in the near future, stay tuned!

Also popular now: