QML Modules in Qt 6.2

QML Modules

With Qt 6.2 there is, for the first time, a comprehensive build system API that allows you to specify a QML module as a complete, encapsulated unit. This is a significant improvement, but as the concept of QML modules was rather under-developed in Qt 5, even seasoned QML developers might now ask "What exactly is a QML module". In our previous post we have scratched the surface by introducing the CMake API used to define them. We'll take a closer look in this post.

The basics

QML modules have existed for a very long time, at least since Qt 5.0.

Every QML module comes with a qmldir file. The qmldir file specifies a URI. Multiple qmldir files with equal URIs are considered to be alternative locations for the same module. This way you can make your module available from both the physical file system and the resource file system. A qmldir file specifies all the bits and pieces we need to know about at runtime, and some more:

  • QML Components declared in *.qml files and their attributes
  • JavaScript code in *.js and *.mjs files
  • dependencies of the module
  • versions of the module and its components
  • location of the *.qmltypes file with details on QML types defined in C++
  • any plugin that belongs to the module

The syntax is straight forward, all plain text, and easily human readable. Writing QML modules is trivial, one might think. This is not quite true, though.

Some History

We used to write such qmldir files manually in Qt5. While the syntax is quite simple, the semantics are not. There are many ways to work subtle errors into qmldir files:

  • Mis-spell the versions and have some components fail on imports of specific versions of the module.
  • Omit some *.qml files and the module works when using those files from the same directory, but not from elsewhere.
  • Mis-spell the name of the *.qmltypes file and throw Qt Creator's QML tooling off (unless the file is called plugins.qmltypes which Qt Creator always tries to read).
  • Omit a dependency and discover that the module doesn't work in certain builds because qmlimportscanner does not know to add the relevant library.

Those errors will not show up in your typical test cycle. Rather they only surface under specific conditions. People hate such things, and rightly so.

Furthermore, you had to duplicate much of the information provided in the qmldir file when writing your build system:

  • *.qml|js|mjs files had to be added to the resource file system
  • The plugin needed to be compiled
  • The *.qmltypes needed to be generated using qmltyperegistrar (or, previously, qmlplugindump)
  • Ideally, qmlcachegen should be used to pre-compile any QML and JavaScript code

Various helper functions were written in qmake to assist with all of this. As none of this was very well thought out, over the course of Qt5, this area developed a whole arsenal of foot guns, right to the point where many people avoided it altogether, and just didn't write any QML modules.

Yet, they did write QML modules. A directory with a few *.qml files without a qmldir file is still a QML module, defined by the so called "implicit import". The implicit import just makes all the QML files in the same directory available under their file names, and doesn't assign any versions, URIs, or similar. Such simple modules certainly are practical in many situations:

  • They are immediately visible to any tooling such as Qt Creator
  • You can just run them using the qml tool, without any further setup
  • You can relocate them by just copying the directory elsewhere

However, they also have some grave drawbacks:

  • You cannot add any C++-declared types to such modules
  • You cannot immediately pre-compile them using qmlcachegen
  • You cannot import them from elsewhere in a structured way
  • You cannot use singletons

For these drawbacks people found a variety of workarounds. C++ types were added via manual calls to qmlRegisterType() in the application's main(). This is another form of degenerate QML module without qmldir file: The module URI specified in the qmlRegisterType() call could be imported in any QML file loaded with the same application. Yet, the URI was unavailable when loading the same QML file with anything else.

The *.qml and *.js files were added to *.qrc files which were then used to pre-compile the code with qmlcachegen. If you then loaded the respective components from the resource file system, you would get the pre-compiled, fast-loading, version. If you continued to load the same component from the physical file system the pre-compiled byte code would be silently ignored and you would just waste some memory by bloating your executable.

*.qml|js|mjs files were mapped into different places in the resource file system, which made them appear as part of more than one module. This way you didn't have to import anything in order to share the code. As a result, the same code was compiled multiple times and the space requirements for the result multiplied as well.

