Skip to main content
Skip to content

Using Rust together with C++

Take advantage of both worlds

The initial Rust tutorial pages focus on creating an application that uses only Rust, but existing KDE applications will rather use Rust to extend their current C++ codebase.

This is possible by using C++ as the entrypoint for the application and using a custom Rust crate as a library that gets linked to it via cxx-qt CMake integration. This integration is internally provided by Corrosion.

As a result, instead of having a simple CMake wrapper on top of Cargo, we will rely on some cxx-qt facilities to generate the library crate.

This tutorial can be skipped if you don't plan on making a project that uses both Rust and C++. It also consists of a slightly modified version of upstream cxx-qt Building with CMake.

Project structure

We will be modifying the existing tutorial example from A full Rust + Kirigami application. The resulting project structure will look like this:

simplemdviewer/
├── README.md
├── CMakeLists.txt -------------------- # Modified
├── Cargo.toml     -------------------- # Modified
├── build.rs
├── org.kde.simplemdviewer.desktop
├── org.kde.simplemdviewer.json
├── org.kde.simplemdviewer.svg
├── org.kde.simplemdviewer.metainfo.xml
└── src/
    ├── main.cpp   -------------------- # New (replaces main.rs)
    ├── lib.rs     -------------------- # New
    ├── mdconverter.rs
    └── qml/
        └── Main.qml

Changes to existing code

Cargo.toml

When using this integration, the originally Rust only code will turn into a separate library that can be linked from C++; this means that a separate CMake target will be created for the crate.

If the crate is named "simplemdviewer", then we can't use "simplemdviewer" for the C++ executable. Therefore, we need a slight modification to the application name in Cargo.toml:

1
2
3
4
5
6
[package]
name = "simplemdviewer_rs"
version = "0.1.0"
authors = [ "Konqi the Konqueror <konqi@kde.org>" ]
edition = "2021"
license = "BSD-2-Clause"

The crate, originally a Rust executable, now must become a static library:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
[package]
name = "simplemdviewer_rs"
version = "0.1.0"
authors = [ "Konqi the Konqueror <konqi@kde.org>" ]
edition = "2021"
license = "BSD-2-Clause"

[lib]
crate-type = ["staticlib"]

[dependencies]
cxx = "1.0"
cxx-qt = "0.7"
cxx-qt-lib = { version = "0.7", features = ["qt_full"] }
cxx-qt-lib-extras = "0.7"
markdown = "=1.0.0-alpha.17"

[build-dependencies]
# The link_qt_object_files feature is required for statically linking Qt 6.
cxx-qt-build = { version = "0.7", features = [ "link_qt_object_files" ] }

src/main.rs -> src/lib.rs

As the Rust code no longer functions as an executable, the entrypoint can no longer be main.rs, so we delete it.

rm src/main.rs

Instead we want to create a lib.rs file whose purpose is purely to import other Rust modules. This lib.rs functions as the entrypoint for the library and simply exposes any modules that might be available. We want to expose it to C++, so it must be a pub module.

You can read more about this in Rust Book: Defining Modules to Control Scope and Privacy.

Create src/lib.rs with the following contents:

1
pub mod mdconverter;

This will expose the code from mdconverter.rs automatically.

Functionally, this is the same as the following if we didn't have a separate mdconverter.rs file:

pub mod mdconverter {
    // The rest of the code from mdconverter.rs here
}

The src/mdconverter.rs file is what will expose things to C++. It requires no modifications.

src/main.cpp

The new entrypoint for the application will be a C++ executable, so we need to create a main.cpp file. For our purposes, we can use a standard C++ file for loading QML that is analogous to what we did with the original main.rs file:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
#include <QApplication>
#include <QQmlApplicationEngine>
#include <QtQml>
#include <QUrl>
#include <QQuickStyle>

int main(int argc, char *argv[])
{
    QApplication app(argc, argv);
    QGuiApplication::setDesktopFileName(QStringLiteral("org.kde.simplemdviewer"));

    QApplication::setStyle(QStringLiteral("breeze"));
    if (qEnvironmentVariableIsEmpty("QT_QUICK_CONTROLS_STYLE")) {
        QQuickStyle::setStyle(QStringLiteral("org.kde.desktop"));
    }

    QQmlApplicationEngine engine;
    engine.load("qrc:/qt/qml/org/kde/simplemdviewer/src/qml/Main.qml");

    return app.exec();
}

