QML Engine Internals, Part 1: QML File Loading

In this series of blog posts, we’ll look under the hood of the QML engine and uncover some of its inner workings. The articles are based on the Qt5 version of QtQuick, QtQuick 2.0.

Most people know  that each element in a QML file is backed by a C++ class. When a QML file is loaded, the QML engine somehow creates one C++ object for all elements in the file. In this blog post, we’ll examine how the QML engine goes from reading a text file to having a complete tree of C++ objects. The Qt documentation on QML has extensive descriptions that cover how QML and C++ work together, a read that is well worth the time. In the blog series, I am assuming the parts covered by that documentation are understood.

The Example

Throughout this post, we’ll use an example that doesn’t do any useful or exciting, but covers some interesting parts of the QML functionality:

import QtQuick 2.0

Rectangle {
    id: root
    width: 360
    height: width + 50
    color: "lightsteelblue"
    property alias myWidth: root.width
    property int counter: 1

    function reactToClick() {
        root.counter++
    }

    Text {
        id: text
        text: qsTr("Hello World: " + counter)
        anchors.centerIn: parent
    }

    MouseArea {
        id: mouseArea
        anchors.fill: parent
        onClicked: {
            reactToClick()
        }
    }
}

This file has three elements in it, Rectangle, Text and MouseArea. These correspond to the C++ classes QQuickRectangle, QQuickText and QQuickMouseArea. Those classes are only exported to QML, the C++ versions are private and not available for users of Qt. The elements are drawn using a OpenGL scenegraph. Drawing and event handling are both orchestrated by QQuickView . The C++ object tree corresponds to the one in the QML file, which we can verify with KDAB’s Qt introspection tool, Gammaray:

As expected, the classes QQuickMouseArea and QQuickText show up in the object tree. But what is QQuickRectangle_QML_0? There is no C++ class with such a name in the Qt sources! We’ll come back to that in a later blog post, for now you can assume that an object of type QQuickRectangle was used.

Let’s go a step further and run the application in the QML profiler:

 

We see that a bunch of time is spent setting up the scene, which is the Creating phase. This is followed by a bit of Painting, which is what we would expect. But what is this Compiling phase? Is it creating machine code? Time to dig into the QML file loading code a bit deeper.

QML File Loading Steps

When loading a QML file, there are 3 distinct steps happening, which we’ll examine in the following sections:

  1. Parsing
  2. Compiling
  3. Creating

Parsing

First of all, the QML file is parsed, which is handled by QQmlScript::Parser. Most of the parser internals are auto-generated from a grammar file. The abstract syntax tree (AST) of our example looks like this:

