Найти в Дзене
Один Rust не п...Rust

Rust с Qt6

Оглавление

Для чего нужна данная статья? :

Научиться использовать 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>