매크로
매크로 (Macros)
이 책 전체에서 println! 같은 매크로를 써왔지만, 매크로가 정확히 무엇이고 어떻게 동작하는지 완전히 살펴보진 않았어요. _매크로(macro)_라는 용어는 Rust의 기능 한 계열을 가리켜요. macro_rules!로 쓰는 선언적 매크로(declarative macro)와 세 종류의 절차적 매크로(procedural macro)가 있죠.
- 구조체와 enum에 쓰는
derive어트리뷰트로 추가할 코드를 지정하는 사용자 정의#[derive]매크로 - 어떤 항목(item)에서든 쓸 수 있는 사용자 정의 어트리뷰트를 정의하는 어트리뷰트-유사 매크로
- 함수 호출처럼 보이지만 인자로 주어진 토큰을 조작하는 함수-유사 매크로
각각을 차례로 다룰게요. 하지만 그 전에, 이미 함수가 있는데 왜 매크로가 필요한지부터 살펴볼게요.
출처: The Rust Book
매크로와 함수의 차이 (The Difference Between Macros and Functions)
근본적으로 매크로는 다른 코드를 쓰는 코드를 작성하는 방법이에요. 이를 _메타프로그래밍(metaprogramming)_이라 불러요. 부록 C에서 여러분을 위해 다양한 트레이트의 구현을 생성하는 derive 어트리뷰트를 다뤘고, 이 책 전체에서 println!과 vec! 매크로도 썼어요. 이 모든 매크로는 _확장(expand)_되어 여러분이 직접 작성한 코드보다 더 많은 코드를 만들어내요.
메타프로그래밍은 작성하고 유지해야 할 코드의 양을 줄이는 데 유용하며, 이는 함수의 역할 중 하나이기도 해요. 하지만 매크로에는 함수에는 없는 추가적인 힘이 몇 가지 있어요.
함수 시그니처는 함수가 가진 파라미터의 개수와 타입을 선언해야 해요. 반면 매크로는 가변 개수의 파라미터를 받을 수 있어요. println!("hello")처럼 인자 하나로 호출할 수도 있고, println!("hello {}", name)처럼 인자 두 개로 호출할 수도 있죠. 게다가 매크로는 컴파일러가 코드의 의미를 해석하기 전에 확장되므로, 예를 들어 주어진 타입에 트레이트를 구현할 수도 있어요. 함수는 그럴 수 없어요. 함수는 런타임에 호출되는데 트레이트는 컴파일 시점에 구현되어야 하거든요.
함수 대신 매크로를 구현하는 단점은, 매크로 정의가 함수 정의보다 더 복잡하다는 점이에요. Rust 코드를 쓰는 Rust 코드를 작성하고 있으니까요. 이런 간접성 때문에 매크로 정의는 일반적으로 함수 정의보다 읽고, 이해하고, 유지하기가 더 어려워요.
매크로와 함수의 또 다른 중요한 차이는, 함수는 어디서 정의하든 어디서든 호출할 수 있는 반면 매크로는 파일에서 호출하기 전에 정의하거나 스코프로 가져와야 한다는 점이에요.
일반 메타프로그래밍용 선언적 매크로 (Declarative Macros for General Metaprogramming)
Rust에서 가장 널리 쓰이는 매크로 형태는 _선언적 매크로(declarative macro)_예요. "매크로 by example", "macro_rules! 매크로", 그냥 "매크로"라고도 불려요. 핵심적으로 선언적 매크로는 Rust match 표현식과 비슷한 것을 작성하게 해줘요. 6장에서 다뤘듯이 match 표현식은 표현식을 받아 그 결과 값을 패턴과 비교하고, 일치하는 패턴과 연관된 코드를 실행하는 제어 구조예요. 매크로 역시 특정 코드와 연관된 패턴에 값을 비교해요. 이 상황에서 값은 매크로에 전달된 문자 그대로의 Rust 소스 코드이고, 패턴은 그 소스 코드의 구조와 비교되며, 각 패턴과 연관된 코드는 일치하면 매크로에 전달된 코드를 대체해요. 이 모든 게 컴파일 중에 일어나요.
매크로를 정의하려면 macro_rules! 구조를 사용해요. vec! 매크로가 어떻게 정의되는지 보면서 macro_rules! 사용법을 살펴볼게요. 8장에서 vec! 매크로로 특정 값들을 가진 새 벡터를 만드는 방법을 다뤘어요. 예를 들어 다음 매크로는 정수 세 개를 담은 새 벡터를 만들어요.
#![allow(unused)]
fn main() {
let v: Vec<u32> = vec![1, 2, 3];
}
vec! 매크로로 정수 두 개짜리 벡터나 문자열 슬라이스 다섯 개짜리 벡터도 만들 수 있어요. 함수로는 똑같이 할 수 없는데, 값의 개수나 타입을 미리 알 수 없기 때문이에요.
Listing 20-35는 vec! 매크로 정의를 약간 단순화한 버전을 보여줘요.
#[macro_export]
macro_rules! vec {
( $( $x:expr ),* ) => {
{
let mut temp_vec = Vec::new();
$(
temp_vec.push($x);
)*
temp_vec
}
};
}
참고: 표준 라이브러리의
vec!매크로 실제 정의에는 올바른 양의 메모리를 미리 할당하는 코드가 포함돼 있어요. 그 코드는 예시를 단순하게 하기 위해 여기서 생략한 최적화예요.
#[macro_export] 어노테이션은 매크로가 정의된 크레이트가 스코프에 들어올 때마다 이 매크로를 사용 가능하게 해야 한다는 뜻이에요. 이 어노테이션이 없으면 매크로를 스코프로 가져올 수 없어요.
그다음 macro_rules!와 정의할 매크로의 이름(느낌표 없이)으로 매크로 정의를 시작해요. 이 경우 vec이라는 이름 뒤에 매크로 정의의 본문을 나타내는 중괄호가 따라와요.
vec! 본문의 구조는 match 표현식의 구조와 비슷해요. 여기에는 ( $( $x:expr ),* ) 패턴을 가진 팔 하나가 있고, => 뒤에 이 패턴과 연관된 코드 블록이 따라와요. 패턴이 일치하면 연관된 코드 블록이 방출(emit)돼요. 이 매크로에는 패턴이 이것 하나뿐이므로 유효한 일치 방법은 단 하나이고, 다른 어떤 패턴도 오류를 낼 거예요. 더 복잡한 매크로는 팔이 여러 개예요.
매크로 정의에서 유효한 패턴 문법은 19장에서 다룬 패턴 문법과 달라요. 매크로 패턴은 값이 아니라 Rust 코드 구조와 대조되기 때문이죠. Listing 20-29의 패턴 구성 요소가 무엇을 뜻하는지 차근차근 살펴볼게요. 매크로 패턴 문법 전체는 Rust Reference를 참고하세요.
먼저 괄호 한 쌍으로 전체 패턴을 감싸요. 매크로 시스템에서 패턴과 일치하는 Rust 코드를 담을 변수를 선언하려면 달러 기호($)를 써요. 달러 기호는 이것이 일반 Rust 변수가 아니라 매크로 변수임을 명확하게 해줘요. 다음에 오는 괄호 한 쌍은 괄호 안의 패턴과 일치하는 값을 캡처해 대체 코드에서 쓰기 위해 담아요. $() 안에는 $x:expr이 있는데, 이는 어떤 Rust 표현식이든 일치시키고 그 표현식에 $x라는 이름을 줘요.
$() 뒤의 쉼표는 $() 안의 코드와 일치하는 코드의 각 인스턴스 사이에 문자 그대로의 쉼표 구분자가 있어야 함을 나타내요. *는 패턴이 * 앞에 오는 것의 0개 이상과 일치함을 지정해요.
이 매크로를 vec![1, 2, 3];으로 호출하면, $x 패턴은 1, 2, 3이라는 세 표현식과 세 번 일치해요.
이제 이 팔과 연관된 코드 본문의 패턴을 살펴볼게요. $()* 안의 temp_vec.push()는 패턴의 $()와 일치하는 각 부분마다 생성되는데, 패턴이 일치하는 횟수에 따라 0번 이상 생성돼요. $x는 각각 일치한 표현식으로 대체되지요. 이 매크로를 vec![1, 2, 3];으로 호출하면, 이 매크로 호출을 대체하는 생성 코드는 다음과 같아요.
{
let mut temp_vec = Vec::new();
temp_vec.push(1);
temp_vec.push(2);
temp_vec.push(3);
temp_vec
}
어떤 타입의 인자든 몇 개든 받아, 지정된 요소들을 담은 벡터를 만드는 코드를 생성하는 매크로를 정의했어요.
매크로 작성법을 더 배우고 싶다면 온라인 문서나, Daniel Keep가 시작하고 Lukas Wirth가 이어온 "The Little Book of Rust Macros" 같은 다른 자료를 참고하세요.
어트리뷰트에서 코드를 생성하는 절차적 매크로 (Procedural Macros for Generating Code from Attributes)
두 번째 매크로 형태는 절차적 매크로(procedural macro)로, 함수처럼 더 동작해요(그리고 프로시저의 한 종류예요). _절차적 매크로_는 어떤 코드를 입력으로 받아 그 코드를 조작하고 출력으로 어떤 코드를 만들어내요. 선언적 매크로처럼 패턴에 대조해서 코드를 다른 코드로 대체하는 게 아니죠. 절차적 매크로의 세 종류는 사용자 정의 derive, 어트리뷰트-유사, 함수-유사이며, 모두 비슷한 방식으로 동작해요.
절차적 매크로를 만들 때 정의는 특별한 크레이트 타입을 가진 자신만의 크레이트에 있어야 해요. 이건 복잡한 기술적 이유 때문인데, 미래에는 없앨 수 있길 바라는 제약이에요. Listing 20-36에서 some_attribute가 특정 매크로 종류를 사용하기 위한 자리표시자일 때, 절차적 매크로를 정의하는 방법을 보여줘요.
use proc_macro::TokenStream;
#[some_attribute]
pub fn some_name(input: TokenStream) -> TokenStream {
}
절차적 매크로를 정의하는 함수는 입력으로 TokenStream을 받고 출력으로 TokenStream을 만들어내요. TokenStream 타입은 Rust와 함께 포함된 proc_macro 크레이트가 정의하며, 토큰의 시퀀스를 나타내요. 이게 매크로의 핵심이에요. 매크로가 조작하는 소스 코드가 입력 TokenStream을 구성하고, 매크로가 만드는 코드가 출력 TokenStream이죠. 함수에는 또 우리가 만드는 절차적 매크로의 종류를 지정하는 어트리뷰트가 붙어 있어요. 같은 크레이트에 여러 종류의 절차적 매크로를 가질 수 있어요.
절차적 매크로의 종류들을 살펴볼게요. 사용자 정의 derive 매크로부터 시작하고, 다른 형태를 다르게 만드는 작은 차이점들을 설명할게요.
사용자 정의 derive 매크로 (Custom derive Macros)
hello_macro라는 연관 함수 하나(hello_macro)를 가진 HelloMacro라는 트레이트를 정의하는 hello_macro라는 크레이트를 만들어 볼게요. 사용자들이 각 타입에 HelloMacro 트레이트를 일일이 구현하게 하는 대신, 사용자가 자신의 타입에 #[derive(HelloMacro)]라고 어노테이션하면 hello_macro 함수의 기본 구현을 얻도록 절차적 매크로를 제공할 거예요. 기본 구현은 Hello, Macro! My name is TypeName!을 출력하는데, 여기서 TypeName은 이 트레이트가 정의된 타입의 이름이에요. 다시 말해 다른 프로그래머가 우리 크레이트로 Listing 20-37 같은 코드를 쓸 수 있게 해주는 크레이트를 작성할 거예요.
use hello_macro::HelloMacro;
use hello_macro_derive::HelloMacro;
#[derive(HelloMacro)]
struct Pancakes;
fn main() {
Pancakes::hello_macro();
}
이 코드는 작업을 마치면 Hello, Macro! My name is Pancakes!를 출력해요. 첫 단계는 이렇게 새 라이브러리 크레이트를 만드는 거예요.
$ cargo new hello_macro --lib
다음으로 Listing 20-38에서 HelloMacro 트레이트와 그 연관 함수를 정의할게요.
pub trait HelloMacro {
fn hello_macro();
}
트레이트와 그 함수가 있네요. 이 시점에서 크레이트 사용자는 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();
}
하지만 hello_macro와 함께 쓰려는 각 타입에 대해 구현 블록을 작성해야 할 거예요. 우리는 사용자들이 이 작업을 하지 않아도 되게 하고 싶어요.
게다가 아직 트레이트가 구현된 타입의 이름을 출력하는 기본 구현으로 hello_macro 함수를 제공할 수 없어요. Rust에는 리플렉션(reflection) 기능이 없어서 런타임에 타입의 이름을 조회할 수 없거든요. 컴파일 시점에 코드를 생성할 매크로가 필요해요.
다음 단계는 절차적 매크로를 정의하는 거예요. 이 글을 쓰는 시점에 절차적 매크로는 자신만의 크레이트에 있어야 해요. 언젠가 이 제한이 풀릴 수도 있어요. 크레이트와 매크로 크레이트를 구성하는 관례는 이래요. foo라는 크레이트에 대해, 사용자 정의 derive 절차적 매크로 크레이트는 foo_derive라고 불려요. hello_macro 프로젝트 안에 hello_macro_derive라는 새 크레이트를 시작해 볼게요.
$ cargo new hello_macro_derive --lib
우리 두 크레이트는 밀접하게 관련되어 있으므로, hello_macro 크레이트의 디렉토리 안에 절차적 매크로 크레이트를 만들어요. hello_macro에서 트레이트 정의를 바꾸면 hello_macro_derive에서 절차적 매크로의 구현도 바꿔야 해요. 두 크레이트는 별도로 게시(publish)해야 하고, 이 크레이트들을 쓰는 프로그래머는 둘 다 의존성으로 추가하고 둘 다 스코프로 가져와야 해요. 대신 hello_macro 크레이트가 hello_macro_derive를 의존성으로 쓰고 절차적 매크로 코드를 다시 내보낼(re-export) 수도 있어요. 하지만 우리가 프로젝트를 구성한 방식 덕분에, derive 기능을 원하지 않는 프로그래머도 hello_macro를 쓸 수 있어요.
hello_macro_derive 크레이트를 절차적 매크로 크레이트로 선언해야 해요. 또 잠시 후 보겠지만 syn과 quote 크레이트의 기능도 필요하므로, 이들을 의존성으로 추가해야 해요. hello_macro_derive의 Cargo.toml 파일에 다음을 추가하세요.
[lib]
proc-macro = true
[dependencies]
syn = "2.0"
quote = "1.0"
절차적 매크로 정의를 시작하려면 Listing 20-40의 코드를 hello_macro_derive 크레이트의 src/lib.rs 파일에 넣으세요. 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)
}
코드를 TokenStream을 파싱하는 역할을 맡은 hello_macro_derive 함수와, 구문 트리를 변환하는 역할을 맡은 impl_hello_macro 함수로 나눴다는 점을 주목하세요. 이렇게 하면 절차적 매크로 작성이 더 편리해져요. 바깥 함수(이 경우 hello_macro_derive)의 코드는 여러분이 보거나 만드는 거의 모든 절차적 매크로 크레이트에서 똑같을 거예요. 안쪽 함수(이 경우 impl_hello_macro)의 본문에 지정하는 코드는 절차적 매크로의 목적에 따라 달라져요.
세 가지 새 크레이트를 소개했어요. proc_macro, syn, quote이죠. proc_macro 크레이트는 Rust와 함께 오므로 _Cargo.toml_의 의존성에 추가할 필요가 없었어요. proc_macro 크레이트는 우리 코드에서 Rust 코드를 읽고 조작하게 해주는 컴파일러의 API예요.
syn 크레이트는 Rust 코드를 문자열에서 연산을 수행할 수 있는 데이터 구조로 파싱해요. quote 크레이트는 syn 데이터 구조를 다시 Rust 코드로 바꿔요. 이 크레이트들은 다루고 싶은 어떤 종류의 Rust 코드든 파싱하는 것을 훨씬 단순하게 만들어줘요. Rust 코드의 완전한 파서를 작성하는 건 결코 간단한 일이 아니거든요.
hello_macro_derive 함수는 라이브러리 사용자가 타입에 #[derive(HelloMacro)]를 지정할 때 호출돼요. 이게 가능한 이유는 여기서 hello_macro_derive 함수에 proc_macro_derive로 어노테이션하고 HelloMacro라는 이름을 지정했기 때문인데, 이는 트레이트 이름과 일치해요. 대부분의 절차적 매크로가 따르는 관례이죠.
hello_macro_derive 함수는 먼저 input을 TokenStream에서 해석하고 연산을 수행할 수 있는 데이터 구조로 변환해요. 여기서 syn이 등장하죠. syn의 parse 함수는 TokenStream을 받아 파싱된 Rust 코드를 나타내는 DeriveInput 구조체를 반환해요. Listing 20-41은 struct Pancakes; 문자열을 파싱할 때 얻는 DeriveInput 구조체의 관련 부분을 보여줘요.
DeriveInput {
// --snip--
ident: Ident {
ident: "Pancakes",
span: #0 bytes(95..103)
},
data: Struct(
DataStruct {
struct_token: Struct,
fields: Unit,
semi_token: Some(
Semi
)
}
)
}
이 구조체의 필드는 우리가 파싱한 Rust 코드가 ident(identifier, 즉 이름)가 Pancakes인 유닛 구조체임을 보여줘요. 이 구조체에는 여러 종류의 Rust 코드를 설명하는 필드가 더 있어요. 자세한 내용은 syn의 DeriveInput 문서를 확인하세요.
곧 impl_hello_macro 함수를 정의할 텐데, 여기서 포함하고 싶은 새 Rust 코드를 만들게 돼요. 하지만 그 전에 derive 매크로의 출력도 TokenStream이라는 점을 주목하세요. 반환된 TokenStream은 크레이트 사용자가 작성한 코드에 추가되므로, 사용자가 크레이트를 컴파일하면 수정된 TokenStream에서 우리가 제공하는 추가 기능을 얻게 돼요.
여기서 unwrap을 호출해 syn::parse 함수 호출이 실패하면 hello_macro_derive 함수가 패닉을 일으키게 한다는 걸 눈치챘을 거예요. 절차적 매크로가 오류에서 패닉하는 것은 필요한 일이에요. proc_macro_derive 함수는 절차적 매크로 API에 맞추기 위해 Result가 아니라 TokenStream을 반환해야 하니까요. 예시를 단순화하기 위해 unwrap을 썼는데, 운영 코드에서는 panic!이나 expect를 사용해 무엇이 잘못됐는지 더 구체적인 오류 메시지를 제공해야 해요.
이제 어노테이션된 Rust 코드를 TokenStream에서 DeriveInput 인스턴스로 바꾸는 코드가 있으니, Listing 20-42처럼 어노테이션된 타입에 HelloMacro 트레이트를 구현하는 코드를 생성해 볼게요.
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()
}
ast.ident으로 어노테이션된 타입의 이름(식별자)을 담은 Ident 구조체 인스턴스를 얻어요. Listing 20-41의 구조체는 Listing 20-37의 코드에 impl_hello_macro 함수를 실행하면, 얻는 ident의 ident 필드 값이 "Pancakes"임을 보여줘요. 따라서 Listing 20-42의 name 변수는 출력하면 "Pancakes"라는 문자열, 즉 Listing 20-37의 구조체 이름이 되는 Ident 구조체 인스턴스를 담게 돼요.
quote! 매크로는 반환하고 싶은 Rust 코드를 정의하게 해줘요. 컴파일러는 quote! 매크로 실행의 직접적인 결과와는 다른 것을 기대하므로, TokenStream으로 변환해야 해요. into 메서드를 호출하면 되는데, 이 메서드는 이 중간 표현을 소비하고 필요한 TokenStream 타입의 값을 반환해요.
quote! 매크로는 또 매우 멋진 템플릿 메커니즘을 제공해요. #name을 입력하면 quote!가 변수 name의 값으로 대체하지요. 일반 매크로가 동작하는 방식과 비슷한 반복도 할 수 있어요. 철저한 소개는 the quote crate's docs를 확인하세요.
우리의 절차적 매크로는 사용자가 어노테이션한 타입에 우리 HelloMacro 트레이트의 구현을 생성하기를 원하는데, #name으로 얻을 수 있어요. 트레이트 구현에는 hello_macro라는 함수 하나가 있고, 그 본문에는 제공하고 싶은 기능, 즉 Hello, Macro! My name is 다음에 어노테이션된 타입의 이름을 출력하는 기능이 들어 있어요.
여기 쓰인 stringify! 매크로는 Rust에 내장되어 있어요. 1 + 2 같은 Rust 표현식을 받아 컴파일 시점에 "1 + 2" 같은 문자열 리터럴로 바꿔요. 이는 표현식을 평가한 다음 결과를 String으로 바꾸는 format!이나 println!과는 달라요. #name 입력이 문자 그대로 출력할 표현식일 가능성이 있으므로 stringify!를 써요. stringify!를 쓰는 것은 또 #name을 컴파일 시점에 문자열 리터럴로 바꿔서 할당(allocation)을 아끼는 효과도 있어요.
이 시점에서 cargo build는 hello_macro와 hello_macro_derive 둘 다에서 성공해야 해요. 이 크레이트들을 Listing 20-37의 코드와 연결해서 절차적 매크로가 동작하는 걸 볼게요! projects 디렉토리에 cargo new pancakes로 새 바이너리 프로젝트를 만드세요. pancakes 크레이트의 _Cargo.toml_에 hello_macro와 hello_macro_derive를 의존성으로 추가해야 해요. 여러분의 버전을 crates.io에 게시한다면 일반 의존성이 될 것이고, 아니라면 다음과 같이 path 의존성으로 지정할 수 있어요.
[dependencies]
hello_macro = { path = "../hello_macro" }
hello_macro_derive = { path = "../hello_macro/hello_macro_derive" }
Listing 20-37의 코드를 _src/main.rs_에 넣고 cargo run을 실행하세요. Hello, Macro! My name is Pancakes!가 출력돼야 해요. 절차적 매크로의 HelloMacro 트레이트 구현은 pancakes 크레이트가 구현할 필요 없이 포함됐어요. #[derive(HelloMacro)]가 트레이트 구현을 추가한 거죠.
다음으로 다른 종류의 절차적 매크로가 사용자 정의 derive 매크로와 어떻게 다른지 살펴볼게요.
어트리뷰트-유사 매크로 (Attribute-Like Macros)
어트리뷰트-유사 매크로는 사용자 정의 derive 매크로와 비슷하지만, derive 어트리뷰트용 코드를 생성하는 대신 새 어트리뷰트를 만들게 해줘요. 더 유연하기도 해요. derive는 구조체와 enum에서만 동작하지만, 어트리뷰트는 함수 같은 다른 항목에도 적용될 수 있어요. 어트리뷰트-유사 매크로 사용 예시를 보죠. 웹 애플리케이션 프레임워크를 쓸 때 함수에 어노테이션하는 route라는 어트리뷰트가 있다고 해볼게요.
#[route(GET, "/")]
fn index() {
이 #[route] 어트리뷰트는 프레임워크가 절차적 매크로로 정의할 거예요. 매크로 정의 함수의 시그니처는 이렇게 생겼을 거예요.
#[proc_macro_attribute]
pub fn route(attr: TokenStream, item: TokenStream) -> TokenStream {
여기 TokenStream 타입의 파라미터가 두 개 있어요. 첫 번째는 어트리뷰트의 내용, 즉 GET, "/" 부분용이에요. 두 번째는 어트리뷰트가 붙은 항목의 본문인데, 이 경우 fn index() {}와 함수 본문의 나머지예요.
그 외에는 어트리뷰트-유사 매크로는 사용자 정의 derive 매크로와 같은 방식으로 동작해요. proc-macro 크레이트 타입으로 크레이트를 만들고, 원하는 코드를 생성하는 함수를 구현하면 돼요!
함수-유사 매크로 (Function-Like Macros)
함수-유사 매크로는 함수 호출처럼 보이는 매크로를 정의해요. macro_rules! 매크로와 비슷하게 함수보다 더 유연한데, 예를 들어 알 수 없는 개수의 인자를 받을 수 있어요. 하지만 macro_rules! 매크로는 앞서 [선언적 매크로] 절에서 다룬 match-유사 문법으로만 정의할 수 있어요. 함수-유사 매크로는 TokenStream 파라미터를 받고, 그 정의는 다른 두 종류의 절차적 매크로가 하는 것처럼 Rust 코드로 그 TokenStream을 조작해요. 함수-유사 매크로의 예시로는 sql! 매크로가 있는데, 이렇게 호출할 수 있을 거예요.
let sql = sql!(SELECT * FROM posts WHERE id=1);
이 매크로는 내부의 SQL 문을 파싱해 구문상 올바른지 검사할 텐데, 이는 macro_rules! 매크로가 할 수 있는 것보다 훨씬 복잡한 처리예요. sql! 매크로는 이렇게 정의될 거예요.
#[proc_macro]
pub fn sql(input: TokenStream) -> TokenStream {
이 정의는 사용자 정의 derive 매크로의 시그니처와 비슷해요. 괄호 안의 토큰을 받아 생성하고 싶은 코드를 반환하지요.
요약 (Summary)
휴! 이제 자주 쓰진 않겠지만, 아주 특정한 상황에서 쓸 수 있다는 걸 알게 될 Rust 기능 몇 가지가 여러분의 도구 상자에 들어갔어요. 오류 메시지 제안이나 다른 사람의 코드에서 이 개념들과 문법을 만났을 때 인식할 수 있도록, 여러 복잡한 주제를 소개했어요. 이 장을 해결책으로 인도하는 참고자료로 사용하세요.
다음으로는 이 책 전체에서 논의한 모든 것을 실천에 옮기고, 프로젝트 하나를 더 해볼게요!
더 알아보기 (Learn more)
- 선언적 매크로 (
macro_rules!) — The Little Book of Rust Macros - 매크로 by example (The Rust Reference)
- 절차적 매크로 (The Rust Reference)
- syn 크레이트, quote 크레이트