(This was generated by graphviz using a dotfile produced with the patch at http://www.kdab.com/~thomas/stuff/ast-graph.diff)

The AST is quite low-level, therefore it is transformed into a higher level structure of Objects, Properties and Values in the next step. This is done using a visitor on the AST. At this level, Objects correspond to QML elements, and Property/Value pairs to QML properties and values like “color” and “lightsteelblue”. Even signal handlers like onClicked are just property/value pairs, in this case the value is the Javascript function body.

Compiling

Now in theory, the structure of Objects, Properties and Values would be enough to examine for creating the associated C++ objects and assigning the values to the properties there. However, the Objects, Properties and Values are still quite raw, and require some post-processing before the C++ objects can be created. This post-processing is done by QQmlCompiler, which explains what the compiling phase we saw in the QML profiler is about. The compiler creates an QQmlCompiledData object for the QML file. Examining QQmlCompiledData and creating C++ objects from it is quite a lot faster than examining the Objects, Properties and Values. When using a QML file multiple times, say for example a Button.qml that is used all over the place in other QML files, Button.qml would only be compiled once. The QQmlCompiledData for Button.qml is kept around, and each time a Button component is used, the C++ objects are created by examining the QQmlCompiledData. The later is the creating phase, which can be seen in the QML profiler output.

To sum up: Parsing a QML file and compiling it is only done once, after that the QQmlCompiledData object is used to quickly create the C++ objects.

Creating

I won’t go into details about QQmlCompiledData, but one thing might have caught your attention: The “QByteArray bytecode” member variable. The instructions to create C++ objects and to assign the correct values to its properties are actually compiled to bytecode, which is then later interpreted by a bytecode interpreter! The bytecode contains a bunch of instructions, and the rest of QQmlCompiledData is just used as auxiliary data when executing the instructions.

In the creating phase, the bytecode is interpreted by the class QQmlVME. Looking into QQmlVME::run(), the interpreter basically loops over all instructions contained in the bytecode, with a big switch statement for the different instruction types. By running the app with QML_COMPILER_DUMP=1, we can see the individual instructions of the bytecode:

Index           Operation               Data1   Data2   Data3   Comments
-------------------------------------------------------------------------------
0               INIT                    4       3       0       0
1               INIT_V8_BINDING 0       17
2               CREATECPP                       0
3               STORE_META
4               SETID                   0                       "root"
5               BEGIN                   16
6               STORE_INTEGER           45      1
7               STORE_COLOR             41                      "ffb0c4de"
8               STORE_COMPILED_BINDING  10      2       0
9               STORE_DOUBLE            9       360
10              FETCH_QLIST             2
11              CREATE_SIMPLE           32
12              SETID                   1                       "text"
13              BEGIN                   16
14              STORE_V8_BINDING        43      0       0
15              FETCH                   19
16              STORE_COMPILED_BINDING  17      1       1
17              POP
18              STORE_OBJECT_QLIST
19              CREATE_SIMPLE           32
20              SETID                   2                       "mouseArea"
21              BEGIN                   16
22              STORE_SIGNAL            42      2
23              FETCH                   19
24              STORE_COMPILED_BINDING  16      0       1
25              POP
26              STORE_OBJECT_QLIST
27              POP_QLIST
28              SET_DEFAULT
29              DONE
-------------------------------------------------------------------------------

CREATE_SIMPLE is the most important instruction, it creates a C++ object, using a database of registered objects in QQmlMetaType.
STORE_INTEGER is the instruction to write an integer value to a property.
STORE_SIGNAL is used to create a bound signal handler.
STORE_*_BINDING is used to create a property binding. More about bindings to come in the next blog post of this series.
SETID obviously sets the identifier of an object, which is not an ordinary property.

The VME has a stack of objects, all instructions like STORE_* operate on the top object. FETCH puts a specific QObject on top of the stack, POP removes the top object. All of the instructions make heavy use of integer indices, for example the STORE_COLOR instruction writes to property 41, which is the property index of the target QObject’s meta object.

To sum up: Once a QML file is compiled, creating an instance of it is just a matter of executing the bytecode of the compiled data.

Conclusion

That is the end of this blog post, we have covered on how a QML file is parsed, processed, compiled and later how the objects are actually created by the VME. I hope you gained some interesting insights about the QML engine.

Stay tuned for the next blog post, in which we’ll examine how property bindings work.

Read Part 2…

 

FacebookTwitterGoogle+

10 thoughts on “QML Engine Internals, Part 1: QML File Loading

  1. Really interesting post! I didn’t know the internals of QML, and they seem quite well thought. For me it’s been a surprise that there’s actually a step that compiles to bytecode. Keep the posts coming =)

  2. Pingback: KDAB - New blog series: QML Engine Internals

  3. I have two minor questions:

    1) What does the “VME” abbreviate in the class name “QQmlVME”?
    2) Can you describe in few words (perhaps give an example) what the visitor does during the low to high-level transformation?

    • 1) I don’t know actually… I suspect something like Virtual Machine Environment

      2) The visitor basically iterates over all nodes in the AST and creates QQmlScript::Objects, Properties and Values for the nodes. The tree of Objects, Properties and Values is the high level structure.
      For example when the an UiObjectDefinition node is encountered, the visitor creates an Object for that, then processes all child AST nodes of the object. These child AST nodes might for example be property definitions, which would then be added as a Property to the parent Object.
      I suggest to step over the code in a debugger to see how it works in detail.

    • QML has no support for precompiling. It sure would be nice, would remove the ‘compiling’ phase from startup.

      There was some talk about this feature, can’t remember where I read about that. The conclusion was that while it is technically a good idea and apparently not too hard to implement, just no one had time to add this feature yet.

Leave a Reply

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


2 × = six