Rust宏可以極大的簡化編寫的難度,學習好宏可以更好的減少冗餘程式碼。
宏的基本概念
Rust中的宏可以分為兩大類:宣告宏(Declarative Macros)和過程宏(Procedural Macros)。
- 宣告宏:也稱為
macro_rules!
宏,使用macro_rules!
關鍵字定義。它是一種基於模式匹配的文字替換宏,類似於C語言中的宏定義。宣告宏在編譯期展開,用匹配的程式碼片段替換宏呼叫處的程式碼。 - 過程宏:是一種更為高階的宏,它透過編寫Rust程式碼來處理輸入的程式碼,並在編譯期間生成新的程式碼。過程宏主要用於屬性宏(Attribute Macros)、類函式宏(Function-Like Macros)和派生宏(Derive Macros)等場景。
宏的實際應用
- 宣告宏在Rust中的應用,我們最常接觸的宏定義
vec!
或者println!
都是標準庫裡提供的,他可以在編譯階段就進行宏展開,在一定程度上犧牲編譯速度有錯誤及時發現從而保證程式執行穩定。 - 過程宏在Rust中也是極為常見,就比如某個類,我們需要clone方法,但是宣告的類並不支援clone,那麼我們就可以在此類宣告
derive(Clone)
如果需要預設的構造方法,那麼同樣可以宣告derive(Default)
#[derive(Clone, Default)]
struct HcluaMacro {
field: u32,
}
此時我們就可以使用:
let obj = HcluaMacro::default();
let obj_clone = obj.clone();
類似的還要在序列化的宏等。
過程宏的實戰
目錄為Rust中的lua庫hclua
做物件的繫結,可以快速的實現Rust物件在Lua中的快速使用繫結。
新建庫
由於過程宏只能在單獨的庫中使用,所以此時我們需要新建單獨的一個專案cargo new hclua-macro
,並在新專案的Cargo.toml中新增
[lib]
proc-macro = true
宣告該專案為過程宏專案。
定義宏ObjectMacro
首先我們得定義ObjectMacro
宏,那麼我們需要宣告:
#[proc_macro_derive(ObjectMacro)]
pub fn object_macro_derive(input: TokenStream) -> TokenStream {
TokenStream::new()
}
此處我們就可以在這基礎上實現額外的程式碼,他將在宣告該宏檔案中自動新增程式碼。
我們做以下測試:
#[proc_macro_derive(ObjectMacro)]
pub fn object_macro_derive(input: TokenStream) -> TokenStream {
quote! {
fn this_is_macro_auto() {
println!("this_is_macro_auto auto func");
}
}.into()
}
其中quote!
可以快速的生成程式碼塊。
展開宏cargo-expand
接下我們需要宏在這個過期中幫我們生成了什麼,我們藉助以下工具cargo-expand
,透過cargo install cargo-expand
進行安裝。
此時用cargo expand
可以發現宏展開後的程式碼如下:
#![feature(prelude_import)]
#[prelude_import]
use std::prelude::rust_2021::*;
#[macro_use]
extern crate std;
use hclua_macro::ObjectMacro;
struct HcluaMacro {
field: u32,
}
fn this_is_macro_auto() {
{
::std::io::_print(format_args!("this_is_macro_auto auto func\n"));
};
}
fn main() {
this_is_macro_auto();
}
此時我們並沒有處理跟類相關的任何東西,我們可以用parse_macro_input!
將輸入轉成ItemStruct
或者DeriveInput
#[proc_macro_derive(ObjectMacro)]
pub fn object_macro_derive(input: TokenStream) -> TokenStream {
let ItemStruct {
ident,
fields,
attrs,
..
} = parse_macro_input!(input);
let name = ident.to_string();
quote! {
fn this_is_macro_auto() {
println!("struct name {}", #name);
}
}.into()
}
在quote中可以用#來序列化局數的變數資料。那麼此時我們執行程式,將會輸出:
struct name HcluaMacro
類名正確的被列印出來。
欄位處理
定義
pub struct Field {
pub attrs: Vec<Attribute>,
pub vis: Visibility,
pub mutability: FieldMutability,
pub ident: Option<Ident>,
pub colon_token: Option<Token![:]>,
pub ty: Type,
}
vis
表示是否公開,就是表示pub
或者pub(super)
orpub(crate)
orpub(in some::module)
或者不公開模式attrs
表示在該欄位上的各種屬性mutability
表示是否可編輯ident
變數的名字,當enum
時只有型別沒有名字
我們就可以透過處理變數的各種情況然後進行操作,比如新增get_#ident
或者set_#ident
等方法。
屬性處理
在此處我們定義了兩種屬性名稱,hclua_field
及 hclua_cfg
,一種配置名稱,一種配置是否可以在Lua中直接訪問的欄位名稱,此時的宏定義:
#[proc_macro_derive(ObjectMacro, attributes(hclua_field, hclua_cfg))]
pub fn object_macro_derive(input: TokenStream) -> TokenStream {
}
如果沒有在此處定義的attrib,在型別裡直接新增會報編譯錯誤。
此處我們判斷是否為hclua_field
欄位進行相應的加工。
let functions: Vec<_> = fields
.iter()
.map(|field| {
let field_ident = field.ident.clone().unwrap();
if field.attrs.iter().any(|attr| attr.path().is_ident("hclua_field")) {
quote! {}
} else {
quote! {}
}
})
.collect();
接下來將自動實現get及set方法。此處functions為TokenStream的陣列,我們將用
#(#functions)*
將此部分內容做展開。
完整宏程式碼:
use proc_macro::TokenStream;
use quote::{format_ident, quote};
use syn::{self, ItemStruct};
use syn::parse_macro_input;
#[proc_macro_derive(ObjectMacro, attributes(hclua_field, hclua_cfg))]
pub fn object_macro_derive(input: TokenStream) -> TokenStream {
let ItemStruct {
ident,
fields,
attrs,
..
} = parse_macro_input!(input);
let functions: Vec<_> = fields
.iter()
.map(|field| {
let field_ident = field.ident.clone().unwrap();
if field.attrs.iter().any(|attr| attr.path().is_ident("hclua_field")) {
let get_name = format_ident!("get_{}", field_ident);
let set_name = format_ident!("set_{}", field_ident);
let ty = field.ty.clone();
quote! {
fn #get_name(&mut self) -> &#ty {
&self.#field_ident
}
fn #set_name(&mut self, val: #ty) {
self.#field_ident = val;
}
}
} else {
quote! {}
}
})
.collect();
let name = ident.to_string();
quote! {
fn this_is_macro_auto() {
println!("struct name {}", #name);
}
impl #ident {
#(#functions)*
}
}.into()
}
將示例程式碼進行如下書寫:
use hclua_macro::ObjectMacro;
#[derive(ObjectMacro)]
struct HcluaMacro {
#[hclua_field]
field: u32,
not_field: u32,
}
fn main() {
this_is_macro_auto();
}
透過cargo expand
將得到如下的程式碼:
#![feature(prelude_import)]
#[prelude_import]
use std::prelude::rust_2021::*;
#[macro_use]
extern crate std;
use hclua_macro::ObjectMacro;
struct HcluaMacro {
#[hclua_field]
field: u32,
not_field: u32,
}
fn this_is_macro_auto() {
{
::std::io::_print(format_args!("struct name {0}\n", "HcluaMacro"));
};
}
impl HcluaMacro {
fn get_field(&mut self) -> &u32 {
&self.field
}
fn set_field(&mut self, val: u32) {
self.field = val;
}
}
fn main() {
this_is_macro_auto();
}
自動實現了get及set方法,符合我們的要求。
注意事項
- 學習曲線:難度相對較高,需要理解
block
,expr
,ident
,item
,literal
,pat
,path
,stmt
,tt
,ty
,vis
等相關內容。 - 除錯難度:由於宏是在編譯時執行的,因此除錯起來可能比較困難。對於嚴重依賴除錯會相對吃力。
- 濫用風險:雖然宏提供了強大的程式碼生成能力,但濫用宏也可能導致程式碼難以理解和維護。因此,在使用宏時儘量的做好規劃及說明。