basysKom AnwendungsEntwicklung

About QML Efficiency: Compilers, Language Server, and Type Annotations
Essential Summary
In our last post we had a look at how to set up QML Modules and how we can benefit from the QML Linter. Today we’re going to set up the QML Language Server to get an IDE-like experience in an editor of our choice. We’ll also help the the QML Compiler generate more efficient code.

The QML Language Server provides the information gathered by the QML Linter through the Language Server Protocol (LSP) to an editor of your liking. LSP is an open, JSON-RPC-based protocol that enables an editor to talk to a language server that supplies language-specific features like code completion, refactoring capabilities, and warning markers. Kindly refer to your editor’s documentation on how to configure it to run the qmlls binary that is distributed alongside Qt Qml.

KDE’s Kate editor illustrating some of qmlls’s capabilities
qmlls in action

Configuring the linter and language server

The build system sets up linter targets automatically. In order for the language server to find your imports, however, it also needs to be configured. It’s possible to specify the “-b” (build directory) argument when running qmlls manually but an editor will normally just run a single instance and point it at the file being edited. Using CMake’s configure_file feature we can generate the configuration file and place it in our source directory automatically. Create a qmlls.ini.in as follows:

[General]
buildDir=${QT_QML_OUTPUT_DIRECTORY} 
and then configure it using CMake:
configure_file(qmlls.ini.in ${CMAKE_SOURCE_DIR}/.qmlls.ini) 

Qt 6.7 can actually do this for you when the project is configured with -DQT_QML_GENERATE_QMLLS_INI=ON. Note that the resulting configuration file is specific to the particular build and should not be checked into version control. You might want to add it to your .gitignore file.

A project can likewise provide a .qmllint.ini in its source directory. This file configures which checks the linter performs and which of them should be treated as a fatal error. A default file which can then be tweaked is created by running:

$ qmllint --write-defaults 
The key difference is that you add your QT_QML_OUTPUT_DIRECTORY to the AdditionalQmlImportPaths key.

Function type annotations

Now that we’ve set up the QML Linter and QML Language Server, let’s have a look at how we can help the QML Compiler generate more efficient code for us:
As you may recall, type information is key to this. It has already been possible since Qt 5.14 to annotate JavaScript functions in QML using a TypeScript-like syntax to improve IDE auto-completion and improve interoperation between C++ and QML. Doing so becomes even more important in Qt 6 where it’s also possible to explicitly specify “void” as a return type. In addition to value types, QML type names can also be used, such as “Rectangle”.

Consider a stop watch and a simple function to format the elapsed time display:

function formatElapsedTime(time) {
    //: Elapsed seconds
    return qsTr("%L1 s").arg(time / 1000);
} 
The QML engine has no idea what type of arguments the function takes and what it returns until it actually executes it. The only thing it could perhaps infer by just looking at it is that it returns a string. Let’s fix that!
function formatElapsedTime(time : int) : string {
    //: Elapsed seconds
    return qsTr("%L1 s").arg(time / 1000);
} 

Despite the type annotations, it’s not possible to overload JavaScript functions: there can only be one “formatElapsedTime” function in our component. Calling the function is also still possible with unexpected types: while a function taking a Rectangle will throw a TypeError when called with a Button (unless Button implements Rectangle, of course), the formatElapsedTime function will happily accept a string for “time” and then try to interpret it as an int.

Additionally, an annotated function interfaces better with C++. Rather than having to wrap its arguments in QVariant, the actual types can be used on the C++ side as well:

QString text;
QMetaObject::invokeMethod(timerPage, "formatElapsedTime", qReturnArg(text), 1200);
qDebug() << text; // "1.2 s" 

Simple as that. Also note the new invokeMethod syntax introduced in Qt 6.5, no more Q_ARG.

QML compiler magic

If we now look at the magic TimerPage_qml.cpp generated by qmlcachegen hidden in our build directory, we can suddenly find an actual C++ function at the end of the file:

// formatElapsedTime at line 22, column 5
QString r8_0;
double r6_0 = double((*static_cast<int*>(argumentsPtr[0])));
double r12_0;
double r2_1;
double r11_0;
QString r2_0;
QString r10_0;
r2_0 = QStringLiteral("%L1 s");
r10_0 = std::move(r2_0);
aotContext->captureTranslation();
r2_0 = QCoreApplication::translate(aotContext->translationContext().toUtf8().constData(), std::move(r10_0).toUtf8().constData(), "", -1);
r8_0 = std::move(r2_0);
r12_0 = r6_0;
r2_1 = double(1000);
r2_1 = (r12_0 / r2_1);
r11_0 = r2_1;
r2_0 = std::move(r8_0).arg(r11_0);
return r2_0; 

It even transformed the qsTr statement into a proper call to QCoreApplication::translate. How cool is that?!

Of course you cannot be expected to read obscure C++ files that still mostly consist of QML byte code. Nevertheless, it would be useful to know in advance if a QML expression is sub-optimal. Running qmlcachegen in verbose mode provides a lot of diagnostics on why it did or didn’t decide to generate C++ code:

set_target_properties(myhmi PROPERTIES
    QT_QMLCACHEGEN_ARGUMENTS "–-verbose"
) 
In the above example the relevant warning is:
Warning: TimerPage.qml:20:5: Functions without type annotations won't be compiled [compiler]
function formatElapsedTime(time) {
^^^^^^^^^ 

Unfortunately, it cannot deal with “optional imports” yet that are used by QtQuick.Controls. After all, they dynamically load a style appropriate for the given platform. This also means that it’s not advisable to generally build the project verbose like this since you will just drown in warnings. Instead, you need to import the relevant style yourself, such as QtQuick.Controls.Windows in order to make full use of the QML Compiler. This might be a valid option for an embedded application but not really for a desktop application that tries to actually fit into its environment.

Conclusion

We have now seen QML compiler magic in action, and I hope you enjoyed this peek under the hood of what makes QML work. With QML modules and type annotations we unlock a wealth of useful tools that make our applications faster to develop, more reliable, and easier to debug. We can probably all agree that making use of modern QML tooling will pay off in the long run.

 
Further reading:

Picture of Kai Uwe Broulik

Kai Uwe Broulik

Kai Uwe Broulik is Software Architect at basysKom where he designs embedded HMI applications based on Qt with C++ and QML. He also trains customers on how to use Qt efficiently. With his more than ten years of experience in Qt he has successfully deployed Qt applications to a variety of platforms, such as mobile phones, desktop environments, as well as automotive and other embedded devices.

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.

Weitere Blogartikel

basysKom Newsletter

We collect only the data you enter in this form (no IP address or information that can be derived from it). The collected data is only used in order to send you our regular newsletters, from which you can unsubscribe at any point using the link at the bottom of each newsletter. We will retain this information until you ask us to delete it permanently. For more information about our privacy policy, read Privacy Policy