CMakeLists.txt

The CMake code will need major modifications to use CxxQt instead of our previous custom Cargo wrapper.

First, since the entrypoint for the application is C++, we need to find the C++ Qt libraries and link them so main.cpp compiles:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
cmake_minimum_required(VERSION 3.28)

project(simplemdviewer)

find_package(ECM 6.0 REQUIRED NO_MODULE)
set(CMAKE_MODULE_PATH ${ECM_MODULE_PATH})
include(KDEInstallDirs)
include(ECMUninstallTarget)

include(ECMFindQmlModule)
ecm_find_qmlmodule(org.kde.kirigami REQUIRED)
find_package(KF6 REQUIRED COMPONENTS QQC2DesktopStyle)
find_package(Qt6 REQUIRED
    COMPONENTS
    Core
    Gui
    Qml
    Widgets
    Quick
    QuickControls2
)

We will no longer need the original Cargo wrapper code, so we can safely remove it:

52
53
54
55
56
57
58
59
60
61
62
# set(CARGO_INNER_TARGET_DIR debug)
# if(CMAKE_BUILD_TYPE STREQUAL "Release" OR CMAKE_BUILD_TYPE STREQUAL "MinSizeRel")
#     set(CARGO_OPTIONS --release)
#     set(CARGO_INNER_TARGET_DIR release)
# endif()
#
# add_custom_target(simplemdviewer
#     ALL
#     COMMAND cargo build --target-dir ${CMAKE_CURRENT_BINARY_DIR} ${CARGO_OPTIONS}
#     BYPRODUCTS ${CMAKE_BINARY_DIR}/${CARGO_INNER_TARGET_DIR}
# )

We need to add CxxQt to the project. We can use CMake's built-in FetchContent functionality to download it from the repository and make it available at configure time:

23
24
25
26
27
28
29
30
31
32
33
find_package(CxxQt QUIET)
if(NOT CxxQt_FOUND)
    include(FetchContent)
    FetchContent_Declare(
        CxxQt
        GIT_REPOSITORY https://github.com/kdab/cxx-qt-cmake.git
        GIT_TAG 0.7
    )

    FetchContent_MakeAvailable(CxxQt)
endif()

And then we import the Rust crate into the project using the manifest path (Cargo.toml) and the package name simplemdviewer_rs. It must link to the same Qt6 libraries specified earlier, otherwise this will result in an ABI incompatibility.

35
36
37
38
39
40
41
42
43
44
45
cxx_qt_import_crate(
    MANIFEST_PATH Cargo.toml
    CRATES simplemdviewer_rs
    QT_MODULES
    Qt6::Core
    Qt6::Gui
    Qt6::Qml
    Qt6::Widgets
    Qt6::Quick
    Qt6::QuickControls2
)

After having imported the library crate, we can import the QML module exposed in the Rust code. Its first argument will be the name of the new linking target; URI must match the URI in build.rs; and SOURCE_CRATE must match the package name in Cargo.toml.

47
48
49
50
cxx_qt_import_qml_module(simplemdviewer_rs_qml_module
    URI "org.kde.simplemdviewer"
    SOURCE_CRATE simplemdviewer_rs
)

We now have a CMake library target that points to Rust code called simplemdviewer_rs_qml_module. The name can be anything, like simplemdviewer_rs_plugin for example.

We can now create the C++ entrypoint executable target, and then link to the newly created QML module target (as well as the necessary Qt6 libraries for C++):

64
65
66
67
68
69
70
71
72
73
add_executable(simplemdviewer src/main.cpp)

