返回文章列表

Rust 巨集基礎與進階應用

Rust 巨集系統提供強大的編譯期程式碼生成能力,簡化重複性工作並提升程式碼靈活性。本文探討巨集定義、展開原理、內建巨集、除錯技巧以及 `vec!`、`json!` 等巨集的實作細節,剖析巨集的模式匹配、Token 流操作、遞迴呼叫等核心概念,並提供開發複雜巨集的實務建議。

程式語言 Rust

Rust 巨集系統在編譯期根據模式匹配生成程式碼,實作比函式更靈活的抽象。macro_rules! 巨集定義方式,能將輸入的 Token 流轉換成 Rust 程式碼,例如 assert_eq! 巨集。Rust 編譯器早期展開巨集,透過模式匹配機制操作 Token,確保輸入符合預期格式。assert_eq! 使用區域性變數避免重複評估表示式帶來的副作用,確保程式碼行為符合預期。巨集範本生成最終程式碼,需注意片段型別使用,避免編譯錯誤。Rust 提供 file!line!column!stringify!concat! 等內建巨集輔助開發。除錯巨集可使用 cargo build --verboselog_syntax!trace_macros! 等工具。json! 巨集示範如何逐步構建複雜巨集,處理不同 JSON 資料型別並支援巢狀結構。

Rust 巨集(Macros)基礎與進階應用

Rust 的巨集系統是一種強大的工具,能夠在編譯期生成程式碼,簡化重複性工作。巨集透過模式匹配來生成程式碼,提供了比函式更靈活的抽象能力。

巨集的基本定義與使用

Rust 主要透過 macro_rules! 來定義巨集,這種方式根據模式匹配,能夠將輸入的 Token 流轉換成輸出的 Rust 程式碼。舉例來說,assert_eq! 巨集的定義如下:

macro_rules! assert_eq {
    ($left:expr, $right:expr) => {
        match (&$left, &$right) {
            (left_val, right_val) => {
                if !(*left_val == *right_val) {
                    panic!("assertion failed: `(left == right)` \
                            (left: `{:?}`, right: `{:?}`)", left_val, right_val)
                }
            }
        }
    };
}

內容解密:

  1. macro_rules! 定義巨集:這是 Rust 定義巨集的主要方式,透過模式匹配來生成程式碼。
  2. $left:expr$right:expr:這兩個是模式匹配中的變數,分別匹配輸入的兩個表示式。它們會被用於後續的範本生成中。
  3. match 陳述式:用於比較兩個表示式的值。這裡使用 match 是為了處理借用檢查,避免移動語義導致的問題。
  4. panic!:當斷言失敗時,觸發 panic! 並輸出詳細的錯誤資訊。

巨集展開的基本原理

Rust 在編譯期展開巨集,這個過程類別似於遞迴下降解析。編譯器讀取原始碼後,會逐步展開巨集呼叫,並將展開後的程式碼納入編譯流程中。這意味著巨集必須在使用前定義。

內容解密:

  1. 早期展開:Rust 編譯器在編譯早期階段就展開巨集,因此巨集必須在使用前定義。
  2. 模式匹配:巨集的模式匹配機制類別似於正規表示式,但操作物件是 Token 而非字元。
  3. Token 匹配:在 assert_eq! 中,逗號和表示式會被逐一匹配,確保輸入符合預期格式。

為何 assert_eq! 需要額外的變數

assert_eq! 的實作中,使用了 left_valright_val 變數來儲存匹配的結果,而不是直接使用 $left$right。這是為了避免多次評估輸入表示式可能帶來的副作用。

簡化版本的錯誤分析

if !($left == $right) {
    panic!("assertion failed: `(left == right)` \
            (left: `{:?}`, right: `{:?}`)", $left, $right)
}

內容解密:

  1. 多次評估問題:如果 $left$right 是具有副作用的表示式(如函式呼叫),直接使用會導致多次評估,破壞預期行為。
  2. 使用區域性變數:透過 match 將表示式的結果儲存到區域性變數,避免了多次評估的問題。

