Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Macro

เราใช้ macro เช่น println! ผ่านหนังสือนี้ แต่เรายังไม่ได้สำรวจเต็มที่ ว่า macro คืออะไรและทำงานยังไง Term macro อ้างถึงตระกูลของฟีเจอร์ใน Rust — macro แบบ declarative ด้วย macro_rules! และสามชนิดของ procedural macro:

  • Custom #[derive] macro ที่ระบุโค้ดที่เพิ่มด้วย attribute derive ที่ใช้บน struct และ enum
  • Attribute-like macro ที่นิยาม attribute custom ใช้ได้บน item ใด
  • Function-like macro ที่ดูเหมือนการเรียกฟังก์ชันแต่ operate บน token ที่ระบุเป็น argument ของพวกมัน

เราจะพูดเกี่ยวกับแต่ละตามลำดับ แต่ก่อนอื่น มาดูว่าทำไมเราต้องการ macro แม้เรามีฟังก์ชันแล้ว

ความแตกต่างระหว่าง Macro และฟังก์ชัน

พื้นฐาน macro คือวิธีของการเขียนโค้ดที่เขียนโค้ดอื่น ซึ่งรู้จักเป็น metaprogramming ใน Appendix C เราพูดถึง attribute derive ซึ่ง generate implementation ของ trait ต่าง ๆ ให้คุณ เราใช้ macro println! และ vec! ผ่านหนังสือด้วย macro ทั้งหมดเหล่านี้ expand เพื่อผลิตโค้ดมากกว่าโค้ดที่คุณเขียนโดยมือ

Metaprogramming มีประโยชน์สำหรับลดปริมาณโค้ดที่คุณต้องเขียนและ maintain ซึ่งก็เป็นบทบาทหนึ่งของฟังก์ชัน อย่างไรก็ตาม macro มีอำนาจ เพิ่มบางอย่างที่ฟังก์ชันไม่มี

signature ฟังก์ชันต้องประกาศจำนวนและ type ของ parameter ที่ฟังก์ชันมี Macro ในทางกลับ รับจำนวนตัวแปรของ parameter ได้ — เราเรียก println!("hello") กับหนึ่ง argument หรือ println!("hello {}", name) กับสอง argument ได้ นอกจากนี้ macro ถูก expand ก่อน compiler ตีความ ความหมายของโค้ด ดังนั้น macro สามารถ ตัวอย่างเช่น implement trait บน type ที่ให้ ฟังก์ชันทำไม่ได้ เพราะมันถูกเรียกที่ runtime และ trait ต้อง ถูก implement ที่ compile time

ข้อเสียของการ implement macro แทนฟังก์ชันคือนิยาม macro complex มากกว่า นิยามฟังก์ชันเพราะคุณกำลังเขียนโค้ด Rust ที่เขียนโค้ด Rust เพราะ indirection นี้ นิยาม macro โดยทั่วไปยากกว่าในการอ่าน เข้าใจ และ maintain กว่านิยามฟังก์ชัน

ความแตกต่างสำคัญอื่นระหว่าง macro และฟังก์ชันคือคุณต้องนิยาม macro หรือนำพวกมันเข้า scope ก่อน คุณเรียกพวกมันในไฟล์ ตรงข้ามกับฟังก์ชัน ที่คุณนิยามได้ที่ไหนและเรียกได้ที่ไหน

Declarative Macro สำหรับ Metaprogramming ทั่วไป

รูปแบบที่ใช้กว้างที่สุดของ macro ใน Rust คือ declarative macro พวกนี้ บางครั้งเรียก “macro by example”, “macro_rules! macro” หรือเพียง “macro” ที่แกนของพวกมัน, declarative macro อนุญาตให้คุณเขียนสิ่งที่ คล้ายกับ expression match ของ Rust ตามที่พูดถึงในบทที่ 6 expression match คือโครงสร้าง control ที่รับ expression เปรียบเทียบค่าผลลัพธ์ ของ expression กับ pattern แล้วรันโค้ดที่ associate กับ pattern ที่ match Macro ยังเปรียบเทียบค่ากับ pattern ที่ associate กับโค้ดเฉพาะ — ในสถานการณ์นี้ ค่าคือ source code Rust literal ที่ส่งให้ macro; pattern ถูกเปรียบเทียบกับโครงสร้างของ source code นั้น และโค้ดที่ associate กับแต่ละ pattern เมื่อ match แทนโค้ดที่ส่งให้ macro ทั้งหมด นี้เกิดขึ้นระหว่าง compilation

