在上一個章節中,我們學習了許多跟文字格式化有關的巨集。在這個章節,我們將會更深入地了解巨集到底是什麼東西,以及實作巨集的方式。
Rust程式語言的巨集,很像是函數,但是它比函數還要更有彈性,而且複雜很多。「巨集」是一個用程式碼產生程式碼的方式,又稱為「元編程」(Metaprogramming),妥善運用可以大量地減少我們需要撰寫的程式碼。當然我們如果能用函數解決的問題就儘量使用函數,因為過度使用巨集通常會使得程式不易閱讀。
在早期版本的Rust,要引用巨集,必須使用extern crate
敘述並加上#[macro_use]
屬性來引用才行。但現在的巨集和函數一樣,可以直接透過路徑或是use
關鍵字來使用。舉例來說:
use serde::{Deserialize, Serialize};
以上程式會將serde
這個crate中的Deserialize
和Serialize
巨集引用進目前程式的scope中,它們可以被用在#[derive()]
屬性的參數中,屬於「程序式巨集」(Procedural Macros)。在之前的章節中經常使用的println!
巨集可以用像是函數一樣的方式來呼叫,屬於「宣告式巨集」(Declarative Macro)。
宣告式巨集
宣告式巨集的用途比程序式巨集還要廣泛,我們可以使用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, 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: MagicLen
或web:MagicLen
都會被拆解為web
、:
和MagicLen
,不用管空格到底有多少個。
那它是怎麼把web:
拆成web
和:
兩個記號的?怎麼不就是一個web:
記號呢?除了底線、英文字母和數字外,其餘的字元皆是記號的分界,但是重複的字元不會被分界。像是a1B_
,只會被拆解為a1B_
;a1B.c
會被拆解為a1B
、.
和c
;a1B..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, 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);
}
注意以上程式的第3行,要在宣告式巨集中使用型別為path
的參數,若要在路徑後再添加其它路徑,需要使用use
關鍵字搭配大括號{}
語法將其引入,且{}
中只能放入一個名稱,不能是一個路徑。下面再舉一個例子:
macro_rules! hello {
($p: path) => {
use $p::{b};
b::hello();
};
}
mod hello {
pub mod a {
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
用來匹配一個如pub
、pub(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:?}");
}
以上程式會編譯失敗,因為程式第13行多了一個逗號,
。
為了解決這個問題,我們可以將匹配樣本改成這樣:
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]
屬性來暴露(export)。如下:
#[macro_export]
macro_rules! hello {
() => {
println!("Hello!");
};
($target:expr) => {
println!("Hello, {}!", $target);
};
}
如此一來,當外部的crate想要引用到我們這個hello!
巨集時,早期的Rust版本要在extern crate
敘述的左方或是上方要加上#[macro_use]
屬性或是#[macro_use(hello)]
屬性,前者表示要使用整個crate公開出去的巨集,後者則表示只需使用hello!
這個巨集(如果要引用多個巨集,用逗號隔開)。如下:
#[macro_use]
extern crate example;
fn main() {
hello!();
}
而現在的Rust只需要使用一般函數的呼叫方式來使用巨集即可,如下:
use example::hello;
fn main() {
hello!();
}
或是:
fn main() {
example::hello!();
}
用#[macro_export]
屬性公開出去的宣告式巨集,不管是在哪個模組中被定義與實作的,巨集的路徑都會直接被放在crate下。但如果我們只想讓宣告式巨集在同一個crate下公開的話,那就不能夠使用#[macro_export]
屬性,而是要使用use
關鍵字搭配pub
關鍵字來設定權限。
如下:
mod m {
macro_rules! hello {
() => {
println!("Hello!");
};
($target:expr) => {
println!("Hello, {}!", $target);
};
}
pub(crate) use hello;
}
使用use
關鍵字來公開沒有被加上#[macro_export]
屬性的宣告式巨集,其最廣的權限只能夠使用pub(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!
巨集公開於crate外時,如果外面的crate是在extern crate
敘述加上#[macro_use]
屬性來引用hello!
巨集,不會有任何問題。但如果沒有使用#[macro_use]
屬性,而是用路徑的方式來直接使用xxx::hello!()
,那麼hello!
巨集裡面的hello!("world");
敘述會因為不知道hello!
巨集是哪個巨集,而編譯失敗。
例如以下這段程式:
fn main() {
xxx::hello!();
}
在編譯時巨集會被解開,變成:
fn main() {
hello!("world");
}
以至於編譯器不知道hello!
是什麼巨集。
用$crate
關鍵字來表示巨集所在的crate
一般而言,在公開的宣告式巨集中用到的不會自動會被Rust引用的項目,最好都明確地寫上其路徑,避免使用到這個巨集的開發者,還要另外用use
關鍵字和#[macro_use]
屬性引用一堆有的沒的項目到要呼叫這個巨集的scope下。
我們可以在macro_rules!
巨集中利用$crate
關鍵字,來表示其所在的crate。所以,剛才提到的hello!
巨集的例子,最好還是改寫成以下這樣:
#[macro_export]
macro_rules! hello {
() => {
$crate::hello!("world");
};
($target:expr) => {
println!("Hello, {}!", $target);
};
}
如此一來,外部的crate就算不使用#[macro_use]
屬性,也還是可以正常地用路徑去呼叫hello!
巨集。
在公開的宣告式巨集中使用其它的宣告式巨集
那如果我們在自己實作的公開的宣告式巨集中,呼叫同樣也是我們自己實作,但是是不同的巨集,又是怎麼樣的一個狀況呢?如下:
macro_rules! hello_inner {
($target:expr) => {
println!("Hello, {}!", $target);
};
}
#[macro_export]
macro_rules! hello {
() => {
$crate::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
關鍵字
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 `Durian`.
為了要讓HelloWorld
特性可以作為程序式巨集來使用,我們必須將它另外使用一個獨立的crate來實作。我們可以建立一個名為hello_world_derive
的函式庫專案,接著修改該專案的Cargo.toml
,添加以下內容:
[lib]
proc-macro = true
[dependencies]
syn = { version = "2", features = ["full"] }
quote = "1"
# proc_macro2 = "1"
syn
和quote
這兩個套件可以幫助我們處理Rust的程式碼,等等會用到。syn
的full
特色建議啟用,免得有些還算常用的結構體不能用。
至於proc_macro2
套件,雖然它是syn
和quote
內部使用的套件,但在一些情況下我們會需要直接使用它,稍候會提到。
[lib]
區塊中的proc-macro
設定項目可以設定這個程式專案是否為程序式巨集的專案。
接著在lib.rs
檔案中,撰寫以下程式:
use proc_macro::TokenStream;
use quote::quote;
use syn::{
parse::{Parse, ParseStream},
parse_macro_input, DeriveInput,
};
#[proc_macro_derive(HelloWorld)]
pub fn hello_world_derive(input: TokenStream) -> TokenStream {
struct MyDeriveInput {
ast: DeriveInput,
}
impl Parse for MyDeriveInput {
fn parse(input: ParseStream) -> Result<Self, syn::Error> {
let ast = input.parse::<DeriveInput>()?;
Ok(MyDeriveInput { ast })
}
}
// Parse the token stream
let derive_input = parse_macro_input!(input as MyDeriveInput);
// Get the identifier of the type.
let name = &derive_input.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()
}
以上程式是用於derive
屬性的程序式巨集的基本程式碼模板。
proc_macro
這個套件是Rust程式語言內建的crate,它可以將Rust的程式碼轉成字串,這樣我就可以撰寫程式產生新的Rust程式碼字串,再交給proc_macro
將字串轉成Rust的程式碼讓Rust編譯。
syn
套件可以解析Rust的程式碼字串,將其轉換成特定的結構體,方便我們寫程式去處理Rust的程式碼。
quote
套件可以接受syn
套件轉出來的結構實體,將其轉回Rust的程式碼字串。
程式第10行,proc_macro_derive
這個屬性可以在屬性的參數定義某個名稱,這個名稱即為外面在使用時要放入derive
屬性的名稱,如果是要實作某個特性,這個名稱最好與該特性的名稱一樣或是改成-ize
(……化)型態。例如我們的這個程序式巨集是為了要快速替結構體實作HelloWorld
特性,所以屬性應該要寫成#[proc_macro_derive(HelloWorld)]
。被加上proc_macro_derive
屬性的函數即為Rust程式碼的處理程序,這個函數必須要有pub
權限,而函數名稱則可以隨意取,自己看得懂就好。
程式第12行,我們定義了一個MyDeriveInput
結構體,它將被用來處理我們要解析的Rust程式碼,並儲存之後要產生Rust程式碼的時候需要用到的資訊。
程式第16行,替MyDeriveInput
實作Parse
特性,並在其parse
函數中撰寫解析程式碼的程式。有了Parse
特性便可以使用syn
的parse_macro_input!
巨集將TokenStream
轉成該結構體。
程式第18行,將Rust的程式碼轉成DeriveInput
結構實體。
程式第20行,將DeriveInput
結構實體交給MyDeriveInput
結構實體來儲存。
程式第28行,取得當前使用了derive
屬性的結構體或是列舉的名稱,如果是Pineapple
結構體使用了derive
屬性,解析出來的名稱就會是Pineapple
。
程式第31行到第37行,利用quote!
巨集,將要產生出來的Rust程式碼寫在這個巨集的程式敘述區塊內。
在quote!
巨集的程式敘述區塊中,可以直接使用井字號#
接上外面的變數名稱,就可以用外面的變數名稱所儲存的值,來取代quote!
巨集的程式敘述區塊中的#
接上變數名稱所形成的空格。
程式第34行,stringify!
巨集是Rust程式語言的標準函式庫提供的,可以在程式編譯階段直接將Rust的表達式直接轉成字串定數,而不會去執行它。
程式第39行,利用quote!
巨集回傳的實體所提供的into
方法,來將該實體轉為TokenStream
結構實體。
將hello_world_derive
函式庫專案的程式寫好之後,就可以使用我們原本的可執行程式專案來引用啦!改寫後的程式如下:
use hello_world_derive::HelloWorld;
trait HelloWorld {
fn hello_world();
}
#[derive(HelloWorld)]
struct Pineapple;
#[derive(HelloWorld)]
struct Durian;
fn main() {
Pineapple::hello_world();
Durian::hello_world();
}
程式在編譯階段時,有用到程序式巨集的部份會被展開成:
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 `Durian`.
不過如果我們加上#[derive(HelloWorld)]
屬性的結構體或是列舉有用到泛型的話,編譯時就會出問題。
例如:
use hello_world_derive::HelloWorld;
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
區塊時會用到的各個部份。程式修改如下:
use proc_macro::TokenStream;
use quote::quote;
use syn::{
parse::{Parse, ParseStream},
parse_macro_input, DeriveInput,
};
#[proc_macro_derive(HelloWorld)]
pub fn hello_world_derive(input: TokenStream) -> TokenStream {
struct MyDeriveInput {
ast: DeriveInput,
}
impl Parse for MyDeriveInput {
fn parse(input: ParseStream) -> Result<Self, syn::Error> {
let ast = input.parse::<DeriveInput>()?;
Ok(MyDeriveInput { ast })
}
}
// Parse the token stream
let derive_input = parse_macro_input!(input as MyDeriveInput);
// Get the identifier of the type.
let name = &derive_input.ast.ident;
let (impl_generics, ty_generics, where_clause) = derive_input.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!
。如下:
use hello_world_derive::HelloWorld;
trait HelloWorld {
fn hello_world();
}
#[derive(HelloWorld)]
#[helloWorld(target = "MagicLen")]
struct Fruit<'a>(&'a str);
fn main() {
Fruit::hello_world();
}
當然,以上程式目前是無法編譯的,因為我們還沒有實作這個功能嘛!
DeriveInput
結構實體的attrs
欄位可以用來取得該項目用到的所有屬性,我們可以針對helloWorld
這個屬性名稱(元數據名稱)來進行處理。程式改寫如下:
use proc_macro::TokenStream;
use quote::quote;
use syn::{
parse::{Parse, ParseStream},
parse_macro_input,
spanned::Spanned,
DeriveInput, Expr, Lit, Meta, MetaNameValue,
};
#[proc_macro_derive(HelloWorld, attributes(helloWorld))]
pub fn hello_world_derive(input: TokenStream) -> TokenStream {
struct MyDeriveInput {
ast: DeriveInput,
target: Option<String>,
}
impl Parse for MyDeriveInput {
fn parse(input: ParseStream) -> Result<Self, syn::Error> {
let ast = input.parse::<DeriveInput>()?;
let mut target: Option<String> = None;
for attr in ast.attrs.iter() {
if attr.path().is_ident("helloWorld") {
match &attr.meta {
Meta::List(list) => {
let named_value: MetaNameValue = list.parse_args()?;
if named_value.path.is_ident("target") {
match named_value.value {
Expr::Lit(lit) => match lit.lit {
Lit::Str(s) => {
target = Some(s.value());
},
lit => {
return Err(syn::Error::new(
lit.span(),
"the value of `target` should be a string literal",
));
},
},
expr => {
return Err(syn::Error::new(
expr.span(),
"the value of `target` should be a string literal",
));
},
}
}
},
meta => {
return Err(syn::Error::new(
meta.span(),
"the `helloWorld` attribute should have a list item",
));
},
}
}
}
Ok(MyDeriveInput { ast, target })
}
}
// Parse the token stream
let derive_input = parse_macro_input!(input as MyDeriveInput);
// Get the identifier of the type.
let name = &derive_input.ast.ident;
let (impl_generics, ty_generics, where_clause) = derive_input.ast.generics.split_for_impl();
// Build the code
let expanded = match derive_input.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
這個屬性可以在屬性的參數定義某個名稱,第二個參數則可以再傳入attributes
這個屬性,來建立新的屬性(在這個程式中建立的新屬性即為helloWorld
)。
我們在MyDeriveInput
結構體新增了一個target
欄位,先去解析helloWorld
屬性是否屬於MetaList
,List代表一個attribute_name(parameters)
這樣可傳入多個屬性或是定數作為參數的屬性。如果helloWorld
屬性是List的話再解析它的參數是否為MetaNameValue
,NameValue代表一個attribute_name = value
這樣的屬性。如果參數是NameValue的話,就去看它是否為名為target
的NameValue屬性,如果是的話就去檢查它的值是否為字串定數。最終即可把抓出的字串定數存進target
變數。
在使用quote!
巨集產生Rust的程式碼字串的時候,先去判斷是否有設定target
參數,有的話就使用它。
在設計程序式巨集的時候,應回傳syn::Error
結構實體(如果需要自行使用syn::Error::new
建立出來,必須傳入正確的Span
結構實體),將錯誤交給parse_macro_input!
巨集來處理,而不要直接使用panic!
巨集。這樣做的目的是要讓編譯器能夠明確地知道發生編譯錯誤的巨集位置在哪,以此來增加程序式巨集函式庫的易用性。
此外,對於沒有定義的屬性用法,應該全都找出來並回傳錯誤訊息,避免應用到我們的巨集函式庫的開發者不小心用錯卻全然不知。所以以上的程式,還可以再做如下的加強:
use proc_macro::TokenStream;
use quote::quote;
use syn::{
parse::{Parse, ParseStream},
parse_macro_input,
spanned::Spanned,
DeriveInput, Expr, Lit, Meta, MetaNameValue,
};
#[proc_macro_derive(HelloWorld, attributes(helloWorld))]
pub fn hello_world_derive(input: TokenStream) -> TokenStream {
struct MyDeriveInput {
ast: DeriveInput,
target: Option<String>,
}
impl Parse for MyDeriveInput {
fn parse(input: ParseStream) -> Result<Self, syn::Error> {
let ast = input.parse::<DeriveInput>()?;
let mut target: Option<String> = None;
for attr in ast.attrs.iter() {
if attr.path().is_ident("helloWorld") {
match &attr.meta {
Meta::List(list) => {
let named_value: MetaNameValue = list.parse_args()?;
if named_value.path.is_ident("target") {
match named_value.value {
Expr::Lit(lit) => match lit.lit {
Lit::Str(s) => {
target = Some(s.value());
},
lit => {
return Err(syn::Error::new(
lit.span(),
"the value of `target` should be a string literal",
));
},
},
expr => {
return Err(syn::Error::new(
expr.span(),
"the value of `target` should be a string literal",
));
},
}
} else {
return Err(syn::Error::new(
named_value.span(),
"unknown attribute",
));
}
},
meta => {
return Err(syn::Error::new(
meta.span(),
"the `helloWorld` attribute should have a list item",
));
},
}
}
}
Ok(MyDeriveInput { ast, target })
}
}
// Parse the token stream
let derive_input = parse_macro_input!(input as MyDeriveInput);
// Get the identifier of the type.
let name = &derive_input.ast.ident;
let (impl_generics, ty_generics, where_clause) = derive_input.ast.generics.split_for_impl();
// Build the code
let expanded = match derive_input.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()
}
替結構體或是列舉的欄位加上屬性
接著要把hello_world
關聯函數改為方法來說明。
先看看以下程式碼:
use proc_macro::TokenStream;
use quote::quote;
use syn::{
parse::{Parse, ParseStream},
parse_macro_input, DeriveInput,
};
#[proc_macro_derive(HelloWorld)]
pub fn hello_world_derive(input: TokenStream) -> TokenStream {
struct MyDeriveInput {
ast: DeriveInput,
}
impl Parse for MyDeriveInput {
fn parse(input: ParseStream) -> Result<Self, syn::Error> {
let ast = input.parse::<DeriveInput>()?;
Ok(MyDeriveInput { ast })
}
}
// Parse the token stream
let derive_input = parse_macro_input!(input as MyDeriveInput);
// Get the identifier of the type.
let name = &derive_input.ast.ident;
let (impl_generics, ty_generics, where_clause) = derive_input.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()
}
use hello_world_derive::HelloWorld;
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
方法時被顯示出來。如下:
use hello_world_derive::HelloWorld;
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
欄位可以用來取得結構體或是列舉的主體(它也可以用來取得聯合體的主體,不過在本系列文章中並不談聯合體)。程式可先改寫如下:
use proc_macro::TokenStream;
use quote::{quote, ToTokens};
use syn::{
parse::{Parse, ParseStream},
parse_macro_input,
spanned::Spanned,
Data, DeriveInput, Fields, Index, Meta, Path,
};
#[proc_macro_derive(HelloWorld, attributes(helloWorld))]
pub fn hello_world_derive(input: TokenStream) -> TokenStream {
enum Target {
Struct(proc_macro2::TokenStream),
}
struct MyDeriveInput {
ast: DeriveInput,
target: Option<Target>,
}
impl Parse for MyDeriveInput {
fn parse(input: ParseStream) -> Result<Self, syn::Error> {
let ast = input.parse::<DeriveInput>()?;
let mut target: Option<Target> = None;
match &ast.data {
Data::Struct(struct_data) => match &struct_data.fields {
Fields::Unnamed(unnamed_fields) => {
for (index, unnamed_field) in unnamed_fields.unnamed.iter().enumerate() {
for attr in unnamed_field.attrs.iter() {
if attr.path().is_ident("helloWorld") {
match &attr.meta {
Meta::List(list) => {
let path: Path = list.parse_args()?;
if path.is_ident("target") {
if target.is_some() {
return Err(syn::Error::new(
path.span(),
"`target` has been set before",
));
}
let field = Index::from(index).into_token_stream();
target = Some(Target::Struct(field));
} else {
return Err(syn::Error::new(
path.span(),
"should only be `target`",
));
}
},
meta => {
return Err(syn::Error::new(
meta.span(),
"the `helloWorld` attribute should have a list item",
));
},
}
}
}
}
},
_ => return Err(syn::Error::new(ast.span(), "unsupported type")),
},
_ => return Err(syn::Error::new(ast.span(), "unsupported type")),
}
Ok(MyDeriveInput { ast, target })
}
}
// Parse the token stream
let derive_input = parse_macro_input!(input as MyDeriveInput);
// Get the identifier of the type.
let name = &derive_input.ast.ident;
let (impl_generics, ty_generics, where_clause) = derive_input.ast.generics.split_for_impl();
// Build the code
let expanded = match derive_input.target {
Some(target) => match target {
Target::Struct(field) => {
quote! {
impl #impl_generics HelloWorld for #name #ty_generics #where_clause {
fn hello_world(&self) {
println!("Hello, {}! My name is {}.", self.#field, stringify!(#name));
}
}
}
},
},
None => {
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
關鍵字來做DeriveInput
結構實體的data
欄位的型樣匹配,可針對不同的類型的結構體和列舉建立出不同的MyDeriveInput
結構實體,以產生出不同的Rust程式碼。
以上程式目前還只支援元組結構體,等等會再加強它,使它支援有欄位名稱的結構體以及列舉。在繼續加強之前,還有一個地方必須要注意,就是在以上程式中,我們有去使用proc_macro2::TokenStream
來避免需要用太多syn
提供的結構體來區分程式碼的型別,所以在Cargo.toml
的[dependencies]
區塊中要去引用proc_macro2
這個crate。
當我們熟悉了以上程式之後,就繼續來加強它吧!程式如下:
use std::str::FromStr;
use proc_macro::TokenStream;
use quote::{quote, ToTokens};
use syn::{
parse::{Parse, ParseStream},
parse_macro_input,
spanned::Spanned,
Data, DeriveInput, Fields, Ident, Index, Meta, Path,
};
#[proc_macro_derive(HelloWorld, attributes(helloWorld))]
pub fn hello_world_derive(input: TokenStream) -> TokenStream {
enum Target {
Struct(proc_macro2::TokenStream),
EnumUnnamedFields { variant_ident: Ident, pat_tuple: proc_macro2::TokenStream },
EnumNamedFields { variant_ident: Ident, ident: Ident },
}
struct MyDeriveInput {
ast: DeriveInput,
target: Option<Target>,
}
impl Parse for MyDeriveInput {
fn parse(input: ParseStream) -> Result<Self, syn::Error> {
let ast = input.parse::<DeriveInput>()?;
let mut target: Option<Target> = None;
match &ast.data {
Data::Struct(struct_data) => match &struct_data.fields {
Fields::Unnamed(unnamed_fields) => {
for (index, unnamed_field) in unnamed_fields.unnamed.iter().enumerate() {
for attr in unnamed_field.attrs.iter() {
if attr.path().is_ident("helloWorld") {
match &attr.meta {
Meta::List(list) => {
let path: Path = list.parse_args()?;
if path.is_ident("target") {
if target.is_some() {
return Err(syn::Error::new(
path.span(),
"`target` has been set before",
));
}
let field = Index::from(index).into_token_stream();
target = Some(Target::Struct(field));
} else {
return Err(syn::Error::new(
path.span(),
"should only be `target`",
));
}
},
meta => {
return Err(syn::Error::new(
meta.span(),
"the `helloWorld` attribute should have a list item",
));
},
}
}
}
}
},
Fields::Named(named_fields) => {
for named_field in named_fields.named.iter() {
for attr in named_field.attrs.iter() {
if attr.path().is_ident("helloWorld") {
match &attr.meta {
Meta::List(list) => {
let path: Path = list.parse_args()?;
if path.is_ident("target") {
if target.is_some() {
return Err(syn::Error::new(
path.span(),
"`target` has been set before",
));
}
let field =
named_field.ident.clone().into_token_stream();
target = Some(Target::Struct(field));
} else {
return Err(syn::Error::new(
path.span(),
"should only be `target`",
));
}
},
meta => {
return Err(syn::Error::new(
meta.span(),
"the `helloWorld` attribute should have a list item",
));
},
}
}
}
}
},
Fields::Unit => (),
},
Data::Enum(enum_data) => {
for variant in enum_data.variants.iter() {
for (index, field) in variant.fields.iter().enumerate() {
for attr in field.attrs.iter() {
if attr.path().is_ident("helloWorld") {
match &attr.meta {
Meta::List(list) => {
let path: Path = list.parse_args()?;
if path.is_ident("target") {
if target.is_some() {
return Err(syn::Error::new(
path.span(),
"`target` has been set before",
));
}
let variant_ident = variant.ident.clone();
match field.ident.as_ref() {
Some(ident) => {
target = Some(Target::EnumNamedFields {
variant_ident,
ident: ident.clone(),
});
},
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 =
proc_macro2::TokenStream::from_str(
pat_tuple.as_str(),
)?;
target = Some(Target::EnumUnnamedFields {
variant_ident,
pat_tuple,
});
},
}
} else {
return Err(syn::Error::new(
path.span(),
"should only be `target`",
));
}
},
meta => {
return Err(syn::Error::new(
meta.span(),
"the `helloWorld` attribute should have a list item",
));
},
}
}
}
}
}
},
_ => return Err(syn::Error::new(ast.span(), "unsupported type")),
}
Ok(MyDeriveInput { ast, target })
}
}
// Parse the token stream
let derive_input = parse_macro_input!(input as MyDeriveInput);
// Get the identifier of the type.
let name = &derive_input.ast.ident;
let (impl_generics, ty_generics, where_clause) = derive_input.ast.generics.split_for_impl();
// Build the code
let expanded = match derive_input.target {
Some(target) => match target {
Target::Struct(field) => {
quote! {
impl #impl_generics HelloWorld for #name #ty_generics #where_clause {
fn hello_world(&self) {
println!("Hello, {}! My name is {}.", self.#field, stringify!(#name));
}
}
}
},
Target::EnumUnnamedFields { variant_ident, pat_tuple } => {
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!()
}
}
}
}
},
Target::EnumNamedFields { variant_ident, 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 => {
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()
}
雖然程序式巨集寫起來有點費工夫,但就是屬於一次功,完成之後就有很方便的程序式巨集可以用了!
function-like 程序式巨集
程序式巨集除了能夠用在定義結構體、列舉時使用外,它還可以如宣告式巨集那樣做成用法像是函數的巨集。只要將proc_macro_derive
屬性替換成proc_macro
屬性就可以了!舉個簡單的例子:
use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, LitStr};
#[proc_macro]
pub fn hello(input: TokenStream) -> TokenStream {
// Parse the token stream
let input = parse_macro_input!(input as LitStr);
let s = input.value();
let output_sentence = format!("Hello, world! My name is `{s}`.");
// Build the code
let expanded = quote! {
#output_sentence
};
expanded.into()
}
上面這個程式建立了一個程序式巨集,巨集名稱為函數名稱,即hello
,它的功能非常近似於:
macro_rules! hello {
($s:literal) => {
concat!("Hello, world! My name is `", $s, "`.")
};
}
concat
是Rust內建的巨集,可以把輸入的任意數量的定數全部串接成字串定數。
程序式巨集hello
的用法如下:
fn main() {
println!(hello_world_macro::hello!("Pineapple"));
}
如果巨集的參數數量固定是1個,可以直接像上面的例子這樣使用syn
這個crate提供的parse_macro_input!
巨集和syn
內建的LitStr
等結構體或是列舉來解析token。而如果巨集的參數數量超過1個或是有使用記號的話,最好自行建立一個結構體,並實作syn::Parse
特性,再搭配parse_macro_input!
巨集來用。
寫法如下:
use proc_macro::TokenStream;
use quote::quote;
use syn::{
parse::{Parse, ParseStream},
parse_macro_input, LitStr, Token,
};
#[proc_macro]
pub fn hello(input: TokenStream) -> TokenStream {
struct MyInput {
v: Vec<String>,
}
impl Parse for MyInput {
fn parse(input: ParseStream) -> Result<Self, syn::Error> {
let mut v: Vec<String> = vec![];
loop {
if input.is_empty() {
break;
}
v.push(input.parse::<LitStr>()?.value());
if input.is_empty() {
break;
}
input.parse::<Token!(,)>()?;
}
Ok(MyInput { v })
}
}
// Parse the token stream
let input = parse_macro_input!(input as MyInput);
let output_sentence = format!("Hello, world! List: {:?}.", input.v);
// Build the code
let expanded = quote! {
#output_sentence
};
expanded.into()
}
不然的話,parse_macro_input!
巨集要改成如以下的寫法,但比較不自由。
use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, punctuated::Punctuated, LitStr, Token};
#[proc_macro]
pub fn hello(input: TokenStream) -> TokenStream {
// Parse the token stream
let input = parse_macro_input!(input with Punctuated::<LitStr, Token![,]>::parse_terminated);
let output_sentence = format!(
"Hello, world! List: {:?}.",
input.into_iter().map(|s| s.value()).collect::<Vec<String>>()
);
// Build the code
let expanded = quote! {
#output_sentence
};
expanded.into()
}
用於derive
屬性外的程序式巨集
上面介紹的用於屬性的程序式巨集必須要搭配derive
屬性一起使用,但其實我們也可以建立出獨立於derive
屬性的屬性,可以被用在任何的項目上,不限於結構體、列舉和聯合體,只是這種程式序巨集比較少會需要自己製作,通常是自行開發框架(framework)才會需要。它的撰寫方式如下:
use std::str::FromStr;
use proc_macro::TokenStream;
use quote::quote;
use syn::{
parse::{Parse, ParseStream},
parse_macro_input, Ident, ItemFn,
};
#[proc_macro_attribute]
pub fn hello(attr: TokenStream, item: TokenStream) -> TokenStream {
struct MyAttrInput {
uppercase: bool,
}
impl Parse for MyAttrInput {
fn parse(input: ParseStream) -> Result<Self, syn::Error> {
let mut uppercase = false;
if !input.is_empty() {
let ident = input.parse::<Ident>()?;
if ident == "uppercase" {
uppercase = true;
} else {
return Err(syn::Error::new(input.span(), "should use `uppercase`"));
}
}
Ok(MyAttrInput { uppercase })
}
}
// backup the item token stream
let item2: proc_macro2::TokenStream = item.clone().into();
// Parse the attr token stream
let attr_input = parse_macro_input!(attr as MyAttrInput);
// Parse the item token stream
let fn_input = parse_macro_input!(item as ItemFn);
let mut name = fn_input.sig.ident.to_string();
let hello_ident =
proc_macro2::TokenStream::from_str(format!("{}_hello", name).as_str()).unwrap();
if attr_input.uppercase {
name = name.to_uppercase();
}
let expanded = quote! {
fn #hello_ident() {
println!("Hello {}!", #name);
}
#item2
};
expanded.into()
}
以上程式建立了一個hello
屬性,可以作用在函數上,加上後會自動實作出一個名為函數名稱_hello
的函數,這個新函數可以印出Hello訊息。hello
屬性也可以在第一個參數傳入uppercase
,來讓印出的訊息中的函數名稱變成大寫。
這個hello
屬性的用法如下:
use hello_world_attribute::hello;
#[hello(uppercase)]
fn main() {
main_hello();
}
以上程式的執行結果如下:
結論
Rust的巨集功能十分強大,可以替我們省下不少撰寫相同或是相似的程式的時間。雖然會大大地增加程式碼的複雜度,但是如果有提供良好的文件的話,用到這些巨集的開發者們即便完全不知道這些巨集背後到底是怎麼實作的也沒關係。在Rust生態圈中,許多框架都會應用強大的巨集功能,有興趣想要更加熟悉用大量巨集來開發程式的這種風格的話,可以自行到crates.io上翻翻看。
下一章節,我們要來學習Rust程式語言的不安全設計方式!
下一章:不安全的Rust。