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.
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:
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
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>
|