Les entrailles du moteur QML

Deuxième partie : les liaisons

Thomas McGuire

Traducteur

Arnold Dumas

Cette série d'articles explorera les entrailles de QML et les mécaniques internes de son moteur d'exécution. Ces articles reposent sur la version Qt Quick 2.0, disponible avec Qt 5.0.

Cet article s'intéresse à la manière dont le moteur QML gère les liaisons.

Commentez

- Titre : Les entrailles du moteur QML

- Auteur : Thomas McGuire

- Traducteur : Arnold Dumas

- Parution : 19 septembre 2013

- Licence : Copyright ® 2012 Thomas McGuire. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.

Sommaire

- Article original

- I. Introduction

- II. Création des liaisons

- III. Exécuter la liaison

- IV. Mettre à jour les liaisons

- V. Conclusion

- VI. Remerciements

Article original

Le blog KDAB est rédigé par les ingénieurs de KDAB s'occupant des formations, de la consultance ainsi que du développement (de Qt et de produits additionnels). Vous pouvez trouver les versions originales.

Cet article est une traduction de l'article original écrit par Thomas McGuire paru le 10 août 2012.

Cet article est une traduction de l'un des articles en anglais écrits par KDAB. Les éventuels problèmes résultant d'une mauvaise traduction ne sont pas imputables à KDAB.

I - Introduction

Dans le dernier article, nous nous sommes intéressés au chargement d'un fichier par le moteur QML. Récapitulons : le fichier QML est analysé, puis des objets C++ sont créés pour chaque élément dudit fichier. Nous avons par exemple vu que, lorsque le fichier QML contenait un élément de type Text, alors le moteur créait en mémoire une instance de la classe C++ QQuickText.

Le chargement des fichiers est presque la seule chose dont est capable le moteur QML. Après cela, à l'exécution, il ne sera plus du tout appelé. Les tâches comme le traitement des événements ou le rendu à l'écran sont prises en charge par d'autres classes C++. Ainsi, un élément de type TextInput voit ses événements traités par QQuickTextInput::keyPressEvent() et les tâches de rendu effectuées par QQuickTextInput::updatePaintNode(), le moteur QML n'intervient plus du tout.

En fait, le moteur QML effectue encore deux tâches importantes lors de l'exécution : la gestion des signaux liés et la mise à jour des liaisons entre propriétés. Les premiers sont des gestionnaires comme onClicked de MouseArea. Cet article s'intéressera aux liaisons.

Considérons tout d'abord le code QML suivant :

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

Cet exemple contient deux types d'assignations de propriétés.

1. L'assignation simple, on assigne la valeur 300 à la propriété width d'un QQuickRectangle. Dans ce cas, c'est l'instruction STORE_DOUBLE de la VME qui sera exécutée lors de la création du composant. La VME appellera simplement QMetaObject::metacall(QMetaObject::WriteProperty…), qui finira par un QQuickRectangle::setWidth(). Après avoir effectué cette assignation, le moteur QML ne touchera plus du tout à la propriété width.

2. Les assignations liées, comme assigner la valeur "Window Area: " + (parent.width * parent.height) à la propriété text ou encore la liaison parent à centerIn. Grâce à la magie des liaisons, la propriété text est automatiquement mise à jour quand la valeur de la propriété width ou de height du rectangle change. Vous vous demandez sûrement comment cela fonctionne ? Je vais casser un mythe, mais il n'y a vraiment rien de magique à tout cela, continuez la lecture et vous saurez tout.

II - Création des liaisons

En regardant les instructions du VME grâce à l'option QML_COMPILER_DUMP=1, on peut voir que les deux liaisons sont créées à l'aide de l'instruction STORE_COMPILED_BINDING :

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

Les liaisons compilées sont une optimisation, intéressons-nous d'abord aux liaisons simples, celles crées avec l'instruction STORE_BINDING. En étudiant le code de QQmlVME::run(), on s'aperçoit qu'il crée une instance de QQmlBinding qui reçoit une chaîne de caractères « function $text() { return "Window Area: " + (parent.width * parent.height) } ». C'est vrai, chaque liaison est une fonction JavaScript ! La partie « function $text() » a été ajoutée par le compilateur QML, puisque le moteur JavaScript V8 ne peut évaluer que les fonctions complètes. La fonction sous forme de chaîne de caractères est ensuite compilée en un objet de type v8::Function par le compilateur V8. Le moteur V8 produit du code machine natif, étant donné qu'il comporte un compilateur à la volée (JIT). L'objet de type v8::Function n'est pas exécuté tout de suite, il est gardé sous le coude.

Résumons tout ce qu'il se passe lors de l'exécution d'une instruction STORE_BINDING : une instance de QQmlBinding est créée, qui compile une v8::Function depuis une fonction reçue sous forme de chaîne de caractères.

III - Exécuter la liaison

Parfois, les liaisons doivent être exécutées, ce qui implique de laisser le moteur V8 évaluer la liaison et d'écrire le résultat dans la propriété cible. Cela est fait tout à la fin de la phase de création : QQmlVME::complete() appelle la méthode update() pour chaque liaison ; dans le cas présent, ce sera QQmlBinding::update(). Cette méthode update() exécute simplement l'objet v8::Function et écrit la valeur de retour à la place de la propriété cible, ici la propriété text de notre rectangle.

