Для чего нужна данная статья? :
Научиться использовать Rust для написания библиотеки и вызывая ее из Qt6 через FFI с использованием ML.
Освоить:
- Использование QML для интерфейса.
- Взаимодействие между Rust и QML через cxx-qt.
- Асинхронную обработку данных в Rust (tokio).
- Динамическое обновление UI из Rust.
Найти компромиссы:
FFI (bindgen, cxx) 🟢 Полный доступ к Qt API
qt-rust (qmetaobject, cxx-qt)🟢 Хорошая интеграция
D-Bus/WebSockets/gRPC🟠 Раздельные процессы
cpp_to_rust🟢 Автоматическая генерация
cdylib + CMake🟠требует C++
slint (Rust-альтернатива Qt)🔴 Не 100% Qt
Зачем Вам это уметь? :
🔹 1. Прямое взаимодействие через C++ и FFI (Foreign Function Interface)
Rust и Qt могут взаимодействовать через C++ с помощью FFI:
- Rust код вызывает C++-библиотеки Qt через extern "C".
- Можно вручную писать C++-код-обертку и использовать CXX (cxx crate) или bindgen для генерации привязок.
- Минусы: сложность ручного связывания, возможные проблемы с совместимостью ABI.
Примеры библиотек:
- cxx — безопасное взаимодействие Rust <-> C++.
- bindgen — автоматическая генерация привязок к C++ API.
🔹 2. Использование qt-rust (Qt и Rust через C++-мост)
Это более удобный способ, чем чистый FFI. qt-rust позволяет использовать Qt с минимальными усилиями.
Примеры:
- qmetaobject — удобная привязка к Qt Meta-Object System (QML + QtWidgets).
- cxx-qt — современный мост между Rust и Qt, который использует cxx и делает возможным интеграцию с QML.
Минусы:
- Внутри всё равно есть C++-мост.
- Пока не поддерживает все возможности Qt.
🔹 3. Использование QML и Rust через D-Bus или WebSockets
Можно использовать QML (Qt Quick) для GUI, а бизнес-логику писать на Rust. Взаимодействие возможно через:
- D-Bus (zbus) — удобный способ IPC между процессами.
- WebSockets (например, через tokio-tungstenite).
- gRPC (tonic) — для более сложного взаимодействия.
Примеры библиотек:
- zbus — D-Bus для Rust.
- tokio-tungstenite — WebSocket-сервер.
- tonic — gRPC для Rust.
Плюсы:
✅ Простота и гибкость.
✅ Rust и Qt могут работать в отдельных процессах.
❌ Чуть больше накладных расходов из-за IPC.
🔹 4. Использование cpp_to_rust (Генерация привязок к Qt)
Есть проекты, которые генерируют привязки к Qt API автоматически:
- Rust Qt Binding Generator — устаревший, но полезный проект.
- cpp_to_rust — автоматическая генерация Rust-оберток для C++ Qt API.
Минусы:
- Часто такие проекты забрасываются.
- Требуют компиляции больших C++ библиотек.
🔹 5. Компиляция Rust в CDylib и подключение к Qt через CMake
Если у вас есть C++ Qt6-проект, можно написать бизнес-логику на Rust, скомпилировать её в cdylib (.so, .dll) и подключить через CMake.
Пример Cargo.toml:
[lib]
crate-type = ["cdylib"]
Пример вызова в C++:
extern "C" int my_rust_function();
Плюсы:
✅ Rust можно использовать без изменения существующего Qt-проекта.
❌ Нужно писать C++-обертки.
🔹 6. Использование Qt с Rust через slint (альтернатива QML)
slint — это альтернатива Qt/QML, написанная на Rust, но с синтаксисом, похожим на QML.
Преимущества:
- Нет зависимости от C++/Qt.
- Написано на Rust и работает нативно.
- Поддержка WebAssembly, Linux, Windows, macOS, Android.
Пример использования:
slint::slint! {
export component App inherits Window {
Text { text: "Hello, World!"; }
}
}
fn main() {
App::new().unwrap().run().unwrap();
}
Минусы:
❌ Пока не поддерживает 100% возможностей Qt.
Пример интеграции Rust с Qt6, используя Rust для написания библиотеки и вызывая ее из Qt6 через FFI:
Создайте новый проект Rust с помощью Cargo. Для этого выполните команды:
cargo new rust_lib_example
cd rust_lib_example
Отредактируйте src/lib.rs в вашем проекте Rust:
// src/lib.rs
#[no_mangle]
pub extern "C" fn add_numbers(a: i32, b: i32) -> i32 {
a + b
}
Этот код экспортирует функцию add_numbers, которую мы собираемся вызывать из Qt6.
Теперь создайте Qt6 проект. Допустим, у вас уже установлен Qt6 и вы используете Qt Creator для разработки. Создайте новый проект Qt Widgets Application.
Отредактируйте mainwindow.cpp в вашем проекте Qt:
// mainwindow.cpp
#include "mainwindow.h"
#include "ui_mainwindow.h"
#include <QMessageBox>
#include <QDebug>
extern "C" {
int add_numbers(int a, int b);
}
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent),
ui(new Ui::MainWindow) {
ui->setupUi(this);
// Пример использования Rust библиотеки из Qt
int result = add_numbers(5, 7);
// Отображение результата в диалоговом окне
QMessageBox::information(this, "Result", QString("Result of adding: %1").arg(result));
// вывод в консоль Qt
qDebug() << "Result of adding:" << result;
}
MainWindow::~MainWindow() {
delete ui;
}
Приложение, которое запрашивает данные с сервера в асинхронном потоке и обновляет UI в реальном времени:
В Cargo.toml добавим:
[dependencies]
cxx-qt = "0.6"
tokio = { version = "1", features = ["full"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
reqwest = { version = "0.11", features = ["json"] } # Для HTTP-запросов
Создаём src/lib.rs:
#[cxx_qt::bridge]
mod ffi {
use cxx_qt::CxxQtThread;
use tokio::time::{sleep, Duration};
use serde::{Serialize, Deserialize};
#[derive(Default)]
pub struct Backend {
thread: CxxQtThread<Backend>,
}
#[derive(Serialize, Deserialize)]
struct DataResponse {
message: String,
}
impl Backend {
#[qinvokable]
pub fn fetch_data(&self) {
let thread = self.thread.clone();
std::thread::spawn(move || {
tokio::runtime::Runtime::new().unwrap().block_on(async {
let response = reqwest::get("https://api.chucknorris.io/jokes/random")
.await
.ok()
.and_then(|res| res.json::<DataResponse>().await.ok());
if let Some(data) = response {
thread
.qt_thread()
.queue(|backend| backend.update_ui(data.message));
}
});
});
}
#[qinvokable]
pub fn start_timer(&self) {
let thread = self.thread.clone();
std::thread::spawn(move || {
tokio::runtime::Runtime::new().unwrap().block_on(async {
loop {
sleep(Duration::from_secs(5)).await;
let random_number: i32 = rand::random::<i32>() % 100;
thread.qt_thread().queue(|backend| backend.update_ui(format!("Random: {}", random_number)));
}
});
});
}
#[qinvokable]
fn update_ui(self: Pin<&mut Self>, new_text: String) {
self.as_mut().set_text(new_text);
}
}
#[cxx_qt::qobject]
pub struct Backend {
#[qproperty]
pub text: String,
}
}
Создаём qml/main.qml:
import QtQuick 6.0
import QtQuick.Controls 6.0
import QtQuick.Layouts 6.0
ApplicationWindow {
visible: true
width: 400
height: 300
title: "Rust + Qt Example"
Backend {
id: backend
text: "Loading..."
}
ColumnLayout {
anchors.centerIn: parent
spacing: 10
Text {
text: backend.text
font.pointSize: 14
Layout.alignment: Qt.AlignHCenter
}
Button {
text: "Fetch Data"
onClicked: backend.fetchData()
}
Button {
text: "Start Timer"
onClicked: backend.startTimer()
}
}
}
Создаём src/main.cpp:
#include <QGuiApplication>
#include <QQmlApplicationEngine>
#include "cxx-qt-gen/ffi.cxxqt.h"
int main(int argc, char *argv[]) {
QGuiApplication app(argc, argv);
QQmlApplicationEngine engine;
auto backend = std::make_shared<ffi::Backend>();
engine.rootContext()->setContextProperty("backend", backend.get());
const QUrl url(QStringLiteral("qrc:/qml/main.qml"));
QObject::connect(
engine.rootObjects().isEmpty() ? nullptr : engine.rootObjects().first(),
&QObject::destroyed,
backend,
[]() { backend.reset(); }
);
engine.load(url);
return app.exec();
}
CMakeLists.txt
cmake_minimum_required(VERSION 3.16)
project(RustQtExample LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 20)
find_package(Qt6 REQUIRED COMPONENTS Core Gui Qml Quick)
find_package(cxx-qt REQUIRED)
cxxqt_add_rustup_toolchain("stable")
cxxqt_add_rust_library(RustQtExample
CRATE_TARGET example
CARGO_MANIFEST_DIR ${CMAKE_SOURCE_DIR}/rust
)
add_executable(RustQtExampleApp main.cpp)
target_link_libraries(RustQtExampleApp PRIVATE Qt6::Core Qt6::Gui Qt6::Qml Qt6::Quick RustQtExample)
Запуск
cmake -B build
cmake --build build
./build/RustQtExampleApp
Использование ML
Создайте новый проект Rust с помощью команды cargo new rust_qt_ml --lib и добавьте следующие зависимости в Cargo.toml:
[dependencies]
tch = "0.7.0" # Rust-привязка к PyTorch для ML
cxx = "1.0" # Для FFI между Rust и C++
cxx-qt = "0.5.0" # Для интеграции с Qt
anyhow = "1.0" # Для удобной обработки ошибок
[build-dependencies]
cxx-qt-build = "0.5.0"
Создайте файл src/lib.rs и добавьте следующий код:
use tch::{nn, Device, Tensor, vision::{resnet, image}};
use cxx::{UniquePtr, CxxString};
use anyhow::Result;
#[cxx::bridge(namespace = "rust_qt_ml")]
mod ffi {
// Определение внешнего интерфейса для Qt
extern "Rust" {
type ImageClassifier;
fn new_classifier() -> Result<Box<ImageClassifier>>;
fn predict(&self, image_path: &CxxString) -> i64;
}
}
// Структура классификатора
pub struct ImageClassifier {
model: resnet::ResNet,
device: Device,
}
impl ImageClassifier {
pub fn new() -> Result<Self> {
let vs = nn::VarStore::new(Device::Cpu); // Используем CPU для простоты
let model = resnet::resnet18(&vs.root(), 1000, true)?; // ResNet18 с весами ImageNet
Ok(ImageClassifier {
model,
device: Device::Cpu,
})
}
pub fn predict(&self, image_path: &CxxString) -> i64 {
let image_path = image_path.to_string();
let img = image::load(&image_path).expect("Не удалось загрузить изображение");
let img = img.to_device(self.device).apply(&self.model); // Применяем модель
let output = img.argmax(1, false); // Получаем предсказание
output.int64_value(&[])
}
}
fn new_classifier() -> Result<Box<ImageClassifier>> {
Ok(Box::new(ImageClassifier::new()?))
}
Создайте файл build.rs для генерации привязок cxx-qt:
use cxx_qt_build::CxxQtBuilder;
fn main() {
CxxQtBuilder::new()
.file("src/lib.rs")
.build();
}
Добавьте в src/lib.rs следующий код для создания Qt-объекта:
#[cxx_qt::bridge(namespace = "rust_qt_ml")]
mod qt_bridge {
use super::ImageClassifier;
#[qobject]
#[derive(Default)]
pub struct MlBridge {
#[qproperty]
prediction: i32,
}
#[qml_element]
impl MlBridge {
#[qinvokable]
pub fn predict_image(&mut self, image_path: String) -> i32 {
let classifier = super::new_classifier().unwrap();
let prediction = classifier.predict(&cxx::CxxString::new(image_path));
self.prediction = prediction as i32;
self.prediction
}
}
}
Создайте файл qml/main.qml для интерфейса:
import QtQuick 2.15
import QtQuick.Controls 2.15
import QtQuick.Dialogs 1.3
import rust_qt_ml 1.0
ApplicationWindow {
visible: true
width: 400
height: 300
title: "Image Classifier"
MlBridge {
id: mlBridge
}
Column {
anchors.centerIn: parent
spacing: 20
Button {
text: "Load Image"
onClicked: fileDialog.open()
}
Label {
id: resultLabel
text: "Prediction: None"
}
}
FileDialog {
id: fileDialog
title: "Select an Image"
nameFilters: ["Image files (*.png *.jpg *.bmp)"]
onAccepted: {
let prediction = mlBridge.predict_image(fileUrl.toString());
resultLabel.text = "Prediction: " + prediction;
}
}
}
Создайте файл src/main.cpp:
#include <QtGui/QGuiApplication>
#include <QtQml/QQmlApplicationEngine>
#include <cxxqt_object.h>
int main(int argc, char *argv[]) {
QGuiApplication app(argc, argv);
QQmlApplicationEngine engine;
qml_register_type<rust_qt_ml::MlBridge>("rust_qt_ml", 1, 0, "MlBridge");
const QUrl url(QStringLiteral("qrc:/qml/main.qml"));
engine.load(url);
return app.exec();
}
Файл проекта Qt (rust_qt_ml.pro)
QT += quick qml
SOURCES += src/main.cpp
RESOURCES += qml.qrc
# Подключение Rust-библиотеки
LIBS += -L$$PWD/target/debug -lrust_qt_ml
INCLUDEPATH += $$PWD/target/cxxbridge
Файл ресурсов (qml.qrc)
<RCC>
<qresource prefix="/qml">
<file>main.qml</file>
</qresource>
</RCC>