Compiling QML to C++: Import paths

This is the seventh installment in the series of blog posts on how to adjust your QML application to take the maximum advantage of qmlsc. In the first post we've set up the environment. You should read that post first in order to understand the others. We're going to look at import paths this time around.

QmlProfilerFlameGraphView.qml contains the following code:

import "../Tracing" // TODO: Turn into module import when possible

The TODO already tells us what is wrong here. We're importing the "Tracing" module by path, not by module name. This is bad because, at compile time, we don't really know what the QML engine will find at that path later. And indeed, qmlsc produces a number of warnings when compiling this file.

It's better to clearly state the intention here, and import the module by URI:

import QtCreator.Tracing 1.0

This should work, right? "Tracing" is a module after all. If you compile this, then open Qt Creator and navigate to the flame graph view in the QML profiler, you will see it doesn't work.

qrc:/QtCreator/QmlProfiler/QmlProfilerFlameGraphView.qml:29:1: FlameGraphView is not a type

In addition a whole number of bindings cannot be compiled in that file. For example:

Error: QmlProfilerFlameGraphView.qml:34:5: Could not compile binding for typeIdRole: Cannot resolve property type  for binding on typeIdRole

How long does this particular binding take? Profiling using QML Profiler reveals it's one invocation that takes 20.2µs of which 18.3µs are spent on JavaScript execution. This is particularly inefficient because the JavaScript execution here is simply looking up the enum value dynamically. At compile time we can know it statically, though.

Maybe there was some deeper reason to import by path rather than by URI after all? Let's take a closer look. If we import by URI, the QML engine has to locate the module in its import path. The default import path consists of

  1. The directory where Qt's own modules are stored, typically a directory called "qml" in your Qt installation.
  2. The directory where the application binary resides.
  3. :/qt/qml in the resource file system.
  4. :/qt-project.org/imports/ in the resource file system.

There are some platform-specific quirks in addition to this, but those don't really matter here. Now, where does Qt Creator's build system put the "Tracing" module? One place we can see already in the error message above is the resource root path. QmlProfilerFlameGraphView.qml of module QtCreator.QmlProfiler seems to reside at :/QtCreator/QmlProfiler/ . When the QML engine looks for a module, the module URI gets translated to a subdirectory of the import path by replacing the dots with slashes. So, adding ":/" to the import path would make this module detectable. We can do this. In the constructor of FlameGraphView in src/plugins/qmlprofiler/flamegraphview.cpp a QQuickWidget m_content gets created. QQuickWidget in turn contains a QQmlEngine, and we can add an import path to that, before we setSource() on it:

m_content->engine()->addImportPath(u":/"_s);

That works, indeed. However, is it a good idea? It depends. If your application uses the resource file system predominantly for QML, you may want to dedicate the whole resource file system to QML. In that case ":/" may be the right choice here. However, if you want to store further data in the resource file system without creating name conflicts with your QML modules, you may want to move your QML modules to a different place. Indeed the build system tells you to be specific here:

CMake Warning at .../cmake/Qt6Qml/Qt6QmlMacros.cmake:460 (message):
  Neither RESOURCE_PREFIX nor AUTO_RESOURCE_PREFIX are specified for
  QmlProfiler.  The resource root directory, ':/', is used as prefix.  If
  this is what you want, specify '/' as RESOURCE_PREFIX.  The recommended
  resource directory to be used as prefix is ':/qt/qml/'.  Specify
  AUTO_RESOURCE_PREFIX to use it.

Let's say we want all our modules to live at ":/qt/qml" in the resource file system. This is what the AUTO_RESOURCE_PREFIX parameter to the qt_add_qml_module CMake function is for. ":/qt/qml" is the new standard location for user QML modules in the resource file system. It is automatically included in the QML Import Path.

Mind that you have to add AUTO_RESOURCE_PREFIX to all your QML modules for it to make sense. Having half of your QML modules at ":/" and the other ones at ":/qt/qml" gives you the worst of both options.

You also need to adapt the entry points to load their QML documents from the new locations. For example, for a QQuickWidget called m_content:

m_content->setSource(QUrl(u"qrc:/qt/qml/QtCreator/QmlProfiler/QmlProfilerFlameGraphView.qml"_s));

Finally, if you rely on extra resources, like shaders, to be positioned relative to your QML modules, you need to also adapt their paths. Mind that qt_add_qml_module() offers an argument RESOURCES. The files given that way are automatically put in the right places. Use it where possible.

After all of this, most of QmlProfilerFlameGraph.qml is indeed compiled to C++. The compiler knows what types we are using. The binding on typeIdRole now takes 10.3µs of which 7.47µs are spent on JavaScript execution. Unfortunately we still have to call a function and marshall the return value here. However, what we see is an almost 50% speedup.

Compatibility

addImportPath exists since Qt 5.0. However, writing QML modules is a lot more complicated in Qt 5 than in Qt 6. This is why directory imports are relatively common in existing code. ":/qt/qml" is a recent addition to the default QML import path, as is AUTO_RESOURCE_PREFIX. They will be released ith Qt 6.5. Until then, you can manually add ":/qt/qml" to your import path:

m_content->engine()->addImportPath(u":/qt/qml"_s);

And you can use RESOURCE_PREFIX instead of AUTO_RESOURCE_PREFIX in CMake for now:

RESOURCE_PREFIX
    /qt/qml

Blog Topics:

Comments