เพื่อนิยาม macro คุณใช้ construct macro_rules! มาสำรวจวิธีใช้ macro_rules! โดยดูที่ macro vec! ถูกนิยามยังไง บทที่ 8 ครอบคลุม วิธีเราใช้ macro vec! เพื่อสร้าง vector ใหม่กับค่าเฉพาะ ตัวอย่างเช่น macro ต่อไปนี้สร้าง vector ใหม่ที่บรรจุสาม integer:

#![allow(unused)]
fn main() {
let v: Vec<u32> = vec![1, 2, 3];
}

เรายังใช้ macro vec! เพื่อทำ vector ของสอง integer หรือ vector ของ ห้า string slice ได้ เราจะไม่สามารถใช้ฟังก์ชันเพื่อทำเหมือนกันเพราะเรา จะไม่รู้จำนวนหรือ type ของค่าล่วงหน้า

Listing 20-35 แสดงนิยาม macro vec! ที่ทำให้ง่ายเล็กน้อย

Filename: src/lib.rs
#[macro_export]
macro_rules! vec {
    ( $( $x:expr ),* ) => {
        {
            let mut temp_vec = Vec::new();
            $(
                temp_vec.push($x);
            )*
            temp_vec
        }
    };
}
Listing 20-35: version ที่ทำให้ง่ายของนิยาม macro vec!

Note: นิยามจริงของ macro vec! ใน standard library รวมโค้ดเพื่อ pre-allocate ปริมาณ memory ถูกล่วงหน้า โค้ดนั้นเป็น optimization ที่ เราไม่รวมที่นี่ เพื่อทำตัวอย่างง่ายกว่า

Annotation #[macro_export] บ่งบอกว่า macro นี้ควรถูกทำให้ใช้ได้เมื่อ ใดก็ตามที่ crate ที่ macro ถูกนิยามใน ถูกนำเข้า scope โดยไม่มี annotation นี้ macro ไม่สามารถถูกนำเข้า scope

แล้วเราเริ่มนิยาม macro ด้วย macro_rules! และชื่อของ macro ที่เรากำลัง นิยาม โดยไม่มี exclamation mark ชื่อ ในกรณีนี้ vec ตามด้วย curly bracket แสดง body ของนิยาม macro

โครงสร้างใน body ของ vec! คล้ายกับโครงสร้างของ expression match ที่นี่เรามีหนึ่ง arm กับ pattern ( $( $x:expr ),* ) ตามด้วย => และ block ของโค้ดที่ associate กับ pattern นี้ ถ้า pattern match block ของโค้ดที่ associate จะถูก emit ให้ว่านี่คือ pattern เดียวใน macro นี้ มีเพียงหนึ่งวิธี valid ที่จะ match — pattern อื่นใดจะส่งผลเป็น error Macro complex มากกว่าจะมีมากกว่าหนึ่ง arm

Syntax pattern ที่ valid ในนิยาม macro ต่างจาก syntax pattern ที่ ครอบคลุมในบทที่ 19 เพราะ pattern macro ถูก match กับโครงสร้างโค้ด Rust แทนค่า มาเดินผ่านสิ่งที่ชิ้น pattern ใน Listing 20-35 หมายถึง — สำหรับ syntax pattern macro เต็ม ดู Rust Reference