target_link_libraries(simplemdviewer
    PRIVATE
    simplemdviewer_rs_qml_module
    Qt6::Core
    Qt6::Gui
    Qt6::Qml
    Qt6::Widgets
    Qt6::Quick

Final test run

At last, build, install and run your new application:

cmake -B build/ --install-prefix ~/.local
cmake --build build/
cmake --install build/
simplemdviewer

Our app so far

CMakeLists.txt
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
cmake_minimum_required(VERSION 3.28)

project(simplemdviewer)

find_package(ECM 6.0 REQUIRED NO_MODULE)
set(CMAKE_MODULE_PATH ${ECM_MODULE_PATH})
include(KDEInstallDirs)
include(ECMUninstallTarget)

include(ECMFindQmlModule)
ecm_find_qmlmodule(org.kde.kirigami REQUIRED)
find_package(KF6 REQUIRED COMPONENTS QQC2DesktopStyle)
find_package(Qt6 REQUIRED
    COMPONENTS
    Core
    Gui
    Qml
    Widgets
    Quick
    QuickControls2
)

find_package(CxxQt QUIET)
if(NOT CxxQt_FOUND)
    include(FetchContent)
    FetchContent_Declare(
        CxxQt
        GIT_REPOSITORY https://github.com/kdab/cxx-qt-cmake.git
        GIT_TAG 0.7
    )

    FetchContent_MakeAvailable(CxxQt)
endif()

cxx_qt_import_crate(
    MANIFEST_PATH Cargo.toml
    CRATES simplemdviewer_rs
    QT_MODULES
    Qt6::Core
    Qt6::Gui
    Qt6::Qml
    Qt6::Widgets
    Qt6::Quick
    Qt6::QuickControls2
)

cxx_qt_import_qml_module(simplemdviewer_rs_qml_module
    URI "org.kde.simplemdviewer"
    SOURCE_CRATE simplemdviewer_rs
)

# set(CARGO_INNER_TARGET_DIR debug)
# if(CMAKE_BUILD_TYPE STREQUAL "Release" OR CMAKE_BUILD_TYPE STREQUAL "MinSizeRel")
#     set(CARGO_OPTIONS --release)
#     set(CARGO_INNER_TARGET_DIR release)
# endif()
#
# add_custom_target(simplemdviewer
#     ALL
#     COMMAND cargo build --target-dir ${CMAKE_CURRENT_BINARY_DIR} ${CARGO_OPTIONS}
#     BYPRODUCTS ${CMAKE_BINARY_DIR}/${CARGO_INNER_TARGET_DIR}
# )

add_executable(simplemdviewer src/main.cpp)

target_link_libraries(simplemdviewer
    PRIVATE
    simplemdviewer_rs_qml_module
    Qt6::Core
    Qt6::Gui
    Qt6::Qml
    Qt6::Widgets
    Qt6::Quick
    Qt6::QuickControls2
)

install(
    TARGETS simplemdviewer
    DESTINATION ${KDE_INSTALL_BINDIR}
)

install(FILES org.kde.simplemdviewer.desktop DESTINATION ${KDE_INSTALL_APPDIR})
install(FILES org.kde.simplemdviewer.metainfo.xml DESTINATION ${KDE_INSTALL_METAINFODIR})
install(FILES org.kde.simplemdviewer.svg DESTINATION ${KDE_INSTALL_FULL_ICONDIR}/hicolor/scalable/apps)
Cargo.toml
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
[package]
name = "simplemdviewer_rs"
version = "0.1.0"
authors = [ "Konqi the Konqueror <konqi@kde.org>" ]
edition = "2021"
license = "BSD-2-Clause"

[lib]
crate-type = ["staticlib"]

[dependencies]
cxx = "1.0"
cxx-qt = "0.7"
cxx-qt-lib = { version = "0.7", features = ["qt_full"] }
cxx-qt-lib-extras = "0.7"
markdown = "=1.0.0-alpha.17"

[build-dependencies]
# The link_qt_object_files feature is required for statically linking Qt 6.
cxx-qt-build = { version = "0.7", features = [ "link_qt_object_files" ] }
build.rs
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
use cxx_qt_build::{CxxQtBuilder, QmlModule};

fn main() {
    CxxQtBuilder::new()
        .qml_module(QmlModule {
            uri: "org.kde.simplemdviewer",
            qml_files: &["src/qml/Main.qml"],
            rust_files: &["src/mdconverter.rs"],
            ..Default::default()
        })
        .build();
}
src/lib.rs
1
pub mod mdconverter;
src/main.cpp
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
#include <QApplication>
#include <QQmlApplicationEngine>
#include <QtQml>
#include <QUrl>
#include <QQuickStyle>

int main(int argc, char *argv[])
{
    QApplication app(argc, argv);
    QGuiApplication::setDesktopFileName(QStringLiteral("org.kde.simplemdviewer"));

    QApplication::setStyle(QStringLiteral("breeze"));
    if (qEnvironmentVariableIsEmpty("QT_QUICK_CONTROLS_STYLE")) {
        QQuickStyle::setStyle(QStringLiteral("org.kde.desktop"));
    }

    QQmlApplicationEngine engine;
    engine.load("qrc:/qt/qml/org/kde/simplemdviewer/src/qml/Main.qml");

    return app.exec();
}
src/mdconverter.rs
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
use std::pin::Pin;

use cxx_qt_lib::QString;

#[cxx_qt::bridge]
mod ffi {

    unsafe extern "C++" {
        include!("cxx-qt-lib/qstring.h");
        type QString = cxx_qt_lib::QString;
    }

    #[auto_cxx_name]
    unsafe extern "RustQt" {
        #[qobject]
        #[qml_element]
        #[qproperty(QString, source_text)]
        type MdConverter = super::MdConverterStruct;

        #[qinvokable]
        fn md_format(self: Pin<&mut MdConverter>) -> QString;
    }
}

#[derive(Default)]
pub struct MdConverterStruct {
    source_text: QString,
}

impl ffi::MdConverter {
    pub fn md_format(self: Pin<&mut Self>) -> QString {
        QString::from(&markdown::to_html(&self.source_text().to_string()))
    }
}
src/qml/Main.qml
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
import QtQuick
import QtQuick.Controls as Controls
import QtQuick.Layouts
import org.kde.kirigami as Kirigami
import org.kde.simplemdviewer

Kirigami.ApplicationWindow {
    id: root

    title: "Simple Markdown Viewer in Rust 🦀"
    minimumWidth: Kirigami.Units.gridUnit * 20
    minimumHeight: Kirigami.Units.gridUnit * 20
    width: minimumWidth
    height: minimumHeight
    pageStack.initialPage: initPage

    Component {
        id: initPage

        Kirigami.Page {
            title: "Markdown Viewer"

            MdConverter {
                id: mdconverter

                sourceText: sourceArea.text
            }

            ColumnLayout {
                anchors {
                    top: parent.top
                    left: parent.left
                    right: parent.right
                }

                Controls.TextArea {
                    id: sourceArea

                    placeholderText: "Write some Markdown code here"
                    wrapMode: Text.WrapAnywhere
                    Layout.fillWidth: true
                    Layout.minimumHeight: Kirigami.Units.gridUnit * 5
                }

                RowLayout {
                    Layout.fillWidth: true

                    Controls.Button {
                        text: "Format"
                        onClicked: formattedText.text = mdconverter.mdFormat()
                    }

                    Controls.Button {
                        text: "Clear"
                        onClicked: {
                            sourceArea.text = "";
                            formattedText.text = "";
                        }
                    }

                }

                Controls.Label {
                    id: formattedText

                    textFormat: Text.RichText
                    wrapMode: Text.WordWrap
                    Layout.fillWidth: true
                    Layout.minimumHeight: Kirigami.Units.gridUnit * 5
                }

            }

        }

    }

}
org.kde.simplemdviewer.desktop
1
2
3
4
5
6
7
[Desktop Entry]
Name=Simple Markdown Viewer in Rust and Kirigami
Exec=simplemdviewer
Icon=org.kde.simplemdviewer
Type=Application
Terminal=false
Categories=Utility
org.kde.simplemdviewer.metainfo.xml
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
<?xml version="1.0" encoding="utf-8"?>
<component type="desktop-application">
  <id>org.kde.simplemdviewer</id>
  <launchable type="desktop-id">org.kde.simplemdviewer.desktop</launchable>
  <name>Simple Markdown Viewer in Rust and Kirigami</name>
  <metadata_license/>
  <project_license>MPL-2.0</project_license>
  <summary>Markdown viewer</summary>
  <description>
    <p>A simple Markdown viewer application</p>
  </description>
  <developer_name>Konqi the Konqueror</developer_name>
  <update_contact>konqi@kde.org</update_contact>
  <releases>
    <release version="0.1.0" date="2025-07-01"/>
  </releases>
</component>