Macro System ใน Rust: สร้าง DSL ของตัวเอง

Sharing is caring!

หนึ่งในฟีเจอร์ที่ทรงพลังที่สุดของ Rust คือ macro system ที่ให้คุณสามารถ “เขียนโค้ดที่เขียนโค้ด” ได้ — ไม่ใช่แค่การแทนที่ข้อความธรรมดาแบบ C แต่สามารถสร้าง syntax ใหม่ สร้าง DSL (Domain Specific Language) และแม้แต่ปรับเปลี่ยน AST ได้โดยตรง!

บทความนี้จะพาคุณเจาะลึก macro ทั้งสองชนิดใน Rust — Declarative macro และ Procedural macro — พร้อมตัวอย่างการสร้าง DSL ที่ใช้ได้จริง


Declarative Macro (macro_rules!)

Declarative macro คือ macro แบบ pattern-matching ที่ใช้ macro_rules!

macro_rules! say_hello {
    () => {
        println!("Hello from macro!");
    };
}

fn main() {
    say_hello!(); // จะพิมพ์ข้อความ
}

รับพารามิเตอร์

macro_rules! square {
    ($x:expr) => {
        $x * $x
    };
}

fn main() {
    let n = square!(4 + 1); // กลายเป็น (4 + 1) * (4 + 1)
    println!("{}", n); // 25
}

ข้อดีคือ macro เหล่านี้ compile เร็ว ไม่ต้องใช้ crate แยก

สร้าง DSL เบื้องต้น

macro_rules! html {
    (<$tag:ident>$content:expr</$tag2:ident>) => {
        format!("<{0}>{1}</{0}>", stringify!($tag), $content)
    };
}

fn main() {
    let page = html!(<h1>"ยินดีต้อนรับ"</h1>);
    println!("{}", page); // <h1>ยินดีต้อนรับ</h1>
}

คุณสามารถสร้าง DSL สำหรับ HTML, SQL หรือ config ได้เองด้วย macro แบบนี้!

ข้อจำกัดของ macro_rules!

  • ไม่สามารถเข้าถึง AST หรือ metadata ของ type ได้
  • ซับซ้อนเมื่อมี pattern หลายรูปแบบ
  • ไม่สามารถแปลงโค้ดให้มีผลตาม logic runtime ได้

Procedural Macro (แบบเทพ!)

Procedural macro คือโค้ด Rust ที่ compile-time เพื่อ generate โค้ดอื่นผ่านการจัดการ AST โดยใช้ proc_macro crate

3 ประเภทของ procedural macro:

  1. Function-like macro: my_macro!(...)
  2. Derive macro: #[derive(MyTrait)]
  3. Attribute macro: #[route], #[inline] เป็นต้น

สร้าง Function-like Macro

// lib.rs
use proc_macro::TokenStream;

#[proc_macro]
pub fn make_answer(_input: TokenStream) -> TokenStream {
    "fn answer() -> i32 { 42 }".parse().unwrap()
}
// main.rs
use my_macro_lib::make_answer;

make_answer!();

fn main() {
    println!("{}", answer()); // 42
}

สร้าง DSL แบบ JSON Mini DSL

#[proc_macro]
pub fn json(input: TokenStream) -> TokenStream {
    let s = input.to_string();
    let json_string = format!("serde_json::json!({})", s);
    json_string.parse().unwrap()
}
fn main() {
    let data = json!({
        "name": "Rust",
        "safe": true,
        "features": ["macro", "ownership"]
    });

    println!("{}", data["name"]);
}

ใช้กับ #[derive]

#[derive(Debug, MyTrait)]
struct Person {
    name: String,
    age: u32,
}

Rust จะเรียก procedural macro เพื่อสร้าง method หรือ impl ให้ struct นี้ตอน compile

ข้อดีของ Macro System ใน Rust

  • เขียน DSL ที่ดู clean และ readable
  • สร้าง derive ได้เอง ทำให้โค้ดสั้นลง
  • ใช้ macro ทำ code generation แทน boilerplate

ข้อเสีย

  • debug ยาก ต้องเข้าใจ AST และ TokenStream
  • error message จาก macro อ่านยากกว่า function ธรรมดา

คำแนะนำ

  • เริ่มจาก macro_rules! และใช้จนคล่อง
  • อ่าน AST ด้วย cargo expand เพื่อเข้าใจ macro output
  • ใช้ syn + quote ใน procedural macro จะช่วยให้โค้ดอ่านง่าย

บทสรุป

Macro system ของ Rust ทรงพลังและปลอดภัย ช่วยให้คุณเขียนโค้ดที่ maintain ได้ง่าย สร้าง abstraction และ DSL ที่เหมาะกับโดเมนของคุณโดยไม่สูญเสียประสิทธิภาพ

หากคุณเข้าใจ macro ของ Rust อย่างลึกซึ้ง — คุณจะกลายเป็น full-power Rustacean!

Leave a Reply

อีเมลของคุณจะไม่แสดงให้คนอื่นเห็น ช่องข้อมูลจำเป็นถูกทำเครื่องหมาย *