ก่อนอื่น เราใช้ชุดของวงเล็บเพื่อครอบคลุม pattern ทั้งหมด เราใช้ dollar sign ($) เพื่อประกาศตัวแปรในระบบ macro ที่จะบรรจุโค้ด Rust ที่ match pattern dollar sign ทำให้ชัดเจนว่านี่คือตัวแปร macro ตรงข้ามกับ ตัวแปร Rust ปกติ ถัดมาคือชุดของวงเล็บที่ capture ค่าที่ match pattern ภายในวงเล็บสำหรับการใช้ในโค้ดแทน ภายใน $() คือ $x:expr ซึ่ง match expression Rust ใดและให้ expression ชื่อ $x

Comma ที่ตาม $() บ่งบอกว่าตัวคั่น comma literal ต้องปรากฏระหว่างแต่ละ instance ของโค้ดที่ match โค้ดใน $() * ระบุว่า pattern match ศูนย์ หรือมากกว่าของสิ่งใดที่นำหน้า *

เมื่อเราเรียก macro นี้ด้วย vec![1, 2, 3];, pattern $x match สาม ครั้งกับสาม expression 1, 2 และ 3

ตอนนี้มาดู pattern ใน body ของโค้ดที่ associate กับ arm นี้ — temp_vec.push() ภายใน $()* ถูก generate สำหรับแต่ละส่วนที่ match $() ใน pattern ศูนย์หรือมากกว่าครั้งขึ้นกับ pattern match กี่ครั้ง $x ถูกแทนด้วยแต่ละ expression ที่ match เมื่อเราเรียก macro นี้ด้วย vec![1, 2, 3];, โค้ดที่ generate ที่แทนการเรียก macro นี้จะเป็นต่อ ไปนี้:

{
    let mut temp_vec = Vec::new();
    temp_vec.push(1);
    temp_vec.push(2);
    temp_vec.push(3);
    temp_vec
}

เรานิยาม macro ที่รับจำนวนใดของ argument ของ type ใดและสามารถ generate โค้ดเพื่อสร้าง vector ที่บรรจุ element ที่ระบุ

เพื่อเรียนรู้เพิ่มเกี่ยวกับวิธีเขียน macro ปรึกษา documentation online หรือ resource อื่น เช่น “The Little Book of Rust Macros” ที่ เริ่มโดย Daniel Keep และต่อโดย Lukas Wirth

Procedural Macro สำหรับ Generate โค้ดจาก Attribute

รูปแบบที่สองของ macro คือ procedural macro ซึ่งทำตัวเหมือนฟังก์ชันมาก กว่า (และเป็นชนิดของ procedure) Procedural macro รับโค้ดบางอย่างเป็น input, operate บนโค้ดนั้น และผลิตโค้ดบางอย่างเป็น output แทนการ match กับ pattern และแทนโค้ดด้วยโค้ดอื่นแบบที่ declarative macro ทำ สามชนิด ของ procedural macro คือ custom derive, attribute-like และ function-like และทั้งหมดทำงานในแฟชั่นที่คล้ายกัน

เมื่อสร้าง procedural macro นิยามต้องอยู่ใน crate ของตัวเองกับ crate type พิเศษ นี่เป็นเหตุผลทางเทคนิค complex ที่เราหวังจะขจัดในอนาคต ใน Listing 20-36 เราแสดงวิธีนิยาม procedural macro ที่ some_attribute คือ placeholder สำหรับการใช้ macro variety เฉพาะ

Filename: src/lib.rs
use proc_macro::TokenStream;

#[some_attribute]
pub fn some_name(input: TokenStream) -> TokenStream {
}
Listing 20-36: ตัวอย่างของการนิยาม procedural macro

ฟังก์ชันที่นิยาม procedural macro รับ TokenStream เป็น input และผลิต TokenStream เป็น output type TokenStream ถูกนิยามโดย crate proc_macro ที่รวมกับ Rust และ represent ลำดับของ token นี่คือแกน ของ macro — source code ที่ macro กำลัง operate บนสร้าง input TokenStream และโค้ดที่ macro ผลิตคือ output TokenStream ฟังก์ชัน ยังมี attribute ติดมาที่บอกว่าชนิดของ procedural macro ใดที่เรากำลัง สร้าง เรามีหลายชนิดของ procedural macro ใน crate เดียวกันได้

