原文標題:Macros in Rust: A tutorial with examples
原文連結:https://blog.logrocket.com/macros-in-rust-a-tutorial-with-examples/
公眾號: Rust 碎碎念
翻譯 by: Praying
在本文中,我們將會涵蓋你需要了解的關於 Rust 巨集(macro)的一切,包括對 Rust 巨集的介紹和如何使用 Rust 巨集的示例。
我們會涵蓋以下內容:
Rust 巨集是什麼?
Rust 巨集的型別
Rust 巨集的宣告
建立宣告式巨集 Rust 中宣告式巨集的高階解析 從結構體中解析後設資料 宣告式巨集的限制
Rust 中的過程巨集
屬性式風格巨集 自定義繼承巨集 函式式風格巨集
Rust 巨集是什麼?
Rust 對巨集(macro)有著非常好的支援。巨集能夠使得你能夠通過寫程式碼的方式來生成程式碼,這通常被稱為超程式設計(metaprogramming)。
巨集提供了類似函式的功能,但是沒有執行時開銷。但是,因為巨集會在編譯期進行展開(expand),所以它會有一些編譯期的開銷。
Rust 巨集非常不同於 C 裡面的巨集。Rust 巨集會被應用於詞法樹(token tree),而 C 語言裡的巨集則是文字替換。
Rust 巨集的型別
Rust 有兩種型別的巨集:
宣告式巨集(Declarative macros)使得你能夠寫出類似 match 表示式的東西,來操作你所提供的 Rust 程式碼。它使用你提供的程式碼來生成用於替換巨集呼叫的程式碼。
過程巨集(Procedural macros)允許你操作給定 Rust 程式碼的抽象語法樹(abstract syntax tree, AST)。過程巨集是從一個(或者兩個)
TokenStream
到另一個TokenStream
的函式,用輸出的結果來替換巨集呼叫。
讓我們來看一下宣告式巨集和過程巨集的更多細節,並討論一些關於如何在 Rust 中使用巨集的例子。
Rust 中的宣告式巨集
巨集通過使用macro_rules!
來宣告。宣告式巨集雖然功能上相對較弱,但提供了易於使用的介面來建立巨集來移除重複性程式碼。最為常見的一個宣告式巨集就是println!
。宣告式巨集提供了一個類似match
的介面,在匹配時,巨集會被匹配分支的程式碼替換。
建立宣告式巨集
macro_rules! add{
// macth like arm for macro
($a:expr,$b:expr)=>{
// macro expand to this code
{
// $a and $b will be templated using the value/variable provided to macro
$a+$b
}
}
}
fn main(){
// call to macro, $a=1 and $b=2
add!(1,2);
}
這段程式碼建立了一個巨集來對兩個數進行相加。[macro_rules!]與巨集的名稱,add
,以及巨集的主體一同使用。
這個巨集沒有對兩個數執行相加操作,它只是把自己替換為把兩個數相加的程式碼。巨集的每個分支接收一個函式的引數,並且引數可以被指定多個型別。如果想要add
函式也能僅接收一個引數,我們可以新增另一個分支:
macro_rules! add{
// first arm match add!(1,2), add!(2,3) etc
($a:expr,$b:expr)=>{
{
$a+$b
}
};
// Second arm macth add!(1), add!(2) etc
($a:expr)=>{
{
$a
}
}
}
fn main(){
// call the macro
let x=0;
add!(1,2);
add!(x);
}
在一個巨集中,可以有多個分支,巨集根據不同的引數展開到不同的程式碼。每個分支可以接收多個引數,這些引數使用$
符號開頭,然後跟著一個 token 型別:
item
——一個項(item),像一個函式,結構體,模組等。block
——一個塊 (block)(即一個語句塊或一個表示式,由花括號所包圍)stmt
—— 一個語句(statement)pat
——一個模式(pattern)expr
—— 一個表示式(expression)ty
——一個型別(type)ident
—— 一個識別符號(indentfier)path
—— 一個路徑(path)(例如,foo
,::std::mem::replace
,transmute::<_, int>
,...)meta
—— 一個後設資料項;位於#[...]
和#![...]
屬性tt
——一個詞法樹vis
——一個可能為空的Visibility
限定詞
在上面的例子中,我們使用$typ
引數,它的 token 型別為ty
,類似於u8
,u16
。這個巨集在對數字進行相加之前轉換為一個特定的型別。
macro_rules! add_as{
// using a ty token type for macthing datatypes passed to maccro
($a:expr,$b:expr,$typ:ty)=>{
$a as $typ + $b as $typ
}
}
fn main(){
println!("{}",add_as!(0,2,u8));
}
Rust 巨集還支援接收可變數量的引數。這個操作非常類似於正規表示式。*
被用於零個或更多的 token 型別,+
被用於零個或者一個引數。
macro_rules! add_as{
(
// repeated block
$($a:expr)
// seperator
,
// zero or more
*
)=>{
{
// to handle the case without any arguments
0
// block to be repeated
$(+$a)*
}
}
}
fn main(){
println!("{}",add_as!(1,2,3,4)); // => println!("{}",{0+1+2+3+4})
}
重複的 token 型別被$()
包裹,後面跟著一個分隔符和一個*
或一個+
,表示這個 token 將會重複的次數。分隔符用於多個 token 之間互相區分。$()
後面跟著*
和+
用於表示重複的程式碼塊。在上面的例子中,+$a
是一段重複的程式碼。
如果你更仔細地觀察,你會發現這段程式碼有一個額外的 0 使得語法有效。為了移除這個 0,讓add
表示式像引數一樣,我們需要建立一個新的巨集,被稱為TT muncher。
macro_rules! add{
// first arm in case of single argument and last remaining variable/number
($a:expr)=>{
$a
};
// second arm in case of two arument are passed and stop recursion in case of odd number ofarguments
($a:expr,$b:expr)=>{
{
$a+$b
}
};
// add the number and the result of remaining arguments
($a:expr,$($b:tt)*)=>{
{
$a+add!($($b)*)
}
}
}
fn main(){
println!("{}",add!(1,2,3,4));
}
TT muncher 以遞迴方式分別處理每個 token,每次處理單個 token 也更為簡單。這個巨集有三個分支:
第一個分支處理是否單個引數通過的情況
第二個分支處理是否兩個引數通過的情況
第三個分支使用剩下的引數再次呼叫
add
巨集
巨集引數不需要用逗號分隔。多個 token 可以被用於不同的 token 型別。例如,圓括號可以結合ident
token 型別使用。Rust 編譯器能夠匹配對應的分支並且從引數字串中匯出變數。
macro_rules! ok_or_return{
// match something(q,r,t,6,7,8) etc
// compiler extracts function name and arguments. It injects the values in respective varibles.
($a:ident($($b:tt)*))=>{
{
match $a($($b)*) {
Ok(value)=>value,
Err(err)=>{
return Err(err);
}
}
}
};
}
fn some_work(i:i64,j:i64)->Result<(i64,i64),String>{
if i+j>2 {
Ok((i,j))
} else {
Err("error".to_owned())
}
}
fn main()->Result<(),String>{
ok_or_return!(some_work(1,4));
ok_or_return!(some_work(1,0));
Ok(())
}
ok_or_return
這個巨集實現了這樣一個功能,如果它接收的函式操作返回Err
,它也返回Err
,或者如果操作返回Ok
,就返回Ok
裡的值。它接收一個函式作為引數,並在一個 match 語句中執行該函式。對於傳遞給引數的函式,它會重複使用。
通常來講,很少有巨集會被組合到一個巨集中。在這些少數情況中,內部的巨集規則會被使用。它有助於操作這些巨集輸入並且寫出整潔的 TT munchers。
要建立一個內部規則,需要新增以@
開頭的規則名作為引數。這個巨集將不會匹配到一個內部的規則除非顯式地被指定作為一個引數。
macro_rules! ok_or_return{
// internal rule.
(@error $a:ident,$($b:tt)* )=>{
{
match $a($($b)*) {
Ok(value)=>value,
Err(err)=>{
return Err(err);
}
}
}
};
// public rule can be called by the user.
($a:ident($($b:tt)*))=>{
ok_or_return!(@error $a,$($b)*)
};
}
fn some_work(i:i64,j:i64)->Result<(i64,i64),String>{
if i+j>2 {
Ok((i,j))
} else {
Err("error".to_owned())
}
}
fn main()->Result<(),String>{
// instead of round bracket curly brackets can also be used
ok_or_return!{some_work(1,4)};
ok_or_return!(some_work(1,0));
Ok(())
}
在 Rust 中使用宣告式巨集進行高階解析
巨集有時候會執行需要解析 Rust 語言本身的任務。
讓我們建立一個巨集把我們到目前為止講過的所有概念融合起來,通過pub
關鍵字使其成為公開的。
首先,我們需要解析 Rust 結構體來獲取結構體的名字,結構體的欄位以及欄位型別。
解析結構體的名字及其欄位
一個struct
(即結構體)宣告在其開頭有一個可見性關鍵字(比如pub
) ,後面跟著struct
關鍵字,然後是struct
的名字和struct
的主體。
macro_rules! make_public{
(
// use vis type for visibility keyword and ident for struct name
$vis:vis struct $struct_name:ident { }
) => {
{
pub struct $struct_name{ }
}
}
}
$vis
將會擁有可見性,$struct_name
將會擁有一個結構體名。為了讓一個結構體是公開的,我們只需要新增pub
關鍵字並忽略$vis
變數。
一個struct
可能包含多個欄位,這些欄位具有相同或不同的資料型別和可見性。ty
token 型別用於資料型別,vis
用於可見性,ident
用於欄位名。我們將會使用*
用於零個或更多欄位。
macro_rules! make_public{
(
$vis:vis struct $struct_name:ident {
$(
// vis for field visibility, ident for field name and ty for field data type
$field_vis:vis $field_name:ident : $field_type:ty
),*
}
) => {
{
pub struct $struct_name{
$(
pub $field_name : $field_type,
)*
}
}
}
}
從struct
中解析後設資料
通常,struct
有一些附加的後設資料或者過程巨集,比如#[derive(Debug)]
。這個後設資料需要保持完整。解析這類後設資料是通過使用meta
型別來完成的。
macro_rules! make_public{
(
// meta data about struct
$(#[$meta:meta])*
$vis:vis struct $struct_name:ident {
$(
// meta data about field
$(#[$field_meta:meta])*
$field_vis:vis $field_name:ident : $field_type:ty
),*$(,)+
}
) => {
{
$(#[$meta])*
pub struct $struct_name{
$(
$(#[$field_meta:meta])*
pub $field_name : $field_type,
)*
}
}
}
}
我們的make_public
巨集現在準備就緒了。為了看一下make_public
是如何工作的,讓我們使用Rust Playground來把巨集展開為真實編譯的程式碼。
macro_rules! make_public{
(
$(#[$meta:meta])*
$vis:vis struct $struct_name:ident {
$(
$(#[$field_meta:meta])*
$field_vis:vis $field_name:ident : $field_type:ty
),*$(,)+
}
) => {
$(#[$meta])*
pub struct $struct_name{
$(
$(#[$field_meta:meta])*
pub $field_name : $field_type,
)*
}
}
}
fn main(){
make_public!{
#[derive(Debug)]
struct Name{
n:i64,
t:i64,
g:i64,
}
}
}
展開後的程式碼看起來像下面這樣:
// some imports
macro_rules! make_public {
($ (#[$ meta : meta]) * $ vis : vis struct $ struct_name : ident
{
$
($ (#[$ field_meta : meta]) * $ field_vis : vis $ field_name : ident
: $ field_type : ty), * $ (,) +
}) =>
{
$ (#[$ meta]) * pub struct $ struct_name
{
$
($ (#[$ field_meta : meta]) * pub $ field_name : $
field_type,) *
}
}
}
fn main() {
pub struct name {
pub n: i64,
pub t: i64,
pub g: i64,
}
}
宣告式巨集的限制
宣告式巨集有一些限制。有些是與 Rust 巨集本身有關,有些則是宣告式巨集所特有的:
缺少對巨集的自動完成和展開的支援
宣告式巨集調式困難
修改能力有限
更大的二進位制
更長的編譯時間(這一條對於宣告式巨集和過程巨集都存在)