在上一個章節中,我們學習了許多跟文字格式化有關的巨集。在這個章節,我們將會更深入地了解巨集到底是什麼東西,以及實作巨集的方式。



Rust程式語言的巨集,很像是函數,但是它比函數還要更有彈性,而且複雜很多。「巨集」是一個用程式碼產生程式碼的方式,又稱為「元編程」(Metaprogramming),妥善運用可以大量地減少我們需要撰寫的程式碼。當然我們如果能用函數解決的問題就儘量使用函數,因為過度使用巨集通常會使得程式不易閱讀。

crate中的巨集並不像函數一樣,可以直接使用extern crate關鍵字來引用進來,必須要搭配#[macro_use]屬性才行。舉例來說:

#[macro_use]
extern crate serde;

以上程式會將serde這個crate中的巨集引用進目前程式的scope中。而且如果沒有用這樣的方式來引用巨集的話,是沒辦法如函數直接透過指定命名空間的方式來使用的。

Rust程式語言的巨集分為兩種,分別是「宣告式巨集」(Declarative Macro)和「程序式巨集」(Procedural Macros)。

宣告式巨集

宣告式巨集的用途比程序式巨集還要廣泛,我們可以使用macro_rules!巨集來定義並實作新巨集。例如:

macro_rules! hello {
    () => {
        println!("Hello!");
    };
}

以上程式,實作了一個hello!巨集,它在不傳入任何的參數時,會再去呼叫println!巨集在螢幕上顯示出Hello!文字。

完整程式如下:

macro_rules! hello {
    () => {
        println!("Hello!");
    };
}

fn main() {
    hello!();
}

macro_rules!巨集的語法格式有點像是match關鍵字,而事實上,編譯器在遇到宣告式巨集的時候,也會如match關鍵字那樣去做類似型樣匹配的動作。例如以上程式,我們用hello!()這個程式敘述來呼叫hello!巨集。由於hello!()沒有加入任何的參數,因此macro_rules!巨集中定義的()這條arm就會被匹配到。

編譯器會在進行程式編譯前,先去匹配並「解開」宣告式巨集內撰寫的程式碼。拿以上的例子來說,編譯器會將程式第8行的hello!巨集解開,變成如下的程式碼:

macro_rules! hello {
    () => {
        println!("Hello!");
    };
}

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

所以程式執行後會在螢幕上顯示出Hello!文字。

我們可以再將hello!巨集進行如下的改寫,加入新的arm,使其能同時支援不傳入任何參數和傳入一個world參數的呼叫方式。

macro_rules! hello {
    () => {
        println!("Hello!");
    };
    (world) => {
        println!("Hello, world!");
    };
}

這邊另外要注意的是,在macro_rules!巨集的arm與arm之間,要用分號;隔開,而且arm的匹配順序是從上到下的。

當我們使用hello!(world)來呼叫hello!巨集時,螢幕上就會顯示出Hello, world!文字,而不是Hello!文字。完整程式如下:

macro_rules! hello {
    () => {
        println!("Hello!");
    };
    (world) => {
        println!("Hello, world!");
    };
}

fn main() {
    hello!(world);
}

所以我們傳給hello!巨集的world到底是什麼東西?一個變數嗎?其實它可以是變數也可以不是變數。例如以下程式:

macro_rules! hello {
    () => {
        println!("Hello!");
    };
    (world) => {
        println!("Hello, world!");
    };
}

fn main() {
    let world = "Magic Len";

    hello!(world);
}

我們真的在呼叫hello!巨集前先宣告了一個world變數。這個程式雖然可以通過編譯,但是編譯器會提示程式第11行宣告的world變數並沒有被用到。

這是怎麼回事?所以程式第13行的world是什麼?在這個程式中,第13行的world其實就如同hello!巨集的一部份,它既不是變數、不是型別、也不是字串。當編譯器看到hello!(world)時,首先會去看它有沒有辦法與()這條arm匹配,在這邊很顯然是不能的,所以又會去看它能不能與(world)這條arm匹配,由於它們的參數都是world,因此匹配成功。

宣告式巨集並不會去區分到底是用小括號()、中括號[]還是大括號{}來傳遞參數。例如:

macro_rules! hello {
    [] => {
        println!("Hello!");
    };
    {world} => {
        println!("Hello, world!");
    };
}

fn main() {
    hello!();
    hello![];
    hello! {};
    hello!(world);
    hello![world];
    hello! {world};
}

以上程式的執行結果如下:

Hello!
Hello!
Hello!
Hello, world!
Hello, world!
Hello, world!

雖然宣告式巨集並沒有區分括號版本,但是習慣上,我們寫在macro_rules!巨集的arm的匹配樣本都是使用小括號。在呼叫巨集時,則是依據傳入參數的類型來決定要使用哪種括號。如果是要如函數那樣傳入參數的話,就使用小括號();如果是要如陣列那樣傳入好幾個元素的話,就使用中括號[];如果是要傳入等等會提到的多種記號(token)的話,就使用大括號{}

我們可以再將hello!巨集進行如下的改寫,加入新的arm,使其能再支援更多的呼叫方式。

macro_rules! hello {
    () => {
        println!("Hello!");
    };
    (world) => {
        println!("Hello, world!");
    };
    (web: MagicLen) => {
        println!("Hello, magiclen.org!");
    };
}

同樣地,當我們使用hello!(web: MagicLen)來呼叫hello!巨集時,螢幕上就會顯示出Hello, magiclen.org!文字。完整程式如下:

macro_rules! hello {
    () => {
        println!("Hello!");
    };
    (world) => {
        println!("Hello, world!");
    };
    (web: MagicLen) => {
        println!("Hello, magiclen.org!");
    };
}

