作為新穎、先進的程式語言,Rust的函式庫還沒有C/C++語言的函式庫來得多且完整。在很多時候,我們還是無可避免地必須要去使用現有C/C++程式語言所實作的函式庫來完成我們需要的功能。雖然一般來說,我們還是會比較喜歡用純Rust程式碼來開發程式,確保程式的安全性以及可移植性,但畢竟要把過去每個C/C++程式語言所實作的函式庫,都使用Rust程式語言來改寫,是非常不切實際的事。在開發成本的考量之下,直接讓Rust程式與C/C++程式語言作連結,就變成是一種很常見的折衷作法。
建置腳本(build.rs)
類似於C/C++程式語言程式專案常使用的「configure」,Rust的Cargo程式專案也提供了「建置腳本」的功能,可以讓程式專案在編譯之前,先編譯與執行某個Rust原始碼檔案來設定程式專案的編譯環境。這個「建置腳本」預設會是程式專案根目錄底下的build.rs
,如果想要更改路徑的話,可以在Cargo.toml
設定檔案中的[package]
區塊,添加build
設定項目。例如:
[package]
name = "example"
version = "0.1.0"
edition = "2021"
build = "src/build.rs"
[dependencies]
以上設定檔將作為建置腳本的Rust原始碼檔案路徑設定在程式專案根目錄中的src
目錄下,檔名為build.rs
。
建置腳本的撰寫方式可以參考Cargo的官方手冊,網址如下:
底下先舉一個最簡單的例子,來說明如何使用Rust程式語言呼叫C/C++語言的函式庫。
Rust + C語言的Hello World
首先,使用以下指令建立出名叫c-hello-world
的Cargo應用程式專案:
在程式專案的根目錄中,新增一個hello-world
目錄,並在該目錄中新增一個hello.c
檔,檔案內容如下:
#include <stdio.h>
void greet() {
printf("Hello world!\n");
}
在hello.c
中,我們引入了stdio.h
標頭檔,並在greet
函數中使用printf
函數來印出Hello world!
字串。
接著,編輯程式專案根目錄中src
目錄內的main.rs
檔,檔案內容如下:
#[link(name = "hello-world")]
extern "C" {
fn greet();
}
fn main() {
unsafe {
greet();
}
}
在extern "C"
關鍵字所組成的程式區塊中內,以Rust程式語言定義函數的語法來對應C/C++語言程式的函數,在程式區塊上方使用#[link(name = "hello-world")]
屬性,其中的hello-world
表示要讓這個程式區塊定義的函數連結到libhello-world
這個函式庫。當然,我們現在還沒有libhello-world
這個東西,所以此時的程式專案是無法成功建置的(但是可以通過編譯檢查)。
在Rust程式中呼叫外部函式庫的函數,必須要在不安全的模式下進行,因此main
函數呼叫greet
函數時,還另外使用了unsafe
關鍵字。
再來,在程式專案的根目錄中,新增一個build.rs
檔案,這個檔案就是我們的建置腳本啦!檔案內容如下:
use std::{env, process::Command};
fn main() {
let out_dir = env::var("OUT_DIR").unwrap();
Command::new("cc")
.args(["hello-world/hello.c", "-O3", "-c", "-fPIC", "-o"])
.arg(&format!("{out_dir}/hello-world.o"))
.status()
.unwrap();
Command::new("ar")
.args(["crus", "libhello-world.a", "hello-world.o"])
.current_dir(out_dir.as_str())
.status()
.unwrap();
println!("cargo:rustc-link-search=native={out_dir}");
println!("cargo:rustc-link-lib=static=hello-world");
}
以上程式,第4行會去取得OUT_DIR
環境變數的值,這個環境變數是Cargo程式專案自帶的,而它的值是建構專案時輸出檔案的儲存路徑。第6行到第10行,其實就如同在程式專案根目錄下,執行了以下指令:
cc hello-world/hello.c -O3 -c -fPIC -o "$OUT_DIR/hello-world.o"
參數-O3
表示要進行最佳化。參數-c
表示讓C/C++編譯器不要進行「link」的動作,因為我們要讓它作為函式庫來使用。參數-fPIC
是要開啟「Position-Independent Code」功能,也是為了要讓程式能夠作為函式庫來使用。參數-o
則是設定目的檔(object file)要輸出的位置。
程式第12行到第16行,其實就如同在OUT_DIR
目錄下,執行了以下指令:
ar crus libhello-world.a hello-world.o
ar crus
指令可以將多個目的檔封裝成單一個.a
靜態函式庫檔案。在此會把hello-world.o
封裝成libhello-world.a
。
程式第18行,設定程式專案在編譯時會去OUT_DIR
目錄尋找原生函式庫,有點類似C/C++編譯器的-L
參數。
程式第19行,設定程式專案以靜態的方式來引用hello-world
這個函式庫(會去尋找libhello-world.a
檔案),有點類似在C/C++編譯器加上-lhello-world
參數。引用了hello-world
函式庫之後,在Rust程式中就可以使用#[link(name = "hello-world")]
屬性和extern
關鍵字來對應C/C++的函數。
此時使用cargo run
來執行程式專案的話,Cargo在編譯程式專案前,會先去編譯並執行專案根目錄下的build.rs
,所以C語言的libhello-world.a
靜態函式庫檔案會先被產生出來,然後在編譯程式專案時會引用這個libhello-world.a
靜態函式庫檔案,將greet
函數的功能帶進Rust程式中。程式執行的時候就可以看到螢幕印出Hello world!
字串啦!
Rust型別和C/C++型別的轉換
如果我們需要在Rust程式和C/C++程式間共享記憶體中的變數資料,就必須要透過FFI(Foreign Function Interface,外部函數介面)來完成。
稍微改寫一下剛才寫的Hello World程式來舉例吧!
#include <stdint.h>
#include <stdio.h>
void greet(char* s, int32_t a, int32_t b) {
printf("Hello %s! %d + %d = %d\n", s, a, b, a + b);
}
use std::ffi::CString;
#[link(name = "hello-world")]
extern {
fn greet(s: *const std::os::raw::c_char, a: i32, b: i32);
}
fn main() {
let prompt = CString::new("Rust").unwrap();
unsafe {
greet(prompt.as_ptr(), 7, 11);
}
}
使用cargo run
來執行程式專案,程式會輸出:
Rust的聯合體(union)
Rust程式語言其實是有提供的聯合體的,只是在純Rust程式中並不會去用到這個。但是在C/C++上還蠻常看到聯合體的。Rust的聯合體可以用union
關鍵字來定義,例如:
union MyUnion {
f1: u32,
f2: f32,
}
這個MyUnion
聯合的大小為32位元,它可以儲存一個32位元的無號整數或是一個32位元的浮點數。
Rust的聯合體也可以實作函數、方法和特性。此外,聯合體的欄位型別,一定要有實作Copy
特性。
我們可以用以下方式來建立出MyUnion
聯合的實體:
let u1 = MyUnion {
f1: 1
};
let u2 = MyUnion {
f2: 2.5
};
修改MyUnion
聯合實體的值的方式如下:
let mut u = MyUnion {
f1: 1
};
u.f2 = 2.5;
取得MyUnion
聯合實體的值時必須要使用unsafe
關鍵字來進入不安全模式。如下:
let u = MyUnion {
f1: 1
};
println!("{}", unsafe { u.f1 });
聯合也可以拿來做型樣匹配,當然,這也是要在不安全模式下進行。如下:
let u = MyUnion {
f1: 10
};
unsafe {
match u {
MyUnion {
f1: 10,
} => {
println!("ten");
},
MyUnion {
f2,
} => {
println!("{f2}");
},
}
}
在做聯合的型樣匹配時,要注意到樣本中的欄位名稱,只是給編譯器對應型別用的,在程式執行階段,並無法得知當前的聯合到底是用哪個欄位的型別,因此如果第一個match
關鍵字裡的arm沒有做任何的條件限定的話,就會永遠被匹配成功。