Building with CMake

In this example, we will demonstrate how to integrate CXX-Qt code into a C++ application using CMake. Cargo builds the CXX-Qt code as a static library, then CMake links it into a C++ executable.

If you don't want to use CMake, and only want to use Cargo to build your project, refer to the previous section.

We'll first want to modify our project structure to separate the different parts of our project.

tutorial
  - cpp
  - qml
  - rust

Move the rust project into the rust folder. Pull out the qml folder back to the top level.

C++ executable

To start our QML application, we'll need a small main.cpp file with an ordinary main function. Puts this in a cpp folder to clearly separate the C++ and Rust code:

#include <QtGui/QGuiApplication>
#include <QtQml/QQmlApplicationEngine>

int
main(int argc, char* argv[])
{
  QGuiApplication app(argc, argv);

  QQmlApplicationEngine engine;

  const QUrl url(
    QStringLiteral("qrc:/qt/qml/com/kdab/cxx_qt/demo/qml/main.qml"));
  QObject::connect(
    &engine,
    &QQmlApplicationEngine::objectCreated,
    &app,
    [url](QObject* obj, const QUrl& objUrl) {
      if (!obj && url == objUrl)
        QCoreApplication::exit(-1);
    },
    Qt::QueuedConnection);

  engine.load(url);

  return app.exec();
}

You can add as much C++ code as you want in addition to this.

Using Rust QObjects in C++

For every #[cxx_qt::bridge] that we define in Rust, CXX-Qt will generate a corresponding C++ header file. They will always be in the cxx-qt-gen/ include path and use the snake_case naming convention. The name of the header file will be the name of the Rust module of your #[cxx_qt::bridge], followed by .cxxqt.h. So in our case: #include cxx-qt-gen/qobject.cxxqt.h

Note that the cxx_file_stem option can be specified in the bridge macro to choose the file name.

Including the generated header allows accessing the MyObject C++ class, just like any other C++ class. Inherit from it, connect signals and slots to it, put it in a QVector, do whatever you want with it. That's the power of CXX-Qt.

Cargo setup

Before we can get started on building Qt with CMake, we first need to make our Cargo build ready for it. If you've generated your project with the cargo new --lib or cargo init --lib [folder] command, your Cargo.toml should look something like this:

[package]
name = "qml-minimal"
version = "0.1.0"
edition = "2021"

[dependencies]

We'll have to do multiple things:

  • Instruct cargo to create a static library
  • Add cxx, cxx-qt, as well as cxx-qt-lib as dependencies
  • Add cxx-qt-build as a build dependency

If you've already followed the Cargo setup, most of this should already be done. Make sure to change the crate-type to "staticlib" though!

In the end, your Cargo.toml should look similar to this.

[package]
name = "qml-minimal"
version = "0.1.0"
authors = [
  "Andrew Hayzen <andrew.hayzen@kdab.com>",
  "Gerhard de Clercq <gerhard.declercq@kdab.com>",
  "Leon Matthes <leon.matthes@kdab.com>"
]
edition = "2021"
license = "MIT OR Apache-2.0"

# This will instruct Cargo to create a static
# library which CMake can link against
[lib]
crate-type = ["staticlib"]

[dependencies]
cxx = "1.0.95"
cxx-qt = "0.6"
cxx-qt-lib = "0.6"

# cxx-qt-build generates C++ code from the `#[cxx_qt::bridge]` module
# and compiles it together with the Rust static library
[build-dependencies]
cxx-qt-build = "0.6"

[features]
# This feature must be enabled for `cargo test` when linking Qt 6 statically.
link_qt_object_files = [ "cxx-qt-build/link_qt_object_files" ]

We'll then also need to add a script named build.rs next to the Cargo.toml:

If you've already followed the Cargo build tutorial, simply modify the existing build.rs file.

use cxx_qt_build::{CxxQtBuilder, QmlModule};

fn main() {
    CxxQtBuilder::new()
        .qml_module(QmlModule {
            uri: "com.kdab.cxx_qt.demo",
            rust_files: &["src/cxxqt_object.rs"],
            qml_files: &["../qml/main.qml"],
            ..Default::default()
        })
        .build();
}

This is what generates and compiles the C++ code for our MyObject class at build time.

Every Rust source file that uses the #[cxx_qt::bridge] macro need to be included in this script. In our case, this is only the src/cxxqt_object.rs file.

This is also where the QML module is defined with a QML uri and version. The files and resources in the module are then exposed in the same way as the qt_add_qml_module CMake function.

Note that in order for CXX-Qt to work, the qmake executable must be located. This is because CXX-Qt relies on qmake to locate the necessary Qt libraries and header files on your system.

This will be done in the CMakeLists.txt file by setting the QMAKE environment variable from CMake. This ensures that CMake and Cargo use the same Qt binaries.

We'll also need to remove the src/main.rs and replace it with a src/lib.rs file. This file only needs to include a single line:

pub mod cxxqt_object;

This simply ensures that our rust module is included in our library.

Feel free to add additional rust modules in your library as well.

CMake setup

Now add a CMakeLists.txt file in the root of your project folder. Start the CMakeLists.txt file like any other C++ project using Qt. For this example, we are supporting both Qt5 and Qt6 with CMake:

cmake_minimum_required(VERSION 3.24)

project(example_qml_minimal)
set(APP_NAME ${PROJECT_NAME})