巨集範本與輸出

巨集範本負責生成最終的 Rust 程式碼,這些程式碼會被納入編譯流程。常見錯誤包括在範本中錯誤使用片段型別(如寫成 $left:expr 而非 $left),這會導致生成的程式碼無法編譯。

內容解密:

  1. 範本語法:巨集範本類別似於常見的範本語言,但輸出的是合法的 Rust 程式碼。
  2. 常見錯誤:在範本中誤用片段型別會導致編譯錯誤,需要仔細檢查巨集定義。

Rust 巨集(Macro)基礎與進階應用

Rust 的巨集是一種強大的工具,能夠在編譯時期生成程式碼。巨集的使用使得開發者能夠寫出更為通用、靈活的程式碼,並且能夠避免重複的程式碼撰寫。

巨集的基本原理

Rust 的巨集系統根據模式匹配(Pattern Matching)。當你呼叫一個巨集時,Rust 會將你的引數與巨集定義中的模式進行匹配。如果匹配成功,巨集就會根據定義的範本生成程式碼。

assert_eq! 巨集的實作

讓我們來看看 assert_eq! 巨集的實作。這個巨集用於檢查兩個表示式是否相等。如果不相等,它會觸發 panic!

macro_rules! assert_eq {
    ($left:expr, $right:expr) => ({
        match (&$left, &$right) {
            (left_val, right_val) => {
                if !(left_val == right_val) {
                    panic!("assertion failed: `(left == right)` \
                           (left: `{:?}`, right: `{:?}`)", left_val, right_val);
                }
            }
        }
    });
}

內容解密:

  1. macro_rules!: 用於定義一個巨集。
  2. $left:expr$right:expr: 這兩個是巨集的引數,分別代表左邊和右邊的表示式。:expr 表示它們是表示式。
  3. match (&$left, &$right): 對 $left$right 取參照,並將它們作為一個 tuple 進行模式匹配。這樣做是為了避免將值移動到巨集中。
  4. if !(left_val == right_val): 檢查兩個值是否相等。如果不相等,則觸發 panic!
  5. panic!: 當斷言失敗時,輸出錯誤訊息。錯誤訊息中包含了左右兩邊的值,以便於除錯。

為什麼使用參照而不是直接傳值?

如果直接傳值,當傳入的是某個變數時,該變數的值會被移動到巨集中,這可能會導致原始變數無法再被使用。因此,使用參照可以避免這種情況。

重複(Repetition)

Rust 的巨集系統支援重複語法,可以用來匹配零個或多個模式。例如,vec! 巨集就使用了這種語法。

macro_rules! vec {
    ($elem:expr ; $n:expr) => {
        ::std::vec::from_elem($elem, $n)
    };
    ( $( $x:expr ),* ) => {
        <[_]>::into_vec(Box::new([ $( $x ),* ]))
    };
    ( $( $x:expr ),+ ,) => {
        vec![ $( $x ),* ]
    };
}

內容解密:

  1. $elem:expr ; $n:expr: 第一個規則用於建立一個具有相同元素 $elem 的向量,重複 $n 次。
  2. ( $( $x:expr ),* ): 第二個規則用於建立一個向量,包含一系列以逗號分隔的表示式 $x
  3. $( ... ),*: 這是重複語法,用於匹配零個或多個以逗號分隔的表示式。
  4. <[_]>::into_vec(Box::new([ $( $x ),* ])): 將匹配到的表示式列表轉換成一個向量。

重複語法的詳細說明

重複語法可以根據不同的分隔符號(例如逗號或分號)和出現次數(零次或多次,或一次或多次)進行匹配。下表列出了可用的重複模式:

