Las macros de tipo atributo y derive personalizadas en Rust se utilizan para tomar un bloque de código en Rust y modificarlo de alguna manera en tiempo de compilación, a menudo para agregar funcionalidad.
Para entender las macros de tipo atributo y derive personalizadas en Rust, primero necesitamos repasar brevemente la implementación de structs en Rust.
Implementaciones para structs: impl
El siguiente struct debería ser fácil de entender. Lo que se vuelve interesante es cuando creamos funciones que operan sobre un struct particular. La forma de hacerlo es con impl:
struct Person {
name: String,
age: u8,
}
Las funciones asociadas y los métodos se implementan para los structs dentro del bloque impl.
Las funciones asociadas pueden compararse con el escenario en Solidity donde se crea una biblioteca para interactuar con un struct. Cuando definimos using lib for MyStruct, nos permite usar la sintaxis myStruct.associatedFunction(). Esto le da a la función acceso a myStruct a través de la palabra clave Self.
Recomendamos usar el Rust Playground, pero para ejemplos más complejos, es posible que tengas que configurar tu IDE.
Veamos un ejemplo a continuación:
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);
}
Uso:
// 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
Traits en Rust
Los traits en Rust son una forma de implementar un comportamiento compartido entre diferentes impls. Piensa en ellos como una interfaz o un contrato abstracto en Solidity: cualquier contrato que utilice la interfaz debe implementar ciertas funciones.
Por ejemplo, supongamos que tenemos un escenario en el que necesitamos definir un struct Car y Boat. Queremos adjuntar un método que nos permita obtener su velocidad en kilómetros por hora. En Rust, podemos lograr esto utilizando un solo trait y compartiendo el método entre los dos structs.
Esto se muestra a continuación:
// 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
}
Cómo las macros pueden modificar structs
En nuestro tutorial sobre macros de tipo función, vimos cómo las macros pueden expandir código como println!(...) y msg!(...) en código extenso de Rust. El otro tipo de macros que nos interesa en el contexto de Solana es la macro de tipo atributo y la macro derive. Podemos ver los tres tipos de macros (de tipo función, de tipo atributo y derive) en el programa inicial que crea Anchor:

Para tener una intuición de lo que hacen las macros de tipo atributo, crearemos dos macros: una para añadir campos a un struct y otra para eliminarlos.
Ejemplo 1: macro de tipo atributo, insertando campos
Para comprender mejor cómo funcionan los atributos y las macros en Rust, crearemos una macro de tipo atributo que:
- toma un struct que no tiene los campos
fooybar, de tipoi32 - inserta esos campos en el struct
- crea un
implcon una función llamadadouble_fooque devuelve el doble del valor entero de lo que contengafoo.
Configuración
Primero creamos un nuevo proyecto de Rust:
cargo new macro-demo --lib
cd macro-demo
touch src/main.rs
Añade lo siguiente al archivo Cargo.toml:
[lib]
proc-macro = true
[dependencies]
syn = {version="1.0.57",features=["full","fold"]}
quote = "1.0.8"
Creando el programa principal
Pega el siguiente código en src/main.rs. Asegúrate de leer los comentarios:
// 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);
}
Algunas observaciones:
- El struct
MyStructno contiene los camposfoo. - La función
double_foono está definida en ninguna parte del código anterior, se asume que existe.
Ahora vamos a crear la macro de tipo atributo que modificará MyStruct tras bambalinas.
Reemplaza el código en src/lib.rs con el siguiente código (asegúrate de leer los comentarios):
// 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
}
}
})
}
Ahora, para probar nuestra macro, ejecutamos el código en main.rs con cargo run src/main.rs.
Obtenemos esta salida:
struct is MyStruct { foo: 10, bar: 20 }
double foo: 20
Ejemplo 2: una macro de tipo atributo, eliminando campos
La mejor manera de pensar en las macros de tipo atributo es que tienen un poder ilimitado en cómo modifican el struct. Repitamos el ejemplo anterior, pero esta vez la macro de tipo atributo eliminará todos los campos del struct.
Reemplaza src/lib.rs con lo siguiente:
// 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 {
}
})
}
Reemplaza src/main.rs con lo siguiente:
use macro_demo::*;
#[destroy_attribute]
struct MyStruct {
baz: i32,
qux: i32,
}
fn main() {
let demo = MyStruct { baz: 3, qux: 4 };
println!("struct is {:?}", demo);
}
Cuando intentes compilarlo con cargo run src/main.rs obtendrás el siguiente error:

Puede parecer extraño, porque el struct claramente tiene esos campos. Sin embargo, ¡la macro de tipo atributo los eliminó!
La macro #[derive(…)]
La macro #[derive(…)] es mucho menos poderosa que la macro de tipo atributo. Para nuestros propósitos, una macro derive aumenta un struct, no lo altera. (Esta no es una definición precisa, pero es suficiente por ahora).
Una macro derive puede, entre otras cosas, adjuntar un impl a un struct.
Por ejemplo, si intentamos hacer lo siguiente:
struct Foo {
bar: i32,
}
pub fn main() {
let foo = Foo { bar: 3 };
println!("{:?}", foo);
}
El código no compilará porque los structs no son “imprimibles”.
Para hacerlos imprimibles, necesitan un impl con una función fmt que devuelva una representación en cadena del struct.
Si en cambio hacemos lo siguiente:
#[derive(Debug)]
struct Foo {
bar: i32,
}
pub fn main() {
let foo = Foo { bar: 3 };
println!("{:?}", foo);
}
Esperamos que imprima:
Foo { bar: 3 }
El atributo derive “aumentó” Foo de tal manera que println! pudo crear una representación en cadena para él.
Resumen
Un impl es un grupo de funciones que operan sobre un struct. Se “adjuntan” al struct utilizando el mismo nombre que el struct. Un trait impone que un impl implemente ciertas funciones. En nuestro ejemplo, adjuntamos el trait Speed al impl Car utilizando la sintaxis impl Speed for Car.
Una macro de tipo atributo recibe un struct y puede reescribirlo por completo.
Una macro derive aumenta un struct con funciones adicionales.
Las macros permiten a Anchor ocultar la complejidad
Veamos nuevamente el programa que Anchor crea durante anchor init:

El atributo #[program] está modificando el módulo tras bambalinas. Por ejemplo, implementa un enrutador que dirige automáticamente las instrucciones entrantes de la blockchain a las funciones apropiadas dentro del módulo.
El struct Initialize {} se aumenta con funcionalidad adicional para ser utilizado en el framework de Solana.
Resumen
Las macros son un tema muy extenso. Nuestra intención aquí es darte una idea de lo que está sucediendo cuando ves #[program] o #[derive(Accounts)]. No te desanimes si te resulta ajeno. No necesitas saber cómo escribir macros para escribir programas en Solana.
Sin embargo, tener una idea de lo que hacen, con suerte, hará que los programas que veas sean menos misteriosos.
Aprende más con RareSkills
Este tutorial es parte de nuestro curso de Solana gratuito.
Publicado originalmente el 16 de febrero de 2024