[Rust] Macro 1: Tạo một derive macro đơn giản nhất
Trong bài học này chúng ta sẽ học cách để tạo ra một Dervice macro đơn giản nhất. Chúng ta sẽ hiểu rõ các bước để tạo ra một macro derive và có cái hình dung ban đầu về cách một dervie macro hoạt động. Chúng ta sẽ không đề cập cách để debug sủ dụng cargo expand cũng như học hết tất cả các cú pháp có thể có macro dạng này. Chúng ta cũng sẽ không bàn để attribute derive macro cũng như các khái niệm nâng cao về hygiene. Nó là sẽ thuộc về một bài tìm hiểu khác. Các bạn có thể tìm hiểu cuốn sách John Geet
Derive Macro là gì?
Đây là macro dạng procedular mà sử dụng với từ khoá derive(đối lập với declarative như function macro println!). Nó giống như một header. Eg: #[foo]
Một số macro quen thuộc như: Debug, Eq hay như Serialize or Desialize của serde crate
#[derive(Debug, Eq, Hash, Clone, PartialEq)]
struct Foo {}
Cách sử dụng
#[derive(DeriveMacroName)]
struct Foo {}
Khai báo trên đầu của struct
Cách nó hoạt động
Toonrg q

Giải thích
- Giả sử chúng ta có một struct
struct Foo {
bar: i32
}
- Macro sẽ
parsestruct này. Nó nhậnstructnày như đầu vào coin nó như dạngTokenStream(Chuỗi các token). Nó tìm cách để đưa thêm một số đoạn code nữa.
impl Bar for Foo {
fn name() -> &'static str {
"Foo"
}
}
- Cuối cùng nò
regenerate(tạo lại) và cũng tạo đầu ra làTokenStream. Chuỗi này sẽ có dạng
struct Foo {
bar: i32
}
impl Bar for Foo {
fn name() -> &'static str {
"Foo"
}
}
Khá là rõ ràng phải không nào.
Ví du với Debug
#[derive(Debug)]
struct Foo {
a: i32,
}
Bạn có thể nghĩ nó sẽ tương đương với
use std::fmt;
struct Foo {
a: i32,
}
impl fmt::Debug for Foo {
fn fmt(&self, f: &mut fmt::Formatter) ->fmt::Result {
write!(f, "Foo: {}", self.a)
}
}
Nhưng thường là nó sẽ có nhiều cái phức tạp hơn.
Thực hành
Mô tả bài toán:
Giả sử chúng ta có một struct như sau
struct Foo {
a: i32,
b: bool,
c: String,
}
let foo = Foo {
a: 4,
b: false,
}
println!("{}", foo.name());
// "Foo"
println!("{:?}", field_names());
// ["a", "b"]
Chúng ta muốn đầu ra sẽ là như sau:
struct Foo {
a: i32,
b: bool,
}
impl Foo {
fn name() -> &'strict str{
"Foo"
}
fn field_names() -> Vec<&str> {
vec!["a", "b"]
}
}
Struct sẽ thực hiện thêm 2 hàm name và field_names trả về tên của struct và in ra tên các thuộc tính trong struct đó
Khởi tạo thư viện và cài đặt package cần thiết
- Tạo một macro với tên là
reflective-derive
cargo new --lib reflective-derive
- Add chỉ thị
proc-macro
[lib]
proc-macro=true
- Thêm 2 thư viện
syncvàquote
cargo add sync quote
Tại sao chúng ta cần 2 thư viện này ?
TokenStream của struct có thể là như sau
| 0 | struct |
| 1 | Foo |
| 2 | { |
| 3 | a |
| 4 | : |
| 5 | i32 |
| 6 | , |
| | ... |
|.. | } |
Khá khó để xử lý một token stream dạng này. Do đó chúng ta cần một thư viện có thể giúp chúng ta để biến đổi tới một struct.
syn sẽ giúp chúng ta từ TokenStream tới dạng struct cây AST của cấu trúc dạng nhưDeriveInput như sau
pub struct DeriveInput {
pub attrs: Vec<Attribute>,
pub vis: Visibility,
pub ident: Ident,
pub generics: Generics,
pub data: Data,
}
Trong đó ident
Còn quote sẽ giúp chúng ta biến đổi ngược lại.
Eg:
| 0 | struct | AST
| 1 | Foo | [struct]
| 2 | { | ident: "Foo"
| 3 | a | visibility: private
| 4 | : | --> ...
| 5 | i32 | [field] [field]
| 6 | , | ident: Some("a") ident: Some("a")
| | ... | visibility:private visibility:private
|.. | } | ... ...
Tạo một hàm
use proc_macro::TokenStream;
use sync::DeriveInput;
#[proc_macro_derive(Reflective)]
fn reflective_derive_macro(item: TokenStream) -> TokenStream {
//parse
let ast: DeriveInput = syn::parse(item).unwrap();
// generate
impl_reflective_trait(ast);
}
- Hàm này nhận đầu vào là
TokenStreamnhư mô tả ở trên và trả về cũng mộtTokenStream - Chúng ta có thể đặt tên hàm là bất kỳ
reflective_derive_macro #[proc_macro_derive(Reflective)]mô tả têncúng cơmcủa nó. Bạn sẽ sử dụngReflectivekhi sử dụng- Sử dụng
syn::parseđể parseTokenStreamnày và trả vềDeriveInputnhư chúng ta đề cập ở trên
Thực hiên hàm impl_reflective_trait taọ hàm name()
fn impl_reflective_trait(ast: DeriveInput) -> TokenStream {
// Lây thông tin của tên struct
// Chúng ta sẽ có "Foo"
let ident: Ident =ast.ident;
// Nó là kiểu `Ident` chúng ta cần biến đổi tới một String
let ident_str = ident.to_string();
// Sử dụng `quote!` để biến đổi từ DeriveInput tới `TokenStream`
quote::quote!{
impl Reflective for #ident_str {
fn name(&self) -> &'static str {
#ident_str
}
}
}.into()
}
Giải thích nằm ở comment. Chúng ta sử dụng #<name> để thay thế biến trong quote! macro
Thực hiên hàm impl_reflective_trait taọ hàm field_names()
Trong DeriveInput chúng ta có trường data: Data
pub enum Data {
Struct(DataStruct),
Enum(DataEnum),
Union(DataUnion),
}
pub struct DataStruct {
pub struct_token: Struct,
pub fields: Fields,
pub semi_token: Option<Semi>,
}
Chúng ta có thể sử dụng để lấy thông tin của các fields trong một struct.
Như trên các bạn cũng có thể thấy rằng derive macro này cũng có thể đặ trước Enum, Union nữa
- Lấy các
field
fn impl_reflective_trait(ast: DeriveInput) -> TokenStream {
...
//
let field_idents: Vec<Ident> = match ast.data {
syn::Data::Struct(data) => data.fields.into_iter().filter_map(|f|f.ident).collect(),
sync::Data::Enum(_) => panic!("doesn't suport for enum"),
sync::Data::Union(_) => panic!("doesn't suport for union"),
}
...
}
- Biến đổi
Vec<Indent>tới Vector của cácString
fn impl_reflective_trait(ast: DeriveInput) -> TokenStream {
...
//
let field_idents: Vec<Ident> = match ast.data {
syn::Data::Struct(data) => data.fields.into_iter().filter_map(|f|f.ident).collect(),
sync::Data::Enum(_) => panic!("doesn't suport for enum"),
sync::Data::Union(_) => panic!("doesn't suport for union"),
}
let field_idents_strs: Vec<String> = field_idents.iter().map(|i| i.to_string).collect();
...
}
- Tạo hàm
field_name()
fn impl_reflective_trait(ast: DeriveInput) -> TokenStream {
...
//
let field_idents: Vec<Ident> = match ast.data {
syn::Data::Struct(data) => data.fields.into_iter().filter_map(|f|f.ident).collect(),
sync::Data::Enum(_) => panic!("doesn't suport for enum"),
sync::Data::Union(_) => panic!("doesn't suport for union"),
}
let field_idents_strs: Vec<String> = field_idents.iter().map(|i| i.to_string).collect();
quote::quote! {
impl Reflective for #ident_str {
fn name(&self) -> &'static str {
#ident_str
}
fm field_names(&self) -> Vec<&'static str> {
vec![#(#field_idents_strs), *]
}
}
}
...
}
- Khi lặp lại ta sử dụng cú pháp
#(),*. Điều này nghĩa là có thể dạng lặp lại 1 hay nhiều lâna,a,b, - Bên trong
#()truyền biến dạng là một mảng