在系統開發中,結合 Rust 的安全性和 C 的效能常常是必要的。本文將介紹如何使用 bindgen
自動生成 C 函式繫結,讓 Rust 程式碼可以直接呼叫 C 函式,以及如何使用 cc
crate 在 Rust 專案中編譯 C 程式碼。此外,我們將以簡化的原子參照計數器 (Arc) 實作為例,示範如何在 Rust 中安全地管理分享資料,並探討 Makefile 的編寫與跨語言專案的建置流程。透過這些技巧,開發者可以更有效地整合 Rust 和 C,發揮兩者的優勢。
使用 bindgen
生成 C 函式繫結
要在 Rust 中使用 C 函式,需要使用 bindgen
來生成 C 函式的繫結。bindgen
是一種工具,能夠自動生成 C 函式的繫結,讓 Rust 可以使用 C 函式。以下是使用 bindgen
的步驟:
- 安裝
bindgen
和cc
:
$ sudo apt-get update && sudo apt-get upgrade
$ sudo apt-get install clang-14 --install-suggests
$ sudo apt-get install make
- 建立一個新專案,例如
add_ffi
:
$ cargo new add_ffi
- 在
Cargo.toml
中新增bindgen
和cc
的依賴:
[dependencies]
cc = "1.0.73"
bindgen = "0.59.2"
- 建立一個
build.rs
檔案,內容如下:
use std::env;
use std::path::PathBuf;
fn main() {
let out = PathBuf::from(env::var("OUT_DIR").unwrap());
bindgen::Builder::default()
.header("add.h")
.generate()
.expect("Unable to generate bindings")
.write_to_file(out.join("bindings.rs"))
.expect("Couldn't write bindings!");
}
- 建立一個
add.h
檔案,內容如下:
int add(int a, int b);
- 建立一個
add.c
檔案,內容如下:
int add(int a, int b) {
return a + b;
}
- 執行
cargo build
來生成繫結和編譯 C 程式碼。
使用 C 函式
現在,可以在 Rust 中使用 C 函式了。以下是範例程式碼:
extern crate add_ffi;
use add_ffi::bindings::*;
fn main() {
let a = 2;
let b = 3;
let result = add(a, b);
println!("{} + {} = {}", a, b, result);
}
這個範例程式碼使用 add
函式來計算 a
和 b
的總和,並將結果印出來。
使用 Rust 和 C 共同開發:一個簡單的範例
在這個範例中,我們將使用 Rust 和 C 共同開發一個簡單的加法函式。首先,我們需要建立一個新的 Rust 專案,並新增必要的依賴項。
新增依賴項
在 Cargo.toml
檔案中,新增以下依賴項:
[build-dependencies]
bindgen = "0.60.1"
cc = "1.0.73"
建立 C 程式
首先,建立一個新的目錄 includes
,並在其中建立一個新的檔案 add.h
。在 add.h
檔案中,新增以下程式碼:
#ifndef ADD_H
#define ADD_H
#include <stdint.h>
uint32_t add(uint32_t a, uint32_t b);
#endif
接下來,建立一個新的檔案 add.c
,並在其中新增以下程式碼:
#include "includes/add.h"
uint32_t add(uint32_t a, uint32_t b){
return a + b;
}
建立 Rust Build Script
在專案根目錄中,建立一個新的檔案 build.rs
,並在其中新增以下程式碼:
use bindgen::Builder;
use cc::Build;
use std::path::PathBuf;
fn main() {
// 編譯 C 程式
Build::new()
.file("add.c")
.include("includes")
.compile("add.so");
// 生成 Rust Bindings
let bindings = Builder::default()
.header("includes/add.h")
.generate()
.unwrap();
}
使用 Rust Bindings
現在,我們可以使用 Rust Bindings 來呼叫 C 函式。建立一個新的檔案 main.rs
,並在其中新增以下程式碼:
extern crate add;
fn main() {
let result = unsafe { add::add(2, 3) };
println!("Result: {}", result);
}
執行程式
執行以下命令來編譯和執行程式:
cargo build
cargo run
這將輸出 Result: 5
。
使用Rust與C進行混合開發
在進行混合開發時,需要考慮到Rust和C的語言差異以及如何進行溝通。以下是使用Rust與C進行混合開發的步驟:
使用bindgen生成Rust繫結
首先,需要使用bindgen生成Rust繫結。bindgen是一個工具,可以自動生成Rust繫結,讓Rust可以呼叫C程式碼。以下是使用bindgen的步驟:
- 安裝bindgen:
cargo install bindgen
- 建立一個新的Rust專案:
cargo new myproject
- 在專案中新增C程式碼:
touch src/main.c
- 使用bindgen生成Rust繫結:
bindgen --output src/bindings.rs src/main.c
使用cc生成C繫結
如果需要在C中呼叫Rust程式碼,需要使用cc生成C繫結。cc是一個工具,可以自動生成C繫結,讓C可以呼叫Rust程式碼。以下是使用cc的步驟:
- 安裝cc:
cargo install cc
- 建立一個新的Rust專案:
cargo new myproject
- 在專案中新增Rust程式碼:
touch src/main.rs
- 使用cc生成C繫結:
cc --output src/main.c src/main.rs
使用extern進行溝通
在Rust和C之間進行溝通時,需要使用extern關鍵字。extern關鍵字可以讓Rust呼叫C程式碼,也可以讓C呼叫Rust程式碼。以下是使用extern的步驟:
- 在Rust中使用extern關鍵字呼叫C程式碼:
extern "C" { fn my_c_function(); }
- 在C中使用extern關鍵字呼叫Rust程式碼:
extern void my_rust_function();
範例
以下是使用Rust與C進行混合開發的範例:
// src/main.rs
extern "C" {
fn add(a: i32, b: i32) -> i32;
}
fn main() {
let result = unsafe { add(2, 3) };
println!("The result is: {}", result);
}
// src/main.c
int add(int a, int b) {
return a + b;
}
在這個範例中,Rust程式碼呼叫C程式碼的add
函式,然後列印預出結果。
Rust 和 C 的混合開發
建立 Makefile
首先,建立一個 Makefile 來管理專案的編譯和連結過程。Makefile 的內容如下:
build:
cargo build
clang main.c -L ./target/debug -lrust_in_c -o main
這個 Makefile 定義了一個 build
指令,該指令先使用 cargo build
編譯 Rust 程式碼,然後使用 clang
編譯 C 程式碼,並連結 Rust 程式碼生成的動態函式庫。
C 程式碼
接下來,編寫 C 程式碼 main.c
。這個程式碼將使用 Rust 程式碼生成的動態函式庫來計算兩個檔案之間的字元差異。
#include <stdlib.h>
#include <stdio.h>
int diff_strings(char* s1, char* s2);
char* read_file(char* path);
int main() {
char* cargoToml = read_file("Cargo.toml");
char* libRs = read_file("src/lib.rs");
printf("Result = %d characters", diff_strings(cargoToml, libRs));
free((void*)cargoToml);
free((void*)libRs);
return 0;
}
這個 C 程式碼定義了兩個函式 diff_strings
和 read_file
,並使用這些函式來計算兩個檔案之間的字元差異。
Rust 程式碼
接下來,編寫 Rust 程式碼 src/lib.rs
。這個程式碼將提供兩個函式 diff_strings
和 read_file
,這些函式將被 C 程式碼呼叫。
use std::ffi::{c_char, c_int, CStr, CString};
use std::fs::read_to_string;
#[no_mangle]
pub extern "C" fn diff_strings(s1: *mut c_char, s2: *mut c_char) -> c_int {
unsafe {
let s1_cstr = CStr::from_ptr(s1);
let s1_str = s1_cstr.to_str().unwrap();
let s1_len = s1_str.len();
let s2_cstr = CStr::from_ptr(s2);
let s2_str = s2_cstr.to_str().unwrap();
let s2_len = s2_str.len();
s2_len.abs_diff(s1_len) as c_int
}
}
#[no_mangle]
pub extern "C" fn read_file(path: *mut c_char) -> *mut c_char {
// ...
}
這個 Rust 程式碼定義了兩個函式 diff_strings
和 read_file
,這些函式將被 C 程式碼呼叫。函式 diff_strings
計算兩個字串之間的字元差異,函式 read_file
讀取一個檔案的內容並傳回一個字串。
Cargo.toml
最後,編寫 Cargo.toml
檔案來組態 Rust 專案。
[lib]
crate-type = ["cdylib"]
name = "rust_in_c"
這個 Cargo.toml
檔案組態了 Rust 專案為動態函式庫,並命名為 rust_in_c
。
執行
使用 make build
指令編譯和連結專案,然後執行生成的 main
可執行檔案。
$ make build
$ ./main
這將輸出兩個檔案之間的字元差異。
混合語言開發:Rust 與 C 的整合
在本章中,我們將探討如何使用 Rust 進行混合語言開發,特別是與 C 的整合。Rust 提供了一種安全且高效的方式來進行系統程式設計,而 C 則是一種成熟且廣泛使用的語言。透過整合這兩種語言,我們可以發揮各自的優點,創造出更強大和更高效的應用程式。
使用 extern "C"
來定義 C 函式
要在 Rust 中使用 C 函式,我們需要使用 extern "C"
來定義它們。這個關鍵字告訴 Rust 編譯器,這個函式是使用 C 的呼叫約定來定義的。以下是一個例子:
#[no_mangle]
pub extern "C" fn read_file(path: *mut c_char) -> *mut c_char {
// ...
}
在這個例子中,我們定義了一個名為 read_file
的 C 函式,它接受一個 *mut c_char
型別的引數 path
,並傳回一個 *mut c_char
型別的值。
使用 CStr
來轉換 C 字串
要在 Rust 中使用 C 字串,我們需要使用 CStr
來轉換它們。以下是一個例子:
use std::ffi::CStr;
let path_cstr = CStr::from_ptr(path);
let path_str = path_cstr.to_str().unwrap();
在這個例子中,我們使用 CStr
來轉換一個 C 字串 path
成為一個 Rust 字串 path_str
。
使用 CString
來建立 C 字串
要在 Rust 中建立一個 C 字串,我們需要使用 CString
。以下是一個例子:
use std::ffi::CString;
let content_cstr = CString::new(content).unwrap();
在這個例子中,我們使用 CString
來建立一個 C 字串 content_cstr
。
使用 into_raw
來傳回 C 字串
要傳回一個 C 字串,我們需要使用 into_raw
來轉換它成一個 *mut c_char
型別的指標。以下是一個例子:
content_cstr.into_raw()
在這個例子中,我們使用 into_raw
來轉換一個 CString
成為一個 *mut c_char
型別的指標。
執行程式
要執行程式,我們需要使用 cargo build
來編譯 Rust 程式碼,然後使用 clang
來編譯 C 程式碼。以下是一個例子:
$ make build
cargo build
Finished dev [unoptimized + debuginfo] target(s) in 0.00s
clang main.c -L ./target/debug -lrust_in_c -o main
在這個例子中,我們使用 cargo build
來編譯 Rust 程式碼,然後使用 clang
來編譯 C 程式碼。
重點
- 使用
extern "C"
來定義 C 函式 - 使用
CStr
來轉換 C 字串 - 使用
CString
來建立 C 字串 - 使用
into_raw
來傳回 C 字串 - 使用
cargo build
來編譯 Rust 程式碼 - 使用
clang
來編譯 C 程式碼
練習
- 實作一個使用
extern "C"
來定義 C 函式的 Rust 程式 - 實作一個使用
CStr
來轉換 C 字串的 Rust 程式 - 實作一個使用
CString
來建立 C 字串的 Rust 程式 - 實作一個使用
into_raw
來傳回 C 字串的 Rust 程式
圖表翻譯:
graph LR A[Rust] -->|extern "C"|> B[C 函式] B -->|CStr|> C[Rust 字串] C -->|CString|> D[C 字串] D -->|into_raw|> E[*mut c_char] E -->|傳回|> F[C 程式]
在這個圖表中,我們展示瞭如何使用 extern "C"
來定義 C 函式,使用 CStr
來轉換 C 字串,使用 CString
來建立 C 字串,使用 into_raw
來傳回 C 字串。
實作簡化版的 Atomic Reference Counter (Arc)
1. 建立 Arc 結構體
首先,我們需要建立一個名為 Arc
的結構體,它將包含指向 ArcInner
的指標和一個標記,表明我們對內部值具有所有權。ArcInner
結構體將包含參照計數和內部資料。
use std::marker::PhantomData;
use std::ptr::NonNull;
use std::sync::atomic::{self, AtomicUsize, Ordering};
use std::ops::Deref;
// 原子參照計數器
pub struct Arc<T> {
// 指向 ArcInner 的指標
ptr: NonNull<ArcInner<T>>,
// 標記,表明我們對 ArcInner 具有所有權
phantom: PhantomData<ArcInner<T>>,
}
// ArcInner 結構體,包含參照計數和內部資料
pub struct ArcInner<T> {
// 參照計數
rc: AtomicUsize,
// 內部資料
data: T,
}
2. 實作 new
方法
接下來,我們需要實作 new
方法,該方法建立一個新的 Arc
例項。
impl<T> Arc<T> {
// 建立一個新的 Arc 例項
pub fn new(data: T) -> Arc<T> {
// 建立一個新的 ArcInner 例項,參照計數為 1
let boxed = Box::new(ArcInner {
rc: AtomicUsize::new(1),
data,
});
// 建立一個新的 NonNull 指標,指向 ArcInner
let ptr = NonNull::new(boxed.as_ref()).unwrap();
// 將 ArcInner 轉換為 raw 指標,並建立 Arc 例項
let arc = Arc {
ptr,
phantom: PhantomData,
};
// 傳回 Arc 例項
arc
}
}
3. 實作 clone
方法
現在,我們需要實作 clone
方法,該方法建立一個新的 Arc
例項,並增加參照計數。
impl<T> Clone for Arc<T> {
fn clone(&self) -> Self {
// 增加參照計數
self.ptr.as_ref().rc.fetch_add(1, Ordering::SeqCst);
// 建立一個新的 Arc 例項,指向相同的 ArcInner
Arc {
ptr: self.ptr,
phantom: PhantomData,
}
}
}
4. 實作 drop
方法
最後,我們需要實作 drop
方法,該方法減少參照計數,並在參照計數為 0 時釋放內部資料。
impl<T> Drop for Arc<T> {
fn drop(&mut self) {
// 減少參照計數
let count = self.ptr.as_ref().rc.fetch_sub(1, Ordering::SeqCst);
// 如果參照計數為 0,釋放內部資料
if count == 1 {
let _ = unsafe { Box::from_raw(self.ptr.as_ptr()) };
}
}
}
5. 實作 Deref
特徵
為了能夠使用 *
來存取內部資料,我們需要實作 Deref
特徵。
impl<T> Deref for Arc<T> {
type Target = T;
fn deref(&self) -> &Self::Target {
&self.ptr.as_ref().data
}
}
現在,你可以使用簡化版的 Arc
來管理分享資料了。這個實作提供了基本的參照計數和內部資料存取功能,但它並不如標準函式庫中的 Arc
那樣強大和安全。
實作原子參考計數器
為了實作一個原子參考計數器(Atomic Reference Counter, Arc),我們需要定義一個結構體來儲存指向內部資料的指標和參考計數。以下是 Arc 的基本實作:
use std::marker::PhantomData;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc as StdArc;
// 定義 ArcInner 結構體
struct ArcInner<T> {
data: T,
rc: AtomicUsize,
}
// 定義 Arc 結構體
pub struct Arc<T> {
ptr: std::ptr::NonNull<ArcInner<T>>,
phantom: PhantomData<T>,
}
impl<T> Arc<T> {
// 建立一個新的 Arc 例項
pub fn new(data: T) -> Self {
let inner = Box::new(ArcInner {
data,
rc: AtomicUsize::new(1),
});
let ptr = std::ptr::NonNull::new(Box::into_raw(inner)).unwrap();
Arc {
ptr,
phantom: PhantomData,
}
}
// 取得 Arc 的參考計數
pub fn count(&self) -> usize {
let inner = unsafe { self.ptr.as_ref() };
inner.rc.load(Ordering::Acquire)
}
// 克隆 Arc
pub fn clone(&self) -> Self {
let inner = unsafe { self.ptr.as_ref() };
let rc = inner.rc.fetch_add(1, Ordering::Relaxed);
if rc == usize::MAX {
panic!("Reference count overflow");
}
Arc {
ptr: self.ptr,
phantom: PhantomData,
}
}
}
// 實作 Deref 特徵
impl<T> std::ops::Deref for Arc<T> {
type Target = T;
fn deref(&self) -> &Self::Target {
let inner = unsafe { self.ptr.as_ref() };
&inner.data
}
}
// 實作 Send 和 Sync 特徵
unsafe impl<T: Sync + Send> Send for Arc<T> {}
unsafe impl<T: Sync + Send> Sync for Arc<T> {}
這個實作提供了基本的 Arc 功能,包括建立、克隆和取得參考計數。它使用 std::sync::atomic
來實作原子操作,確保參考計數的更新是執行緒安全的。同時,它也實作了 Deref
特徵,允許使用 *
運算子來取得 Arc 內的資料。最後,它實作了 Send
和 Sync
特徵,允許 Arc 在多執行緒環境中安全地分享和傳遞。
圖表翻譯:
classDiagram class Arc~T~ { - ptr: std::ptr::NonNull~ArcInner~T~~ - phantom: PhantomData~T~ + new(data: T) Arc~T~ + count() usize + clone() Arc~T~ } class ArcInner~T~ { - data: T - rc: AtomicUsize } Arc~T~ --* ArcInner~T~
這個圖表展示了 Arc 和 ArcInner 的關係,Arc 內部包含一個指向 ArcInner 的指標和一個 PhantomData 例項。ArcInner 則包含了實際的資料和參考計數。這個圖表有助於理解 Arc 的內部結構和其與 ArcInner 的關係。
從底層實作到高階應用的全面檢視顯示,Rust 與 C 的混合開發,本質上是透過外部函式介面 (FFI) 實作兩種語言間的溝通橋樑。bindgen 工具的運用,自動化產生 Rust 繫結,大幅簡化了整合 C 程式碼的複雜度,讓開發者得以更專注於業務邏輯的開發。分析 C 程式碼與 Rust 程式碼的互動模式,可以發現,透過 extern "C"
宣告以及 CStr
、CString
等工具的輔助,Rust 能夠有效地操作 C 的資料結構,實作底層效能與高階安全性的巧妙平衡。然而,FFI 的使用也存在潛在風險,例如記憶體管理的責任轉移,需要開發者更謹慎地處理指標和記憶體釋放,避免出現記憶體洩漏或 dangling pointer 等問題。展望未來,隨著 Rust 語言的日漸成熟以及社群的蓬勃發展,預期將會有更多工具和最佳實務出現,進一步降低混合語言開發的門檻。對於追求效能與安全的系統級開發,Rust 與 C 的整合將成為主流趨勢,值得技術團隊深入研究並積極應用於實務專案中。玄貓認為,掌握 Rust 與 C 混合開發的技巧,將賦予開發者更大的彈性,在效能、安全性和開發效率之間取得最佳平衡。