Rust 中的类属性宏(Attribute-like macros)和自定义派生宏(custom derive macros)用于在编译时接收一段 Rust 代码并以某种方式对其进行修改,通常是为了添加功能。
要想理解 Rust 中的类属性宏和自定义派生宏,我们首先需要简要介绍一下 Rust 中的结构体实现(implementation structs)。
结构体实现:impl
下面的结构体应该很容易理解。有趣的是,当我们创建对特定结构体进行操作的函数时,我们将使用 impl 来实现这一点:
struct Person {
name: String,
age: u8,
}
关联函数(Associated functions)和方法(methods)在 impl 块内为结构体实现。
关联函数可以类比为 Solidity 中为了与结构体交互而创建库(library)的场景。当我们定义 using lib for MyStruct 时,它允许我们使用 myStruct.associatedFunction() 语法。这使得函数可以通过 Self 关键字访问 myStruct。
我们建议使用 Rust Playground,但对于更复杂的示例,你可能需要配置你的 IDE。
让我们看下面的例子:
struct Person {
age: u8,
name: String,
}
// Implement a method `new()` for the `Person` struct, allowing initialization of a `Person` instance
impl Person {
// Create a new `Person` with the provided `name` and `age`
fn new(name: String, age: u8) -> Self {
Person { name, age }
}
fn can_drink(&self) -> bool {
if self.age >= 21 as u8 {
return true;
}
return false;
}
fn age_in_one_year(&self) -> u8 {
return &self.age + 1;
}
}
fn main() {
// Usage: Create a new `Person` instance with a name and age
let person = Person::new(String::from("Jesserc"), 19);
// use some impl functions
println!("{:?}", person.can_drink()); // false
println!("{:?}", person.age_in_one_year()); // 20
println!("{:?}", person.name);
}
用法:
// Usage: Create a new `Person` instance with a name and age
let person = Person::new(String::from("Jesserc"), 19);
// use some impl functions
person.can_drink(); // false
person.age_in_one_year(); // 20
Rust Traits(特征)
Rust 中的 trait 是一种在不同 impl 之间实现共享行为的方法。可以把它们想象成 Solidity 中的接口(interface)或抽象合约(abstract contract)——任何使用该接口的合约都必须实现特定的函数。
例如,假设我们需要定义一个 Car 和 Boat 结构体。我们想要附加一个方法,允许我们获取它们以公里/小时为单位的速度。在 Rust 中,我们可以通过使用单个 trait 并在两个结构体之间共享该方法来实现这一点。
如下所示:
// Traits are defined with the `trait` keyword followed by their name
trait Speed {
fn get_speed_kph(&self) -> f64;
}
// Car struct
struct Car {
speed_mph: f64,
}
// Boat struct
struct Boat {
speed_knots: f64,
}
// Traits are implemented for a type using the `impl` keyword as shown below
impl Speed for Car {
fn get_speed_kph(&self) -> f64 {
// Convert miles per hour to kilometers per hour
self.speed_mph * 1.60934
}
}
// We also implement the `Speed` trait for `Boat`
impl Speed for Boat {
fn get_speed_kph(&self) -> f64 {
// Convert knots to kilometers per hour
self.speed_knots * 1.852
}
}
fn main() {
// Initialize a `Car` and `Boat` type
let car = Car { speed_mph: 60.0 };
let boat = Boat { speed_knots: 30.0 };
// Get and print the speeds in kilometers per hour
let car_speed_kph = car.get_speed_kph();
let boat_speed_kph = boat.get_speed_kph();
println!("Car Speed: {} km/h", car_speed_kph); // 96.5604 km/h
println!("Boat Speed: {} km/h", boat_speed_kph); // 55.56 km/h
}
宏如何修改结构体
在我们关于类函数宏(function-like macros)的教程中,我们看到了宏如何在大型 Rust 代码中展开像 println!(...) 和 msg!(...) 这样的代码。在 Solana 的语境下,我们需要关注的其他类型的宏是类属性宏和派生宏。我们可以在 Anchor 创建的初始程序中看到这三种宏(类函数、类属性和派生):