fn main() {
    hello!(web: MagicLen);
}

事實上,就算我們在呼叫hello!巨集時,把web: MagicLen的空格 省略掉,程式也還是可以通過編譯。如下:

macro_rules! hello {
    () => {
        println!("Hello!");
    };
    (world) => {
        println!("Hello, world!");
    };
    (web: MagicLen) => {
        println!("Hello, magiclen.org!");
    };
}

fn main() {
    hello!(web:MagicLen);
}

這是因為宣告式巨集會將傳入的參數拆解成好幾個記號(token),來做型樣匹配,因此不論是web: MagicLenweb:MagicLen都會被拆解為web:MagicLen,不用管空格到底有多少個。

那它是怎麼把web:拆成web:兩個記號的?怎麼不就是一個web:記號呢?除了底線、英文字母和數字外,其餘的字元皆是記號的分界,但是重複的字元不會被分界。像是a1B_,只會被拆解為a1B_a1B.c會被拆解為a1B.ca1B..c會被拆解為a1B..c

如果我們要把寫在macro_rules!巨集的arm的匹配樣本內的參數當變數來用的話,該參數名稱必須要以$為開頭,而且其後還要使用:語法來定義參數的型別。如下:

macro_rules! hello {
    () => {
        println!("Hello!");
    };
    ($target:expr) => {
        println!("Hello, {}!", $target);
    };
}

hello!巨集的第二個arm中,我們定義了一個型別為expr$target參數。這個$target不是用來直接匹配$target這兩個記號,而是用來匹配任意的表達式。

例如以下程式:

macro_rules! hello {
    () => {
        println!("Hello!");
    };
    ($target:expr) => {
        println!("Hello, {}!", $target);
    };
}

fn main() {
    hello!();

    let world = "world";

    hello!(world);
}

執行結果如下:

Hello!
Hello, world!

在這個程式中,hello!(world)中的world就是變數了,而不是巨集的一部份。

宣告式巨集參數的型別

以下分別說明與舉例參數可以使用的型別:

literal

literal用來匹配一個定數。

macro_rules! hello {
    ($target:literal) => {
        println!("Hello, {}!", $target);
    };
}

fn main() {
    hello!("world");
}
expr

expr用來匹配一個表達式。

macro_rules! hello {
    ($target:expr) => {
        println!("Hello, {}!", $target);
    };
}

fn main() {
    hello!("world");
}
ident

ident用來匹配一個變數、函數、常數、型別名稱。

macro_rules! wrapper {
    ($name:ident) => {
        struct $name<T> {
            data: T
        }

        impl<T> From<T> for $name<T> {
            fn from(data: T) -> Self {
                Self {
                    data
                }
            }
        }
    };
}

wrapper!(Wrapper);

fn main() {
    let _wrapper = Wrapper::from(String::new());
}
ty

ty用來匹配一個型別,包含泛型。

macro_rules! wrapper {
    ($name:ident, $typ: ty) => {
        struct $name {
            data: $typ
        }

        impl From<$typ> for $name {
            fn from(data: $typ) -> Self {
                Self {
                    data
                }
            }
        }
    };
}

wrapper!(StringWrapper, String);

fn main() {
    let string_wrapper = StringWrapper::from(String::new());
}
lifetime

lifetime用來匹配一個生命周期,例如'static'a

macro_rules! wrapper {
    ($name:ident, & $lt: lifetime $typ: ty) => {
        struct $name {
            data: & $lt $typ
        }

        impl $name {
            fn from(data: & $lt $typ) -> Self {
                Self {
                    data
                }
            }
        }
    };
}

