이전 글에서 Rust 선언형 매크로의 메타변수에 대해 다룬 내용을 바탕으로, 이번에는 Rust의 다양한 아이템 구문을 분석하는 방법을 살펴봅니다. Rust 크레이트(crate)는 여러 아이템(item)으로 구성되며, fn, struct, enum, impl, mod 등 각 정의가 하나의 아이템이 됩니다. 이 글에서는 Function과 struct 아이템의 구문 분석에 초점을 맞춥니다.
함수 아이템 분석
가장 기본적인 함수 정의부터 시작해보겠습니다.
fn display_message() {
// 함수 본문
}
위 display_message 함수는 키워드 fn으로 시작하고, 그 뒤에 함수 이름($func_name: ident), 빈 매개변수 목록을 나타내는 (), 그리고 함수 본문인 $body_block: block이 따라옵니다. 이를 매치하는 매크로는 다음과 같이 작성할 수 있습니다.
macro_rules! parse_simple_fn {
(fn $func_name: ident () $body_block: block) => {
// 매칭된 함수 정의를 그대로 다시 생성
fn $func_name() $body_block
};
}
parse_simple_fn! {
fn greet_user(){
println!("안녕하세요!");
}
}
// greet_user(); // 이 함수는 매크로에 의해 정의됩니다.
조금 더 복잡한 형태의 함수를 매치해봅시다. 흔히 볼 수 있는 속성, 가시성, 비동기 키워드, 매개변수, 반환 타입을 포함한 함수입니다.
#[allow(dead_code)]
pub async fn fetch_data(id: u32) -> String {
format!("데이터-{}", id)
}
완벽한 함수 구문 정의는 Rust Reference에서 찾아볼 수 있습니다. 여기서는 분석 방법에 집중하고, 필요한 경우 복잡한 부분을 추가로 분해하여 처리할 수 있음을 보여줍니다.
macro_rules! parse_advanced_fn {
(
#[$attribute: meta]
$visibility: vis async fn $func_name: ident ($param_name: ident : $param_type: ty) -> $return_type:ty $body_block: block
) => {
// 매칭된 모든 요소를 다시 조합하여 새로운 함수 생성
#[$attribute]
$visibility async fn $func_name($param_name: $param_type) -> $return_type $body_block
};
}
// parse_advanced_fn! { /* 위의 fetch_data 함수를 매칭하여 사용 */ }
속성의 개수나 매개변수의 개수가 고정적이지 않은 경우는 어떨까요?
#[derive(Debug)]
#[allow(unused_variables)]
pub async fn process_inputs(value1: i32, value2: f64) -> f64 {
value1 as f64 * value2
}
이를 처리하기 위해 반복자($()*)와 선택적 패턴($(,)?)을 활용합니다.
macro_rules! parse_flexible_fn {
(
$(#[$attr: meta])* // 0개 이상의 속성
$vis: vis async fn $func_name: ident ($($param_name: ident : $param_type: ty),* $(,)?) -> $return_type:ty $func_body: block
) => {
$(#[$attr])*
$vis async fn $func_name($($param_name: $param_type),*) -> $return_type $func_body
};
}
함수에 async 키워드가 없을 수도 있습니다. 이때는 매크로에 추가적인 분기(rule)를 정의하여 처리할 수 있습니다.
macro_rules! parse_complete_fn {
// async 함수 처리
(
$(#[$attr: meta])*
$vis: vis async fn $func_name: ident ($($param_name: ident : $param_type: ty),* $(,)?) -> $return_type:ty $func_body: block
) => {
$(#[$attr])*
$vis async fn $func_name($($param_name: $param_type),*) -> $return_type $func_body
};
// 일반 함수 처리
(
$(#[$attr: meta])*
$vis: vis fn $func_name: ident ($($param_name: ident : $param_type: ty),* $(,)?) -> $return_type:ty $func_body: block
) => {
$(#[$attr])*
$vis fn $func_name($($param_name: $param_type),*) -> $return_type $func_body
};
}
이렇게 단순히 함수 정의를 복사하는 것이 무슨 이점이 있을까요? 매크로를 사용하면 매치된 구문 안에 원하는 코드를 삽입하여 컴파일 시점에 로직을 추가하거나 변경할 수 있습니다. 다음 예시는 함수 호출 전후에 로그를 출력하는 코드를 주입합니다.
macro_rules! enhance_function {
(
$(#[$attr: meta])*
$vis: vis fn $func_name: ident ($($param_name: ident : $param_type: ty),* $(,)?) -> $return_type:ty $func_body: block
) => {
// 함수 정의 시점에 로그 출력
println!("INFO: 함수 정의 감지됨 -> {}({})", stringify!($func_name), stringify!($($param_name: $param_type),*));
$(#[$attr])*
$vis fn $func_name($($param_name: $param_type),*) -> $return_type {
// 함수 호출 시점에 로그 출력
print!("TRACE: {} 호출됨 (", stringify!($func_name));
$(print!("{}: {}, ", stringify!($param_name), $param_name);)*
println!(")");
// 원본 함수 본문 실행
$func_body
}
};
}
enhance_function! {
#[allow(unused_variables)]
pub fn calculate_area(width: u32, height: u32) -> u32 {
width * height
}
}
// 매크로에 의해 생성된 함수 호출
calculate_area(10, 20);
위 코드를 실행하면 다음과 같은 출력을 볼 수 있습니다.
INFO: 함수 정의 감지됨 -> calculate_area(width : u32, height : u32)
TRACE: calculate_area 호출됨 (width: 10, height: 20, )
구조체(struct) 아이템 분석
구조체에는 크게 두 가지 형태가 있습니다: 필드를 가진 구조체와 튜플 구조체, 그리고 유닛 구조체입니다.
#[derive(Debug)]
struct UserProfile {
pub username: String,
email: String,
}
#[derive(Clone)]
struct Point(f64, f64);
struct EmptyData;
이제 이들을 매치하는 매크로를 작성해봅시다.
macro_rules! parse_struct_definition {
// 필드를 가진 구조체 (e.g., struct UserProfile { ... })
(
$(#[$struct_attr: meta])* // 구조체에 붙은 속성
$vis: vis struct $struct_name: ident {
$(
$(#[$field_attr: meta])* // 필드에 붙은 속성
$field_vis: vis $field_name: ident : $field_type: ty
),* // 0개 이상의 필드
$(,)? // 선택적 후행 쉼표
}
) => {
$(#[$struct_attr])*
$vis struct $struct_name {
$(
$(#[$field_attr])*
$field_vis $field_name: $field_type
),*
}
};
// 튜플 구조체 (e.g., struct Point(f64, f64);)
(
$(#[$struct_attr: meta])*
$vis: vis struct $struct_name: ident (
$($field_type: ty),* // 0개 이상의 필드 타입 (이름 없음)
$(,)?
);
) => {
$(#[$struct_attr])*
$vis struct $struct_name (
$($field_type),*
);
};
// 유닛 구조체 (e.g., struct EmptyData;)
(
$(#[$struct_attr: meta])*
$vis: vis struct $struct_name: ident;
) => {
$(#[$struct_attr])*
$vis struct $struct_name;
};
}
parse_struct_definition! {
#[derive(Debug, Default)]
pub struct Configuration {
pub path: String,
count: u32,
}
}
parse_struct_definition! {
#[derive(Copy, Clone)]
pub struct Coordinates(f32, f32, f32);
}
parse_struct_definition! {
struct Marker;
}