Внутренности движка QML. Часть 1: Загрузка файлов

в 17:36, , рубрики: c++, cml, compilation, dynamic language, file loading, javascript, qt, Qt Software

В этой серии статей мы заглянем под капот движка QML и раскроем некоторые из особенностей его внутренней работы.Статьи основаны на Qt5 версия QtQuick, QtQuick 2.0.

Большинство людей знают, что каждый элемент в QML файле опирается на конкретный C++ класс. Когда QML файл загружен, движок QML как-то создает один C++ объект для всех элементов в файле. В этом посте мы рассмотрим, как движок QML переходит от чтения текстового файла, включающего полное дерево C++ объектов. В документации Qt присутствует раздел с обширным описанием взаимодействия QML и C++, прочтение которого стоит потраченного времени. В данной серии статей, я предполагаю что пользователь прочёл и понимает описанное в документации.

Пример

В этой статье мы будем использовать пример, который не делает ничего особенного, но включает в себя некоторые интересные части QML:

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

Этот файл состоит из трёх элементов: Rectangle, Text и MouseArea. Они соответствуют С++ классам QQuickRectangle, QQuickText и QQuickMouseArea. Кроме того, эти классы лишь экспортируются в QML, являются приватными и недоступны пользователям Qt напрямую из C++. Элементы рисуются на экране через OpenGL при помощи QML Scenegraph (Дерево сцены QML). Порядок отрисовки и обработки событий определяет QQuickView. QML-файл один в один соответствует генерируемому дереву объектов C++, которое можно проверить при помощи разработанного командой Qt KDAB инструмента, под названием Gammaray.

image

Как и ожидалось, классы QQuickMouseArea и QQuickText отображаться в дереве объектов. Но что такое QQuickRectangle_QML_0? Такого C++ класса просто не существует в исходниках Qt! Мы вернёмся к этому в более поздних постах, пока же будем просто считать, что был использован объект QQuickRectangle.

Пойдем дальше и запустим приложение в QML Profiler:
image

Как мы видим, львиная доля времени уходит на фазу создания (Creating). И совсем немножко — на стадию отрисовки (Painting), итог работы которой мы и видит на экране. Но что такое фаза компиляции (Compiling)? Подразумевается создание машинного или байт-кода? Давайте взглянем на алгоритм загрузки файла чуть глубже.

Этапы загрузки QML-файла

Загрузку QML файла можно разделить на 3 независимых этапа, которые мы рассмотрим в следующих разделах:

  • Parsing (Разбор)
  • Compiling (Компиляция)
  • Creating (Создание)

Разбор

Прежде всего, QML файл анализируется, при помощи QQmlScript :: Parser. Большинство внутренних правил парсера генерируются автоматически из файла грамматики. Абстрактное синтаксическое дерево (AST) из нашего примера будет выглядеть так:
image
(Картинка была сгенерирована при помощи graphviz и этого патча)

Изначально AST работает на достаточно низком уровне и на следующем шаге своей работы, на более высоком уровне он превращается в структуры объектов (Objects), свойств (Properties) и значений (Values). Этим занимается AST::Visitor. На этом уровне, объекты (Objects) соответствуют QML элементам, а пары свойство/значение (Property/Value), такие как «color» и «lightsteelblue» соответствуют свойствам и значениям данных QML-элементов. Даже обработчики сигналов, вроде onClicked на данном этапе всего лишь пары свойство/значение, в данном случае значение — это тело Javascript функции.

Компиляция

В теории, имея структуру объектов, свойств и значений, мы уже можем создать ассоциированные с данными QML-элементами C++-объекты и присвоить им соответствующие значения. Тем не менее, объекты, свойства и значения все еще довольно сырые, и требуют некоторой пост-обработки, прежде чем смогут быть созданы C++ объекты. Пост-обработка осуществляется объектом QQmlCompiler, который объясняет что подразумевается под стадией «Компиляция» в нашем профайлере. Компилятор создает объект QQmlCompiledData для заданного QML файла.

Работа с QQmlCompiledData и создание C++ объектов из него происходит гораздо быстрее, чем работа напрямую с объектами, свойствами и значениями. Как было сказано выше, дерево объектов строится для каждого отдельного QML-файла. При многократном использовании QML-файла, к примеру Button.qml, который повсеместно используется в других QML-файлах, для него лишь единожды будет составлен объект QQmlCompiledData, который будет сохранён в памяти и использован для создания C++ объекта каждый раз при встрече с включением файла Button.qml во время разбора приложения. После этого наступает этап создания, который мы видели в окошке профайлера.

Подведем итог: Разбор каждого QML файла и его компиляция выполняется только один раз, после чего объект QQmlCompiledData используется для быстрого создания C++ объектов.

Создание

Я не буду вдаваться в подробности о QQmlCompiledData, но одна вещь могла бы привлечь ваше внимание: переменная-член «QByteArray bytecode». Инструкции по созданию С++ объектов и правильное присвоение значений его свойствам составлена в виде байт-кода, который затем интерпретируется интерпретатором байт-кода (от переводчика: интерпретатор интерпретировал-интерпретировал, да не… =) ). Байт-код содержит набор инструкций, и остальное содержимое в QQmlCompiledData используется только в качестве вспомогательных данных при выполнении инструкции.

На этапе создания, байт-код интерпретируется классом QQmlVME. Функция QQmlVME::run(), интерпретатора последовательно выполняет инструкции байткода, основываясь на большом switch-операторе. При запуске приложения с флагом QML_COMPILER_DUMP = 1, мы можем видеть отдельные инструкции байт-кода:

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 является наиболее важной инструкцией, он создает C++ объект, используя базу данных о зарегистрированных объектах: QQmlMetaType.
  • STORE_INTEGER — инструкция присваивания целочисленного значения свойству.
  • STORE_SIGNAL используется для создания связанного обработчика сигнала.
  • STORE_ * _BINDING используется для создания привязки свойств. Подробнее о привязках мы поговорим в следующем посте этой серии.
  • SETID, очевидно, задает идентификатор объекта, который не является обычным свойством.

VME имеет стек объектов, все инструкции типа STORE_* работают с объектом находящемся на верхнем уровне. FETCH помещает конкретный QObject на вершину стека, POP удаляет верхний объект. Все инструкции широко используют числовые индексы, например, инструкция STORE_COLOR записывает в свойство 41, которое является индексом мета-свойства object.dinary, целевого объекта QObject.

Подводя итог: После того, как QML файл будет скомпилирован, создание экземпляра его C++ объекта — это всего лишь вопрос выполнения байт-кода сгенерированного по переданным скомпилированным данным.

Заключение

В конце данной заметки мы познакомились с тем, как разбираются, обрабатываются и компилируются QML-файлы. А затем как с помощью VME создаются связанные с ними объекты. Я надеюсь, что вы вынесли для себя что-то интересное о движке QML.

Оставайтесь с нами до следующего поста, в котором мы рассмотрим, как работают привязки (Bindings) в QML.

От переводчика: Продолжать мне в том же духе или остановиться? Судя по скромному количеству комментариев к предыдущим переводам, тема не очень то интересна аудитории Хабра.

PS: И да, я знаю что переводчик из меня так себе, но я стараюсь как могу =) Ошибки и замечания — прошу в личку.

Автор: vitaly_KF

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js