มาดูชนิดต่างกันของ procedural macro เราจะเริ่มด้วย custom derive macro แล้วอธิบายความไม่คล้ายเล็กน้อยที่ทำให้รูปแบบอื่นต่างกัน

Custom derive Macro

มาสร้าง crate ชื่อ hello_macro ที่นิยาม trait ชื่อ HelloMacro กับ หนึ่ง associated function ชื่อ hello_macro แทนที่จะทำให้ user ของ เรา implement trait HelloMacro สำหรับแต่ละ type ของพวกเขา เราจะให้ procedural macro เพื่อให้ user annotate type ของพวกเขาด้วย #[derive(HelloMacro)] เพื่อได้ default implementation ของฟังก์ชัน hello_macro Default implementation จะ print Hello, Macro! My name is TypeName! ที่ TypeName คือชื่อของ type ที่ trait นี้ถูกนิยามบน ในคำพูดอื่น เราจะเขียน crate ที่เปิดใช้ programmer อื่นเขียนโค้ดแบบ Listing 20-37 โดยใช้ crate ของเรา

Filename: src/main.rs
use hello_macro::HelloMacro;
use hello_macro_derive::HelloMacro;

#[derive(HelloMacro)]
struct Pancakes;

fn main() {
    Pancakes::hello_macro();
}
Listing 20-37: โค้ดที่ user ของ crate ของเราจะสามารถเขียนเมื่อใช้ procedural macro ของเรา

โค้ดนี้จะ print Hello, Macro! My name is Pancakes! เมื่อเราเสร็จ ขั้น ตอนแรกคือทำ library crate ใหม่ แบบนี้:

$ cargo new hello_macro --lib

ถัดไป ใน Listing 20-38 เราจะนิยาม trait HelloMacro และ associated function ของมัน

Filename: src/lib.rs
pub trait HelloMacro {
    fn hello_macro();
}
Listing 20-38: trait ง่ายที่เราจะใช้กับ macro derive

เรามี trait และฟังก์ชันของมัน ที่จุดนี้ user crate ของเราสามารถ implement trait เพื่อบรรลุ functionality ที่ต้องการได้ ดังใน Listing 20-39

Filename: src/main.rs
use hello_macro::HelloMacro;

struct Pancakes;

impl HelloMacro for Pancakes {
    fn hello_macro() {
        println!("Hello, Macro! My name is Pancakes!");
    }
}

fn main() {
    Pancakes::hello_macro();
}
Listing 20-39: ดูยังไงถ้า user เขียน implementation โดยมือของ trait HelloMacro

อย่างไรก็ตาม พวกเขาจะต้องเขียน implementation block สำหรับแต่ละ type ที่พวกเขาต้องการใช้กับ hello_macro — เราต้องการประหยัดพวกเขาจากการ ต้องทำงานนี้

นอกจากนี้ เรายังไม่สามารถให้ฟังก์ชัน hello_macro กับ default implementation ที่จะ print ชื่อของ type ที่ trait ถูก implement บน — Rust ไม่มีความสามารถ reflection ดังนั้นมันไม่สามารถ look up ชื่อของ type ที่ runtime เราต้องการ macro เพื่อ generate โค้ดที่ compile time

ขั้นตอนถัดไปคือนิยาม procedural macro ในเวลาเขียนนี้ procedural macro ต้องอยู่ใน crate ของตัวเอง ในที่สุด restriction นี้อาจถูกยกขึ้น Convention สำหรับการจัดโครงสร้าง crate และ macro crate เป็นต่อไปนี้ — สำหรับ crate ชื่อ foo crate procedural macro derive custom ถูก เรียก foo_derive มาเริ่ม crate ใหม่เรียก hello_macro_derive ภายใน project hello_macro ของเรา:

$ cargo new hello_macro_derive --lib

