rust derive proc macro [Attribute List]

wangki·2025년 9월 6일
0

Rust

목록 보기
50/54

개요

derive 매크로를 사용 시 어떻게 파싱하고 TokenStream으로 반환하여 코드를 만드는지 알아보겠다. 매번 사용은 하지만 정확하게 짚고 넘어가고 싶다.

내용

attributes에는 종류가 크게 3가지 있다.
1. Path
2. List
3. NameValue

테스트를 하기위해서 필드 어트리뷰트는 제외하고 했다.


fn print_attribute_parsing(attrs: &Vec<Attribute>) -> TokenStream {
    attrs.iter().for_each(|attr| {
        match &attr.meta {
            syn::Meta::Path(path) => {
                let Some(ident) = path.get_ident() else {
                    panic!("fail to parse ident");
                };
                println!("path attribute name: {}", ident.to_string());
            },
            syn::Meta::List(meta_list) => {
                let Some(ident) = meta_list.path.get_ident() else {
                    panic!("fail to parse ident");
                };
                println!("list path attribute name: {}", ident.to_string());
            },
            syn::Meta::NameValue(meta_name_value) => {
                let Some(ident) = meta_name_value.path.get_ident() else {
                    panic!("fail to parse ident");
                };
                println!("namevalue path attribute name: {}", ident.to_string());                
            },
        }
    });

    quote! {

    }.into()
}

attribute를 파싱하여 화면에 출력하는 코드를 만들었다.

#[derive(Test)]
#[a]
#[b(1,2,3)]
#[c = "test"]
struct TestStruct {
    name: String,
    age: u8,
}
trait Test {}

위처럼 테스트할 attribute를 달아준 뒤 실행을 했다.

각 속성 타입에 맞게 잘 나온걸 볼 수 있다. Path이 경우 간단히 파싱하고 끝이 나지만 List, NameValue의 경우 추가적으로 파싱을 해주어야한다.

List부터 파싱 해보도록 하겠다.

#[validate(range(min = 18, max = 100))]

위처럼 범위 속성을 부여하여 유효성 검사를 할 수 있다.
사용 방법은 아주 다양한 것 같다.

#[derive(Test)]
#[b(name = "kim", test)]
struct TestStruct {
    name: String,
    age: u8,
}

  syn::Meta::List(meta_list) => {
                match meta_list.parse_nested_meta(|nested_meta| {
                    if nested_meta.path.is_ident("name") {
                        println!("nested data name");
                    } else if nested_meta.path.is_ident("test") {
                        println!("nested data test");
                    } else {
                        println!("nested data nothing");
                        return Err(nested_meta.error("fail"));
                    }

                    Ok(())
                }) {
                    Ok(_) => {},
                    Err(e) => println!("e: {}", e),
                }
                
                // parsing_meta_list(meta_list);
            },

위 코드로 파싱을 하면 출력값이 아래와 같이 출력된다.

name은 잘 읽었지만 test를 읽는 과정에서 실패한 것이다. 이유는 name이 단순 path로 예상하여 ,가 올 것으로 예상한 것이다.

let lit_str: syn::LitStr = nested_meta.value()?.parse()?;
println!("nested data name: {}", lit_str.value());

위 코드를 추가하여 Token을 소비할 때, NameValue인 것을 인지 시켜줘야 한다. 그러면 출력 값이 아래처럼 정상으로 나온다.

추가

Token! 매크로

Token! 매크로는 특정 기호나 키워드를 Rust 타입으로 변환 해주는 마법 같은 도구이다. 이를 통해 syn을 사용한 구문 분석 코드를 더 쉽고 효율적으로 작성할 수 있다.

use syn::token::Comma 
Token![,]

,라는 키워드를 rust에서 syn::token::Comma로 인식을 하는데 개발자가 일일이 알 필요가 없다. Token매크로를 활용해서 원하는 기호나 키워드를 넣어주면 자동으로 타입을 반환해 주는 매크로이다. 가독성과 효율성을 높일 수 있다고 이해하면 될 것 같다.

먼저 attributes에 대해서 알아보도록 하겠다. struct나 enum에 붙일 수 있고 각 필드들에도 붙일 수 있다. 해당 attribute를 통해서 매크로 내에서 원하는 코드를 생성해 줄 수 있다고 이해하면 된다.

결론

다음에 추가로 NameValue 속성에 대해서 알아보겠다.
TokenStream으로 받아서 DeriveInput으로 파싱 하여 다양하게 코드를 생성하는 것이 흥미롭다. 예전에 ai의 도움을 받아 proc_macro를 작성해 봤지만, 정확히 어떻게 파싱이 되고 Token들이 사용이 되는지 확인할 수 있었다.

0개의 댓글