原文標題:Macros in Rust: A tutorial with examples
原文連接:https://blog.logrocket.com/macros-in-rust-a-tutorial-with-examples/
公衆號: Rust 碎碎念
翻譯 by: Prayinghtml
在本文中,咱們將會涵蓋你須要瞭解的關於 Rust 宏(macro)的一切,包括對 Rust 宏的介紹和如何使用 Rust 宏的示例。
git
咱們會涵蓋如下內容:github
Rust 宏是什麼?web
Rust 宏的類型正則表達式
Rust 宏的聲明express
Rust 中的過程宏編程
Rust 對宏(macro)有着很是好的支持。宏可以使得你可以經過寫代碼的方式來生成代碼,這一般被稱爲元編程(metaprogramming)。
curl
宏提供了相似函數的功能,可是沒有運行時開銷。可是,由於宏會在編譯期進行展開(expand),因此它會有一些編譯期的開銷。
編輯器
Rust 宏很是不一樣於 C 裏面的宏。Rust 宏會被應用於詞法樹(token tree),而 C 語言裏的宏則是文本替換。ide
Rust 有兩種類型的宏:
聲明式宏(Declarative macros)使得你可以寫出相似 match 表達式的東西,來操做你所提供的 Rust 代碼。它使用你提供的代碼來生成用於替換宏調用的代碼。
過程宏(Procedural macros)容許你操做給定 Rust 代碼的抽象語法樹(abstract syntax tree, AST)。過程宏是從一個(或者兩個)TokenStream
到另外一個TokenStream
的函數,用輸出的結果來替換宏調用。
讓咱們來看一下聲明式宏和過程宏的更多細節,並討論一些關於如何在 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 語言自己的任務。
讓咱們建立一個宏把咱們到目前爲止講過的全部概念融合起來,經過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 宏自己有關,有些則是聲明式宏所特有的:
缺乏對宏的自動完成和展開的支持
聲明式宏調式困難
修改能力有限
更大的二進制
更長的編譯時間(這一條對於聲明式宏和過程宏都存在)