跳到主要内容

Rust 结构体和类似属性以及自定义派生宏

更新日期:Feb 20

Rust 属性和自定义派生宏

在 Rust 中,类似属性和自定义派生宏用于在编译时获取一段 Rust 代码并以某种方式修改它,通常是为了添加功能。

为了理解 Rust 中的类属性宏和自定义派生宏,我们首先需要简单介绍一下 Rust 中的实现结构 —— impl

用于结构体的实现:impl

以下结构体应该很容易理解。有趣的是当我们创建在特定结构体上操作的函数时。我们使用 impl 来实现这一点:

struct Person {
name: String,
age: u8,
}

关联函数和方法是在 impl 块内为结构体实现的。

关联函数可以类比于 Solidity 中为与结构体交互创建的库的情况。当我们定义 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 中的接口或抽象合约 — 使用接口的任何合约必须实现某些函数。

例如,假设我们有一个场景,我们需要定义一个 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
}

宏如何修改结构体

在我们关于类似函数的宏的教程中,我们看到宏如何在大型 Rust 代码中扩展类似于 println!(...) 和 msg!(...) 的代码。在 Solana 的上下文中,我们关心的另一种宏是 类似属性 宏和 派生 宏。我们可以在 anchor 创建的起始程序中看到这三种(类似函数、类似属性和派生)宏:

img

为了直观地理解类似属性宏正在做什么,我们将创建两个宏:一个用于向结构体添加字段,另一个用于移除它们。

示例 1:类似属性宏,插入字段

为了更好地理解 Rust 属性和宏的工作原理,我们将创建一个类似属性宏 ,该宏:

  1. 接受一个没有类型为 i32 的字段 foobar 的结构体
  2. 将这些字段插入到结构体中
  3. 创建一个带有名为 double_foo 的函数的 impl,该函数返回 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 编译时,你将收到以下错误:

img

这可能看起来很奇怪,因为结构体明显具有这些字段。但是,类似属性宏将它们移除了!

#[derive(…)]

#[derive(…)] 宏比类似属性宏弱得多。对于我们的目的,派生宏 增强 了一个结构体,而不是改变它。(这不是一个精确的定义,但现在足够了)。

派生宏可以,除其他事项外,将一个 impl 附加到一个结构体上。

例如,如果我们尝试执行以下操作:

struct Foo {
bar: i32,
}

pub fn main() {
let foo = Foo { bar: 3 };
println!("{:?}", foo);
}

该代码将无法编译,因为结构体不可“打印”。

要使它们可打印,它们需要一个带有返回结构体的字符串表示的函数 fmtimpl

如果我们改为执行以下操作:

#[derive(Debug)]
struct Foo {
bar: i32,
}

pub fn main() {
let foo = Foo { bar: 3 };
println!("{:?}", foo);
}

我们期望它打印:

Foo { bar: 3 }

派生属性“增强”了 Foo,以便 println! 可以为其创建一个字符串表示。

总结

impl 是一组在结构体上操作的函数。它们通过使用与结构体相同的名称“附加”到结构体上。Trait 强制 impl 实现某些函数。在我们的示例中,我们使用 impl Speed for Car 的语法将 Trait Speed 附加到 impl Car 上。

类似属性宏接受一个结构体,并可以完全重写它。

派生宏通过附加额外的函数增强了一个结构体。

宏允许 Anchor 隐藏复杂性

让我们再次看看 anchor 在 anchor init 期间创建的程序:

img

属性 #[program] 在幕后修改了模块。例如,它实现了一个路由器,自动将传入的区块链指令定向到模块内适当的函数。

结构体 Initialize {} 被增强了额外的功能,以在 Solana 框架中使用。

总结

宏是一个非常庞大的主题。我们在这里的目的是让你了解当你看到 #[program]#[derive(Accounts)] 时发生了什么。如果感到陌生,请不要气馁。你不需要能够编写宏来编写 Solana 程序

然而,了解它们的作用将有助于使你看到的程序变得不那么神秘。

通过 RareSkills 进一步学习

本教程是我们免费的 Solana 课程的一部分。