Rust宏之derive的設計及實戰

问蒙服务框架發表於2024-10-18

Rust宏可以極大的簡化編寫的難度,學習好宏可以更好的減少冗餘程式碼。

宏的基本概念

Rust中的宏可以分為兩大類:宣告宏(Declarative Macros)和過程宏(Procedural Macros)。

  1. 宣告宏:也稱為macro_rules!宏,使用macro_rules!關鍵字定義。它是一種基於模式匹配的文字替換宏,類似於C語言中的宏定義。宣告宏在編譯期展開,用匹配的程式碼片段替換宏呼叫處的程式碼。
  2. 過程宏:是一種更為高階的宏,它透過編寫Rust程式碼來處理輸入的程式碼,並在編譯期間生成新的程式碼。過程宏主要用於屬性宏(Attribute Macros)、類函式宏(Function-Like Macros)和派生宏(Derive Macros)等場景。

宏的實際應用

  1. 宣告宏在Rust中的應用,我們最常接觸的宏定義vec!或者println!都是標準庫裡提供的,他可以在編譯階段就進行宏展開,在一定程度上犧牲編譯速度有錯誤及時發現從而保證程式執行穩定。
  2. 過程宏在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,
}
  1. vis表示是否公開,就是表示pub或者pub(super) or pub(crate) or pub(in some::module)或者不公開模式
  2. attrs表示在該欄位上的各種屬性
  3. mutability表示是否可編輯
  4. 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方法,符合我們的要求。

注意事項

  1. 學習曲線:難度相對較高,需要理解block,expr, ident, item, literal, pat, path, stmt, tt, ty, vis等相關內容。
  2. 除錯難度:由於宏是在編譯時執行的,因此除錯起來可能比較困難。對於嚴重依賴除錯會相對吃力。
  3. 濫用風險:雖然宏提供了強大的程式碼生成能力,但濫用宏也可能導致程式碼難以理解和維護。因此,在使用宏時儘量的做好規劃及說明。

相關文章