Rust 為嵌入式軟件提供高性能、高可靠性和強大的安全性,并幫助開發人員檢查并減少復雜、低級應用程序常見的內存和線程錯誤。憑借這些明顯的優勢,Rust 在嵌入式軟件開發人員中越來越受歡迎。同樣,作為跨平臺嵌入式應用程序的框架之一,Qt 為嵌入式領域帶來了強大的業務邏輯功能。由于 Rust 還沒有的圖形用戶界面 (GUI),這意味著 Rust-Qt 集成對嵌入式開發人員越來越感興趣,并已成為向嵌入式 Rust 應用程序添加 GUI 的關鍵方法之一。
將兩者整合在一起時,挑戰就來了。如果沒有仔細的計劃和努力,組合的 Rust-Qt 應用程序就會成為這兩種語言弱屬性的犧牲品。Qt/C++對Rust的調用可能不安全,域間可能會出現并發問題,Rust的高性能和可移植性可能會打折扣。
本文介紹了為嵌入式應用程序實現 Rust 和 Qt 世界的推薦原則。
Rust 和 Qt 的背景
Rust 擁有豐富的庫生態系統,用于序列化和反序列化、異步操作、解析不安全輸入、線程、靜態分析等,而 Qt 是一個 C++ 工具包,支持跨各種平臺的豐富的、基于 GUI 的應用程序,從 iOS 到嵌入式Linux。Qt 應用程序包括表示業務邏輯的 C++ 插件、定義 GUI 組件和布局的 QML 以及用于 GUI 腳本的 JavaScript。
將 Rust 庫集成到 Qt 框架中,為開發人員提供了一種強大的機制來提供高性能和安全的基礎,從而推動復雜的用戶體驗??紤]一個農業儀表板應用程序,該應用程序需要安全訪問后端 I/O 微控制器或從基于 RTOS 的單板計算機提取數據的飛機駕駛艙顯示器。
一種推薦的應用程序架構是將業務邏輯保留在 Rust 后端,并為用戶界面使用 Qt/C++ 插件。這解耦了兩個域,將 Qt/QML 用于 GUI 開發的快速迭代速度和靈活性從業務級代碼中分離出來。
如何為嵌入式軟件集成 Rust 和 Qt
將 Qt 與 Rust 集成的常見方法是讓 Rust 調用 Qt 的 C++ 庫。雖然有幾種綁定方法,但這些方法通常不是 Rust 慣用的,并且往往會失去安全優勢,如圖 1 所示。此外,Qt 的大多數 Rust 綁定不會將 Rust 代碼暴露給 C++,這使得它們難以集成到現有的 C++ 代碼庫中。
![wKgZomYjJ9GAVCZLAAKcgD1p9mk410.png](https://file1.elecfans.com/web2/M00/D1/7A/wKgZomYjJ9GAVCZLAAKcgD1p9mk410.png)
圖 1. Rust-Qt 綁定的挑戰()
一種更有效的方法是以安全的方式連接兩種語言,盡可能多地保留 Rust 固有的安全優勢。為此,Rust 需要能夠使用自己的QObject子類和實例以的妥協來擴展 Qt 對象系統。
后一種方法的一個例子是開源庫CXX-Qt,它由 KDAB 發起并管理其正在進行的開發和改進。該庫將 Qt 強大的面向對象和元對象系統與 Rust 相結合,并基于和擴展了另一個開源 Rust/C++ 互操作性庫CXX 。在 CXX-Qt 中,新的QObject子類由 Rust 模塊中的項目組成,以執行橋接功能。這些子類就像 QML 和 C++ 中的任何其他QObject一樣被實例化,將 Rust 特性暴露給 Qt。
CXX-Qt 定義的每個QObject都包含兩個組件:
一個基于 C++ 的對象,它是一個公開屬性和可調用方法的包裝器
存儲屬性、實現可調用對象、管理內部狀態并處理來自屬性和后臺線程的更改請求的Rust結構
CXX-Qt 自動生成代碼以在 Rust 和 Qt/C++ 域之間傳輸數據,并使用名為CXX的庫在兩者之間進行通信。
CXX-Qt 橋接方法的主要原則
為了解釋有效的 Rust-Qt 橋是如何工作的,我們將描述 CXX-Qt 庫背后的幾個關鍵原則。
在 Rust 中聲明 QObject
Qt 的設計本質上是面向對象的,對于 C++ 和 QML 都是如此,而 Rust 不支持繼承和多態性等常見的類特性。為了克服這個限制,CXX-Qt 在 Rust 中擴展了 Qt 對象系統,以提供一種自然的方式來集成兩種語言,同時保持慣用的 Rust 代碼。
CXX-Qt 橋可以包括以下部分:
模塊周圍的宏指示內容與 CXX-Qt 相關
定義 QObject、Qt 的任何屬性和任何私有狀態的結構
Qobject 結構的可選實現,其中函數可以標記為 qinvokable,允許從 QML 和 C++ 調用它們
定義 QObject 信號的枚舉
普通 CXX 塊
CXX-Qt 在代碼生成期間將此 Rust 模塊擴展為QObject的 C++ 子類和RustObj結構,如圖 2 所示。
圖 2. CXX-Qt 如何擴展 QObjects 以供運行時使用()
下面是一個 QObject 示例,其中包含三個用于跨域操作的可調用方法和一個 Rust-only 方法:
#[cxx_qt::bridge]
mod my_object {
不安全的外部“C++”{
包括!(“cxx-qt-lib/qstring.h”);
輸入 QString = cxx_qt_lib::QString;
包括!(“cxx-qt-lib/qurl.h”);
輸入 QUrl = cxx_qt_lib::QUrl;
}
#[cxx_qt::qobject]
#[推導(默認)]
酒吧結構我的對象{
#[q屬性]
is_connected:布爾值,
#[q屬性]
網址:QUrl,
}
#[cxx_qt::qsignals(MyObject)]
酒吧枚舉連接{
連接的,
錯誤{消息:QString},
}
實現 qobject::MyObject {
#[qinvokable]
pub fn connect(mut self: Pin《&mut Self》, url: QUrl) {
self.as_mut().set_url(url);
如果自己
.as_ref()
.url()
.to_string()
.starts_with(“https://kdab.com”)
{
self.as_mut().set_is_connected(true);
self.emit(Connection::Connected);
} 別的 {
self.as_mut().set_is_connected(false);
self.emit(連接::錯誤{
message: QString::from(“URL 不以 https://kdab.com” 開頭),
});
}
}
}
}
使用#[qinvokable]屬性聲明的方法(例如上面的connect)暴露給 QML 和 C++,參數和返回類型在 Qt 端匹配。使用#[qproperty] 屬性(例如上面的 is_connected)聲明的 QObject 結構中的字段作為 Q_PROPERTY 公開給 QML 和 C++。用#[cxx_qt::qsignals(T)] 屬性標記的枚舉定義了在 QObject T 上聲明的信號。請注意,CXX-Qt 會自動在 snake_case (Rust) 和 CamelCase (Qt) 命名之間進行轉換。
在此示例中,connect 方法用于從 Rust 改變 QObject 的 url 和 connected 屬性。然后發出一個信號,指示連接是否成功或是否發生錯誤。
沒有用屬性標記的方法或字段被認為是 Rust 私有的,可用于管理內部狀態或用作線程實例數據。
由于QObject是橋的 C++ 端擁有的構造 Qt 對象,因此它會在運行時銷毀 C++ 對象時被銷毀。
跨橋聲明通用數據類型
原始數據類型和 CXX 類型可以跨橋使用,不需要在 Rust 和 Qt 之間進行轉換。庫 cxx_qt_lib 提供代表常見 Qt 類型(例如 QColor、QString、QVariant 等)的 Rust 類型,以供跨橋使用。
隨著項目的發展,我們計劃在cxx_qt_lib中加入更多常用的Qt類型,比如Qt容器類型(如QHash和 QVector) 和其他對 Qt C++ 或 QML 有用的類型。然后,我們希望通過向 Rust 生態系統中已建立的板條箱添加轉換來增強這些類型。例如,我們計劃支持從 QColor 到顏色包類型的轉換或從 QDateTime 到日期時間包類型的轉換,或者使用 Rust 包對 Qt 類型進行(反)序列化等。
保持線程安全
Rust-Qt 應用程序中安全線程背后的一般概念是每當 Rust 代碼執行時在 C++ 端獲取一個鎖,以防止該代碼被多個線程執行。這意味著所有直接從 C++ 調用的 Rust 代碼,例如可調用對象,僅在 Qt 線程上執行。
為了允許開發人員將狀態從后臺 Rust 線??程同步到 Qt,CXX-Qt 提供了一個幫助程序,允許您從后臺線程對 Rust 閉包進行排隊。然后從 Qt 事件循環中執行閉包。
// 在可調用對象中,請求 qt 線程的句柄
讓 qt_thread = self.qt_thread();
// 產生一個 Rust 線??程
std::thread::spawn(移動 || {
讓 value = compute_value_on_rust_thread();
// 使用閉包移動值并在 Qt 事件循環中運行任務
線程
.queue(移動|mut qobject| {
// 發生在 Qt 事件循環中
qobject.set_value(值);
})
.unwrap();
});
將 Rust QObject 暴露給 QML
要在 Qt 應用程序中使用 Rust 代碼,必須將 Rust QObject 導出到 QML。CXX-Qt 通過為Rust 中定義的每個QObject子類生成一個 C++ 類來簡化這個過程——開發人員只需要在 main.cpp 中包含兩個語句:
包括 QObject 類,例如,
#include “cxx-qt-gen/my_object.h”
將 QObject 類導出到 QML,例如,
qml注冊類型( “com.kdab.cxx_qt.demo” , 1 , 0 , “MyObject” );
構建系統
CXX-Qt 支持使用 CMake 或 Cargo 構建。
使用 CMake,Corrosion(以前稱為 cmake-cargo)用于將 Rust crate 作為靜態庫導入,然后鏈接到您的 Qt 應用程序可執行文件或庫。這允許將 CXX-Qt 輕松集成到現有的 CMake Qt 應用程序中。構建 Rust crate 時,build.rs 文件會編譯 CXX-Qt 生成的任何 C++ 代碼。
對于使用 Rust 的 Cargo 構建系統的項目,CXX-Qt 也可以在沒有 CMake 的情況下從 Cargo 使用。使用此方法,Rust 項目的 build.rs 文件還會觸發 Qt 資源的構建、任何其他 C++ 文件的編譯以及鏈接到任何其他 Qt 模塊。
結論
隨著集成 Rust 和 Qt 的用例的增長,開發團隊應該知道他們除了一對一直接綁定之外的選擇。CXX-Qt 庫提供了一種可行的橋接機制,以慣用的方式保留了 Rust 的線程安全和性能優勢,同時還允許開發人員使用普通的 Qt 和 Rust 代碼。
通過理解這里的概念,包括 Qt 對象和 Rust QObject之間的映射、Rust 的常見 Qt 類型以及定義運行時互操作性的宏和代碼生成,開發人員可以更好地為他們的應用程序開發選擇正確的 Rust-Qt 集成機制。
評論