매크로
이 책 전반에서 println! 같은 매크로를 계속 사용해 왔지만, 매크로가 정확히 무엇이고
어떻게 동작하는지는 아직 깊이 다루지 않았습니다. 러스트에서 매크로(macro) 라는 말은
하나의 단일 기능이 아니라, 여러 기능군을 가리킵니다. 구체적으로는 macro_rules! 를
사용한 선언적 매크로와, 세 종류의 프로시저럴 매크로가 있습니다.
- 구조체와 enum에 붙는
derive속성과 함께 코드를 생성하는 사용자 정의#[derive]매크로 - 어떤 항목이든 붙일 수 있는 커스텀 속성을 정의하는 attribute-like 매크로
- 함수 호출처럼 보이지만 인수로 전달된 토큰 자체를 다루는 function-like 매크로
이제 이들을 차례로 살펴보겠습니다. 하지만 먼저, 함수가 이미 있는데도 왜 매크로가 필요한지 부터 봅시다.
매크로와 함수의 차이
근본적으로 매크로는 “다른 코드를 생성하는 코드”를 작성하는 방법입니다. 이를
메타프로그래밍(metaprogramming) 이라고 부릅니다. 부록 C에서는 derive 속성이
여러 트레이트 구현을 자동으로 만들어 준다는 이야기를 했고, 이 책 전체에서 println!
과 vec! 매크로도 사용해 왔습니다. 이런 매크로들은 여러분이 손으로 쓴 코드보다 더
많은 코드를 확장(expand) 하여 생성합니다.
메타프로그래밍은 작성하고 유지해야 할 코드 양을 줄이는 데 유용합니다. 이 점은 함수도 마찬가지입니다. 하지만 매크로에는 함수가 가지지 못한 몇 가지 추가 능력이 있습니다.
함수 시그니처는 함수가 받는 매개변수의 개수와 타입을 미리 선언해야 합니다. 반면
매크로는 가변 개수의 매개변수를 받을 수 있습니다. 예를 들어
println!("hello") 는 인수 하나로 호출할 수 있고,
println!("hello {}", name) 은 두 인수로 호출할 수 있습니다. 또한 매크로는
컴파일러가 코드 의미를 해석하기 전 에 확장되므로, 예를 들어 특정 타입에 트레이트를
구현하는 코드까지도 만들어 낼 수 있습니다. 함수는 런타임에 호출되는 것이므로, 컴파일
시점에 필요한 트레이트 구현을 대신할 수 없습니다.
하지만 함수 대신 매크로를 구현하는 데는 단점도 있습니다. 매크로 정의는 “러스트 코드를 만드는 러스트 코드”를 써야 하므로, 일반 함수 정의보다 훨씬 복잡합니다. 이런 한 단계의 간접성 때문에, 매크로 정의는 보통 함수 정의보다 읽고 이해하고 유지보수하기 어렵습니다.
또 하나 중요한 차이는, 함수는 파일 어디에 정의하든 어디에서든 호출할 수 있지만, 매크로는 그 파일 안에서 호출하기 전에 먼저 정의되어 있거나 스코프로 들어와 있어야 한다는 점입니다.
일반적인 메타프로그래밍을 위한 선언적 매크로
러스트에서 가장 널리 쓰이는 매크로 형태는 선언적 매크로(declarative macro) 입니다.
이것들은 “매크로 바이 예제”, “macro_rules! 매크로”, 혹은 그냥 “매크로”라고도
불립니다. 핵심적으로 선언적 매크로는, 러스트의 match 식과 비슷한 것을 작성하게 해
줍니다. 6장에서 이야기했듯 match 식은 어떤 식의 결과 값을 여러 패턴과 비교하고,
매칭된 패턴에 연결된 코드를 실행하는 제어 구조입니다. 매크로도 값과 패턴을 비교한 뒤,
맞는 패턴에 연결된 코드를 실행합니다. 다만 여기서 “값”은 매크로에 전달된 러스트 소스
코드 그 자체이고, 패턴은 그 소스 코드의 구조와 매칭됩니다. 그리고 패턴과 맞는 코드가
기존 코드를 대체합니다. 이 모든 일은 컴파일 중에 일어납니다.
매크로를 정의하려면 macro_rules! 구문을 사용합니다. 이를 실제로 보기 위해,
vec! 매크로가 어떻게 정의되는지 살펴봅시다. 8장에서 우리는 vec! 매크로로 특정
값들을 담은 새 벡터를 만들 수 있다고 설명했습니다. 예를 들어 다음 매크로 호출은
정수 세 개를 담은 새 벡터를 만듭니다.
#![allow(unused)]
fn main() {
let v: Vec<u32> = vec![1, 2, 3];
}
같은 매크로는 정수 두 개를 담은 벡터도, 문자열 슬라이스 다섯 개를 담은 벡터도 만들 수 있습니다. 함수로는 같은 일을 할 수 없습니다. 매개변수 개수도 타입도 미리 알 수 없기 때문입니다.
목록 20-35는 vec! 매크로 정의를 조금 단순화한 버전입니다.
#[macro_export]
macro_rules! vec {
( $( $x:expr ),* ) => {
{
let mut temp_vec = Vec::new();
$(
temp_vec.push($x);
)*
temp_vec
}
};
}
vec! 매크로 정의의 단순화된 버전Note: 실제 표준 라이브러리의 vec! 정의에는 필요한 메모리를 미리 정확히 할당하는
코드도 들어 있습니다. 하지만 이 예제를 단순하게 유지하기 위해 그 최적화 코드는
생략했습니다.
#[macro_export] 주석은 이 매크로가 정의된 크레이트가 스코프로 들어올 때마다 함께
사용 가능해야 한다는 뜻입니다. 이 속성이 없으면 매크로를 바깥에서 가져다 쓸 수 없습니다.
그 다음 macro_rules! 와 느낌표 뒤에, 느낌표를 뺀 매크로 이름을 적으며 정의를
시작합니다. 여기서는 이름이 vec 이고, 그 뒤의 중괄호가 매크로 정의 본문입니다.
vec! 본문 구조는 match 식과 비슷합니다. 여기에는 ( $( $x:expr ),* )
라는 패턴을 가진 arm 하나가 있고, 그 뒤에 => 와 그 패턴에 매칭되었을 때 내보낼
코드 블록이 옵니다. 이 매크로에는 arm이 하나뿐이므로, 패턴에 맞는 경우도 하나뿐이고
그 외는 모두 오류가 납니다. 더 복잡한 매크로는 arm이 여러 개일 수 있습니다.
매크로 정의 안에서 쓸 수 있는 패턴 문법은 19장에서 다룬 패턴 문법과는 다릅니다. 매크로 패턴은 값이 아니라 “러스트 코드의 구조”와 매칭되기 때문입니다. 목록 20-35의 패턴이 각각 무슨 뜻인지 하나씩 봅시다. 전체 패턴 문법은 Rust Reference 에서 볼 수 있습니다.
먼저 괄호 한 쌍으로 전체 패턴을 감쌉니다. 그리고 달러 기호($)를 사용해,
패턴과 매칭된 러스트 코드를 저장할 매크로 시스템 안의 변수를 선언합니다. 달러 기호는
이것이 일반 러스트 변수가 아니라 “매크로 변수”라는 점을 분명히 해 줍니다.
그 다음 $() 로 둘러싼 부분은, 괄호 안 패턴과 맞는 값들을 캡처해서 나중에
치환 코드에서 사용할 수 있게 합니다. 이 $() 안의 $x:expr 은 어떤 러스트 식과도
매칭되고, 그 식에 $x 라는 이름을 붙입니다.
$() 뒤의 쉼표는 $() 안 패턴과 매칭되는 코드 조각 각각 사이에 리터럴 쉼표가
있어야 함을 뜻합니다. 그리고 * 는 그 앞의 패턴이 0번 이상 반복될 수 있다는 뜻입니다.
우리가 vec![1, 2, 3]; 처럼 매크로를 호출하면, $x 패턴은 1, 2, 3
세 번과 매칭됩니다.
이제 arm의 본문 안 코드 패턴을 봅시다. 여기서는 $()* 안에 들어 있는
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 macro) 입니다. 선언적 매크로와
달리 함수처럼 동작합니다. 즉, 패턴과 비교한 뒤 코드를 바꾸는 대신, 입력으로 코드를 받고
그 코드를 가공한 뒤 다시 코드를 만들어 냅니다. 프로시저럴 매크로에는 custom derive,
attribute-like, function-like 세 종류가 있고, 작동 방식은 모두 비슷합니다.
프로시저럴 매크로를 만들 때는, 특별한 크레이트 타입을 가진 별도의 크레이트에 정의를
두어야 합니다. 기술적인 이유가 있지만, 앞으로는 이 제약이 사라질 수도 있습니다.
목록 20-36은 프로시저럴 매크로를 정의하는 일반적인 형태를 보여 줍니다. 여기서
some_attribute 는 구체적인 매크로 종류를 대신하는 자리표시자입니다.
use proc_macro::TokenStream;
#[some_attribute]
pub fn some_name(input: TokenStream) -> TokenStream {
}
프로시저럴 매크로를 정의하는 함수는 TokenStream 을 입력으로 받고, TokenStream
을 출력으로 반환합니다. TokenStream 타입은 러스트와 함께 제공되는 proc_macro
크레이트가 정의하며, 토큰들의 시퀀스를 나타냅니다. 즉, 매크로가 다루는 소스 코드
자체입니다. 매크로가 입력 코드에 대해 수행한 결과가 다시 출력 TokenStream 이
됩니다. 또한 함수 앞에 붙는 속성은 우리가 어떤 종류의 프로시저럴 매크로를 만들고
있는지 지정합니다. 하나의 크레이트 안에 여러 종류의 프로시저럴 매크로를 둘 수도
있습니다.
이제 각각의 종류가 어떻게 다른지 보겠습니다. 먼저 사용자 정의 derive 매크로부터
시작한 뒤, 나머지 두 종류는 무엇이 조금 다른지 설명하겠습니다.
사용자 정의 derive 매크로
hello_macro 라는 크레이트를 하나 만들고, 그 안에 hello_macro 라는 연관 함수
하나를 가진 HelloMacro 트레이트를 정의한다고 해 봅시다. 사용자가 각자의 타입에
대해 HelloMacro 를 직접 구현하게 만들 수도 있겠지만, 대신
#[derive(HelloMacro)] 를 붙이기만 하면 기본 구현을 얻을 수 있는 프로시저럴 매크로를
제공하겠습니다. 그 기본 구현은 Hello, Macro! My name is TypeName! 을 출력하는데,
여기서 TypeName 은 트레이트가 구현된 타입 이름입니다. 다시 말해, 우리는 우리
크레이트 사용자가 목록 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
그 다음 목록 20-38처럼 HelloMacro 트레이트와 그 연관 함수를 정의합니다.
pub trait HelloMacro {
fn hello_macro();
}
derive 매크로와 함께 사용할 단순한 트레이트이제 트레이트와 함수는 준비되었습니다. 물론 이 시점에서도 목록 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 트레이트를 수동으로 구현해야 한다면 어떻게 생길지하지만 우리는 사용자가 hello_macro 를 쓰고 싶은 각 타입마다 이런 구현 블록을 손으로
쓰지 않아도 되게 만들고 싶습니다.
게다가 현재로서는 hello_macro 함수 기본 구현 안에서 “이 트레이트가 어떤 타입에
구현되었는지”의 이름을 출력하는 것도 불가능합니다. 러스트에는 런타임 리플렉션이 없기
때문입니다. 결국 타입 이름을 이용한 코드를 컴파일 시점에 생성 하려면 매크로가
필요합니다.
다음 단계는 프로시저럴 매크로를 정의하는 것입니다. 이 글을 쓰는 시점에서, 프로시저럴
매크로는 반드시 별도의 크레이트 안에 있어야 합니다. 나중에는 이 제약이 사라질 수도
있습니다. 보통 크레이트와 매크로 크레이트는 다음과 같이 이름을 짓습니다. foo 라는
크레이트가 있다면, 그 사용자 정의 derive 프로시저럴 매크로 크레이트는 보통
foo_derive 라고 부릅니다. 따라서 hello_macro 프로젝트 안에
hello_macro_derive 라는 새 크레이트를 만듭니다.
$ cargo new hello_macro_derive --lib
이 두 크레이트는 매우 밀접하게 연결되어 있으므로, hello_macro 크레이트 디렉터리
안에 프로시저럴 매크로 크레이트를 함께 만드는 것입니다. 만약 hello_macro 안의
트레이트 정의를 바꾸면, hello_macro_derive 안의 프로시저럴 매크로 구현도 함께
바꿔야 하기 때문입니다. 이 두 크레이트는 따로따로 배포되어야 하고, 사용하는 사람도
둘 다 의존성으로 추가해 스코프로 가져와야 합니다. 물론 hello_macro 가
hello_macro_derive 를 내부 의존성으로 삼고 그것을 다시 재수출하게 만들 수도
있습니다. 하지만 지금처럼 프로젝트를 구성해 두면, 사용자는 derive 기능이 필요
없을 때 hello_macro 만 쓸 수도 있습니다.
이제 hello_macro_derive 크레이트를 “프로시저럴 매크로 크레이트”라고 선언해야
합니다. 또한 곧 사용하게 될 syn, quote 크레이트도 의존성으로 추가해야 합니다.
Cargo.toml 에 다음 내용을 넣습니다.
[lib]
proc-macro = true
[dependencies]
syn = "2.0"
quote = "1.0"
이제 프로시저럴 매크로 정의를 시작할 준비가 되었습니다.
hello_macro_derive 크레이트의 src/lib.rs 에 목록 20-40의 코드를 넣어 보세요.
아직 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 함수와 impl_hello_macro 함수로 나눴다는 점에
주목하세요. hello_macro_derive 는 입력 TokenStream 을 파싱하는 역할을 하고,
impl_hello_macro 는 파싱된 구문 트리를 변형하는 역할을 합니다. 이런 식으로 나누면
프로시저럴 매크로를 작성하기가 훨씬 편합니다. 바깥 함수(hello_macro_derive)의 코드는
거의 모든 프로시저럴 매크로 크레이트에서 비슷하게 생기고, 안쪽 함수
(impl_hello_macro) 안의 코드는 여러분 매크로의 구체적인 목적에 따라 달라집니다.
여기서는 proc_macro, syn, quote
세 새 크레이트가 등장했습니다. proc_macro 는 러스트와 함께 제공되므로
Cargo.toml 에 따로 추가할 필요가 없었습니다. proc_macro 크레이트는 우리 코드
안에서 러스트 코드를 읽고 조작할 수 있게 해 주는 컴파일러 API입니다.
syn 크레이트는 문자열 형태의 러스트 코드를, 우리가 연산을 수행할 수 있는 자료구조로
파싱합니다. quote 크레이트는 syn 자료구조를 다시 러스트 코드로 바꿔 줍니다.
이 두 크레이트 덕분에 우리가 처리하고 싶은 거의 모든 러스트 코드를 훨씬 쉽게 다룰 수
있습니다. 러스트 전체 문법 파서를 직접 작성하는 것은 매우 어려운 일이기 때문입니다.
우리 라이브러리 사용자가 어떤 타입에 #[derive(HelloMacro)] 를 붙이면,
hello_macro_derive 함수가 호출됩니다. 이것이 가능한 이유는, 함수 앞에
proc_macro_derive 속성을 붙이고 이름도 트레이트와 같은 HelloMacro 로
지정했기 때문입니다. 이것이 대부분의 프로시저럴 매크로가 따르는 관례입니다.
hello_macro_derive 함수는 먼저 input 으로 들어온 TokenStream 을 우리가
해석하고 조작할 수 있는 자료구조로 바꿉니다. 여기서 syn 이 사용됩니다.
syn::parse 함수는 TokenStream 을 받아, 파싱된 러스트 코드를 표현하는
DeriveInput 구조체를 반환합니다. 목록 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
)
}
)
}
DeriveInput 인스턴스이 구조체 필드들은 우리가 파싱한 러스트 코드가 Pancakes 라는 식별자(ident)를
가진 유닛 구조체라는 사실을 보여 줍니다. 물론 실제 구조체에는 훨씬 더 많은 필드가
있고, 더 다양한 러스트 코드를 설명합니다. 자세한 내용은 syn 의 DeriveInput
문서를 참고하세요.
곧 impl_hello_macro 함수를 정의해 우리가 삽입하고 싶은 새 러스트 코드를 만들게
되겠지만, 그 전에 한 가지를 더 짚고 넘어갑시다. derive 매크로의 출력 역시
TokenStream 입니다. 이 반환 TokenStream 은 우리 크레이트 사용자가 쓴 코드에
추가로 더해집니다. 즉, 그 사용자의 크레이트를 컴파일할 때 이 추가 기능까지 함께
얻게 됩니다.
여기서 syn::parse 호출 실패 시 unwrap 으로 패닉하도록 한 것도 보였을 것입니다.
프로시저럴 매크로는 API 시그니처상 Result 가 아니라 TokenStream 을 반환해야
하므로, 이런 실패 상황에서는 패닉할 수밖에 없습니다. 여기서는 단순하게 unwrap
을 사용했지만, 실제 코드에서는 panic! 이나 expect 로 좀 더 구체적인 에러
메시지를 제공하는 것이 좋습니다.
이제 매크로 속성이 붙은 러스트 코드를 TokenStream 에서 DeriveInput 으로 바꾸는
과정은 준비되었습니다. 다음으로는, 이 정보를 바탕으로 “주석이 붙은 타입에
HelloMacro 를 구현하는 코드”를 실제로 만들어 보겠습니다. 목록 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 트레이트 구현 생성하기우리는 ast.ident 를 사용해, 매크로 주석이 붙은 타입 이름을 담은 Ident 구조체
인스턴스를 얻습니다. 목록 20-41을 떠올려 보면, 목록 20-37 코드에 대해
impl_hello_macro 를 실행하면 이 ident 필드는 "Pancakes" 값을 가집니다.
따라서 목록 20-42의 name 변수는, 출력하면 "Pancakes" 라는 문자열로 보이는
Ident 구조체를 담게 됩니다.
quote! 매크로는 우리가 반환하고 싶은 러스트 코드를 정의하게 해 줍니다. 다만
컴파일러는 quote! 실행 결과 그대로가 아니라 TokenStream 을 원하므로,
into 메서드를 호출해 중간 표현을 소비하고 필요한 TokenStream 타입으로
바꿉니다.
quote! 매크로에는 아주 멋진 템플릿 기능도 있습니다. 예를 들어 #name 이라고
쓰면, quote! 가 그것을 변수 name 의 값으로 치환해 줍니다. 반복도 어느 정도
가능합니다. 자세한 내용은 quote 크레이트 문서를 참고하세요.
우리가 원하는 것은, 사용자가 주석을 붙인 타입 위에 HelloMacro 트레이트 구현을
자동으로 생성하는 것입니다. 그러려면 #name 을 사용해 그 타입 이름을 코드 안에
넣으면 됩니다. 트레이트 구현 안에는 hello_macro 함수 하나가 있고, 이 함수의
본문은 Hello, Macro! My name is 다음에 “주석이 붙은 타입의 이름”을 출력하는
코드를 담습니다.
여기서 쓰는 stringify! 매크로는 러스트에 내장되어 있습니다. 1 + 2 같은 러스트
식 하나를 받아, 컴파일 시점에 "1 + 2" 같은 문자열 리터럴로 바꿔 줍니다.
이는 식을 먼저 평가한 뒤 결과를 String 으로 바꾸는 format! 이나 println!
와는 다릅니다. #name 입력은 “그대로 출력하고 싶은 식”에 해당할 수 있으므로
stringify! 를 사용하는 것입니다. 또한 stringify! 를 쓰면 #name 을 컴파일
시점에 문자열 리터럴로 만들어 주므로, 런타임 할당도 줄일 수 있습니다.
이 시점이면 hello_macro 와 hello_macro_derive 둘 모두에서 cargo build
가 성공해야 합니다. 이제 이 두 크레이트를 목록 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" }
이제 목록 20-37의 코드를 src/main.rs 에 넣고 cargo run 을 실행하면
Hello, Macro! My name is Pancakes! 가 출력됩니다. pancakes 크레이트는
직접 HelloMacro 를 구현하지 않았지만, #[derive(HelloMacro)] 덕분에
프로시저럴 매크로가 그 구현을 자동으로 만들어 넣은 것입니다.
다음으로는 다른 종류의 프로시저럴 매크로가 사용자 정의 derive 와 어떻게 다른지
살펴봅시다.
attribute-like 매크로
Attribute-like 매크로는 custom derive 매크로와 비슷하지만, derive 속성용 코드를
생성하는 대신 새 속성 자체를 정의할 수 있게 해 줍니다. 또한 더 유연합니다.
derive 는 구조체와 enum에만 붙일 수 있지만, attribute-like 매크로는 함수 같은
다른 항목에도 붙일 수 있습니다. 예를 들어 웹 프레임워크에서 함수에 붙는 route
속성이 있다고 해 봅시다.
#[route(GET, "/")]
fn index() {
이 #[route] 속성은 프레임워크가 제공하는 프로시저럴 매크로이며, 정의 시그니처는
대략 다음과 같습니다.
#[proc_macro_attribute]
pub fn route(attr: TokenStream, item: TokenStream) -> TokenStream {
여기에는 TokenStream 타입 매개변수 두 개가 있습니다. 첫 번째는 속성 내용, 즉
GET, "/" 부분입니다. 두 번째는 이 속성이 붙어 있는 항목 자체, 여기서는
fn index() {} 와 함수 본문 전체입니다.
이를 제외한 나머지는 custom derive 매크로와 같습니다. 즉, proc-macro 크레이트
타입을 가진 별도 크레이트를 만들고, 원하는 코드를 생성하는 함수를 구현하면 됩니다.
function-like 매크로
Function-like 매크로는 함수 호출처럼 보이는 매크로를 정의합니다. macro_rules!
매크로와 비슷하게 함수처럼 생긴 문법을 가지지만, 일반 함수보다 훨씬 유연합니다.
예를 들어 인수 개수를 미리 알 필요가 없습니다. 하지만 macro_rules! 매크로는 앞서
“일반적인 메타프로그래밍을 위한 선언적 매크로” 절에서
살펴본 “패턴 매칭 같은 문법”으로만 정의할 수 있습니다. 반면 function-like 매크로는
다른 프로시저럴 매크로들과 마찬가지로 TokenStream 을 인수로 받고, 러스트 코드로
그 TokenStream 을 조작합니다. 예를 들어 SQL 문장을 파싱하는 sql! 매크로는
다음처럼 호출될 수 있습니다.
let sql = sql!(SELECT * FROM posts WHERE id=1);
이 매크로는 내부 SQL 문장을 파싱하고, 문법적으로 올바른 SQL 인지를 검사할 수 있습니다.
이런 처리는 macro_rules! 만으로는 하기 어려울 정도로 복잡합니다. 이 sql!
매크로 정의는 다음처럼 생깁니다.
#[proc_macro]
pub fn sql(input: TokenStream) -> TokenStream {
이 정의는 custom derive 매크로 시그니처와 비슷합니다. 괄호 안 토큰들을 입력으로
받아, 우리가 원하는 코드를 출력으로 돌려주기 때문입니다.
정리
후우! 이제 여러분의 러스트 도구 상자에는 아마 자주 쓰지는 않겠지만, 아주 특정한 상황에서 유용한 기능들도 들어가게 되었습니다. 우리는 여러 복잡한 주제를 소개했기 때문에, 앞으로 오류 메시지나 다른 사람 코드에서 이런 개념을 만나더라도 적어도 그것이 무엇인지 알아볼 수 있을 것입니다. 이 장은 나중에 해결책을 찾을 때 참고서로 활용하세요.