สอง crate ของเราเกี่ยวข้องอย่างแน่น ดังนั้นเราสร้าง crate procedural macro ภายในไดเรกทอรีของ crate hello_macro ของเรา ถ้าเราเปลี่ยนนิยาม trait ใน hello_macro เราจะต้องเปลี่ยน implementation ของ procedural macro ใน hello_macro_derive ด้วย สอง crate จะต้องถูก publish แยก และ programmer ที่ใช้ crate เหล่านี้จะต้องเพิ่มทั้งสองเป็น dependency และนำพวกมันเข้า scope ทั้งสอง เราสามารถมี crate hello_macro ใช้ hello_macro_derive เป็น dependency และ re-export โค้ด procedural macro แทนได้ อย่างไรก็ตาม วิธีที่เราจัดโครงสร้าง project ทำให้เป็นไป ได้สำหรับ programmer ที่จะใช้ hello_macro แม้พวกเขาไม่ต้องการ functionality derive

เราต้องประกาศ crate hello_macro_derive เป็น crate procedural macro เราจะต้องการ functionality จาก crate syn และ quote ด้วย ดังที่คุณ จะเห็นในไม่ช้า ดังนั้นเราต้องเพิ่มพวกมันเป็น dependency เพิ่มต่อไปนี้ ไปยังไฟล์ Cargo.toml สำหรับ hello_macro_derive:

Filename: hello_macro_derive/Cargo.toml
[lib]
proc-macro = true

[dependencies]
syn = "2.0"
quote = "1.0"

เพื่อเริ่มนิยาม procedural macro วางโค้ดใน Listing 20-40 ในไฟล์ src/lib.rs ของคุณสำหรับ crate hello_macro_derive สังเกตว่าโค้ดนี้ จะไม่ compile จนกว่าเราเพิ่มนิยามสำหรับฟังก์ชัน impl_hello_macro

Filename: hello_macro_derive/src/lib.rs
use proc_macro::TokenStream;
use quote::quote;

#[proc_macro_derive(HelloMacro)]
pub fn hello_macro_derive(input: TokenStream) -> TokenStream {
    // Construct a representation of Rust code as a syntax tree
    // that we can manipulate.
    let ast = syn::parse(input).unwrap();

    // Build the trait implementation.
    impl_hello_macro(&ast)
}
Listing 20-40: โค้ดที่ crate procedural macro ส่วนใหญ่จะต้องการเพื่อ process โค้ด Rust

สังเกตว่าเราแยกโค้ดเป็นฟังก์ชัน hello_macro_derive ซึ่งรับผิดชอบการ parse TokenStream และฟังก์ชัน impl_hello_macro ซึ่งรับผิดชอบการ transform syntax tree — นี่ทำให้การเขียน procedural macro สะดวกมากขึ้น โค้ดในฟังก์ชันภายนอก (hello_macro_derive ในกรณีนี้) จะเหมือนกัน สำหรับเกือบทุก crate procedural macro ที่คุณเห็นหรือสร้าง โค้ดที่คุณ ระบุใน body ของฟังก์ชันภายใน (impl_hello_macro ในกรณีนี้) จะต่างกัน ขึ้นกับจุดประสงค์ของ procedural macro ของคุณ

เราแนะนำสาม crate ใหม่ — proc_macro, syn และ quote crate proc_macro มากับ Rust ดังนั้น เราไม่ต้องเพิ่มนั่นให้ dependency ใน Cargo.toml crate proc_macro คือ API ของ compiler ที่อนุญาตให้เราอ่านและ manipulate โค้ด Rust จาก โค้ดของเรา

Crate syn parse โค้ด Rust จาก string เป็นโครงสร้างข้อมูลที่เราทำ operation บนได้ Crate quote เปลี่ยนโครงสร้างข้อมูล syn กลับเป็น โค้ด Rust crate เหล่านี้ทำให้ง่ายขึ้นมากในการ parse โค้ด Rust ชนิดใด ที่เราอาจต้องการจัดการ — เขียน parser เต็มสำหรับโค้ด Rust ไม่ใช่งาน ง่าย