Directory imports (import "some/where/else/" as Shared) were used to import modules without qmldir files. This created an extra late binding between QML modules. You could replace the code in some/where/else and everything would seem to work, until you tried to do something the module found there at runtime doesn't support. Without knowing the name of the module to be imported, the QML engine could not verify that it actually imported the right thing. Figuring out the directory referred to by such a directory import was an interesting exercise whenever a project had separate source, build, install, and possibly deployment locations.

Towards the end of the Qt5 life cycle, it became apparent that this situation was not quite ideal.

In Qt 6.2, the concept of QML modules receives a breath of fresh air. The CMake API for QML modules for the first time provides a toolbox for creating well behaved QML modules and various provisions around it make sure they are safe enough to use.

QML Modules in Qt 6.2

When using Qt 6.2 you can declare a QML module using the CMake API for QML modules and all the complicated issues mentioned above are someone else's problems. In particular:

  • the qmldir and *.qmltypes files are automatically generated
  • C++ types annotated with QML_ELEMENT and friends are automatically registered
  • qmlcachegen is automatically invoked
  • The module is provided both in the physical and in the resource file system.
  • When loaded from the physical file system, the module redirects any access to QML and JavaScript files into the resource file system, so that the pre-compiled versions are used.
  • You can combine QML files and C++-based types seamlessly in the same module
  • A backing library and an optional plugin are created. You can link the backing library into your application to avoid loading the plugin at run time (but see below for caveats).

For all of those points we have to add a "* unless configured otherwise". QML modules in Qt 6.2 are sane by default. This alone makes them much easier to work with than what you are used to in Qt 5. You can, however, still shoot your foot if you really want to. You'll just have a slightly harder time reaching the gun.

Of course this comes with a catch: You have to change how you write QML and QML-exposed C++ code, as well as your build system in order to take full advantage of QML modules. Considering the benefits, it should be worth it, at least for new applications. There are a few things that need special consideration, though. As the basic idea of the CMake API for QML modules is already covered in our previous blog post in this series, I'll jump right into the more advanced use cases.

Multiple QML modules in one binary

In Qt5, you can just call qmlRegisterType() and pass any string as URI. This way you can define multiple (degenerate) QML modules in one C++ file. People have come to appreciate this functionality as it allows a simple way of structuring your QML application. In Qt6, this particular techique is not recommended. You should not manually call qmlRegisterType(), after all. Furthermore, per CMake target you can only specify one QML module, with one URI. This is on purpose, as otherwise you would have to specify which files belong to which module. Such a mapping would give you ample opportunity for interaction between bullets and extremities. Instead, we define multiple CMake targets, each with its own QML module, and link them all together. If the extra targets are all static libraries, the result will be one binary, carrying multiple QML modules.

In a most straight forward way, we can create an application like this:

myProject
    | - CMakeLists.txt
    | - main.cpp
    | - main.qml
    | - onething.h
    | - onething.cpp
    | - ExtraModule
        | - CMakeLists.txt
        | - Extra.qml
        | - extrathing.h
        | - extrathing.cpp

Without going into great detail on the contents of this module, let's assume main.qml contains an instantiation of Extra.qml, about like this:

import ExtraModule
Extra { ... }

However, let's first look at the extra module. This has to be a static library so that we can link it into the main program. Therefore, we state as much in ExtraModule/CMakeLists.txt:

qt_add_library(extra_module STATIC)
qt_add_qml_module(extra_module
    URI "ExtraModule"
    VERSION 1.0
    QML_FILES
        Extra.qml
    SOURCES
        extrathing.cpp extrathing.h
)

This generates two targets: extra_module for the backing library, and extra_moduleplugin for the plugin. The plugin, being a static library too, cannot be loaded at runtime, though. We will come back to this.

