Rust 巨集系統在編譯期根據模式匹配生成程式碼,實作比函式更靈活的抽象。macro_rules! 巨集定義方式,能將輸入的 Token 流轉換成 Rust 程式碼,例如 assert_eq! 巨集。Rust 編譯器早期展開巨集,透過模式匹配機制操作 Token,確保輸入符合預期格式。assert_eq! 使用區域性變數避免重複評估表示式帶來的副作用,確保程式碼行為符合預期。巨集範本生成最終程式碼,需注意片段型別使用,避免編譯錯誤。Rust 提供 file!、line!、column!、stringify!、concat! 等內建巨集輔助開發。除錯巨集可使用 cargo build --verbose、log_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)
}
}
}
};
}
內容解密:
macro_rules!定義巨集:這是 Rust 定義巨集的主要方式,透過模式匹配來生成程式碼。$left:expr和$right:expr:這兩個是模式匹配中的變數,分別匹配輸入的兩個表示式。它們會被用於後續的範本生成中。match陳述式:用於比較兩個表示式的值。這裡使用match是為了處理借用檢查,避免移動語義導致的問題。panic!:當斷言失敗時,觸發panic!並輸出詳細的錯誤資訊。
巨集展開的基本原理
Rust 在編譯期展開巨集,這個過程類別似於遞迴下降解析。編譯器讀取原始碼後,會逐步展開巨集呼叫,並將展開後的程式碼納入編譯流程中。這意味著巨集必須在使用前定義。
內容解密:
- 早期展開:Rust 編譯器在編譯早期階段就展開巨集,因此巨集必須在使用前定義。
- 模式匹配:巨集的模式匹配機制類別似於正規表示式,但操作物件是 Token 而非字元。
- Token 匹配:在
assert_eq!中,逗號和表示式會被逐一匹配,確保輸入符合預期格式。
為何 assert_eq! 需要額外的變數
在 assert_eq! 的實作中,使用了 left_val 和 right_val 變數來儲存匹配的結果,而不是直接使用 $left 和 $right。這是為了避免多次評估輸入表示式可能帶來的副作用。
簡化版本的錯誤分析
if !($left == $right) {
panic!("assertion failed: `(left == right)` \
(left: `{:?}`, right: `{:?}`)", $left, $right)
}
內容解密:
- 多次評估問題:如果
$left或$right是具有副作用的表示式(如函式呼叫),直接使用會導致多次評估,破壞預期行為。 - 使用區域性變數:透過
match將表示式的結果儲存到區域性變數,避免了多次評估的問題。
巨集範本與輸出
巨集範本負責生成最終的 Rust 程式碼,這些程式碼會被納入編譯流程。常見錯誤包括在範本中錯誤使用片段型別(如寫成 $left:expr 而非 $left),這會導致生成的程式碼無法編譯。
內容解密:
- 範本語法:巨集範本類別似於常見的範本語言,但輸出的是合法的 Rust 程式碼。
- 常見錯誤:在範本中誤用片段型別會導致編譯錯誤,需要仔細檢查巨集定義。
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);
}
}
}
});
}
內容解密:
macro_rules!: 用於定義一個巨集。$left:expr和$right:expr: 這兩個是巨集的引數,分別代表左邊和右邊的表示式。:expr表示它們是表示式。match (&$left, &$right): 對$left和$right取參照,並將它們作為一個 tuple 進行模式匹配。這樣做是為了避免將值移動到巨集中。if !(left_val == right_val): 檢查兩個值是否相等。如果不相等,則觸發panic!。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 ),* ]
};
}
內容解密:
$elem:expr ; $n:expr: 第一個規則用於建立一個具有相同元素$elem的向量,重複$n次。( $( $x:expr ),* ): 第二個規則用於建立一個向量,包含一系列以逗號分隔的表示式$x。$( ... ),*: 這是重複語法,用於匹配零個或多個以逗號分隔的表示式。<[_]>::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 ),* ]
};
}
內容解密:
- 第一條規則匹配逗號分隔的表示式列表,並為每個表示式生成對
push方法的呼叫,將元素加入臨時向量。 - 第二條規則處理尾隨逗號的情況,透過遞迴呼叫
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");
內容解密:
env!巨集用於取得編譯環境變數,在此取得 crate 的版本號。include_str!巨集將指設定檔案內容作為字串常值包含進來。
除錯巨集
除錯巨集可能具有挑戰性,因為 Rust 在報錯時不會顯示完全展開的程式碼。以下是幾種除錯工具:
- 使用
cargo build --verbose結合-Z unstable-options --pretty expanded選項檢視完全展開的程式碼。 - 使用
log_syntax!()巨集在編譯期列印資訊。 - 使用
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>());
}
內容解密:
trace_macros!巨集用於追蹤巨集展開過程,顯示每個巨集呼叫前後的程式碼變化。
開發複雜巨集:以 json! 為例
開發複雜巨集需要逐步構建並測試。json! 巨集展示瞭如何使用巨集規則建立 JSON 資料結構。
開發建議:
- 從簡單模式開始,逐步增加複雜度。
- 使用除錯工具檢查巨集展開結果。
- 確保巨集行為符合預期,特別是在錯誤處理方面。
透過本文的介紹,讀者應該對 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())
};
}
內容解密:
- 遞迴呼叫:在處理陣列和物件時,
json!巨集會遞迴呼叫自身,以處理巢狀的 JSON 結構。 - Token Tree:使用
tt碎片型別可以匹配任意的 token tree,使得巨集能夠處理不同型別的 JSON 資料。 - 模式匹配:巨集使用模式匹配來區分不同的 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 資料型別。