在開發應用程式的時候,常會需要在程式加入圖片、聲音等等的素材。以開發Web應用程式來說,還會需要用到很多HTML模板、CSS、JavaScript、語言檔、資料庫設定檔等等的素材檔案。這些素材檔案常因為放不進程式執行檔中,而只好另外放在檔案系統的某個目錄下,由程式在執行階段的時候再去讀取。像這樣程式和素材分開的存放的作法雖然很常見,但其實會引發許多問題。例如,在設定程式執行環境的時候,工序會變得複雜,系統管理員必須要按照程式的說明文件,將指定的素材放置到指定的路徑之下。又或是把暴露在執行檔之外的素材,交付給客戶使用之後,難保他們不會將那些素材外流或是另作它用。講難聽一點就是有可能會被盜圖、盜設計排版、盜JS程式、盜資料庫,素材被他人盜用在其它的專案上。又或是一樣的程式,只不過把外部素材改一改、換一換之後,就被他人當成是自己做的應用程式來賣了。



那麼,Rust程式語言要如何把外部檔案素材給匯入至程式內呢?

include_bytes

Rust程式語言內建include_bytes!巨集,可以傳入一個檔案路徑,編譯器在編譯程式的時候就會去讀取該檔案,並將該檔案內容直接轉成u8陣列,以&'static [u8]的型別回傳。

fn main() {
    let file_data = include_bytes!("/path/to/file"); // &'static [u8]

    // do something ...
}

include_str

Rust程式語言內建include_str!巨集,類似include_bytes!巨集,同樣可以傳入一個檔案路徑,編譯器也會在編譯程式的時候去讀取該檔案。不同的地方在於「include_str」巨集會將檔案以UTF-8編碼成Rust程式語言的字串,以&'static str的型別回傳。

fn main() {
    let file_text = include_str!("/path/to/file"); // &'static str

    // do something ...
}

include_bytes!include_str!的相對路徑

include_bytes!include_str!巨集所輸入的路徑可以使用絕對路徑或是相對路徑。相對路徑是相對於使用include_bytes!include_str!巨集的.rs原始碼檔案的路徑,舉例來說,如果原始碼檔案路徑為/path/to/file.rs,則include_bytes!("123.jpg")所使用到的檔案為/path/to/123.jpginclude_bytes!("../123.jpg")所使用到的檔案為/path/123.jpg

若是覺得路徑相對於.rs原始碼檔案的路徑在使用上會不太方便的話,也可以配合concat!env!巨集,將外部素材的路徑指定到Cargo程式專案的根目錄或根目錄的子目錄下。

fn main() {
    let file_data = include_bytes!(concat!(env!("CARGO_MANIFEST_DIR"), "/", "path/to/file"));

    // do something ...
}

如以上程式,就會去讀取Cargo程式專案的根目錄下的path/to/file這個檔案。

include

Rust程式語言另外還有內建一個include!巨集,這個巨集的使用方式跟include_bytes!include_str!差不多,只不過它是直接把檔案內容作為程式碼使用。如果您的外部素材是一個符合Rust程式語言語法的陣列,或許可以使用這個include!巨集。但是在大部分的情況下,應該要儘量少用它。

["Hello!", "Rust!"]
fn main() {
    let array = include!("/path/to/file_array");

    // do something ...
}

能解決編譯時間增加的套件──Lazy Static Include

將外部素材與程式一同編譯,除了會使讓編譯出來的檔案體積增大外,還會大大增加編譯所需時間,使得開發效率下降。為了有效解決這個問題,筆者開發了一個「Lazy Static Include」套件,以lazy_static_include_bytes!lazy_static_include_str!lazy_static_include_array!巨集來取代內建的include_bytes!include_str!include!(用於陣列時)巨集。

顧名思義,這個套件利用Lazy Load的機制,在程式執行階段使用到外部素材檔案的時候,才會去讀取檔案。但是當我們使用release模式來編譯Cargo程式專案的時候,這個套件就會把外部素材檔案和Rust程式編譯在一起。換句話說,就算我們正式發行程式的時候外部素材檔案和Rust程式是編譯在一起的,但是在開發階段(非release模式時),由於是採用Lazy Load來讀取檔案,因此不會影響到編譯時間,開發效率自然也就不會下降啦!

Crates.io

Cargo.toml

lazy-static-include = "*"

巨集的使用

lazy_static_include_bytes!lazy_static_include_str!的用法差不多,以逗號分隔多個靜態變數名稱,並於每個靜態變數後面使用=>語法來傳入要讀取的檔案路徑,路徑可以用字串定數或是「元祖(tuple)」來表示。每個靜態變數名稱前可以加上pub關鍵字來進行可見度的修飾,pub關鍵字前可以加上文件註解或是其它屬性。

例如:

use lazy_static_include::*;

lazy_static_include_str! {
    /// doc
    TEST => "data/test.txt",
    /// doc
    pub TEST2 => ("data", "test-2.txt"),
}

assert_eq!("This is just a test text.", TEST);
assert_eq!("Some text...", TEST2);

lazy_static_include_bytes!lazy_static_include_str!巨集定義出來的靜態變數,其型別是lazy_static的智慧型指標,如果要獲得&'static [u8]或是&'static str型別,可以使用*(星號)進行「解參考」的動作。

use lazy_static_include::*;

lazy_static_include_bytes! {
    /// doc
    TEST => "data/test.txt",
}

let data: &'static [u8] = *TEST;
include_array

lazy_static_include_array巨集的話,其實也是跟include巨集一樣不太建議使用。第一個參數傳入靜態變數的名稱和陣列型別(以冒號:分隔),第二個參數傳入要讀取的檔案路徑,路徑會相對於Cargo程式專案的根目錄。如果靜態變數是公開的(public),可以在靜態變數名稱前加上pub關鍵字。

例如:

[123, 456, 789, 1_000, 500000000000u64]
["Hi", "Hello", "哈囉"]
use lazy_static_include::*;

lazy_static_include_array! {
    /// doc
    TEST: [u64; 5] => "data/u64_array.txt",
    /// doc
    pub TEST2: [&'static str; 3] => ("data", "string_array.txt")
}

assert_eq!(123, TEST[0]);
assert_eq!(456, TEST[1]);
assert_eq!(789, TEST[2]);
assert_eq!(1000, TEST[3]);
assert_eq!(500000000000u64, TEST[4]);

assert_eq!("Hi", TEST2[0]);
assert_eq!("Hello", TEST2[1]);
assert_eq!("哈囉", TEST2[2]);