在現代 GUI 應用程式開發中,進度追蹤是不可或缺的功能。本文將引導讀者使用 Rust 和 GTK 函式函式庫,從零開始建構一個進度追蹤器。我們將深入探討 GTK Widget 的建立、佈局與事件處理,並示範如何利用多執行緒更新進度條,同時確保介面保持回應。此外,文章也涵蓋了 Rust 與 C 程式碼整合的實務技巧,包括 Foreign Function Interface (FFI) 的運用,以及不安全 Rust 程式碼的注意事項,例如 Send 和 Sync 特徵,以確保跨執行緒操作的安全性。
建立 MainView
impl MainView {
pub fn new() -> Self {
// 建立一個新的 GTK+ 容器
let container = gtk::Box::new(gtk::Orientation::Vertical, 12);
container.set_border_width(6);
container.set_vexpand(true);
container.set_hexpand(true);
// 建立一個新的進度條
let progress = gtk::ProgressBar::new();
// 建立一個新的按鈕
let button = gtk::Button::new_with_label("Start");
// 將進度條和按鈕新增到容器中
container.add(&progress);
container.add(&button);
Self {
container,
progress,
button,
}
}
}
接下來,我們需要建立一個 Header
來管理我們的應用程式的標題欄。
建立 Header
impl Header {
pub fn new() -> Self {
// 建立一個新的 GTK+ 標題欄
let container = gtk::HeaderBar::new();
container.set_title(Some("Progress Tracker"));
container.set_show_close_button(true);
Self { container }
}
}
現在,我們需要建立一個 Widgets
來管理我們的應用程式的所有小部件。
建立 Widgets
impl Widgets {
pub fn new(application: >k::Application) -> Self {
// 建立一個新的 MainView
let main_view = MainView::new();
// 建立一個新的 Header
let header = Header::new();
// 建立一個新的視窗堆積疊
let view_stack = gtk::Stack::new();
view_stack.set_border_width(6);
view_stack.set_vexpand(true);
view_stack.set_hexpand(true);
// 將 MainView 和 Header 新增到視窗堆積疊中
view_stack.add(&main_view.container);
view_stack.add(&header.container);
// 建立一個新的應用程式視窗
let window = gtk::ApplicationWindow::new(application);
window.set_window_position(gtk::WindowPosition::Center);
window.set_default_size(300, 100);
window.add(&view_stack);
window.show_all();
Self {
window,
header,
view_stack,
main_view,
}
}
}
最後,我們需要建立一個 Application
來管理我們的應用程式的進度條和視窗管理。
建立 Application
impl Application {
pub fn new(app: >k::Application) -> Self {
// 建立一個新的 Widgets
let widgets = Widgets::new(app);
// 建立一個新的進度條控制器
let active = Rc::new(Cell::new(false));
// 將進度條控制器新增到視窗中
widgets.main_view.button.connect_clicked(move |_| {
if active.get() {
return;
}
active.set(true);
// 建立一個新的進度條通道
let (tx, rx) = glib::MainContext::default().channel(glib::PRIORITY_DEFAULT);
// 啟動進度條
tx.send(true).unwrap();
});
Self {
widgets,
active,
}
}
}
圖表翻譯
graph LR A[MainView] --> B[Header] B --> C[Widgets] C --> D[Application] D --> E[ProgressBar] E --> F[Button] F --> G[Channel] G --> H[Progress]
圖表說明
MainView
是我們的應用程式的主視窗。Header
是我們的應用程式的標題欄。Widgets
是我們的應用程式的所有小部件。Application
是我們的應用程式的進度條和視窗管理。ProgressBar
是我們的應用程式的進度條。Button
是我們的應用程式的按鈕。Channel
是我們的應用程式的進度條通道。Progress
是我們的應用程式的進度條控制器。
進度條更新與任務完成處理
在這個範例中,我們將使用 Rust 的 glib 函式庫來建立一個簡單的進度條更新機制。當我們的任務完成時,我們將顯示一個完成的檢視,並在 1.5 秒後重置進度條和檢視。
首先,我們需要建立一個通道來傳遞資料給主執行緒。這個通道將被用來傳送任務進度更新的資料。
let (tx, rx) = glib::MainContext::channel(glib::PRIORITY_DEFAULT);
接下來,我們將建立一個新執行緒來執行任務。這個執行緒將使用一個 for 迴圈來迴圈 10 次,並在每次迴圈中傳送一個資料給主執行緒。
thread::spawn(move || {
for v in 1..=10 {
let _ = tx.send(Some(v));
thread::sleep(Duration::from_millis(500));
}
let _ = tx.send(None);
});
在主執行緒中,我們將使用 attach
函式來接收傳送的資料。這個函式需要一個上下文和一個函式作為引數。我們將使用 glib::clone!
來建立一個弱參照到 active
和 widgets
,並將預設傳回值設為 glib::Continue(false)
。
rx.attach(None, glib::clone!(@weak active, @weak widgets =>
@default-return glib::Continue(false), move |value| {
// 處理接收到的資料
}));
現在,我們將處理接收到的資料。當我們接收到一個 Some
值時,我們將更新進度條的分數。
Some(value) => {
widgets.main_view.progress.set_fraction(f64::from(value) / 10.0);
// ...
}
當任務完成時(即 value
等於 10),我們將顯示完成的檢視,並在 1.5 秒後重置進度條和檢視。
if value == 10 {
widgets.view_stack.set_visible_child(&widgets.completed_view);
glib::timeout_add_local(Duration::from_millis(1500), glib::clone!(@weak widgets => @default-return glib::Continue(false), move || {
widgets.main_view.progress.set_fraction(0.0);
widgets.view_stack.set_visible_child(&widgets.main_view);
}));
}
這樣,我們就完成了進度條更新和任務完成處理的功能。當任務完成時,進度條將重置,檢視將切換回主檢視。
GTK 應用程式開發:進度追蹤器
在這個章節中,我們將會探討如何使用 GTK 開發一個進度追蹤器應用程式。這個應用程式將會展示一個進度條,並且可以追蹤任務的進度。
應用程式結構
首先,我們需要定義應用程式的結構。這個結構將會包含應用程式的名稱、ID 和旗標。
fn main() {
glib::set_program_name(Some("Progress Tracker"));
let application = gtk::Application::new(
Some("com.progress_tracker"),
gio::ApplicationFlags::empty(),
);
}
連線啟動事件
接下來,我們需要連線啟動事件,以便在應用程式啟動時執行特定的程式碼。
application.connect_startup(move |app| {
let application = Application::new(app);
let application_container = RefCell::new(Some(application));
});
連線關閉事件
我們也需要連線關閉事件,以便在應用程式關閉時執行特定的程式碼。
application.connect_shutdown(move |app| {
let application = application_container.borrow_mut().take().expect("Shutdown called multiple times");
drop(application);
});
連線啟動事件
最後,我們需要連線啟動事件,以便在應用程式啟動時顯示進度條。
application.connect_activate(move |app| {
// 顯示進度條
});
執行應用程式
現在,我們可以執行應用程式了。
application.run();
測試應用程式
我們可以使用 cargo run --release
來測試應用程式。應用程式將會顯示一個進度條,並且可以追蹤任務的進度。
進度追蹤器啟動視窗
進度追蹤器啟動視窗將會顯示一個進度條,並且可以追蹤任務的進度。
進度條執行中
進度條執行中將會顯示進度條的進度。
進度追蹤器任務完成
進度追蹤器任務完成將會顯示進度條的完成狀態。
這就是我們的進度追蹤器應用程式了。這個應用程式可以追蹤任務的進度,並且可以顯示進度條的進度。
不安全的Rust和FFI
簡介
Rust是一種記憶體安全的語言,透過所有權模型幫助開發者避免資料競爭和段錯誤等問題。然而,在某些情況下,例如嵌入式專案或使用C繫結,開發者可能需要直接存取記憶體並忽略其生命週期或所有權。在這種情況下,開發者必須瞭解自己在做什麼,否則可能會導致資料腐壞或不正常的行為。
使用不安全的Rust程式碼基本上允許開發者告訴編譯器:「嘿,我知道這段程式碼可能不太符合你的風格,但相信我,我知道自己在做什麼。」如果你正在開發一個低階別的程式,硬體始終是不安全的,不允許開發者自己處理它將限制Rust作為系統程式語言的多樣性。事實上,Rust標準函式庫包含了很多不安全的程式碼,但由於它們是由專業人員編寫的,所以不需要擔心。
什麼是不安全的?
以下幾點被視為不安全的:
- Dereferencing一個原始指標(Raw pointers are *const/*mut foo)
- 呼叫一個不安全的函式(unsafe fn bar())
- 存取或更新一個可變的靜態繫結(static mut baz)
- 實作一個不安全的特徵(unsafe impl Send for Foo)
- 存取聯合體的欄位
結構
在本章中,我們將涵蓋以下主題:
- 使用不安全關鍵字
- 在Rust中使用C
- 在C中使用Rust
目標
透過本章,讀者將能夠瞭解什麼是不安全的Rust以及如何明智地使用它,並學習如何使用外部函式介面(FFI)將不同的語言整合到Rust中和反之亦然。雖然Rust是一種安全的語言,但作為一種系統程式語言,它也需要允許開發者進行低階別的存取,就像在C中可以做到的,這就是不安全的Rust的用途所在。
使用不安全關鍵字
要宣告一段程式碼為不安全,你可以使用不安全區塊或在陳述式前加上不安全關鍵字。讓我們考慮以下不安全的操作: Dereferencing一個原始指標。如何讓一個繫結指向另一個繫結的記憶體地址?我們可以使用原始指標,即*const foo或*mut foo(不可變和可變的原始指標,分別)。
原始指標類似於C指標;它們允許忽略繫結的所有權/生命週期。它們沒有有效記憶體的保證,允許為空指標,不會自動實作Drop。
如果我們有一個可變的繫結x,並將其指定為32,如何建立一個原始指標y,指向x?我們可以使用as關鍵字並使用x,如下所示:
// raw_pointer.rs
fn main() {
let x: i32 = 32;
let y = &x as *const i32;
}
但是,如果我們想列印y呢?型別*const i32沒有預設的格式化程式,因此我們需要對其進行解參照。但是,如果記憶體為空呢?在這種情況下,該操作將是不安全的!好吧,我們知道x存在且尚未超出範圍,因此使用預先存在的知識,我們應該可以安全地使用不安全區塊來列印y:
// raw_pointer.rs
fn main() {
let x: i32 = 32;
let y = &x as *const i32;
unsafe {
println!("{}", *y);
}
}
圖表翻譯:
此圖示為Rust中使用不安全關鍵字的流程圖。
flowchart TD A[開始] --> B[宣告不安全程式碼] B --> C[使用不安全區塊或關鍵字] C --> D[解參照原始指標] D --> E[列印結果] E --> F[結束]
內容解密:
在Rust中使用不安全關鍵字需要謹慎,因為它允許開發者直接存取記憶體並忽略其生命週期或所有權。使用不安全關鍵字的時候,開發者必須瞭解自己在做什麼,否則可能會導致資料腐壞或不正常的行為。
Rust語言中的不安全程式碼
Rust是一種強調安全性的語言,透過所有權和借用系統來確保記憶體安全。但是在某些情況下,開發者可能需要使用不安全的程式碼來提高效能或實作特定的功能。
不安全函式
Rust中的不安全函式是使用unsafe
關鍵字來定義的。這種函式可以包含不安全的程式碼,例如直接存取記憶體或使用不安全的函式。然而,呼叫不安全函式的程式碼必須放在unsafe
塊中,以確保開發者瞭解其中的風險。
unsafe fn change_active(active: bool) {
ACTIVE = active
}
靜態可變繫結
Rust中的靜態可變繫結是使用static mut
關鍵字來定義的。這種繫結可以被不安全函式修改。
static mut ACTIVE: bool = false;
將智慧指標轉換為原始指標
Rust中的智慧指標可以使用into_raw
方法轉換為原始指標。然而,這種轉換會消耗智慧指標,原始指標需要手動釋放。
fn to_raw(box_: Box<i32>) -> *mut i32 {
println!("Turning box of value {} into raw", *box_);
Box::<i32>::into_raw(box_)
}
釋放原始指標
Rust中的原始指標需要手動釋放,可以使用drop
函式來實作。
unsafe fn free_raw(raw: *mut i32) {
println!("Dropping raw pointer of value: {}", *raw);
drop(raw)
}
主函式
Rust中的主函式可以使用不安全函式和原始指標。
fn main() {
let box_: Box<i32> = Box::new(32);
let raw: *mut i32 = to_raw(box_);
// ...
unsafe {
free_raw(raw);
}
}
Rust 中的不安全特徵(Unsafe Traits)
在 Rust 中,特徵(Traits)是一種用於定義分享行為的方法。然而,在某些情況下,我們可能需要使用不安全的特徵(Unsafe Traits)來實作特定的行為。在本節中,我們將探討如何在 Rust 中使用不安全特徵。
定義不安全特徵
要定義一個不安全特徵,我們需要使用 unsafe
關鍵字。例如:
unsafe trait TrustMe {
fn return_box(self) -> Box<Self> where Self: Sized {
Box::new(self)
}
unsafe fn return_ptr(self) -> (*mut Self, u32) where Self: Sized {
let b = self.return_box();
let ptr: *mut Self = Box::into_raw(b);
// ...
}
}
在上面的例子中,我們定義了一個名為 TrustMe
的不安全特徵。這個特徵有兩個方法:return_box
和 return_ptr
。return_box
方法傳回一個盒裝的 Self
例項,而 return_ptr
方法傳回一個可變的原始指標和一個 u32
的 ID。
實作不安全特徵
要實作一個不安全特徵,我們需要使用 unsafe
關鍵字。例如:
struct Empty;
impl TrustMe for Empty {
fn return_box(self) -> Box<Self> {
Box::new(self)
}
unsafe fn return_ptr(self) -> (*mut Self, u32) {
let b = self.return_box();
let ptr: *mut Self = Box::into_raw(b);
// ...
}
}
在上面的例子中,我們實作了 TrustMe
特徵 для Empty
型別。
使用不安全特徵
要使用不安全特徵,我們需要使用 unsafe
關鍵字。例如:
fn main() {
let e = Empty;
let b = e.return_box();
let (ptr, id) = unsafe { e.return_ptr() };
// ...
}
在上面的例子中,我們使用 return_box
方法傳回一個盒裝的 Empty
例項,並使用 return_ptr
方法傳回一個可變的原始指標和一個 u32
的 ID。
圖表翻譯:
graph LR A[定義不安全特徵] --> B[實作不安全特徵] B --> C[使用不安全特徵] C --> D[記憶體安全評估] D --> E[風險和收益評估]
在上面的圖表中,我們展示了使用不安全特徵的流程。首先,我們需要定義不安全特徵,然後實作它。接下來,我們可以使用不安全特徵,但需要進行記憶體安全評估和風險和收益評估。
Rust 中的 Send 和 Sync 特徵
在 Rust 中,Send
和 Sync
是兩個重要的特徵,分別控制著跨執行緒的資料傳遞和分享。
Send 特徵
Send
特徵表示一個型別可以安全地跨執行緒傳遞。當一個型別實作了 Send
特徰時,意味著它可以被安全地從一個執行緒傳遞到另一個執行緒。
Sync 特徵
Sync
特徵表示一個型別可以安全地在多個執行緒中分享。當一個型別實作了 Sync
特徰時,意味著它可以被安全地在多個執行緒中分享和存取。
實作 Send 和 Sync 特徰
要實作 Send
和 Sync
特徰,需要滿足以下條件:
- 型別必須是安全的,可以跨執行緒傳遞和分享。
- 型別必須實作
Send
和Sync
特徰的方法。
以下是實作 Send
和 Sync
特徰的範例:
use std::marker::Send;
use std::marker::Sync;
// 定義一個實作 Send 和 Sync 特徰的型別
#[derive(Debug)]
struct Empty;
unsafe impl Send for Empty {}
unsafe impl Sync for Empty {}
使用 Arc 來分享資料
Arc
是 Rust 中的一個智慧指標,提供了對分享資料的安全存取。Arc
代表 “atomic reference count”,它使用原子操作來管理參照計數,從而實作了跨執行緒的資料分享。
以下是使用 Arc
來分享資料的範例:
use std::sync::Arc;
use std::thread;
// 定義一個實作 Send 和 Sync 特徰的型別
#[derive(Debug)]
struct Empty;
unsafe impl Send for Empty {}
unsafe impl Sync for Empty {}
fn main() {
// 建立一個 Arc 例項
let empty = Arc::new(Empty);
// 複製 Arc 例項
let empty_clone = Arc::clone(&empty);
// 建立一個新執行緒
let handle = thread::spawn(move || {
// 在新執行緒中存取分享資料
println!("Accessing shared data: {:?}", empty_clone);
});
// 等待新執行緒完成
handle.join().unwrap();
}
結合 Send、Sync 和 Arc
以下是結合 Send
、Sync
和 Arc
的範例:
use std::sync::Arc;
use std::thread;
// 定義一個實作 Send 和 Sync 特徰的型別
#[derive(Debug)]
struct Empty;
unsafe impl Send for Empty {}
unsafe impl Sync for Empty {}
// 定義一個實作 TrustMe 特徰的型別
trait TrustMe {}
// 實作 TrustMe 特徰
unsafe impl TrustMe for Empty {}
// 定義一個函式來建立和傳回一個 Arc 例項
fn create_empty() -> Arc<Empty> {
Arc::new(Empty)
}
fn main() {
// 建立一個 Arc 例項
let empty = create_empty();
// 複製 Arc 例項
let empty_clone = Arc::clone(&empty);
// 建立一個新執行緒
let handle = thread::spawn(move || {
// 在新執行緒中存取分享資料
println!("Accessing shared data: {:?}", empty_clone);
});
// 等待新執行緒完成
handle.join().unwrap();
}
這個範例展示瞭如何結合 Send
、Sync
和 Arc
來實作跨執行緒的資料分享和傳遞。
Rust 與 C 的整合:使用 Foreign Function Interface (FFI)
Rust 是一種強大的系統程式語言,提供了記憶體安全和執行緒安全的功能。然而,在某些情況下,使用 C 函式或程式碼可能是必要的。這是因為 C 是一種成熟的語言,具有豐富的函式函式庫和資源。因此,Rust 提供了 Foreign Function Interface (FFI) 的功能,允許開發者在 Rust 中使用 C 函式或程式碼。
這篇文章涵蓋了 Rust GUI 程式設計,不安全 Rust 程式碼,以及與 C 語言的互通性等多個導向。從建立簡單的進度追蹤器應用程式開始,逐步深入到更底層的記憶體管理和跨語言互動。
從技術架構視角來看,文章清晰地展現了 GTK+ 應用程式的基本組成部分:MainView
、Header
、Widgets
和 Application
。這些元件的互動和職責劃分合理,符合 GUI 程式設計的常見模式。然而,程式碼範例中缺乏對進度條更新和任務完成處理的具體實作,僅提供了程式碼片段和概念性的說明,這使得讀者難以理解完整的執行流程。尤其是在處理多執行緒和通道的部分,程式碼片段過於簡略,沒有展現如何安全地更新 UI 元素。
文章接著探討了 Rust 中的不安全程式碼,涵蓋了原始指標、不安全函式、靜態可變繫結等概念。這部分內容對於理解 Rust 的底層機制至關重要,但也存在一些不足。例如,對於 unsafe
關鍵字的使用場景和潛在風險的說明不夠充分,缺乏更具體的示例和最佳實務。此外,關於 Send
和 Sync
特徵的解釋雖然點到了重點,但沒有深入探討其在併發程式設計中的重要性,以及如何正確地使用 Arc
等智慧指標來實作執行緒安全。
最後,文章簡要介紹了 Rust 與 C 語言的整合,提到了 FFI 的概念。這部分內容相對淺顯,僅停留在概念層面,沒有提供實際的程式碼範例和操作。對於想要深入瞭解 Rust 與 C 互通性的讀者來說,資訊量略顯不足。
對於想要學習 Rust GUI 程式設計和底層程式設計的開發者,這篇文章提供了一個初步的入門。然而,文章需要補充更完整的程式碼範例、更詳細的說明和更深入的分析,才能更好地引導讀者理解和應用這些概念。玄貓認為,Rust 的安全性和效能使其成為系統程式設計的理想選擇,但開發者需要對不安全程式碼和 FFI 有更深入的理解,才能充分發揮 Rust 的潛力。在未來的 Rust 發展中,更便捷和安全的跨語言互動方式將會是重要的發展方向。