In myProject/CMakeLists.txt you need to specify the QML module that main.qml and any types declared in onething.h are part of:

qt_add_executable(main_program main.cpp)

qt_add_qml_module(main_program
    VERSION 1.0
    URI myProject
    QML_FILES
        main.qml
    SOURCES
        onething.cpp onething.h
)

From there, you add the subdirectory for the extra module.

add_subdirectory(ExtraModule)

In order to link the extra module you may be inclined to link the extra_module target into main_program. It would be nice if that just worked. However, due to the way C and C++ linking works, it won't get us very far. The linker is free to eliminate unreferenced symbols from the binary. Usually all references to a QML module are written in QML, which the linker cannot see. The result is that the extra module is likely to be discarded. In order to work around this, we need to do two things:

  1. In the extra module, define a symbol we can refer to.
  2. In the main program, create a reference to the symbol from the extra module.

We observe that Qt plugins already contain symbols we can use for this purpose: The static plugin instances we generally use for importing plugins in static builds of Qt. We note further that we can use the new Q_IMPORT_QML_PLUGIN macro in qqmlextensionplugin.h to create a reference to this symbol. So we add to our main.cpp:

#include <QtQml/qqmlextensionplugin.h>
Q_IMPORT_QML_PLUGIN(ExtraModulePlugin)

ExtraModulePlugin is the name of the generated plugin class. It's composed of the module URI with Plugin appended to it. And then, in the main program's CMakeLists.txt, we link the plugin, not the backing library, into the main program:

target_link_libraries(main_program PRIVATE extra_moduleplugin)

We intend to streamline this use case in 6.3.

Exporting multiple major versions from the same module

In Qt5, if you want to export multiple major versions of a QML module, you can simply mix and match type registrations as you like, or you can create version coded directories for your QML modules. In Qt6, with the new CMake API for QML modules at least, you cannot do this. Version-coded directories like QtQuick/Controls/2/ have proven to be a rather complicated affair that is best avoided. Furthermore, qt_add_qml_module will by default only consider the major version given in its URI argument, even if the individual types declare other versions in their version tags. This is deliberate. Making a module available under more than one version adds overhead. It should not happen automatically. Furthermore, if a module is available under more than one version, then we also need to decide what versions the individual QML files are available under. In order to explicitly declare further major versions, you can use the PAST_MAJOR_VERSIONS option to qt_add_qml_module as well as the QT_QML_SOURCE_VERSIONS property on individual QML files. Let's look at an example:

set_source_files_properties(Thing.qml
    PROPERTIES
        QT_QML_SOURCE_VERSIONS "1.4;2.0;3.0"
)

set_source_files_properties(OtherThing.qml
    PROPERTIES
        QT_QML_SOURCE_VERSIONS "2.2;3.0"
)

qt_add_qml_module(my_module
    URI MyModule
    VERSION 3.2
    PAST_MAJOR_VERSIONS 
        1 2
    QML_FILES
        Thing.qml
        OtherThing.qml
        OneMoreThing.qml
    SOURCES
        everything.cpp everything.h
)

In this scenario, MyModule is available in major versions 1, 2, and 3. The maximum version available is 3.2. You can import any version 1.x or 2.x with a positive x. For Thing.qml and OtherThing.qml we have added explicit version information. Thing.qml is available from version 1.4, and OtherThing.qml is available from version 2.2. Mind that we have to specify the later versions, too, in each set_source_files_properties(). This is because you may remove QML files from a module when bumping the major version. There is no explicit version information for OneMoreThing.qml. This means that OneMoreThing.qml is available in all major versions, from minor version 0.

With this setup, the generated registration code will contain a qmlRegisterModule() for each of the major versions. This way they can all be imported.