为了对类属性宏的作用有更直观的了解,我们将创建两个宏:一个用于向结构体添加字段,另一个用于移除字段。
示例 1:类属性宏,插入字段
为了更好地理解 Rust 属性和宏的工作原理,我们将创建一个类属性宏,它将:
- 接收一个没有
foo和bar(类型为i32)字段的结构体 - 将这些字段插入到结构体中
- 创建一个
impl,其中包含一个名为double_foo的函数,该函数返回foo所持有的整数值的两倍。
设置
首先我们创建一个新的 Rust 项目:
cargo new macro-demo --lib
cd macro-demo
touch src/main.rs
将以下内容添加到 Cargo.toml 文件中:
[lib]
proc-macro = true
[dependencies]
syn = {version="1.0.57",features=["full","fold"]}
quote = "1.0.8"
创建主程序
将以下代码粘贴到 src/main.rs 中。请务必阅读注释内容:
// src/main.rs
// Import the macro_demo crate and bring all items into scope with the `*` wildcard
// (basically everything in this crate, including our macro in `src/lib.rs`
use macro_demo::*;
// Apply the `foo_bar_attribute` procedural attribute-like macro we created in `src/lib.rs` to `struct MyStruct`
// The procedural macro will generate a new struct definition with specified fields and methods
#[foo_bar_attribute]
struct MyStruct {
baz: i32,
}
fn main() {
// Create a new instance of `MyStruct` using the `default()` method
// This method is provided by the `Default` trait implementation generated by the macro
let demo = MyStruct::default();
// Print the contents of `demo` to the console
// The `Debug` trait implementation generated by the macro allows formatted output with `println!`
println!("struct is {:?}", demo);
// Call the `double_foo()` method on `demo`
// This method is generated by the macro and returns double the value of the `foo` field
let double_foo = demo.double_foo();
// Print the result of calling `double_foo` to the console
println!("double foo: {}", double_foo);
}
一些观察结果:
- 结构体
MyStruct内部没有foo字段。 - 函数
double_foo在上面的代码中没有定义在任何地方,我们假定它已经存在。
现在让我们创建这个类属性宏,它将在幕后修改 MyStruct。
将 src/lib.rs 中的代码替换为以下代码(请务必阅读注释内容):
// src/lib.rs
// Importing necessary external crates
extern crate proc_macro;
use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, ItemStruct};
// Declaring a procedural attribute-like macro using the `proc_macro_attribute` directive
// This makes the macro usable as an attribute
#[proc_macro_attribute]
// The function `foo_bar_attribute` takes two arguments:
// _metadata: The arguments provided to the macro (if any)
// _input: The TokenStream the macro is applied to
pub fn foo_bar_attribute(_metadata: TokenStream, _input: TokenStream) -> TokenStream {
// Parse the input TokenStream into an AST node representing a struct
let input = parse_macro_input!(_input as ItemStruct);
let struct_name = &input.ident; // Get the name of the struct
// Constructing the output TokenStream using the quote! macro
// The quote! macro allows for writing Rust code as if it were a string,
// but with the ability to interpolate values
TokenStream::from(quote! {
// Derive Debug trait for #struct_name to enable formatted output with `println()`
#[derive(Debug)]
// Defining a new struct #struct_name with two fields: foo and bar
struct #struct_name {
foo: i32,
bar: i32,
}
// Implementing the Default trait for #struct_name
// This provides a default() method to create a new instance of #struct_name
impl Default for #struct_name {
// The default method returns a new instance of #struct_name
// with foo set to 10 and bar set to 20
fn default() -> Self {
#struct_name { foo: 10, bar: 20}
}
}
impl #struct_name {
// Defining a method double_foo for #struct_name
// This method returns double the value of foo
fn double_foo(&self) -> i32 {
self.foo * 2
}
}
})
}
现在,为了测试我们的宏,我们使用 cargo run src/main.rs 来运行 main.rs 中的代码。
我们会得到以下输出:
struct is MyStruct { foo: 10, bar: 20 }
double foo: 20
示例 2:类属性宏,移除字段
理解类属性宏的最佳方式是:它们在修改结构体时拥有无限的权力。让我们重复上面的例子,但这一次,类属性宏将移除结构体中的所有字段。
将 src/lib.rs 替换为以下内容:
// src/lib.rs
// Importing necessary external crates
extern crate proc_macro;
use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, ItemStruct};
#[proc_macro_attribute]
pub fn destroy_attribute(_metadata: TokenStream, _input: TokenStream) -> TokenStream {
let input = parse_macro_input!(_input as ItemStruct);
let struct_name = &input.ident; // Get the name of the struct
TokenStream::from(quote! {
// This returns an empty struct with the same name
#[derive(Debug)]
struct #struct_name {
}
})
}
将 src/main.rs 替换为以下内容:
use macro_demo::*;
#[destroy_attribute]
struct MyStruct {
baz: i32,
qux: i32,
}
fn main() {
let demo = MyStruct { baz: 3, qux: 4 };
println!("struct is {:?}", demo);
}
当你尝试用 cargo run src/main.rs 编译它时,你会得到以下错误:

这看起来似乎很奇怪,因为该结构体明明有这些字段。然而,类属性宏把它们移除了!
#[derive(…)] 宏
#[derive(…)] 宏的功能远不如类属性宏强大。就我们的目的而言,派生宏是扩充结构体,而不是改变它。(虽然这不是一个精确的定义,但目前来说已经足够了)。
派生宏除其他功能外,还可以为结构体附加一个 impl。
例如,如果我们尝试执行以下操作:
struct Foo {
bar: i32,
}
pub fn main() {
let foo = Foo { bar: 3 };
println!("{:?}", foo);
}
这段代码无法编译,因为结构体是“不可打印的(printable)”。
要使它们变得可打印,它们需要一个带有着 fmt 函数的 impl,该函数返回结构体的字符串表示形式。
如果我们改为这样做:
#[derive(Debug)]
struct Foo {
bar: i32,
}
pub fn main() {
let foo = Foo { bar: 3 };
println!("{:?}", foo);
}
我们期望它会打印出:
Foo { bar: 3 }
derive 属性以某种方式“扩充”了 Foo,使得 println! 能够为它创建一个字符串表示形式。
总结
impl 是一组对结构体进行操作的函数。它们通过使用与结构体相同的名称“附加”到该结构体上。trait 强制要求 impl 实现某些特定的函数。在我们的例子中,我们使用 impl Speed for Car 语法将 Speed trait 附加到了 Car 的 impl 上。
类属性宏接收一个结构体并可以完全重写它。
派生宏则通过额外的函数来扩充结构体。
宏允许 Anchor 隐藏复杂性
让我们再来看看在使用 anchor init 期间 Anchor 创建的程序:

#[program] 属性在幕后修改了模块。例如,它实现了一个路由器,自动将传入的区块链指令定向到模块内相应的函数上。
结构体 Initialize {} 也被扩充了额外的功能,以便在 Solana 框架中使用。
总结
宏是一个非常庞大的主题。我们的目的是让你在看到 #[program] 或 #[derive(Accounts)] 时,能大概了解幕后发生了什么。如果感觉很陌生,请不要气馁。你不需要为了编写 Solana 程序而去学习如何编写宏。
然而,了解它们的作用,希望能让你所看到的程序不再那么神秘。
通过 RareSkills 了解更多
本教程是我们免费 Solana 课程的一部分。
首发于 2024 年 2 月 16 日