Macro
เราใช้ macro เช่น println! ผ่านหนังสือนี้ แต่เรายังไม่ได้สำรวจเต็มที่
ว่า macro คืออะไรและทำงานยังไง Term macro อ้างถึงตระกูลของฟีเจอร์ใน
Rust — macro แบบ declarative ด้วย macro_rules! และสามชนิดของ
procedural macro:
- Custom
#[derive]macro ที่ระบุโค้ดที่เพิ่มด้วย attributederiveที่ใช้บน 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! ที่ทำให้ง่ายเล็กน้อย
#[macro_export]
macro_rules! vec {
( $( $x:expr ),* ) => {
{
let mut temp_vec = Vec::new();
$(
temp_vec.push($x);
)*
temp_vec
}
};
}
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 เฉพาะ
use proc_macro::TokenStream;
#[some_attribute]
pub fn some_name(input: TokenStream) -> TokenStream {
}
ฟังก์ชันที่นิยาม 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 ของเรา
use hello_macro::HelloMacro;
use hello_macro_derive::HelloMacro;
#[derive(HelloMacro)]
struct Pancakes;
fn main() {
Pancakes::hello_macro();
}
โค้ดนี้จะ print Hello, Macro! My name is Pancakes! เมื่อเราเสร็จ ขั้น
ตอนแรกคือทำ library crate ใหม่ แบบนี้:
$ cargo new hello_macro --lib
ถัดไป ใน Listing 20-38 เราจะนิยาม trait HelloMacro และ associated
function ของมัน
pub trait HelloMacro {
fn hello_macro();
}
deriveเรามี trait และฟังก์ชันของมัน ที่จุดนี้ user crate ของเราสามารถ implement trait เพื่อบรรลุ functionality ที่ต้องการได้ ดังใน Listing 20-39
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();
}
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:
[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
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)
}
สังเกตว่าเราแยกโค้ดเป็นฟังก์ชัน 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
)
}
)
}
DeriveInput ที่เราได้เมื่อ parse โค้ดที่มี attribute ของ macro ใน Listing 20-37Field ของ 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
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()
}
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!