wrapper!(StringWrapper, &'static str);

fn main() {
    let string_wrapper = StringWrapper::from("123");
}
tt

tt用來匹配一個記號。

macro_rules! declare {
    ($keyword: tt, $name: ident, $typ: ty, $value: expr) => {
        $keyword $name: $typ = $value;
    };
}

declare!(static, A, i32, 1);

fn main() {
    println!("{}", A);
}
item

item用來匹配一個結構體、列舉、函數、模組。

macro_rules! basic_derive {
    ($item: item) => {
        #[derive(Debug, Clone, Default)]
        $item
    };
}

basic_derive!(struct MyStruct {
    name: String
});

fn main() {
    let a = MyStruct {
        name: "name".to_string()
    };

    println!("{:?}", a);
}

item的匹配範圍其實也有包含該項目的屬性和文件註解。

macro_rules! basic {
    ($item: item) => {
        $item
    };
}

basic!{
    #[derive(Debug, Clone, Default)]
    struct MyStruct {
        name: String
    }
}

fn main() {
    let a = MyStruct {
        name: "name".to_string()
    };

    println!("{:?}", a);
}
block

block用來匹配一個程式區塊。

macro_rules! validated_string {
    ($name: ident, $data: ident, $handler:block, $err: ty) => {
        struct $name(String);

        impl $name {
            fn from_string($data: String) -> Result<Self, $err> {
                let s = match $handler {
                    Ok(s)=> s,
                    Err(e)=> return Err(e)
                };

                Ok(Self(s))
            }
        }
    };
}

#[derive(Debug)]
enum Errors {
    IncorrectFormat
}

validated_string!(HelloString, data, {
    if data.starts_with("Hello") {
        Ok(data)
    } else {
        Err(Errors::IncorrectFormat)
    }
}, Errors);

fn main() {
    let validated_string = HelloString::from_string("Hello!".to_string()).unwrap();
}
stmt

stmt用來匹配一個程式敘述。

macro_rules! one_statement_function {
    ($name: ident, $do: stmt) => {
        fn $name() {
            $do
        }
    };
}

one_statement_function!(hello, println!("Hello!"));

fn main() {
    hello();
}
pat

pat用來匹配match關鍵字的匹配樣本。

macro_rules! match_one {
    ($v: expr, $p: pat, $b: block) => {
        match $v {
            $p => $b
            _ => unimplemented!()
        }
    };
}

fn main() {
    let v = Some(50);

    match_one!(v, Some(a), {
        println!("{}", a);
    });
}
path

path用來匹配一個路徑。

macro_rules! hello {
    ($p: path) => {
        use $p::{hello};

        hello();
    };
}

mod hello {
    pub mod a {
        pub fn hello() {
            println!("Hello!");
        }
    }

    pub mod b {
        pub fn hello() {
            println!("Hello.");
        }
    }
}

fn main() {
    hello!(hello::a);
}
meta

meta用來匹配屬性或是其中的一個參數。

macro_rules! item_with_derive {
    ($item: item, $meta: meta) => {
        #[derive($meta)]
        $item
    };
}

item_with_derive!(struct MyStruct {
    name: String
}, Debug);

fn main() {
    let a = MyStruct {
        name: "name".to_string()
    };

    println!("{:?}", a);
}

meta經常會寫成$(#[$attr: meta])*,來匹配所有屬性和文件註解。

macro_rules! struct_with_meta {
    ($(#[$attr: meta])* struct $($t:tt)*) => {
        $(#[$attr])*
        struct $($t)*
    };
}

struct_with_meta! {
    /// My struct!!
    #[derive(Debug)]
    struct MyStruct {
        name: String
    }
}

fn main() {
    let a = MyStruct {
        name: "name".to_string()
    };

    println!("{:?}", a);
}
vis

vis用來匹配一個如pubpub(crate)等的關鍵字。vis也可以完全不匹配任何字元,表示要使用預設的可見度。

macro_rules! wrapper {
    ($v:vis $name:ident, $typ: ty) => {
        $v struct $name {
            data: $typ
        }

        impl From<$typ> for $name {
            fn from(data: $typ) -> Self {
                Self {
                    data
                }
            }
        }
    };
}

wrapper!(pub StringWrapper, String);

fn main() {
    let string_wrapper = StringWrapper::from(String::new());
}

多次匹配

利用$()語法,將想要進行多次匹配的部份括起來,接著再接上星號*或加號+來表示要進行任意次數的匹配還是至少一次的匹配。在讀取時,也是用同樣的語法,將要多次使用的部份括起來。

直接給個例子會比較清楚:

macro_rules! item_with_derives {
    ($item: item $(, $meta: meta)+ ) => {
        #[derive( $($meta,)+ )]
        $item
    };
}

item_with_derives!(struct MyStruct {
    name: String
}, Debug, Clone);

fn main() {
    let a = MyStruct {
        name: "name".to_string()
    };

    println!("{:?}", a);
}

在使用$()語法來匹配時,其實也可以在$()和星號*或加號+之間再加上一個記號,作為分隔參數的記號。例如:

macro_rules! item_with_derives {
    ($item: item, $($meta: meta),+ ) => {
        #[derive( $($meta,)+ )]
        $item
    };
}

item_with_derives!(struct MyStruct {
    name: String
}, Debug, Clone);

fn main() {
    let a = MyStruct {
        name: "name".to_string()
    };

    println!("{:?}", a);
}

不過上述的兩種分隔方式,均無法成功匹配參數最後又多了一個分隔記號的情形。例如:

macro_rules! item_with_derives {
    ($item: item, $($meta: meta),+) => {
        #[derive( $($meta,)+ )]
        $item
    };
}

item_with_derives!(struct MyStruct {
        name: String
    },
    Debug,
    Clone,
);

fn main() {
    let a = MyStruct {
        name: "name".to_string()
    };

    println!("{:?}", a);
}

以上程式會編譯失敗,因為程式第12行多了一個逗號,

為了解決這個問題,我們可以將匹配樣本改成這樣:

macro_rules! item_with_derives {
    ($item: item, $($meta: meta),+ $(,)*) => {
        #[derive( $($meta,)+ )]
        $item
    };
}

item_with_derives!(struct MyStruct {
        name: String
    },
    Debug,
    Clone,,,,,
);

fn main() {
    let a = MyStruct {
        name: "name".to_string()
    };

    println!("{:?}", a);
}

如此一來要加幾個逗號都不是問題!

可有可無的匹配

利用$()語法,將想要進行選用而不強制匹配的部份括起來,接著再接上問號?。在讀取時,也是用同樣的語法,將要選用的部份括起來。例如:

macro_rules! declare {
    ($keyword: tt, $name: ident $(: $typ: ty)?, $value: expr) => {
        $keyword $name $(: $typ)? = $value;
    };
}

fn main() {
    declare!(let, a: i32, 1);
    declare!(let, b, 2);

    println!("{} {}", a, b);
}

前面提到的結尾逗號的問題,也可以用這種匹配方式來解決。如下:

macro_rules! item_with_derives {
    ($item: item, $($meta: meta),+ $(,)?) => {
        #[derive( $($meta,)+ )]
        $item
    };
}

item_with_derives!(struct MyStruct {
        name: String
    },
    Debug,
    Clone,
);

fn main() {
    let a = MyStruct {
        name: "name".to_string()
    };

    println!("{:?}", a);
}

將宣告式巨集公開出去

如果要讓我們實作的宣告式巨集能夠在crate之外被使用的話,要替其加上#[macro_export]屬性。如下:

#[macro_export]
macro_rules! hello {
    () => {
        println!("Hello!");
    };
    ($target:expr) => {
        println!("Hello, {}!", $target);
    };
}

如此一來,當外部的crate想要引用到我們這個hello!巨集時,可以在extern crate關鍵字的左方或是上方要加上#[macro_use]屬性或是#[macro_use(hello)]屬性,前者表示要使用整個crate公開出去的巨集,後者則表示只需使用hello!這個巨集(如果要引用多個巨集,用逗號隔開)。

如下:

#[macro_use]
extern crate example;

fn main() {
    hello!();
}

當然,如果不想要直接將巨集引用至目前的整個crate下,也可以像是呼叫crate中的函數一樣,用路徑去呼叫巨集。例如:

extern crate example;

fn main() {
    example::hello!();
}

只不過巨集的路徑會直接被放在crate下,不管它是在哪個模組中被定義與實作的。

在宣告式巨集中使用其它的宣告式巨集

我們其實已經看過在宣告式巨集中使用其它的宣告式巨集的例子了。如下:

macro_rules! hello {
    () => {
        println!("Hello!");
    };
}

所以這樣的寫法還有什麼要注意的嗎?

上面這個hello!巨集之所以能正常使用,是因為Rust會自動引用標準函式庫的println!巨集,使我們不需要預先在extern crate關鍵字引入某個crate時,使用#[macro_use]屬性來說明要引用println!巨集。

換句話說,在不使用路徑呼叫的前提下,在宣告式巨集中使用其它的宣告式巨集時,該巨集必須要事先被引用至目前的scope下。

如果我們在自己實作的宣告式巨集中,又去呼叫同樣的巨集的話,如下:

macro_rules! hello {
    () => {
        hello!("world");
    };
    ($target:expr) => {
        println!("Hello, {}!", $target);
    };
}

hello!巨集需要公開出去時,可以直接加上#[macro_use]屬性,不會有任何問題。

那如果我們在自己實作的宣告式巨集中,呼叫同樣也是我們自己實作,但是是不同的巨集,又是怎麼樣的一個狀況呢?如下:

macro_rules! hello_inner {
    ($target:expr) => {
        println!("Hello, {}!", $target);
    };
}

#[macro_export]
macro_rules! hello {
    () => {
        hello_inner!("world");
    };
}

雖然我們有把hello!巨集加上#[macro_export]屬性,但是hello_inner!巨集並沒有加上,因為我們不想要公開hello_inner!巨集。可是這樣一來,就會導致外部crate在呼叫hello!巨集的時候無法去呼叫到hello_inner!巨集。

為了解決這個問題,我們也還是得去公開hello_inner!巨集,只不過除了替它加上#[macro_export]屬性之外,還可以再加上#[doc(hidden)]屬性,使其不會出現在API文件上。如下:

#[doc(hidden)]
#[macro_export]
macro_rules! hello_inner {
    ($target:expr) => {
        println!("Hello, {}!", $target);
    };
}

#[macro_export]
macro_rules! hello {
    () => {
        hello_inner!("world");
    };
}

$crate關鍵字來表示巨集所在的crate

一般而言,在公開的宣告式巨集中用到的不會自動會被Rust引用的項目,最好都明確地寫上其路徑,避免使用到這個巨集的開發者,還要另外用use關鍵字和#[macro_use]屬性引用一堆有的沒的項目到要呼叫這個巨集的scope下。

我們可以在macro_rules!中利用$crate關鍵字,來表示其所在的crate。所以,剛才提到的hello_inner!巨集的例子,最好還是改寫成以下這樣:

#[doc(hidden)]
#[macro_export]
macro_rules! hello_inner {
    ($target:expr) => {
        println!("Hello, {}!", $target);
    };
}

#[macro_export]
macro_rules! hello {
    () => {
        $crate::hello_inner!("world");
    };
}

如此一來,就算不使用#[macro_use]屬性,外部的crate也還是可以正常地用路徑去呼叫hello!巨集。如下:

extern crate example;

fn main() {
    example::hello!();
}

自動替宣告式巨集中用到的宣告式巨集都加上$crate關鍵字

macro_rules!巨集還提供了一個古怪,但是很方便的用法,那就是它可以自動替宣告式巨集中直接用到(不透過路徑)的宣告式巨集都加上$crate關鍵字。在替我們的巨集加上#[macro_export]屬性時,可以再加上local_inner_macros參數,使其變成#[macro_export(local_inner_macros)]。如下:

#[macro_export(local_inner_macros)]
macro_rules! hello {
    () => {
        hello!("world");
    };
    ($target:expr) => {
        println!("Hello, {}!", $target);
    };
}

不過以上程式會編譯失敗,因為println!巨集並不在$crate下。所以println!巨集必須要改用路徑的方式來呼叫,如下:

#[macro_export(local_inner_macros)]
macro_rules! hello {
    () => {
        hello!("world");
    };
    ($target:expr) => {
        ::std::println!("Hello, {}!", $target);
    };
}

程序式巨集

程序式巨集更像是函數,它允許輸入Rust的程式碼,對這個程式碼做處理來產生出新的程式碼,不像是宣告式巨集只能利用型樣匹配來透過一些參數和已經寫好的程式碼範本來產生程式碼。當然,程序式巨集的實作方式也會比宣告式巨集複雜許多。先前我們使用過的#[derive(Debug)]就是一種程序式巨集的用法。我們可以把任意的特性當作程序式巨集來使用,舉例來說:

trait HelloWorld {
    fn hello_world();
}

#[derive(HelloWorld)]
struct Pineapple;

#[derive(HelloWorld)]
struct Durian;

fn main() {
    Pineapple::hello_world();
    Durian::hello_world();
}

我們希望以上程式碼可以輸出:

Hello world, my name is `Pineapple`.
Hello world, my name is `Durian`.

為了要讓HelloWorld特性可以作為程序式巨集來使用,我們必須將它另外使用一個獨立的crate來實作。我們可以建立一個名為hello_world_derive的函式庫專案,接著修改該專案的Cargo.toml,添加以下內容:

[lib]
proc-macro = true

[dependencies]
syn = {version = "1", features = ["full"]}
quote = "1"

synquote這兩個套件可以幫助我們處理Rust的程式碼,等等會用到。synfull特色建議啟用,免得有些還算常用的結構體不能用。[lib]區塊中的proc-macro設定項目可以設定這個程式專案是否為程序式巨集的專案。

接著在lib.rs檔案中,撰寫以下程式:

extern crate proc_macro;
extern crate syn;
#[macro_use]
extern crate quote;

use proc_macro::TokenStream;
use syn::DeriveInput;

#[proc_macro_derive(HelloWorld)]
pub fn hello_world_derive(input: TokenStream) -> TokenStream {
    // Parse the token stream
    let ast: DeriveInput = syn::parse(input).unwrap();

    // Get the ident (type's name).
    let name = &ast.ident;

    // Build the code
    let expanded = quote! {
        impl HelloWorld for #name {
            fn hello_world() {
                println!("Hello, world! My name is `{}`.", stringify!(#name));
            }
        }
    };

    expanded.into()
}

以上程式,proc_macro_derive這個屬性可以綁定某個特性名稱和函數,使其可以搭配derive屬性使用,來產生出Rust的程式碼。proc_macro這個套件是Rust程式語言內建的crate,它可以將Rust的程式碼轉成字串來處理。syn套件可以將Rust的程式碼字串轉成DeriveInput結構實體,用來儲存Rust程式碼的AST(Abstract Syntax Tree),方便我們去處理Rust的程式碼。quote套件可以接受syn套件轉出來的DeriveInput結構實體,將其轉回Rust的程式碼。

程式第12行,將Rust的程式碼轉成特定的資料結構。程式第15行,用變數儲存當前使用了derive屬性的結構體或是列舉名稱,如果是Pineapple結構體,名稱就會是Pineapple。程式第18行到第24行,利用quote!巨集,將要產生出來的Rust程式碼寫在這個巨集的程式敘述區塊內。

quote!巨集的程式敘述區塊中,可以直接使用井字號#接上外面的變數名稱,就可以用外面的變數名稱所儲存的值,來取代quote!巨集的程式敘述區塊中的#接上變數名稱所形成的空格。程式第29行所使用的stringify!巨集是Rust程式語言的標準函式庫提供的,可以在程式編譯階段直接將Rust的表達式直接轉成字串定數,而不會去執行它。

程式第26行,利用quote!巨集回傳的實體所提供的into方法,來將該實體轉為TokenStream結構實體。

hello_world_derive函式庫專案的程式寫好之後,就可以使用我們原本的可執行程式專案來引用啦!改寫後的程式如下:

#[macro_use]
extern crate hello_world_derive;

trait HelloWorld {
    fn hello_world();
}

#[derive(HelloWorld)]
struct Pineapple;

#[derive(HelloWorld)]
struct Durian;

fn main() {
    Pineapple::hello_world();
    Durian::hello_world();
}

程序式巨集的crate必須要加上#[macro_use]屬性來引用。

程式在編譯階段時,有用到程序式巨集的部份會被展開成:

trait HelloWorld {
    fn hello_world();
}

struct Pineapple;

impl HelloWorld for Pineapple {
    fn hello_world() {
        println!("Hello, world! My name is `{}`.", stringify!(Pineapple));
    }
}

struct Durian;

impl HelloWorld for Durian {
    fn hello_world() {
        println!("Hello, world! My name is `{}`.", stringify!(Durian));
    }
}

fn main() {
    Pineapple::hello_world();
    Durian::hello_world();
}

程式執行結果如下:

Hello, world! My name is `Pineapple`.
Hello, world! My name is `Durian`.

不過如果我們加上#[derive(HelloWorld)]屬性的結構體或是列舉有用到泛型的話,編譯時就會出問題。

例如:

#[macro_use]
extern crate hello_world_derive;

trait HelloWorld {
    fn hello_world();
}

#[derive(HelloWorld)]
struct Fruit<'a>(&'a str);

fn main() {
    Fruit::hello_world();
}

這是因為在quote!巨集中撰寫impl區塊時,並沒有去處理當要實作的結構體或是列舉有泛型的情形。我們可以利用DeriveInput結構實體的generics欄位儲存的實體,其提供的split_for_impl方法,將泛型的程式碼轉為撰寫impl區塊時會用到的各個部份。程式修改如下:

extern crate proc_macro;
extern crate syn;
#[macro_use]
extern crate quote;

use proc_macro::TokenStream;
use syn::DeriveInput;

#[proc_macro_derive(HelloWorld)]
pub fn hello_world_derive(input: TokenStream) -> TokenStream {
    // Parse the token stream
    let ast: DeriveInput = syn::parse(input).unwrap();

    // Get the ident (type's name).
    let name = &ast.ident;

    let (impl_generics, ty_generics, where_clause) = ast.generics.split_for_impl();

    // Build the code
    let expanded = quote! {
        impl #impl_generics HelloWorld for #name #ty_generics #where_clause {
            fn hello_world() {
                println!("Hello, world! My name is `{}`.", stringify!(#name));
            }
        }
    };

    expanded.into()
}

如此一來,#[derive(HelloWorld)]屬性就可以同時支援有泛型或是沒泛型的結構體或是列舉。

替屬性添加額外的元數據(meta)參數

如果我們想要更進一步的控制#[derive(HelloWorld)]屬性,可以讓其允許從元數據中輸入參數。

好比說,我們可以替結構體再加上#[helloWorld(target = "xxx")]屬性,來控制hello_world關聯函數印出來的文字為Hello, xxx!。如下:

#[macro_use]
extern crate hello_world_derive;

trait HelloWorld {
    fn hello_world();
}

#[derive(HelloWorld)]
#[helloWorld(target = "MagicLen")]
struct Fruit<'a>(&'a str);

fn main() {
    Fruit::hello_world();
}

當然,以上程式目前是無法編譯的,因為我們還沒有實作這個功能嘛!

DeriveInput結構實體的attrs欄位可以用來取得該項目用到的所有屬性,我們可以針對helloWorld這個屬性名稱(元數據名稱)來進行處理。程式改寫如下:

extern crate proc_macro;
extern crate syn;
#[macro_use]
extern crate quote;

use proc_macro::TokenStream;
use quote::ToTokens;
use syn::DeriveInput;
use syn::{Lit, Meta, NestedMeta};

#[proc_macro_derive(HelloWorld, attributes(helloWorld))]
pub fn hello_world_derive(input: TokenStream) -> TokenStream {
    // Parse the token stream
    let ast: DeriveInput = syn::parse(input).unwrap();

    // Get the ident (type's name).
    let name = &ast.ident;

    let (impl_generics, ty_generics, where_clause) = ast.generics.split_for_impl();

    let mut target: Option<String> = None;

    for attr in ast.attrs {
        if let Some(attr_meta_name) = attr.path.get_ident() {
            if attr_meta_name == "helloWorld" {
                let attr_meta = attr.parse_meta().unwrap();

                match attr_meta {
                    Meta::List(list) => {
                        for p in list.nested {
                            match p {
                                NestedMeta::Meta(meta) => match meta {
                                    Meta::List(_) => panic!(
                                        "Incorrect format for using the `helloWorld` attribute."
                                    ),
                                    Meta::NameValue(named_value) => {
                                        let named_value_name =
                                            named_value.path.into_token_stream().to_string();

                                        match named_value_name.as_str() {
                                            "target" => {
                                                let value = named_value.lit;

                                                match value {
                                                        Lit::Str(s) => {
                                                            target = Some(s.value());
                                                        }
                                                        _ => panic!("You should assign a target by `target = \"something\"`."),
                                                    }
                                            }
                                            _ => {
                                                panic!(
                                                    "Unknown meta parameter `{}`.",
                                                    named_value_name
                                                )
                                            }
                                        }
                                    }
                                    Meta::Path(path) => {
                                        let path_name =
                                            path.segments.into_token_stream().to_string();

                                        match path_name.as_str() {
                                                "target" => panic!("You should assign a target by `target = \"something\"`."),
                                                _ => panic!("Unknown meta parameter `{}`.", path_name),
                                            }
                                    }
                                },
                                NestedMeta::Lit(_) => {
                                    panic!("Incorrect format for using the `helloWorld` attribute.")
                                }
                            }
                        }
                    }
                    _ => panic!("Incorrect format for using the `helloWorld` attribute."),
                }
            }
        }
    }

    // Build the code
    let expanded = match target {
        Some(target) => {
            quote! {
                impl #impl_generics HelloWorld for #name #ty_generics #where_clause {
                    fn hello_world() {
                        println!("Hello, {}! My name is {}", #target, stringify!(#name));
                    }
                }
            }
        }
        None => {
            quote! {
                impl #impl_generics HelloWorld for #name #ty_generics #where_clause {
                    fn hello_world() {
                        println!("Hello, world! My name is {}", stringify!(#name));
                    }
                }
            }
        }
    };

    expanded.into()
}

proc_macro_derive這個屬性的第一個參數可以綁定某個特性名稱和函數,使其可以搭配derive屬性使用,第二個參數則可以再傳入attributes這個屬性,來建立新的屬性(在這個程式中建立的新屬性即為helloWorld)。

這個程式比較重要的部份是syn::Meta這個列舉。其List變體代表一個attribute_name(parameters)這樣可傳入多個屬性或是定數作為參數的屬性;NameValue變體是代表一個attribute_name = value這樣的屬性,其中的value為一個定數;Path變體則是代表一個attribute_name這樣的屬性。其中,attribute_name可以是一個辨識名稱(identifier)或是一個路徑(path)。

List變體的參數是用syn::NestedMeta這個列舉來表示。其Meta變體代表一個屬性;Lit變體則代表一個定數。

替結構體或是列舉的欄位加上屬性

接著要把hello_world關聯函數改為方法來說明。

先看看以下程式碼:

extern crate proc_macro;
extern crate syn;
#[macro_use]
extern crate quote;

use proc_macro::TokenStream;
use syn::DeriveInput;

#[proc_macro_derive(HelloWorld)]
pub fn hello_world_derive(input: TokenStream) -> TokenStream {
    // Parse the token stream
    let ast: DeriveInput = syn::parse(input).unwrap();

    // Get the ident (type's name).
    let name = &ast.ident;

    let (impl_generics, ty_generics, where_clause) = ast.generics.split_for_impl();

    // Build the code
    let expanded = quote! {
        impl #impl_generics HelloWorld for #name #ty_generics #where_clause {
            fn hello_world(&self) {
                println!("Hello, world! My name is `{}`.", stringify!(#name));
            }
        }
    };

    expanded.into()
}
#[macro_use]
extern crate hello_world_derive;

trait HelloWorld {
    fn hello_world(&self);
}

#[derive(HelloWorld)]
struct Fruit<'a>(&'a str);

fn main() {
    let apple = Fruit("apple");

    apple.hello_world();
}

我們希望可以替使用了#[derive(HelloWorld)]屬性的結構體或是列舉的某個字串欄位加上#[helloWorld(target)]屬性,使該字串欄位可以在呼叫hello_world方法時被顯示出來。如下:

#[macro_use]
extern crate hello_world_derive;

trait HelloWorld {
    fn hello_world(&self);
}

#[derive(HelloWorld)]
struct Fruit<'a>(
    #[helloWorld(target)]
    &'a str
);

fn main() {
    let apple = Fruit("apple");

    apple.hello_world();
}

DeriveInput結構實體的data欄位可以用來取得結構體或是列舉的主體(它也可以用來取得聯合結構的主體,不過在本系列文章中並不談聯合結構)。程式改寫如下:

extern crate proc_macro;
extern crate syn;
#[macro_use]
extern crate quote;

use proc_macro::TokenStream;
use quote::ToTokens;
use syn::{Data, DeriveInput, Fields, Index, Meta, NestedMeta, Pat};

#[proc_macro_derive(HelloWorld, attributes(helloWorld))]
pub fn hello_world_derive(input: TokenStream) -> TokenStream {
    // Parse the token stream
    let ast: DeriveInput = syn::parse(input).unwrap();

    // Get the ident (type's name).
    let name = &ast.ident;

    let (impl_generics, ty_generics, where_clause) = ast.generics.split_for_impl();

    match ast.data {
        Data::Struct(struct_data) => {
            match struct_data.fields {
                Fields::Named(named_fields) => {
                    for named_field in named_fields.named {
                        for attr in named_field.attrs.iter() {
                            if let Some(attr_meta_name) = attr.path.get_ident() {
                                if attr_meta_name == "helloWorld" {
                                    let attr_meta = attr.parse_meta().unwrap();

                                    match attr_meta {
                                        Meta::List(list) => {
                                            for p in list.nested {
                                                match p {
                                                    NestedMeta::Meta(meta) => {
                                                        match meta {
                                                            Meta::List(_) => panic!("Incorrect format for using the `helloWorld` attribute."),
                                                            Meta::NameValue(_) => panic!("Incorrect format for using the `helloWorld` attribute."),
                                                            Meta::Path(path) => {
                                                                let path_name = path.into_token_stream().to_string();

                                                                match path_name.as_str() {
                                                                    "target" => {
                                                                        let field = named_field.ident.unwrap();

                                                                        // Build the code
                                                                        let expanded = quote! {
                                                                            impl #impl_generics HelloWorld for #name #ty_generics #where_clause {
                                                                                fn hello_world(&self) {
                                                                                    println!("Hello, {}! My name is `{}`.", self.#field, stringify!(#name));
                                                                                }
                                                                            }
                                                                        };

                                                                        return expanded.into();
                                                                    }
                                                                    _ => panic!("Unknown meta parameter `{}`.", path_name),
                                                                }
                                                            }
                                                        }
                                                    }
                                                    NestedMeta::Lit(_) => panic!("Incorrect format for using the `helloWorld` attribute."),
                                                }
                                            }
                                        }
                                        _ => panic!("Incorrect format for using the `helloWorld` attribute."),
                                    }
                                }
                            }
                        }
                    }
                }
                Fields::Unnamed(unnamed_fields) => {
                    for (index, unnamed_field) in unnamed_fields.unnamed.into_iter().enumerate() {
                        for attr in unnamed_field.attrs.iter() {
                            if let Some(attr_meta_name) = attr.path.get_ident() {
                                if attr_meta_name == "helloWorld" {
                                    let attr_meta = attr.parse_meta().unwrap();

                                    match attr_meta {
                                        Meta::List(list) => {
                                            for p in list.nested {
                                                match p {
                                                    NestedMeta::Meta(meta) => {
                                                        match meta {
                                                            Meta::List(_) => panic!("Incorrect format for using the `helloWorld` attribute."),
                                                            Meta::NameValue(_) => panic!("Incorrect format for using the `helloWorld` attribute."),
                                                            Meta::Path(path) => {
                                                                let path_name = path.into_token_stream().to_string();

                                                                match path_name.as_str() {
                                                                    "target" => {
                                                                        let field = Index::from(index);

                                                                        // Build the code
                                                                        let expanded = quote! {
                                                                            impl #impl_generics HelloWorld for #name #ty_generics #where_clause {
                                                                                fn hello_world(&self) {
                                                                                    println!("Hello, {}! My name is `{}`.", self.#field, stringify!(#name));
                                                                                }
                                                                            }
                                                                        };

                                                                        return expanded.into();
                                                                    }
                                                                    _ => panic!("Unknown meta parameter `{}`.", path_name),
                                                                }
                                                            }
                                                        }
                                                    }
                                                    NestedMeta::Lit(_) => panic!("Incorrect format for using the `helloWorld` attribute."),
                                                }
                                            }
                                        }
                                        _ => panic!("Incorrect format for using the `helloWorld` attribute."),
                                    }
                                }
                            }
                        }
                    }
                }
                Fields::Unit => (),
            }
        }
        Data::Enum(enum_data) => {
            for variant in enum_data.variants {
                for (index, field) in variant.fields.iter().enumerate() {
                    for attr in field.attrs.iter() {
                        if let Some(attr_meta_name) = attr.path.get_ident() {
                            if attr_meta_name == "helloWorld" {
                                let attr_meta = attr.parse_meta().unwrap();

                                match attr_meta {
                                    Meta::List(list) => {
                                        for p in list.nested {
                                            match p {
                                                NestedMeta::Meta(meta) => {
                                                    match meta {
                                                        Meta::List(_) => panic!("Incorrect format for using the `helloWorld` attribute."),
                                                        Meta::NameValue(_) => panic!("Incorrect format for using the `helloWorld` attribute."),
                                                        Meta::Path(path) => {
                                                            let path_name = path.into_token_stream().to_string();

                                                            match path_name.as_str() {
                                                                "target" => {
                                                                    let variant_ident = variant.ident;

                                                                    // Build the code
                                                                    let expanded = match field.ident.as_ref() {
                                                                        Some(ident) => {
                                                                            quote! {
                                                                                impl #impl_generics HelloWorld for #name #ty_generics #where_clause {
                                                                                    fn hello_world(&self) {
                                                                                        match self {
                                                                                            #name::#variant_ident{#ident, ..} => {
                                                                                                println!("Hello, {}! My name is `{}`.", #ident, stringify!(#name));
                                                                                            }
                                                                                            _ => unreachable!()
                                                                                        }
                                                                                    }
                                                                                }
                                                                            }
                                                                        }
                                                                        None => {
                                                                            // to form a pattern tuple like (_, _, _, ..., _, v, ..)

                                                                            let mut pat_tuple = String::from("(");

                                                                            for _ in 0..index {
                                                                                pat_tuple.push_str("_,");
                                                                            }

                                                                            pat_tuple.push_str("v,..)");

                                                                            let pat_tuple: Pat = syn::parse_str(&pat_tuple).unwrap();

                                                                            quote! {
                                                                                impl #impl_generics HelloWorld for #name #ty_generics #where_clause {
                                                                                    fn hello_world(&self) {
                                                                                        match self {
                                                                                            #name::#variant_ident #pat_tuple => {
                                                                                                println!("Hello, {}! My name is `{}`.", v, stringify!(#name));
                                                                                            }
                                                                                            _ => unreachable!()
                                                                                        }
                                                                                    }
                                                                                }
                                                                            }
                                                                        }
                                                                    };

                                                                    return expanded.into();
                                                                }
                                                                _ => panic!("Unknown meta parameter `{}`.", path_name),
                                                            }
                                                        }
                                                    }
                                                }
                                                NestedMeta::Lit(_) => panic!("Incorrect format for using the `helloWorld` attribute."),
                                            }
                                        }
                                    }
                                    _ => panic!(
                                        "Incorrect format for using the `helloWorld` attribute."
                                    ),
                                }
                            }
                        }
                    }
                }
            }
        }
        Data::Union(_) => (),
    }

    // Build the code
    let expanded = quote! {
        impl #impl_generics HelloWorld for #name #ty_generics #where_clause {
            fn hello_world(&self) {
                println!("Hello, world! My name is `{}`.", stringify!(#name));
            }
        }
    };

    expanded.into()
}

為了要對不同的類型的結構體和列舉產生出不同的程式碼,我們需要依靠很多match關鍵字來做型樣匹配。雖然這樣寫起來有點費工夫,但就是屬於一次功,完成之後就有很方便的程序式巨集可以用了!

程序式巨集除了能夠用在定義結構體、列舉時使用外,在Rust 1.45之後,它還可以如宣告式巨集那樣做成用法像是函數的巨集。只要將proc_macro_derive屬性替換成proc_macro屬性就可以了!舉個簡單的例子:

extern crate proc_macro;

#[macro_use]
extern crate syn;

#[macro_use]
extern crate quote;

use proc_macro::TokenStream;
use syn::LitStr;

#[proc_macro]
pub fn hello(input: TokenStream) -> TokenStream {
    // Parse the token stream
    let s = parse_macro_input!(input as LitStr).value();

    let output_sentence = format!("Hello, world! My name is `{}`.", s);

    // Build the code
    let code = quote! {
        #output_sentence
    };

    code.into()
}

上面這個程式建立了一個程序式巨集hello,它的功能非常近似於:

macro_rules! hello {
    ($s:literal) => {
        concat!("Hello, world! My name is `", $s, "`.")
    };
}

concat是Rust內建的巨集,可以把輸入的任意數量的定數全部串接成字串定數。

程序式巨集hello的用法如下:

#[macro_use]
extern crate hello_world_marco;

fn main() {
    println!(hello!("Pineapple"));
}

如果巨集的參數數量固定是1個,可以直接像上面的例子這樣使用syn這個crate提供的parse_macro_input巨集和syn內建的LitStr等結構體或是列舉來解析token。而如果巨集的參數數量超過1個,最好自行建立一個結構體,並實作syn::Parse特性,再搭配parse_macro_input巨集來用。不過由於這部份的作法比較複雜,這篇文章就不再深入探討了。

結論

Rust的巨集功能十分強大,可以替我們省下不少撰寫相同或是相似的程式的時間。雖然會大大地增加程式碼的複雜度,但是如果有提供良好的文件的話,用到這些巨集的開發者們即便完全不知道這些巨集背後到底是怎麼實作的也沒關係。在Rust生態圈中,許多框架都會應用強大的巨集功能,有興趣想要更加熟悉用大量巨集來開發程式的這種風格的話,可以自行到crates.io上翻翻看。

下一章節,我們要來學習Rust程式語言的不安全設計方式!

下一章:不安全的Rust