Overview
Rust offers high performance, strong safety, and reliability for embedded software, helping developers detect and reduce common memory and concurrency errors in complex low-level applications. As a result, Rust is increasingly popular among embedded developers. Qt is a cross-platform framework that provides rich GUI and application logic support across platforms, from iOS to embedded Linux. Because Rust lacks a mature native GUI framework, integrating Rust with Qt has become an important approach for adding GUIs to embedded Rust applications.
Challenges of Combining Rust and Qt
Integrating the two stacks raises challenges. Without careful design, a combined Rust-Qt application can negate Rust's safety guarantees. Direct calls from Qt/C++ into Rust may be unsafe, domain boundaries can introduce concurrency issues, and Rust's performance and portability can be compromised. A recommended architecture is to keep business logic in a Rust backend and implement the user interface as Qt/C++ plugins, decoupling the two domains and preserving quick iteration for the UI while keeping core logic in Rust.
Integration Approaches
A common approach is to have Rust call Qt's C++ libraries via bindings. Many binding approaches, however, are not idiomatic to Rust and often sacrifice safety. In addition, most Rust bindings for Qt do not expose Rust objects to C++, which makes integration with existing C++ codebases difficult.
A more effective approach is to connect the two languages safely while preserving as much of Rust's safety as possible. This requires Rust to be able to extend the Qt object system with its own QObject subclasses and instances.
CXX-Qt: A Bridging Example
An example of this approach is the open source CXX-Qt library, originally initiated by KDAB. CXX-Qt combines Qt's object and meta-object system with Rust and builds on the cxx interoperability library. In CXX-Qt, new QObject subclasses are composed as Rust modules that perform the bridging. These subclasses are instantiated and expose Rust features to Qt just like any other QObject used from QML and C++.
Structure of a CXX-Qt QObject
Each QObject defined by CXX-Qt contains two components:
- a C++-side object that wraps and exposes properties and callable methods
- a Rust structure that stores properties, implements callable behavior, manages internal state, and handles changes from properties and background threads
CXX-Qt generates code to transfer data between Rust and Qt/C++, using the cxx library for interoperability.
Principles of the CXX-Qt Bridge
To explain how a robust Rust-Qt bridge works, the following key principles underlie the CXX-Qt library.
Declaring QObjects in Rust
Qt is inherently object-oriented, which contrasts with Rust's lack of traditional inheritance and polymorphism. CXX-Qt extends the Qt object system in Rust, allowing a natural integration of the two languages while preserving idiomatic Rust code.
A CXX-Qt bridge module can include:
- macros indicating the module is CXX-Qt related
- a structure defining the QObject, its qproperties, and any private state
- an optional implementation block for the QObject where functions can be marked qinvokable so they are callable from QML and C++
- an enum defining signals for the QObject
- ordinary cxx blocks
During code generation, CXX-Qt expands the Rust module into a C++ subclass of QObject and a RustObj structure.
Example QObject defined in Rust (simplified):
#[cxx_qt::bridge]
mod my_object {
unsafe extern "C++" {
include!("cxx-qt-lib/qstring.h");
type QString = cxx_qt_lib::QString;
include!("cxx-qt-lib/qurl.h");
type QUrl = cxx_qt_lib::QUrl;
}
#[cxx_qt::qobject]
#[derive(Default)]
pub struct MyObject {
#[qproperty]
is_connected: bool,
#[qproperty]
url: QUrl,
}
#[cxx_qt::qsignals(MyObject)]
pub enum Connection {
Connected,
Error { message: QString },
}
impl qobject::MyObject {
#[qinvokable]
pub fn connect(mut self: Pin<&mut Self>, url: QUrl) {
self.as_mut().set_url(url);
if self.as_ref()
.url()
.to_string()
.starts_with("https://kdab.com") {
self.as_mut().set_is_connected(true);
self.emit(Connection::Connected);
} else {
self.as_mut().set_is_connected(false);
self.emit(Connection::Error{
message: QString::from("URL does not start with https://kdab.com"),
});
}
}
}
}
Methods marked with #[qinvokable] (for example connect above) are exposed to QML and C++; parameter and return types match on the Qt side. Fields in the QObject structure marked with #[qproperty] are exposed as Q_PROPERTY to QML and C++. Enums marked with #[cxx_qt::qsignals(T)] define signals declared on QObject T. CXX-Qt automatically converts between snake_case (Rust) and CamelCase (Qt) naming conventions.
Methods or fields not marked with q attributes are considered Rust-private and can be used to manage internal state or serve as thread-local data. Because the QObject is owned by the C++ side of the bridge, the Rust object is destroyed when the corresponding C++ QObject is destroyed at runtime.
Cross-bridge Common Data Types
Primitive types and cxx types can be used across the bridge without conversion. The cxx_qt_lib crate provides Rust representations of common Qt types such as QColor, QString, QVariant, and so on for use across the bridge.
As projects evolve, additional Qt types and container types (for example QHash and QVector) can be added to cxx_qt_lib, and conversions to established Rust ecosystem types can be implemented, such as converting QColor to a Rust color crate type or QDateTime to a Rust datetime type, and enabling (de)serialization between Qt types and Rust crates.
Maintaining Thread Safety
The general concept for thread safety in Rust-Qt applications is to acquire a lock on the C++ side whenever Rust code executes to prevent concurrent execution from multiple threads. This means Rust code called directly from C++, such as qinvokable methods, executes on the Qt thread.
To allow developers to synchronize state from background Rust threads to Qt, CXX-Qt provides a helper that allows queuing Rust closures from background threads to be executed later on the Qt event loop:
// Request a handle to the Qt thread in a qinvokable
let qt_thread = self.qt_thread();
// Spawn a Rust thread
std::thread::spawn(move || {
let value = compute_value_on_rust_thread();
// Move the value into a closure and schedule a task on the Qt event loop
thread
.queue(move |mut qobject| {
// Runs on the Qt event loop
qobject.set_value(value);
})
.unwrap();
});
Exposing Rust QObjects to QML
To use Rust code in a Qt application, Rust QObjects must be exported to QML. CXX-Qt simplifies this by generating a C++ class for each QObject subclass defined in Rust. The developer typically needs two lines in main.cpp:
- include the generated QObject header, for example:
#include "cxx-qt-gen/my_object.h"
- register the QObject type with QML, for example:
qmlRegisterType<my_object::MyObject>("com.kdab.cxx_qt.demo", 1, 0, "MyObject");
Build Systems
CXX-Qt supports both CMake and Cargo builds.
With CMake, corrosion (formerly cmake-cargo) is used to import the Rust crate as a static library, which can then be linked into the Qt application executable or library. During the Rust crate build, build.rs compiles any CXX-Qt-generated C++ code.
For projects built with Cargo, CXX-Qt can be used directly from Cargo without CMake. In that setup, the Rust project's build.rs triggers building Qt resources, compiling any additional C++ files, and linking to the required Qt modules.
Conclusion
As use cases for integrating Rust and Qt increase, development teams should be aware of alternatives to one-to-one direct bindings. The CXX-Qt library provides a bridging mechanism that preserves Rust's thread safety and performance while allowing developers to use standard Qt and Rust code. Understanding the concepts described here, including the mapping between Qt objects and Rust QObjects, common Qt types in Rust, and the macros and code generation that define runtime interoperability, helps developers choose the right Rust-Qt integration approach for their applications.