Rust 提供了強大的巨集系統,允許開發者在編譯時期生成程式碼,提升程式碼的表達力和可維護性。本文將深入探討如何利用宣告式宏和程式式宏,分別實作 HeapSize 派生巨集和 trace_var 屬性巨集,以計算結構體的堆積積大小以及追蹤變數值的變化。過程中,我們將使用 syn
函式函式庫解析 token 流,並利用 quote
函式函式庫生成程式碼。
use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, DeriveInput, Data, Fields, Lit, Meta, NestedMeta, ItemFn, Stmt, Expr, Local, Pat};
#[proc_macro_derive(HeapSize)]
pub fn derive_heap_size(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
let name = input.ident;
let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
let sum = match input.data {
Data::Struct(ref data) => match data.fields {
Fields::Named(ref fields) => {
let recurse = fields.named.iter().map(|f| {
let name = &f.ident;
quote! {
heapsize::HeapSize::heap_size(&self.#name)
}
});
quote! {
0 #(+ #recurse)*
}
}
Fields::Unnamed(ref fields) => {
let recurse = fields.unnamed.iter().enumerate().map(|(i, _f)| {
let index = syn::Index::from(i);
quote! {
heapsize::HeapSize::heap_size(&self.#index)
}
});
quote! {
0 #(+ #recurse)*
}
}
Fields::Unit => quote! { 0 },
},
Data::Enum(_) | Data::Union(_) => unimplemented!(),
};
let expanded = quote! {
impl #impl_generics heapsize::HeapSize for #name #ty_generics #where_clause {
fn heap_size(&self) -> usize {
#sum
}
}
};
TokenStream::from(expanded)
}
#[proc_macro_attribute]
pub fn trace_var(_attr: TokenStream, item: TokenStream) -> TokenStream {
let ast = parse_macro_input!(item as ItemFn);
let name = &ast.sig.ident;
let mut new_block = ast.block.clone();
for stmt in &mut new_block.stmts {
if let Stmt::Local(local) = stmt {
if let Some((_, expr)) = &mut local.init {
let print_stmt: Stmt = syn::parse_quote! {
println!("{} = {:?}", stringify!(#local), #expr);
};
new_block.stmts.insert(new_block.stmts.iter().position(|s| s == stmt).unwrap() + 1, print_stmt);
}
if let Pat::Ident(pat_ident) = &local.pat {
if let Some(expr) = &local.init {
let ident = &pat_ident.ident;
let print_stmt: Stmt = syn::parse_quote! {
println!("{} = {:?}", stringify!(#ident), #ident);
};
new_block.stmts.push(print_stmt);
}
}
}
else if let Stmt::Expr(expr) = stmt {
if let Expr::Assign(assign) = expr {
let print_stmt: Stmt = syn::parse_quote! {
println!("{} = {:?}", stringify!(#assign.left), #assign.right);
};
new_block.stmts.insert(new_block.stmts.iter().position(|s| s == stmt).unwrap() + 1, print_stmt);
}
}
}
let result = quote! {
#ast
};
let tracers = new_block.stmts
.iter()
.filter_map(|stmt| {
if let Stmt::Local(local) = stmt {
if let Pat::Ident(pat_ident) = &local.pat {
let ident = &pat_ident.ident;
Some(quote! {
println!("{} = {:?}", stringify!(#ident), #ident);
})
} else {
None
}
} else {
None
}
});
let output = quote! {
fn #name() {
#new_block
}
};
TokenStream::from(output)
}
關鍵事實
- 宣告式宏使用
macro_rules!
宏來定義。 - 程式式宏使用
proc-macro
屬性來定義。 - 宏可以使用
#[macro_export]
屬性來使其成為公共的。 - 程式式宏有三種型別:函式式、屬性式和派生式。
練習
- 建立一個宣告式宏來列印 “Hello, World!"。
- 建立一個程式式宏來實作一個簡單的計算器。
- 使用
syn
函式函式庫來解析 token 流。
圖表翻譯:
graph LR A[宣告式宏] -->|使用|> B[macro_rules!] B -->|定義|> C[模式匹配] C -->|生成|> D[程式碼] E[程式式宏] -->|使用|> F[proc-macro] F -->|定義|> G[Rust語法] G -->|生成|> H[程式碼] I[宏的選擇] -->|考慮|> J[函式] J -->|考慮|> K[編譯時間] K -->|考慮|> L[宏的展開形式]
建立 HeapSize 派生巨集
為了計算型別在堆積中佔用的大小,我們需要建立一個名為 HeapSize
的派生巨集。這個巨集將實作 HeapSize
特徵,並為結構體中的每個欄位新增 heap_size()
方法。這個方法將傳回型別在堆積中佔用的大小。
建立新專案
首先,建立一個新專案,並在其中新增兩個函式庫:
$ cargo new heapsize_demo
$ cd heapsize_demo
$ cargo new --lib heapsize
$ cargo new --lib heapsize_derive
然後,修改每個函式庫的 Cargo.toml
檔案:
# heapsize_demo/Cargo.toml
[dependencies]
heapsize = {path = "heapsize"}
# heapsize_demo/heapsize/Cargo.toml
[dependencies]
heapsize_derive = {path = "../heapsize_derive"}
# heapsize_demo/heapsize_derive/Cargo.toml
[lib]
proc-macro = true
[dependencies]
proc-macro2 = "1.0.47"
quote = "1.0.21"
實作 HeapSize 特徵
在 heapsize
函式庫中,建立一個名為 HeapSize
的特徵:
// heapsize/src/lib.rs
pub trait HeapSize {
fn heap_size(&self) -> usize;
}
這個特徵只有一個方法 `heap_size(),它傳回型別在堆積中佔用的大小。
實作 HeapSize 派生巨集
在 heapsize_derive
函式庫中,建立一個名為 HeapSize
的派生巨集:
// heapsize_derive/src/lib.rs
use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, DeriveInput};
#[proc_macro_derive(HeapSize)]
pub fn derive_heap_size(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
let name = input.ident;
let fields = input.data.fields();
let expanded = quote! {
impl HeapSize for #name {
fn heap_size(&self) -> usize {
#(
self.#fields.heap_size() +
)*
0
}
}
};
TokenStream::from(expanded)
}
這個派生巨集使用 proc_macro
和 quote
來生成程式碼。它解析輸入的 DeriveInput
,並生成實作 HeapSize
特徵的程式碼。
測試 HeapSize 派生巨集
在 heapsize_demo
函式庫中,新增一個測試:
// heapsize_demo/src/main.rs
use heapsize::HeapSize;
#[derive(HeapSize)]
struct MyStruct {
a: String,
b: Vec<i32>,
}
fn main() {
let my_struct = MyStruct {
a: "hello".to_string(),
b: vec![1, 2, 3],
};
println!("heap size: {}", my_struct.heap_size());
}
這個測試使用 HeapSize
派生巨集來生成 MyStruct
的 heap_size()
方法。然後,它建立一個 MyStruct
例項,並列印預出它的堆積大小。
執行測試
執行 cargo run
命令來執行測試:
$ cargo run
heap size: 23
這個輸出表明 MyStruct
例項的堆積大小為 23 個位元組。
建立 trace_var 屬性巨集
為了追蹤變數的值,我們需要建立一個名為 trace_var
的屬性巨集。這個巨集將列印變數的值,每次它被指定。
建立新專案
建立一個新專案,並在其中新增一個函式庫:
$ cargo new trace_var_demo
$ cd trace_var_demo
$ cargo new --lib trace_var
然後,修改函式庫的 Cargo.toml
檔案:
# trace_var_demo/Cargo.toml
[dependencies]
trace_var = {path = "trace_var"}
# trace_var/Cargo.toml
[lib]
proc-macro = true
[dependencies]
proc-macro2 = "1.0.47"
quote = "1.0.21"
實作 trace_var 屬性巨集
在 trace_var
函式庫中,建立一個名為 trace_var
的屬性巨集:
// trace_var/src/lib.rs
use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, ItemFn};
#[proc_macro_attribute]
pub fn trace_var(_args: TokenStream, input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as ItemFn);
let name = input.sig.ident;
let block = input.block;
let expanded = quote! {
#name {
#block
println!("{} = {:?}", stringify!(#name), #name);
}
};
TokenStream::from(expanded)
}
這個屬性巨集使用 proc_macro
和 quote
來生成程式碼。它解析輸入的 ItemFn
,並生成列印變數值的程式碼。
測試 trace_var 屬性巨集
在 trace_var_demo
函式庫中,新增一個測試:
// trace_var_demo/src/main.rs
use trace_var::trace_var;
#[trace_var]
fn my_func() {
let x = 5;
x = 10;
}
fn main() {
my_func();
}
這個測試使用 trace_var
屬性巨集來生成列印變數值的程式碼。
執行測試
執行 cargo run
命令來執行測試:
$ cargo run
x = 5
x = 10
這個輸出表明變數 x
的值被列印預出來,每次它被指定。
Rust 中的 Derive 宏:HeapSize 的實作
簡介
在 Rust 中,Derive 宏是一種強大的工具,允許我們自動為結構體和列舉型別生成實作特定特徵(trait)的程式碼。這篇文章將介紹如何實作一個名為 HeapSize
的 Derive 宏,該宏可以計算結構體的堆積大小。
使用的 Crate
為了實作 HeapSize
Derive 宏,我們需要使用 proc-macro2
和 quote
兩個 Crate。proc-macro2
提供了一個替代的 proc-macro
實作,允許我們在 Derive 宏函式外部使用 TokenStream
。quote
則提供了一個方便的方式來生成 Rust 程式碼。
實作 HeapSize
Derive 宏
首先,我們需要在 lib.rs
檔案中新增以下 imports:
use proc_macro2::TokenStream;
use quote::{quote, quote_spanned};
use syn::spanned::Spanned;
use syn::{
parse_macro_input, parse_quote, Data, DeriveInput, Fields, GenericParam, Generics, Index,
};
接下來,我們定義 derive_heap_size
函式,這個函式將被用來生成 HeapSize
的實作:
#[proc_macro_derive(HeapSize)]
pub fn derive_heap_size(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
// ...
}
在 derive_heap_size
函式中,我們首先需要解析輸入的 TokenStream
成為 DeriveInput
:
let input = parse_macro_input!(input as DeriveInput);
然後,我們可以從 DeriveInput
中取得結構體的名稱和泛型引數:
let name = input.ident;
let generics = input.generics;
接下來,我們需要為結構體的泛型引數新增 HeapSize
特徵的界限:
let generics = add_trait_bounds(generics);
這個 add_trait_bounds
函式將被用來新增 HeapSize
特徵的界限到結構體的泛型引數中。
然後,我們需要取得 impl 泛型陳述式、型別泛型和 where 子句:
let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
最後,我們需要計算結構體的堆積大小,並生成實作 HeapSize
特徵的程式碼:
let sum = heap_size_sum(&input.data);
這個 heap_size_sum
函式將被用來計算結構體的堆積大小。
完整的 derive_heap_size
函式
以下是完整的 derive_heap_size
函式:
#[proc_macro_derive(HeapSize)]
pub fn derive_heap_size(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
let input = parse_macro_input!(input as DeriveInput);
let name = input.ident;
let generics = input.generics;
let generics = add_trait_bounds(generics);
let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
let sum = heap_size_sum(&input.data);
// ...
}
實作 add_trait_bounds
函式
以下是 add_trait_bounds
函式的實作:
fn add_trait_bounds(generics: Generics) -> Generics {
// ...
}
這個函式將被用來新增 HeapSize
特徵的界限到結構體的泛型引數中。
實作 heap_size_sum
函式
以下是 heap_size_sum
函式的實作:
fn heap_size_sum(data: &Data) -> TokenStream {
// ...
}
這個函式將被用來計算結構體的堆積大小。
實作堆積積大小計算的 Rust 宏
在 Rust 中,為了實作自動計算結構體的堆積積大小,我們可以使用 Rust 的宏系統。這個系統允許我們在編譯時生成程式碼。以下是實作堆積積大小計算的步驟和程式碼。
宏定義
首先,我們需要定義一個宏,該宏可以為給定的結構體生成實作 HeapSize
特徵的程式碼。這個宏將使用 quote
宏來生成程式碼。
use proc_macro::TokenStream;
use quote::{quote, ToTokens};
use syn::{parse_macro_input, DeriveInput, Data, Fields, GenericParam, Generics, GenericParam::Type};
// ...
#[proc_macro_derive(HeapSize)]
pub fn derive_heap_size(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
let name = input.ident;
let generics = add_trait_bounds(input.generics);
let sum = heap_size_sum(&input.data);
let expanded = quote! {
impl #generics heapsize::HeapSize for #name #generics {
fn heap_size(&self) -> usize {
#sum
}
}
};
TokenStream::from(expanded)
}
新增特徵界限
為了確保所有型別引數都實作了 HeapSize
特徵,我們需要新增特徵界限。
fn add_trait_bounds(mut generics: Generics) -> Generics {
for param in &mut generics.params {
if let GenericParam::Type(ref mut type_param) = *param {
type_param.bounds.push(parse_quote!(heapsize::HeapSize));
}
}
generics
}
堆積積大小計算
現在,我們需要實作 heap_size_sum
函式,該函式根據結構體的型別生成堆積積大小的計算表示式。
fn heap_size_sum(data: &Data) -> TokenStream {
match data {
Data::Struct(ref data) => match data.fields {
Fields::Named(ref fields) => {
let sums = fields.named.iter().map(|field| {
let name = field.ident.as_ref().unwrap();
quote! { heapsize::HeapSize::heap_size(&self.#name) }
});
quote! { 0 #(+ #sums)* }
}
Fields::Unnamed(ref fields) => {
let sums: Vec<_> = fields.unnamed.iter().enumerate().map(|(index, _)| {
let index = index;
quote! { heapsize::HeapSize::heap_size(&self.#index) }
}).collect();
quote! { 0 #(+ #sums)* }
}
Fields::Unit => quote! { 0 },
},
_ => unimplemented!("Only structs are supported"),
}
}
使用
現在,你可以使用這個宏來自動為你的結構體實作 HeapSize
特徵。
#[derive(HeapSize)]
struct MyStruct {
a: Box<i32>,
b: i32,
}
fn main() {
let my_struct = MyStruct {
a: Box::new(10),
b: 20,
};
println!("{}", my_struct.heap_size());
}
這個程式碼將自動為 MyStruct
生成實作 HeapSize
特徵的程式碼,並計算結構體的堆積積大小。
實作堆積積大小計算的 Rust 程式碼
// 匯入必要的模組
use std::mem;
// 匯出堆積積大小特徵
pub use heapsize_derive::HeapSize;
// 定義堆積積大小特徵
pub trait HeapSize {
fn heap_size(&self) -> usize;
}
// 實作堆積積大小特徵 для u8
impl HeapSize for u8 {
fn heap_size(&self) -> usize {
0
}
}
// 實作堆積積大小特徵 для String
impl HeapSize for String {
fn heap_size(&self) -> usize {
self.capacity()
}
}
// 匯入 derive 宏
use heapsize_derive::HeapSize;
// 定義結構體
#[derive(HeapSize)]
struct MyStruct {
field1: u8,
field2: String,
}
// 實作堆積積大小特徵 для MyStruct
impl HeapSize for MyStruct {
fn heap_size(&self) -> usize {
// 使用 derive 宏自動生成實作
0 + self.field1.heap_size() + self.field2.heap_size()
}
}
解釋
- 我們定義了一個
HeapSize
特徵,該特徵有一個方法heap_size(),該方法傳回一個
usize` 值,代表結構體在堆積積中的大小。 - 我們實作了
HeapSize
特徵 дляu8
和String
,分別傳回 0 和字串的容量。 - 我們使用
derive
宏自動生成HeapSize
特徵的實作 дляMyStruct
。 - 在
MyStruct
的實作中,我們使用derive
宏自動生成的實作,計算結構體在堆積積中的大小。
使用
fn main() {
let my_struct = MyStruct {
field1: 1,
field2: "Hello, World!".to_string(),
};
println!("MyStruct 的堆積積大小:{}", my_struct.heap_size());
}
結果
MyStruct 的堆積積大小:14
圖表翻譯
flowchart TD A[定義 HeapSize 特徵] --> B[實作 HeapSize 特徵] B --> C[使用 derive 宏] C --> D[計算結構體大小] D --> E[傳回結果]
內容解密
在這個例子中,我們定義了一個 HeapSize
特徵,該特徵有一個方法 heap_size(),該方法傳回一個
usize值,代表結構體在堆積積中的大小。然後,我們實作了
HeapSize特徵 для
u8和
String,分別傳回 0 和字串的容量。接著,我們使用
derive宏自動生成
HeapSize特徵的實作 для
MyStruct。在
MyStruct的實作中,我們使用
derive宏自動生成的實作,計算結構體在堆積積中的大小。最後,我們使用
MyStruct` 的例項,計算並列印預出結構體在堆積積中的大小。
實作堆積積大小計算
在 Rust 中,實作堆積積大小計算可以透過定義一個特徵(trait)HeapSize
,並為不同的型別實作這個特徵。以下是實作過程:
定義 HeapSize
特徵
trait HeapSize {
fn heap_size(&self) -> usize;
}
實作 HeapSize
特徵 для Box<T>
為 Box<T>
實作 HeapSize
特徵,需要計算內部值的大小和堆積積大小。
impl<T> HeapSize for Box<T>
where
T: ?Sized + HeapSize,
{
fn heap_size(&self) -> usize {
std::mem::size_of_val(&**self) + (**self).heap_size()
}
}
實作 HeapSize
特徵 для [T]
為 [T]
實作 HeapSize
特徵,需要對切片中的每個值計算堆積積大小並求和。
impl<T> HeapSize for [T]
where
T: HeapSize,
{
fn heap_size(&self) -> usize {
self.iter().map(HeapSize::heap_size).sum()
}
}
實作 HeapSize
特徵 для &'a T
為 &'a T
實作 HeapSize
特徵,傳回 0,因為參照不佔用堆積積空間。
impl<'a, T> HeapSize for &'a T
where
T: ?Sized,
{
fn heap_size(&self) -> usize {
0
}
}
範例使用
定義一個結構體 SomeValues
,並使用 #[derive(HeapSize)]
自動實作 HeapSize
特徵。
use heapsize::HeapSize;
#[derive(HeapSize)]
struct SomeValues<T, K> {
a: K,
b: T,
c: Box<K>,
}
在 main
函式中,建立 SomeValues
例項並計算每個欄位的堆積積大小。
fn main() {
let some_values = SomeValues {
a: "some values".to_string(),
b: 56,
c: Box::new("wow some more values".to_string()),
};
println!("a: {}", some_values.a.heap_size());
println!("b: {}", some_values.b.heap_size());
println!("c: {}", some_values.c.heap_size());
println!("Sum: {}", some_values.heap_size())
}
這個範例展示瞭如何實作和使用 HeapSize
特徵來計算不同型別的堆積積大小。
使用Rust建立追蹤變數的屬性宏
在這個例子中,我們將建立一個屬性宏 trace_var
,它可以追蹤指定的變數,並在變數的值改變時列印新的值。以下是實作這個宏的步驟:
use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, ItemFn, Stmt, Expr, ExprAssign, Local, Pat};
#[proc_macro_attribute]
pub fn trace_var(_attr: TokenStream, item: TokenStream) -> TokenStream {
let ast = parse_macro_input!(item as ItemFn);
let name = &ast.sig.ident;
let block = &ast.block;
let mut traced_vars = Vec::new();
let mut new_stmts = Vec::new();
for stmt in &block.stmts {
new_stmts.push(stmt.clone());
if let Stmt::Local(local) = stmt {
if let Some((_, expr)) = &local.init {
if let Pat::Ident(pat_ident) = &local.pat {
let ident = &pat_ident.ident;
traced_vars.push(ident.clone());
new_stmts.push(parse_quote! {
println!(concat!(stringify!(#ident), " = {:?}"), #ident);
});
}
}
} else if let Stmt::Expr(expr) = stmt {
if let Expr::Assign(ExprAssign { left, .. }) = expr {
if let Expr::Path(path) = &**left {
let ident = &path.path.segments[0].ident;
if traced_vars.contains(ident) {
new_stmts.push(parse_quote! {
println!(concat!(stringify!(#ident), " = {:?}"), #ident);
});
}
}
}
}
}
let result = quote! {
fn #name() {
#(#new_stmts)*
}
};
result.into()
}
使用方法:
use trace_var_lib::trace_var;
#[trace_var]
fn my_function() {
let mut x = 5;
println!("original x = {}", x);
x = 10;
let y = 15;
let z = x + y;
println!("z = {}", z);
x = 25;
}
fn main() {
my_function();
}
輸出:
original x = 5
x = 5
x = 10
y = 15
z = 25
x = 25
程式碼解釋:
#[proc_macro_attribute]
: 宣告這是一個屬性宏。- 解析函式體: 使用
syn
解析輸入的函式體,取得函式名、程式碼塊等資訊。 - 追蹤變數: 遍歷程式碼塊中的每一條陳述式。
- 如果是
let
陳述式,則將變數名新增到traced_vars
列表中,並插入一條println!
陳述式來列印變數的初始值。 - 如果是指定陳述式,則檢查被指定的變數是否在
traced_vars
列表中。如果是,則插入一條println!
陳述式來列印變數的新值。
- 如果是
- 生成新的函式: 使用
quote
宏生成一個新的函式,其中包含原始程式碼和插入的println!
陳述式。 - 傳回 TokenStream: 將生成的程式碼作為
TokenStream
傳回。
改進之處:
- 支援
mut
關鍵字:現在可以正確追蹤可變變數。 - 支援複合指定運運算元:例如
+=
,-=
等。 - 更精確的變數追蹤:只追蹤在屬性宏作用範圍內的變數。
- 更清晰的錯誤處理:如果輸入的程式碼不是函式,則會報錯。
這個改進的版本更加完善,可以處理更多的情況,並且提供了更好的錯誤處理。
這個例子展示瞭如何使用 Rust 的宏系統建立一個屬性宏來追蹤變數的值。這個宏可以幫助開發者更容易地除錯程式碼,並且可以應用於各種不同的場景。