[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ẽ
parse
struct này. Nó nhậnstruct
nà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
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
| 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ộ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ơm
của nó. Bạn sẽ sử dụngReflective
khi sử dụng- Sử dụng
syn::parse
để parseTokenStream
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á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