模式意義
$( ... )*匹配零次或多次,無分隔符號
$( ... ),*匹配零次或多次,以逗號分隔
$( ... );*匹配零次或多次,以分號分隔
$( ... )+匹配一次或多次,無分隔符號
$( ... ),+匹配一次或多次,以逗號分隔
$( ... );+匹配一次或多次,以分號分隔

透過使用這些重複模式,可以在巨集中處理任意長度的引數列表,從而實作更為靈活和通用的程式碼生成。

Rust 巨集系統深入解析:自定義巨集與內建巨集

Rust 的巨集系統是一種強大的元程式設計工具,允許開發者在編譯期生成程式碼。本文將探討 Rust 巨集的定義、內建巨集、除錯技巧以及如何開發複雜的巨集。

自定義巨集:以 vec! 為例

Rust 中的 vec! 巨集用於建立向量,其實作展示了巨集規則的強大功能。以下是一個簡化的 vec! 實作:

macro_rules! vec {
    ( $( $x:expr ),* ) => {
        {
            let mut temp_vec = Vec::new();
            $( temp_vec.push($x); )*
            temp_vec
        }
    };
    ( $( $x:expr ),+ ,) => {
        vec![ $( $x ),* ]
    };
}

內容解密:

  1. 第一條規則匹配逗號分隔的表示式列表,並為每個表示式生成對 push 方法的呼叫,將元素加入臨時向量。
  2. 第二條規則處理尾隨逗號的情況,透過遞迴呼叫 vec! 巨集去除多餘的逗號。

內建巨集

Rust 編譯器提供了多個內建巨集,無法使用 macro_rules! 實作,但對於自定義巨集非常有用:

  • file!(), line!(), column!():傳回當前檔案名稱、行號和列號。
  • stringify!(...tokens...):將給定的 token 轉換為字串常值。
  • concat!(str0, str1, ...):將多個字串常值連線成一個。

範例程式碼:

let version = env!("CARGO_PKG_VERSION");
const SHADER_CODE: &str = include_str!("shader.glsl");

內容解密:

  1. env! 巨集用於取得編譯環境變數,在此取得 crate 的版本號。
  2. include_str! 巨集將指設定檔案內容作為字串常值包含進來。

除錯巨集

除錯巨集可能具有挑戰性,因為 Rust 在報錯時不會顯示完全展開的程式碼。以下是幾種除錯工具:

  1. 使用 cargo build --verbose 結合 -Z unstable-options --pretty expanded 選項檢視完全展開的程式碼。
  2. 使用 log_syntax!() 巨集在編譯期列印資訊。
  3. 使用 trace_macros!(true) 開啟巨集呼叫追蹤。

範例程式碼:

#![feature(trace_macros)]
fn main() {
    trace_macros!(true);
    let numbers = vec![1, 2, 3];
    trace_macros!(false);
    println!("total: {}", numbers.iter().sum::<u64>());
}

內容解密:

  1. trace_macros! 巨集用於追蹤巨集展開過程,顯示每個巨集呼叫前後的程式碼變化。

開發複雜巨集:以 json! 為例

開發複雜巨集需要逐步構建並測試。json! 巨集展示瞭如何使用巨集規則建立 JSON 資料結構。

開發建議:

  1. 從簡單模式開始,逐步增加複雜度。
  2. 使用除錯工具檢查巨集展開結果。
  3. 確保巨集行為符合預期,特別是在錯誤處理方面。

透過本文的介紹,讀者應該對 Rust 的巨集系統有了更深入的理解,從自定義巨集到內建巨集,再到除錯技巧和複雜巨集的開發。合理利用這些工具和技術,可以大大提高 Rust 程式的表達力和效率。

JSON宏的設計與實作

在前面的章節中,我們定義了一個用於表示JSON資料的列舉(enum):

#[derive(Clone, PartialEq, Debug)]
enum Json {
    Null,
    Boolean(bool),
    Number(f64),
    String(String),
    Array(Vec<Json>),
    Object(Box<HashMap<String, Json>>)
}