# Rust always links against non-debug Windows runtime on *-msvc targets
# Note it is best to set this on the command line to ensure all targets are consistent
# https://github.com/corrosion-rs/corrosion/blob/master/doc/src/common_issues.md#linking-debug-cc-libraries-into-rust-fails-on-windows-msvc-targets
# https://github.com/rust-lang/rust/issues/39016
if (CMAKE_CXX_COMPILER_ID STREQUAL "MSVC")
  set(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreadedDLL")
endif()

set(CMAKE_AUTOMOC ON)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

if(NOT USE_QT5)
    find_package(Qt6 COMPONENTS Core Gui Qml QuickControls2 QmlImportScanner)
endif()
if(NOT Qt6_FOUND)
    find_package(Qt5 5.15 COMPONENTS Core Gui Qml QuickControls2 QmlImportScanner REQUIRED)
endif()

Locate Corrosion, a tool for integrating Rust libraries into CMake. If Corrosion is not installed, automatically download it:

find_package(Corrosion QUIET)
if(NOT Corrosion_FOUND)
    include(FetchContent)
    FetchContent_Declare(
        Corrosion
        GIT_REPOSITORY https://github.com/corrosion-rs/corrosion.git
        GIT_TAG v0.4.2
    )

    FetchContent_MakeAvailable(Corrosion)
endif()

To ensure that cxx-qt-build uses the same version of Qt as your CMake targets, use the Qt CMake target to locate the qmake executable. Then, pass qmake executable path to build.rs with the environment variable QMAKE using corrosion_set_env_vars.

# The path to the qmake executable path needs to be passed to the Rust
# library's build script to ensure it uses the same installation of Qt as CMake.
get_target_property(QMAKE Qt::qmake IMPORTED_LOCATION)

Use Corrosion to create a CMake library target for the Rust library. CXX-Qt requires a few more steps beyond using a typical Rust library with Corrosion:

set(CRATE qml-minimal)
# Corrosion creates a CMake target with the same name as the crate.
corrosion_import_crate(MANIFEST_PATH rust/Cargo.toml CRATES ${CRATE})

# The Rust library's build script needs to be told where to output the
# generated headers so CMake can find them. To do this, tell Corrosion
# to set the CXXQT_EXPORT_DIR environment variable when calling `cargo build`.
# Also, set the QMAKE environment variable to ensure the Rust library uses
# the same installation of Qt as CMake.
set(CXXQT_EXPORT_DIR "${CMAKE_CURRENT_BINARY_DIR}/cxxqt")
corrosion_set_env_vars(${CRATE}
    "CXXQT_EXPORT_DIR=${CXXQT_EXPORT_DIR}"
    "QMAKE=${QMAKE}"
)

# Create an INTERFACE library target to link libraries to and add include paths.
# Linking this to both the application and the tests avoids having to setup
# the include paths and linked libraries for both of those.
add_library(${APP_NAME}_lib INTERFACE)

# Include the headers generated by the Rust library's build script. Each
# crate gets its own subdirectory under CXXQT_EXPORT_DIR. This allows you
# to include headers generated by multiple crates without risk of one crate
# overwriting another's files.
target_include_directories(${APP_NAME}_lib INTERFACE "${CXXQT_EXPORT_DIR}/${CRATE}")

target_link_libraries(${APP_NAME}_lib INTERFACE
    # WHOLE_ARCHIVE is needed for the generated QML plugin to register on startup,
    # otherwise the linker will discard the static variables that initialize it.
    "$<LINK_LIBRARY:WHOLE_ARCHIVE,${CRATE}-static>"
    Qt::Core
    Qt::Gui
    Qt::Qml
    Qt::QuickControls2
)

Finally, create the CMake executable target and link it to the Rust library:

# Define the executable with the C++ source
add_executable(${APP_NAME} cpp/main.cpp)

# Link to the Rust library
target_link_libraries(${APP_NAME} PRIVATE ${APP_NAME}_lib)

# If we are using a statically linked Qt then we need to import any qml plugins
qt_import_qml_plugins(${APP_NAME})

Your project should now have a structure similar to this:

$ tree -I target/ -I tests
.
├── CMakeLists.txt
├── cpp
│   └── main.cpp
├── qml
│   └── main.qml
└── rust
    ├── build.rs
    ├── Cargo.toml
    └── src
        ├── cxxqt_object.rs
        └── lib.rs

5 directories, 7 files

Build the project like any other CMake project:

$ cmake -S . -B build
$ cmake --build build

If this fails for any reason, take a look at the examples/qml_minimal folder, which contains the complete example code.

This should now configure and compile our project. If this was successful, you can now run our little project.

$ build/examples/qml_minimal/example_qml_minimal

You should now see the two Labels that display the state of our MyObject, as well as the two buttons to call our two Rust functions.

Windows with MSVC

If you're building CXX-Qt on Windows using MSVC generator, you need to ensure that set(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreadedDLL") is set in CMake (or use the -DCMAKE_MSVC_RUNTIME_LIBRARY=MultiThreadedDLL flag) when building with the Debug configuration. This flag is necessary to ensure that the correct C Runtime Library is used. Then you can build using cmake --build build --config Debug.

This issue is caused by a bug in the cc crate (as described in https://github.com/rust-lang/cc-rs/pull/717), which has not been merged yet. Specifically, the problem is that cc generated code always links to the MultiThreaded runtime, even when building in Debug mode. We hope that this step won't be necessary in the future, once the cc crate fix is merged and released.