Mais attendez, comment est-ce que V8 connaît les valeurs de parent.width et de parent.height ? En fait, comment est-il au courant du parent de l'objet ? La réponse est simple : il ne le connaît pas ! Le moteur V8 n'a aucune idée de quel QObject correspond à tel objet dans le fichier QML tout comme le nom des propriétés. Quand le moteur V8 rencontre un objet ou une propriété inconnue, il demande une référence sur l'objet, un object wrapper, au moteur QML ; cette référence trouve l'objet ou la propriété correspondants et les rend au moteur V8. Voyons la façon dont on accède à la propriété width de QQuickItem en analysant les traces d'exécution :

#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 ()

On peut voir que la référence est implémentée dans le fichier qv8qobjectwrapper.cpp et qu'il finit par appeler QObject::qt_metacall(QMetaObject::ReadProperty…) pour accéder à la valeur de la propriété. Elle a été appelée par du code de V8, code lui-même généré par notre objet v8::Function. Le code machine généré ne contient pas le bloc de pile, ce qui explique l'incapacité de GDB à montrer les traces d'exécution après les points d'interrogation. La trace d'exécution présentée est en fait la réunion de deux traces différentes, ce qui explique la numérotation discontinue.

Le moteur V8 utilise une référence afin d'accéder aux valeurs des propriétés. Dans le même esprit, il utilise la même technique pour trouver les objets eux-mêmes, par exemple l'objet parent qui est utilisé durant l'évaluation de la liaison.

Pour résumer : une liaison est évaluée en exécutant le code de v8::Function ; le moteur V8 accède aux objets et propriétés inconnus par le biais de références retournées par Qt. La valeur retournée par la v8::Function est ensuite écrite dans la propriété cible.

IV - Mettre à jour les liaisons

Nous savons maintenant comment la propriété text connaît sa valeur initiale. Mais comment fonctionne la mise à jour de ces liaisons ? Comment le moteur QML sait-il qu'il doit de nouveau exécuter les liaisons lorsque la propriété largeur ou hauteur change ?

La réponse à cette question réside dans la référence, qui, comme vous vous en souvenez, est appelée par le moteur Vv8 quand il a besoin d'accéder à une propriété. La référence fait plus que simplement retourner la valeur des propriétés : elle capture toutes les propriétés auxquelles le moteur a accédé. Aussi, lorsque l'on accède à une propriété, elle appelle la fonction capturée de la liaison qui est exécutée à ce moment, il s'agit dans notre exemple de QQmlJavaScriptExpression::GuardCapture::captureProperty() (QQmlBinding hérite de QQmlJavaScriptExpression).

Dans la fonction de capture, la liaison se connecte simplement sur le signal de la propriété capturée. Quand le signal NOTIFY est émis, un slot connecté à la liaison est appelé et exécute de nouveau la liaison. Si vous n'avez jamais entendu parler du signal NOTIFY, ne vous inquiétez pas, c'est plutôt simple : quand une propriété est déclarée via la macro Q_PROPERTY, il est possible de déclarer dans la foulée un signal NOTIFY ; ce signal est émis par l'objet lorsque l'une de ses propriétés change, quelle qu'elle soit.

Par exemple, la déclaration pour la propriété width de QQuickItem ressemble à cela :

Q_PROPERTY(qreal width READ width WRITE setWidth NOTIFY widthChanged)

Dans ce scénario, quand la propriété width est utilisée lors de la première exécution de la liaison, le code de la propriété est lié au signal widthChanged(). Ensuite, lorsque le QQuickItem émet le signal widthChanged(), le slot qui lui est connecté via la liaison est appelé et cette liaison est évaluée.

C'est pourquoi il est important d'avoir un signal NOTIFY et de l'émettre lorsque les valeurs des propriétés sont modifiées. Si vous oubliez de le faire, les liaisons ne seront pas réévaluées et la plupart des liaisons de propriétés ne fonctionneront pas correctement. D'un autre côté, si le signal NOTIFY est émis à tout va, alors les liaisons seront réévaluées alors que ce n'est pas nécessaire.

Pour résumer : lorsque l'on accède à une propriété, la référence appelle une fonction de la liaison, qui connecte le signal NOTIFY afin de se réévaluer lui-même à chaque changement de cette propriété.

V - Conclusion

Cet article vous aura expliqué le fonctionnement interne des liaisons. Pour faire un résumé vraiment simpliste, on pourrait dire que chaque liaison est compilée en une fonction JavaScript appelée à chaque fois qu'une propriété référencée voit sa valeur modifiée.

Le prochain article de la série traitera des différents types de liaisons. On ne s'est intéressé pour le moment qu'aux liaisons les plus simples, QQmlBinding. On sait pourtant qu'il en existe d'autres, comme les liaisons compilées. Après la lecture de ce prochain article, vous connaîtrez les rouages de ce type de liaisons.

VI - Remerciements

Au nom de toute l'équipe Qt, j'aimerais adresser le plus grand remerciement à KDAB pour nous avoir autorisés à traduire cet article !

Je tiens à remercier Thibaut Cuvelier pour ses conseils et ClaudeLELOUP pour sa relecture.