在預設的情況下,Rust程式都會與標準函式庫和C函式庫作連結,所以我們可以輕易地讓Rust程式能夠使用作業系統所提供的執行緒、檔案、網路等等的功能。但是這樣就會有個問題,那就是我們的程式會變得必需依賴於執行在某些特定的作業系統上,而無法獨立運作在無作業系統的環境中。



不使用標準函式庫的Rust函式庫

將一個Rust函式庫設定為不使用標準函式庫的方式很簡單,只要在Cargo函式庫程式專案根目錄下的src/lib.rs檔案中,加上#![no_std]屬性即可。

例如:

#![no_std]

pub fn add_two(n: i32) -> i32 {
    n + 2
}

加上#![no_std]屬性的Rust函式庫內就無法去使用std這個crate下的所有功能,其中包括方便的format!println!等巨集,甚至連StringVec等用到堆積空間的結構體都會沒辦法使用。

alloc

alloc是Rust 1.36後正式開放的內建crate,可以用來替代原本標準函式庫內提供的用來操作堆積空間相關的結構。例如BoxStringVecHashMap等。

例如:

#![no_std]

pub fn separate_chars(s: String) -> Vec<char> {
    s.chars().collect()
}

以上程式會無法編譯,因為現在的程式scope中並沒有引用StringVec結構體。

但是我們可以用alloc來解決這個問題,如下:

#![no_std]

extern crate alloc;

use alloc::string::String;
use alloc::vec::Vec;

pub fn separate_chars(s: String) -> Vec<char> {
    s.chars().collect()
}

alloc也有提供vec!format!巨集,可以透過在extern crate關鍵字的左方或上方加上#[marco_use]屬性,或是直接使用路徑來呼叫。

例如:

#![no_std]

#[macro_use]
extern crate alloc;

use alloc::string::String;
use alloc::vec::Vec;


pub fn get_greeting_strings(target: &str) -> Vec<String> {
    vec![alloc::format!("Hello, {}!", target), format!("Hi, {}!", target)]
}

no_std作為可選的特色

如果我們想要讓我們的Rust函式庫可以在使用標準函式庫的時候提供完整功能,而在不使用標準函式庫的情況下也可以提供部份功能的話。可以在Cargo.toml設定檔中加入[features]區塊,加入一個no_std設定項目(特色名稱其實可以隨意,但通常會取名叫no_std)。如下:

[features]
no_std = []

接著將原先寫在src/lib.rs內的#![no_std]屬性改為#![cfg_attr(feature = "no_std", no_std)]屬性,如下:

#![cfg_attr(feature = "no_std", no_std)]

pub fn add_two(n: i32) -> i32 {
    n + 2
}

至於如果有用到alloc這個crate的話,不需要像以下這樣把alloc相關的項目也加上#[cfg(feature = "no_std")]屬性,因為std其實也是去使用alloc。像是std::vec::Vecalloc::vec::Vec就是同樣的結構體。

#![cfg_attr(feature = "no_std", no_std)]

#[cfg(feature = "no_std")]
extern crate alloc;

#[cfg(feature = "no_std")]
use alloc::string::String;
#[cfg(feature = "no_std")]
use alloc::vec::Vec;


pub fn separate_chars(s: String) -> Vec<char> {
    s.chars().collect()
}

不使用標準函式庫的Rust應用程式

雖然Rust的函式庫可以輕易地不去使用標準函式庫,但是Rust的可執行應用程式就沒有那麼容易了。

舉例來說,我們可以用以下指令建立一個專案名稱叫hello的Cargo應用程式新專案:

cargo new --bin hello

然後直接使用cargo build指令編譯出應用程式執行檔:

cargo build

接著用ldd指令查看執行檔動態連結的函式庫:

ldd target/debug/hello

rust-no-std

我們可以看到,就連最簡單的「Hello World」程式也用到了很多作業系統提供的函式庫。

為了使Rust的應用程式能不相依於標準函式庫,我們必須做以下幾件事。

src/main.rs檔案中加上#![no_std]屬性

我們也可以在src/main.rs檔案中加上#![no_std]屬性,讓該Rust應用程式不去使用標準函式庫。例如把剛才建立出來的hello應用程式專案中的src/main.rs檔案,修改成:

#![no_std]

fn main() {
    println!("Hello, world!");
}

然後嘗試使用cargo build指令編譯出應用程式執行檔:

cargo build

rust-no-std

會發現程式專案無法成功編譯了!其中一個原因是println!這個巨集並沒有被定義,因為它原本就是屬於標準函式庫的功能,而我們的Rust程式卻已經宣告不使用標準函式庫了,自然就無法找到println!巨集。我們可以先嘗試把println!這個巨集的程式敘述移除,只留下空的main函數,

#![no_std]

fn main() {
}

再次使用cargo build指令:

cargo build

rust-no-std