ฟังก์ชัน hello_macro_derive จะถูกเรียกเมื่อ user ของ library ของเรา ระบุ #[derive(HelloMacro)] บน type นี่เป็นไปได้เพราะเรา annotate ฟังก์ชัน hello_macro_derive ที่นี่ด้วย proc_macro_derive และระบุ ชื่อ HelloMacro ซึ่ง match ชื่อ trait ของเรา — นี่คือ convention ที่ procedural macro ส่วนใหญ่ตาม

ฟังก์ชัน hello_macro_derive แปลง input จาก TokenStream เป็น โครงสร้างข้อมูลที่เราตีความและทำ operation บนได้ก่อน นี่คือที่ syn เข้ามาเล่น ฟังก์ชัน parse ใน syn รับ TokenStream และ return struct DeriveInput ที่ represent โค้ด Rust ที่ parse Listing 20-41 แสดงส่วนที่เกี่ยวข้องของ struct DeriveInput ที่เราได้จากการ parse string struct Pancakes;

DeriveInput {
    // --snip--

    ident: Ident {
        ident: "Pancakes",
        span: #0 bytes(95..103)
    },
    data: Struct(
        DataStruct {
            struct_token: Struct,
            fields: Unit,
            semi_token: Some(
                Semi
            )
        }
    )
}
Listing 20-41: instance DeriveInput ที่เราได้เมื่อ parse โค้ดที่มี attribute ของ macro ใน Listing 20-37

Field ของ struct นี้แสดงว่าโค้ด Rust ที่เรา parse คือ unit struct กับ ident (identifier หมายถึงชื่อ) ของ Pancakes มี field มากขึ้นบน struct นี้สำหรับการอธิบายโค้ด Rust ทุกชนิด — check documentation syn สำหรับ DeriveInput สำหรับข้อมูลเพิ่มเติม

ไม่ช้าเราจะนิยามฟังก์ชัน impl_hello_macro ซึ่งคือที่เราจะสร้างโค้ด Rust ใหม่ที่เราต้องการรวม แต่ก่อนเราทำ สังเกตว่า output สำหรับ macro derive ของเราก็เป็น TokenStream TokenStream ที่ return ถูกเพิ่ม ให้โค้ดที่ user crate ของเราเขียน ดังนั้นเมื่อพวกเขา compile crate ของพวกเขา พวกเขาจะได้ functionality เพิ่มที่เราให้ใน TokenStream ที่แก้

คุณอาจสังเกตว่าเรากำลังเรียก unwrap เพื่อทำให้ฟังก์ชัน hello_macro_derive panic ถ้าการเรียกฟังก์ชัน syn::parse fail ที่นี่ มันจำเป็นสำหรับ procedural macro ของเราที่จะ panic บน error เพราะฟังก์ชัน proc_macro_derive ต้อง return TokenStream แทน Result เพื่อ conform กับ API procedural macro เราทำให้ตัวอย่างนี้ ง่ายโดยใช้ unwrap — ในโค้ด production คุณควรให้ข้อความ error เฉพาะมากขึ้นเกี่ยวกับสิ่งที่ผิดพลาดโดยใช้ panic! หรือ expect

ตอนนี้เรามีโค้ดเพื่อเปลี่ยนโค้ด Rust ที่ annotate จาก TokenStream เป็น instance DeriveInput มา generate โค้ดที่ implement trait HelloMacro บน type ที่ annotate ดังที่แสดงใน Listing 20-42

Filename: hello_macro_derive/src/lib.rs
use proc_macro::TokenStream;
use quote::quote;

#[proc_macro_derive(HelloMacro)]
pub fn hello_macro_derive(input: TokenStream) -> TokenStream {
    // Construct a representation of Rust code as a syntax tree
    // that we can manipulate
    let ast = syn::parse(input).unwrap();

    // Build the trait implementation
    impl_hello_macro(&ast)
}

