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ẽ parse struct này. Nó nhận struct này như đầu vào coin nó như dạng TokenStream (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 namefield_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 syncquote
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à TokenStream như mô tả ở trên và trả về cũng một TokenStream
  • Chúng ta có thể đặt tên hàm là bất kỳ reflective_derive_macro
  • #[proc_macro_derive(Reflective)] mô tả tên cúng cơm của nó. Bạn sẽ sử dụng Reflective khi sử dụng
  • Sử dụng syn::parse để parse TokenStream này và trả về DeriveInput như 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ác String
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ân a, a,b,
  • Bên trong #() truyền biến dạng là một mảng