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
#![allow(unused)] fn main() { #[derive(Debug, Eq, Hash, Clone, PartialEq)] struct Foo {} }
Cách sử dụng
#![allow(unused)] fn main() { #[derive(DeriveMacroName)] struct Foo {} }
Khai báo trên đầu của struct
Cách nó hoạt động
Giải thích Giả sử chúng ta có một struct
#![allow(unused)] fn main() { 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.
#![allow(unused)] fn main() { 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
#![allow(unused)] fn main() { 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
#![allow(unused)] fn main() { #[derive(Debug)] struct Foo { a: i32, } }
Bạn có thể nghĩ nó sẽ tương đương với
#![allow(unused)] fn main() { 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
#![allow(unused)] fn main() { 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:
#![allow(unused)] fn main() { 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
#![allow(unused)] fn main() { cargo new --lib reflective-derive Add chỉ thị proc-macro [lib] proc-macro=true }
Thêm 2 thư viện sync và 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
#![allow(unused)] fn main() { | 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
#![allow(unused)] fn main() { 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:
#![allow(unused)] fn main() { | 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
#![allow(unused)] fn main() { 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()
#![allow(unused)] fn main() { 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 #
Thực hiện hàmimpl_reflective_traittaọ hàmfield_names() Trong DeriveInput chúng ta có trường data: Data
#![allow(unused)] fn main() { 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
#![allow(unused)] fn main() { 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
#![allow(unused)] fn main() { 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()
#![allow(unused)] fn main() { 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