fn impl_hello_macro(ast: &syn::DeriveInput) -> TokenStream {
    let name = &ast.ident;
    let generated = quote! {
        impl HelloMacro for #name {
            fn hello_macro() {
                println!("Hello, Macro! My name is {}!", stringify!(#name));
            }
        }
    };
    generated.into()
}
Listing 20-42: Implement trait HelloMacro โดยใช้โค้ด Rust ที่ parse

เราได้ instance struct Ident ที่บรรจุชื่อ (identifier) ของ type ที่ annotate โดยใช้ ast.ident Struct ใน Listing 20-41 แสดงว่าเมื่อเรารัน ฟังก์ชัน impl_hello_macro บนโค้ดใน Listing 20-37, ident ที่เราได้ จะมี field ident กับค่า "Pancakes" ดังนั้นตัวแปร name ใน Listing 20-42 จะบรรจุ instance struct Ident ที่ เมื่อ print จะเป็น string "Pancakes" ชื่อของ struct ใน Listing 20-37

Macro quote! ให้เรานิยามโค้ด Rust ที่เราต้องการ return Compiler คาด สิ่งที่ต่างจากผลโดยตรงของการ execute macro quote! ดังนั้นเราต้อง แปลงมันเป็น TokenStream เราทำสิ่งนี้โดยเรียกเมธอด into ซึ่ง consume representation intermediate นี้และ return ค่าของ type TokenStream ที่ต้องการ

Macro quote! ยังให้ template mechanic ที่เจ๋งมาก — เราเข้า #name และ quote! จะแทนมันด้วยค่าในตัวแปร name คุณยังทำการซ้ำคล้ายกับวิธี ที่ macro ปกติทำงานได้ ดู docs ของ crate quote สำหรับ การแนะนำที่ละเอียด

เราต้องการให้ procedural macro ของเรา generate implementation ของ trait HelloMacro ของเราสำหรับ type ที่ user annotate ซึ่งเราได้ โดยใช้ #name Implementation trait มีหนึ่งฟังก์ชัน hello_macro ที่ body บรรจุ functionality ที่เราต้องการให้ — print Hello, Macro! My name is แล้วชื่อของ type ที่ annotate

Macro stringify! ที่ใช้ที่นี่ถูก build ใน Rust มันรับ expression Rust เช่น 1 + 2 และที่ compile time เปลี่ยน expression เป็น string literal เช่น "1 + 2" นี่ต่างจาก format! หรือ println! ซึ่งเป็น macro ที่ evaluate expression แล้วเปลี่ยนผลเป็น String มีความเป็นไป ได้ที่ input #name อาจเป็น expression ที่จะ print literal ดังนั้น เราใช้ stringify! ใช้ stringify! ยังประหยัด allocation โดยแปลง #name เป็น string literal ที่ compile time

ที่จุดนี้ cargo build ควร complete สำเร็จในทั้ง hello_macro และ hello_macro_derive มาเชื่อม crate เหล่านี้ให้โค้ดใน Listing 20-37 เพื่อเห็น procedural macro ในการ action! สร้าง binary project ใหม่ใน ไดเรกทอรี projects ของคุณโดยใช้ cargo new pancakes เราต้องเพิ่ม hello_macro และ hello_macro_derive เป็น dependency ใน Cargo.toml ของ crate pancakes ถ้าคุณกำลัง publish version ของคุณของ hello_macro และ hello_macro_derive ไปยัง crates.io พวกมันจะเป็น dependency ปกติ — ถ้าไม่ คุณระบุพวกมันเป็น dependency path ดังต่อไปนี้:

[dependencies]
hello_macro = { path = "../hello_macro" }
hello_macro_derive = { path = "../hello_macro/hello_macro_derive" }

วางโค้ดใน Listing 20-37 ใน src/main.rs และรัน cargo run — มันควร print Hello, Macro! My name is Pancakes! Implementation ของ trait HelloMacro จาก procedural macro ถูกรวมโดย crate pancakes ไม่ต้อง implement มัน — #[derive(HelloMacro)] เพิ่ม implementation trait

ถัดไป มาสำรวจว่าชนิดอื่นของ procedural macro ต่างจาก custom derive macro ยังไง

Attribute-Like Macro

Attribute-like macro คล้ายกับ custom derive macro แต่แทนการ generate โค้ดสำหรับ attribute derive พวกมันอนุญาตให้คุณสร้าง attribute ใหม่ พวกมันยืดหยุ่นมากกว่าด้วย — derive เพียงทำงานสำหรับ struct และ enum; attribute apply ให้ item อื่นได้ด้วย เช่นฟังก์ชัน นี่คือตัวอย่างของการใช้ attribute-like macro สมมุติคุณมี attribute ชื่อ route ที่ annotate ฟังก์ชันเมื่อใช้ framework web application:

#[route(GET, "/")]
fn index() {

attribute #[route] นี้จะถูกนิยามโดย framework เป็น procedural macro Signature ของฟังก์ชันนิยาม macro จะดูแบบนี้:

#[proc_macro_attribute]
pub fn route(attr: TokenStream, item: TokenStream) -> TokenStream {

ที่นี่ เรามีสอง parameter ของ type TokenStream ตัวแรกคือสำหรับเนื้อหา ของ attribute — ส่วน GET, "/" ตัวที่สองคือ body ของ item ที่ attribute ติด — ในกรณีนี้ fn index() {} และส่วนที่เหลือของ body ฟังก์ชัน

นอกเหนือจากนั้น attribute-like macro ทำงานในวิธีเดียวกับ custom derive macro — คุณสร้าง crate กับ crate type proc-macro และ implement ฟังก์ชันที่ generate โค้ดที่คุณต้องการ!

Function-Like Macro

Function-like macro นิยาม macro ที่ดูเหมือนการเรียกฟังก์ชัน คล้ายกับ macro macro_rules! พวกมันยืดหยุ่นมากกว่าฟังก์ชัน — ตัวอย่างเช่น พวก มันรับจำนวน argument ที่ไม่รู้ได้ อย่างไรก็ตาม macro macro_rules! ถูกนิยามได้เพียงโดยใช้ syntax แบบ match ที่เราพูดถึงในส่วน “Declarative Macro สำหรับ Metaprogramming ทั่วไป” ก่อนหน้า Function-like macro รับ parameter TokenStream และนิยามของ พวกมัน manipulate TokenStream นั้นโดยใช้โค้ด Rust แบบที่สองชนิดอื่น ของ procedural macro ทำ ตัวอย่างของ function-like macro คือ macro sql! ที่อาจถูกเรียกแบบนี้:

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

Macro นี้จะ parse statement SQL ภายในมันและ check ว่ามันถูก syntax ซึ่งคือการ processing complex มากกว่าที่ macro macro_rules! ทำได้ Macro sql! จะถูกนิยามแบบนี้:

#[proc_macro]
pub fn sql(input: TokenStream) -> TokenStream {

นิยามนี้คล้ายกับ signature ของ custom derive macro — เรารับ token ที่อยู่ภายในวงเล็บและ return โค้ดที่เราต้องการ generate

สรุป

โอ้! ตอนนี้คุณมีฟีเจอร์ Rust บางอย่างใน toolbox ของคุณที่คุณน่าจะไม่ใช้ บ่อย แต่คุณจะรู้พวกมันใช้ได้ในสถานการณ์เฉพาะมาก เราแนะนำหัวข้อ complex หลายตัวเพื่อให้เมื่อคุณเจอพวกมันในคำแนะนำข้อความ error หรือในโค้ดของ คนอื่น คุณจะสามารถจำแนกแนวคิดและ syntax เหล่านี้ ใช้บทนี้เป็น reference เพื่อนำทางคุณไปยังวิธีแก้

ถัดไป เราจะเอาทุกอย่างที่เราพูดถึงผ่านหนังสือไป practice และทำอีกหนึ่ง project!