blog.dopana

Back

Bài thứ mười sáu và là bài khép lại loạt bài Rust Intermediate. Khái niệm “Macro” trong Rust dùng để chỉ một tập hợp các tính năng phục vụ cho Metaprogramming (Siêu lập trình) — tức là viết mã nguồn có khả năng tự sinh ra mã nguồn khác tại thời điểm biên dịch.

Khác với hàm số thông thường nhận tham số giá trị, Macro nhận cú pháp (tokens) và mở rộng thành mã Rust hoàn chỉnh trước khi toàn bộ chương trình được trình biên dịch xử lý.

graph TD
    A["Hệ thống Macros trong Rust"] --> B["Declarative Macros (Khai báo)<br/>macro_rules!"]
    A --> C["Procedural Macros (Thủ tục)<br/>Hoạt động trên TokenStream"]
    C --> D["Custom Derive<br/>#[derive(CustomTrait)]"]
    C --> E["Attribute-like<br/>#[route(GET, '/')]"]
    C --> F["Function-like<br/>sql!(...)"]

Giải thích đơn giản (ELI5): Chiếc máy in khuôn thông minh#

Hãy tưởng tượng sự khác biệt giữa Hàm số (Function) và Macro:

  • Hàm số: Giống như bạn đưa nguyên liệu vào máy ép trái cây, máy chạy và trả về một ly nước ép khi chương trình đang chạy (Runtime).
  • Macro: Giống như một chiếc máy in 3D tạo ra một chiếc máy ép trái cây hoàn chỉnh ngay trước khi nhà máy bắt đầu hoạt động (Compile-time).

1. Declarative Macros với macro_rules!#

Đây là loại macro phổ biến nhất trong Rust, sử dụng cơ chế so khớp mẫu cú pháp (pattern matching trên tokens) tương tự như biểu thức match:

Ví dụ: Tái hiện lại macro vec! quen thuộc của Rust:

Giải mã cú pháp#

  • $x:expr: Khớp với bất kỳ biểu thức (expression) Rust nào và gán vào biến $x.
  • $( ... ),*: Lặp lại mẫu bên trong, phân tách bằng dấu phẩy ,, khớp 0 hoặc nhiều lần (*).

2. Procedural Macros (Macro thủ tục)#

Nếu declarative macros dùng so khớp mẫu đơn giản, thì Procedural Macros nhận vào một luồng token (TokenStream), xử lý tùy ý bằng mã Rust hoàn chỉnh, và trả về một TokenStream mới.

Procedural Macros được chia làm 3 loại:

A. Custom Derive Macro#

Tự động sinh mã triển khai trait khi bạn dùng #[derive(MyTrait)]:

// Định nghĩa macro (nằm trong crate proc-macro chuyên dụng)
#[proc_macro_derive(HelloMacro)]
pub fn hello_macro_derive(input: TokenStream) -> TokenStream {
    // Phân tích cú pháp AST và sinh mã impl HelloMacro for StructName
}

// Cách sử dụng
#[derive(HelloMacro)]
struct Pancakes;
rust

B. Attribute-like Macro#

Cho phép bạn tạo ra các thuộc tính tùy chỉnh gắn lên hàm, struct, enum:

#[route(GET, "/users")]
fn get_users() {
    // ...
}
rust

C. Function-like Macro#

Trông giống như hàm nhận cú pháp đặc biệt để sinh mã phức tạp:

let sql = sql!(SELECT * FROM users WHERE id = 1);
rust

So sánh: Functions vs Macros#

Tiêu chíHàm số (Functions)Macros
Thời điểm thực thiThời điểm chạy (Runtime)Thời điểm biên dịch (Compile-time)
Số lượng đối sốCố định kiểu & số lượng tham sốNhận số lượng đối số tùy ý (Variadic)
Nhận diện kiểuPhải định nghĩa kiểu cụ thểThao tác trên cây cú pháp trừu tượng (AST/Tokens)
Độ phức tạp mã nguồnDễ viết, dễ đọc và bảo trìCú pháp phức tạp hơn, khó debug hơn

Tóm tắt#

  • Macros mang lại khả năng siêu lập trình (metaprogramming), giúp bạn sinh mã tự động mà không mất chi phí hiệu năng lúc chạy.
  • macro_rules! là giải pháp nhanh chóng và tiện lợi cho hầu hết các macro dạng khai báo.
  • Procedural Macros mang lại sức mạnh không giới hạn để thao tác trực tiếp trên AST/Tokens thông qua 3 dạng: Custom Derive, Attribute-like, và Function-like.

Tài liệu tham khảo#