Having explained all of this, I encourage you to take a step back. Quite obviously the versioning adds a lot of complexity and some run time overhead. Versions in QML are so deeply integrated into the language because we support unqualified lookup of members in the root object of a QML document from any of its child objects. This makes it source-incompatible to add members to components. The new members might shadow existing members of other objects. The qmllint tool allows you to check for such unqualified access. If you eliminate all unqualified access from your QML code, you won't need to use versions anymore. To support this version-less approach, in Qt 6 import statements do not need to specify a version. You can do import QtQuick and it will import the latest version of module QtQuick. If you convert all your imports to omit versions, the actual version number of your module does not matter anymore inside the QML language. A saner versioning mechanism can then be applied on a different level, for example as a general purpose software package manager.

Custom directory layouts

The easiest way to structure QML modules is to keep them in directories named by their URIs. For example, a module My.Extra.Module would live in a directory My/Extra/Module relative to the application that uses it. This way they can easily be found at run time and by any tools. It is easy to see that you may outgrow this convention, eventually. What if the same module is used by multiple applications? What if you want to group all your QML modules in one place, rather than having them pollute the root directory of your project?

For these requirements you can use the QT_QML_OUTPUT_DIRECTORY variable, as well as the RESOURCE_PREFIX, and IMPORT_PATH options to qt_add_qml_module().

Most commonly, you will want to collect your QML modules into a specific output directory, let's say a subdirectory qml/ in your build directory. This is what QT_QML_OUTPUT_DIRECTORY is used for. To achieve this, simply set the following in your top-level CMakeLists.txt:

set(QT_QML_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/qml)

You will notice that the output directories of your QML modules move to the new location. Likewise, the qmllint and qmlcachegen invocations are automatically adapted to use the new output directory as import path. The new output directory is not part of the default QML import path, though. Therefore, you have to add it explicitly at run time, so that your QML modules can be found. There are multiple options for adding import paths at run time. The most common techniques are the QML_IMPORT_PATH environment variable for ad-hoc import paths, used for debugging or testing, and the QQmlEngine::addImportPath() function for fixed import paths that should always be available.

Now that the physical file system is taken care of, you may still want to move your QML modules into a different place in the resource file system. This is what the RESOURCE_PREFIX option is for. For now, you have to specify it separately in each qt_add_qml_module. Your QML module will then be placed under the specified prefix, with a the target path generated from the URI appended. For example, consider the following module:

qt_add_qml_module(
    URI My.Great.Module
    VERSION 1.0
    RESOURCE_PREFIX /example.com/qml
    QML_FILES
        A.qml
        B.qml
)

This will add a directory :/example.com/qml/My/Great/Module to the resource file system and place the QML module defined above in it. You don't strictly need to add your resource prefix to the QML import path as the module can still be found in the physical file system. However, it generally is a good idea to add the resource prefix to the QML import path because loading from the resource file system is faster than loading from the physical file system for most modules.

In addition to all of this, there may be modules you depend on that are entirely outside the current project, in a separate import path. Even if you add the respective import paths at run time, qmllint and other tools may then stumble over your QML code because they can't find such dependencies at compile time. This is what you can use the IMPORT_PATH option for. For example:

qt_add_qml_module(
    URI My.Dependent.Module
    VERSION 1.0
    QML_FILES
        C.qml
    IMPORT_PATH "/some/where/else"
)

Eliminating run time file system access

Especially when you are linking all your QML modules into the same binary, as detailed above, you probably don't want your application to query the file system for QML modules. If all your QML modules are always loaded from the resource file system, you can deploy your application as a single binary. Let's first consider the simple case:

QQmlEngine qmlEngine;
qmlEngine.addImportPath(QStringLiteral(":/"));
// Use qmlEngine to load your main.qml file.

If you've linked all your modules into the application and if you're following the default resource directory layout, this is all you have to do. Do not add any further import paths as those might override the one you've just added.

If you have specified a custom RESOURCE_PREFIX you have to add the custom resource prefix to the import path instead. You can also use multiple resource prefixes, and add them all. However, due to the overhead of searching multiple import paths for QML modules, you should limit yourself here.

