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:

Parsing Compiling 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…