會發現程式專案還是無法成功編譯,這是因為Rust的執行檔還會自動用到「panic」等程式執行失敗時的處理機制。而Rust的「panic」機制屬於「可插件化的」(pluggable)的功能,稱為「language item」,簡稱為「lang items」,中譯為「語言項目」。語言項目提供了介面,能把一些功能從Rust程式語言中切割出來,在函式庫內完成實作,有許多Rust的內建功能都是在標準函式庫中實作出來的。所以當我們禁用標準函式庫的時候,存在於標準函式庫內的「panic」等相關實作就無法被使用了。不過通常我們不會自己去實作語言項目,因為語言項目的介面大部份不穩定(需用#[lang = "item_name"]屬性來實作的語言項目是不穩定的,而且要用Rust Nightly版本才能編譯),編譯器甚至不會對它做型別檢查。

那我們不自行實作「panic」相關的語言項目的話,還有什麼方式可以讓這個程式正常編譯呢?

關閉堆疊解開(Stack Unwinding)

先前介紹過Rust錯誤處理的方式,在預設的情況下,當Rust程式發生panic時,會對那個時間點的堆疊資料進行「解開」的動作,這個動作會需要eh_personality這個語言項目的支援。因此為了要讓Rust程式在panic時不「解開」堆疊,我們可以在Cargo.toml設定檔中,加入[profile.dev][profile.release]區塊,並將panic設定項目的值設為abort。如下:

[profile.dev]
panic = "abort"

[profile.release]
panic = "abort"

如此一來不論有沒有使用「release」模式來編譯程式專案,程式在panic時都不會去「解開」堆疊。

實作Panic

其實我們還是得實作「panic」相關的語言項目,不過還好除去堆疊解開的功能,剩下來的部份已經有穩定的介面了。將某個參數只有一個&PanicInfo的函數加上#[panic_handler]屬性,可以用來實作panic_impl這個語言項目。這個函數即為程式在panic時會執行的函數。

如下:

#![no_std]

use core::panic::PanicInfo;

#[panic_handler]
fn panic(_info: &PanicInfo) -> ! {
    loop {}
}

fn main() {
}

以上程式,我們讓程式在panic時進入無窮迴圈。

此時再嘗試使用cargo build指令編譯出應用程式執行檔:

cargo build

rust-no-std

會發現程式專案還是無法成功編譯,編譯器提示需要使用start這個語言項目。

start語言項目

在介紹Rust程式語言時,總會提到它幾乎沒有Runtime或是擁有非常小的Runtime,這個是在指什麼?其實就是start語言項目啦!當Rust程式被作業系統執行的時候,會先去啟動「crt0」(C Runtime zero),這個是初始化C語言應用程式執行環境(如建立堆疊和初始化暫存器)的程式,當執行環境建立好後就會去啟動Rust的Runtime(start語言項目),Rust的Runtime會保護堆疊有無溢出、命令參數的儲存以及panic時堆疊的「解開」功能。當一切該初始化的東西都都初始化完後,Rust的Runtime就會去呼叫main函數,也就是一般我們在撰寫Rust應用程式時的程式進入點。

由於我們在程式碼加上了#![no_std]屬性,因此Rust編譯器並不會使用標準函式庫中的start語言項目的實作。不過start語言項目並沒有穩定的實作介面,所以我們在此就不去實作start語言項目了。

為了要讓Rust應用程式在沒有start語言項目的情況下也能夠被執行,我們必須要自行決定Rust程式的程式進入點,所以要先在程式碼另外加上#![no_main]屬性,不去使用「crt0」。如下:

#![no_std]
#![no_main]

use core::panic::PanicInfo;

#[panic_handler]
fn panic(_info: &PanicInfo) -> ! {
    loop {}
}

fn main() {
}

實作程式進入點

不同的作業系統在執行程式的時候,所使用的程式進入點並不一定相同。好比說Linux作業系統在執行程式的時候,會先去呼叫程式的_start函數。我們可以將程式修改如下:

#![no_std]
#![no_main]

use core::panic::PanicInfo;

#[panic_handler]
fn panic(_info: &PanicInfo) -> ! {
    loop {}
}

#[no_mangle]
pub extern fn _start() -> ! {
    loop {}
}

以上程式第11行到第14行,我們替Rust程式加上Linux作業系統的程式進入點,執行結果就是卡在一個無窮迴圈裡。加上pub extern關鍵字和#[no_mangle]屬性是為了要保留函數名稱,確保作業系統可以呼叫得到。

編譯執行檔

嘗試使用cargo build指令編譯出應用程式執行檔:

cargo build

rust-no-std

會發現程式專案還是無法成功編譯,這是因為Rust的編譯器預設會去使用「crt0」(C runtime zero)作為程式進入點,但我們在程式碼內使用#![no_std]屬性,因此程式無法跟C語言的函式庫成功作連結(link)。為了讓Rust的編譯器不使用「crt0」,在編譯的時候就不能用cargo build來編譯,而是要改用cargo rustc來呼叫並調整Rust編譯器(也就是rustc)在連結階段所使用的參數。指令如下:

cargo rustc -- -C link-arg=-nostartfiles

以上指令,利用了rustc-C參數來將連結參數link-arg設定為-nostartfiles,也就是告訴Rust編譯器不要去使用「crt0」。

如此一來就能夠成功編譯了!

rust-no-std