然而,直接使用這個列舉來建立JSON資料會比較冗長。為了簡化這個過程,我們希望能夠使用一個類別似JSON的語法來建立JSON資料:

let students = json!([
    {
        "name": "Jim Blandy",
        "class_of": 1926,
        "major": "Tibetan throat singing"
    },
    {
        "name": "Jason Orendorff",
        "class_of": 1702,
        "major": "Knots"
    }
]);

碎片型別(Fragment Types)

在編寫複雜的巨集(macro)時,第一步是確定如何匹配(match)或解析(parse)所需的輸入。由於JSON資料有多種不同的形式(例如物件、陣列、數字等),因此我們的巨集需要有多個規則(rule)來處理這些不同的情況。

初步設計

初步來看,我們可以為每種JSON型別設計一個規則:

macro_rules! json {
    (null) => { Json::Null };
    ([ ... ]) => { Json::Array(...) };
    ({ ... }) => { Json::Object(...) };
    (???) => { Json::Boolean(...) };
    (???) => { Json::Number(...) };
    (???) => { Json::String(...) };
}

然而,這種設計並不能正確區分最後三種情況(布林值、數字和字串),因為巨集模式(macro pattern)無法單獨依靠 token 來區分它們。

使用 Token Tree

為瞭解決這個問題,我們可以使用 tt 碎片型別,它可以匹配單個 token tree。JSON 值可以被視為單個 token tree:數字、字串、布林值和 null 是單個 token,而物件和陣列則是被括號包圍的 token tree。

macro_rules! json {
    (null) => {
        Json::Null
    };
    ([ $( $element:tt ),* ]) => {
        Json::Array(vec![ $( json!($element) ),* ])
    };
    ({ $( $key:tt : $value:tt ),* }) => {
        Json::Object(Box::new(vec![ $( ($key.to_string(), json!($value)) ),* ].into_iter().collect()))
    };
    ($other:tt) => {
        // TODO: 傳回 Number, String, 或 Boolean
    };
}

遞迴實作

為了支援 JSON 陣列和物件的巢狀結構,我們需要在巨集中使用遞迴呼叫。上面的程式碼已經展示瞭如何使用遞迴來支援 JSON 陣列和物件。

完整實作

完整的 json! 巨集實作如下:

macro_rules! json {
    (null) => {
        Json::Null
    };
    ([ $( $element:tt ),* ]) => {
        Json::Array(vec![ $( json!($element) ),* ])
    };
    ({ $( $key:tt : $value:tt ),* }) => {
        Json::Object(Box::new(vec![ $( ($key.to_string(), json!($value)) ),* ].into_iter().collect()))
    };
    ($other:tt) => {
        // 簡單地將 $other 當作字串處理
        Json::String($other.to_string())
    };
}

內容解密:

  1. 遞迴呼叫:在處理陣列和物件時,json! 巨集會遞迴呼叫自身,以處理巢狀的 JSON 結構。
  2. Token Tree:使用 tt 碎片型別可以匹配任意的 token tree,使得巨集能夠處理不同型別的 JSON 資料。
  3. 模式匹配:巨集使用模式匹配來區分不同的 JSON 資料型別,並產生相應的 Rust 程式碼。

測試與驗證

為了驗證 json! 巨集的正確性,我們可以編寫一些測試案例:

#[test]
fn json_null() {
    assert_eq!(json!(null), Json::Null);
}

#[test]
fn json_array_with_json_element() {
    let macro_generated_value = json!([
        {
            "pitch": 440.0
        }
    ]);
    let hand_coded_value = Json::Array(vec![
        Json::Object(Box::new(vec![
            ("pitch".to_string(), Json::Number(440.0))
        ].into_iter().collect()))
    ]);
    assert_eq!(macro_generated_value, hand_coded_value);
}

這些測試案例可以幫助我們確保 json! 巨集正確地處理了不同的 JSON 資料型別。