The path :/qt-project.org/imports/ is part of the default QML import path. If you use it, you don't have to specially add it. Qt's own QML modules are placed there, though. You have to be careful not to overwrite them. This is why it's not the default resource prefix for user projects. For modules that are heavily re-used across different projects :/qt-project.org/imports/ is recommended. By using it you can avoid forcing all your users to add custom import paths.

Installation

If you have not linked all your modules together, then the QML engine needs to access the physical file system in order to load them. This is desirable if you want to keep your application modular and situationally load different QML modules at run time. Eventually, you will have to take care of installation and deployment for your QML modules then. The CMake API for QML modules does not provide an abstraction for this, yet. We intend to provide one in Qt 6.3. Until then, you can manually use CMake's install function to install QML modules.

The NO options

There are a number of options to qt_add_qml_module that start with capital NO. They start with capital NO because you should not use them. You can stop reading here.

The most prominent NO option is probably NO_GENERATE_PLUGIN_SOURCE. Unfortunately image providers still need to be configured per QML engine. Therefore, if you bundle an image provider in your QML module, you need to implement the QQmlEngineExtensionPlugin::initializeEngine() method. This, in turn, makes it necessary to write your own plugin. We are working hard to find a different place for image providers. They should not be tied to the engine because they are fundamentally a graphical affair that should completely live in QtQuick. For now, however, we have to work with what we have. Let's consider a module that provides its own plugin source:

qt_add_qml_module(imageproviderplugin
    VERSION 1.0
    URI "ImageProvider"
    PLUGIN_TARGET imageproviderplugin
    NO_PLUGIN_OPTIONAL
    NO_GENERATE_PLUGIN_SOURCE
    CLASS_NAME ImageProviderExtensionPlugin
    QML_FILES
        AAA.qml
        BBB.qml
    SOURCES
        moretypes.cpp moretypes.h
        myimageprovider.cpp myimageprovider.h
        plugin.cpp
)

You may declare an image provider in myimageprovider.h, like this:

class MyImageProvider : public QQuickImageProvider
{
    [...]
};

In plugin.cpp you can then define the QQmlEngineExtensionPlugin:

#include <myimageprovider.h>
#include <QtQml/qqmlextensionplugin.h>

class ImageProviderExtensionPlugin : public QQmlEngineExtensionPlugin
{
    Q_OBJECT
    Q_PLUGIN_METADATA(IID QQmlEngineExtensionInterface_iid)
public:
    void initializeEngine(QQmlEngine *engine, const char *uri) final
    {
        Q_UNUSED(uri);
        engine->addImageProvider("myimg", new MyImageProvider);
    }
};

This will make the image provider available. It's important to note that we are dropping the split between the plugin and the backing library here. Both are the same CMake target imageproviderplugin. This is done so that the linker does not drop parts of our module in various scenarios. It may make it more difficult to access types declared in moretypes.h from elsewhere.

You may have noticed they you get another NO option for free in the above CMakeLists.txt: NO_PLUGIN_OPTIONAL. Usually, if the QML engine finds that it already knows a type registration function for a given URI, it will not busy itself with loading a plugin for that same URI. It will just register the types when the module is imported. The way it would know the type registration function would be by linking the backing library (or, in this case, the plugin) into your application and making sure the linker doesn't ignore you. As plugin loading is not free, skipping it when unnecessary is generally a good idea. In our imageproviderplugin case, however, the plugin does more than just make the types in moretypes.h available. It also initializes the image provider. Thus, the QML engine still has to load the plugin and call the initializeEngine method even if the type registration function is known. The NO_PLUGIN_OPTIONAL option does exactly that: It declares the plugin as mandatory.

All the other NO options are actually a big NO and should only be used as temporary porting aids. They have been introduced for various cases we found in Qt itself, where a proper fix could not be finished in time for Qt 6.2.


Blog Topics:

Comments