rust derive proc macro [Attribute NameValue]

wangki·2025년 9월 7일

Rust

목록 보기
51/56

개요

NameValue 속성에 대해서 파싱하는 방법을 알아보겠다.

struct Test {
	#[default = "kim"]
	name: String,
}

위처럼 default값을 설정하는 NameValue 속성을 파싱 하여 자동으로 코드를 생성해 보도록 하겠다.

내용

TokenStreamsyn크레이트를 활용하여 DeriveInput으로 파싱할 수 있다.

매크로내에서 에러 핸들링

compile_error! 사용 이유

panic!은 런타임에 프로그램이 비정상적으로 종료될 때 사용된다. 하지만 매크로는 컴파일 타임에 코드를 생성하기 때문에, 매크로 내에서 문제가 발생했다면 컴파일 과정에서 해당 문제를 포착하여 친절하고 명확한 에러 메시지를 사용자에게 보여주는 것이 더 좋다.

compile_error!는 매크로 확장이 실패하면 컴파일러가 사용자에게 에러 메시지를 직접 전달하여, 사용자가 코드를 수정할 수 있도록 도와준다.

#[proc_macro_derive(Test, attributes(a,b,c, default))]
pub fn test_derive_macro(input: TokenStream) -> TokenStream {
    let ast: DeriveInput = syn::parse(input).unwrap();

    // 일단 필드가 하나라고 가정..
    let Data::Struct(struct_data) = ast.data else {
        return quote! {
            compile_error!("구조체에만 사용하세요.");
        }.into();
    };

    quote! {

    }.into()
}

테스트 하기위해서 간단하게 매크로를 만들었다. Enum에 사용해보도록 하겠다.

#[derive(Test)]
enum FailEnum {
    #[default = "kim"]
    Name(String),
}


처음으로 compile_error!를 사용해 봤는데 뭐가 잘못되었는지 확실하게 알 수 있는 것 같다. 매크로의 경우 은근 개발 중간에 어디가 문제인지 찾기 힘든데 좋은 꿀팁인 것 같다.

#[proc_macro_derive(Test, attributes(a,b,c, default))]
pub fn test_derive_macro(input: TokenStream) -> TokenStream {
    let ast: DeriveInput = syn::parse(input).unwrap();
    let ident = ast.ident;

    // 일단 필드가 하나라고 가정..
    let Data::Struct(struct_data) = ast.data else {
        return quote! {
            compile_error!("구조체에만 사용하세요.");
        }.into();
    };

    let default_field_map = struct_data.fields.iter().map(|f| {
        let Some(value) = parsing_default_attribute(f) else {
            return quote! {}.into();
        };

        let ident = f.ident.clone().unwrap();

        quote! {
            #ident: #value
        }
    });

    quote! {
        impl #ident {
            pub fn new() -> Self {
                Self {
                    #(#default_field_map,)*
                }
            }
        }
    }.into()
}

fn parsing_default_attribute(field: &Field) -> Option<LitStr> {
    let Some(res) = field.attrs.iter().find_map(|attr| {
        // attribute의 이름이 `default`인지 확인
        // default가 아니라면 return
        if !attr.meta.path().is_ident("default") {
            // default에 매칭되는 value를 구해야함
            return None;
        }

        // attribute가 NameValue가 아니라면 return
        let Meta::NameValue(meta_name_value) = &attr.meta else {
            return None;
        };

        // 일단 여기서는 String인 경우만 취급하겠음...
        let Expr::Lit(lit) = &meta_name_value.value else {
            return None;
        };

        let Lit::Str(val) = &lit.lit else {
            return None;
        };

        Some(val.clone())
    }) else {
        return None;
    };

    Some(res)
}

위처럼 매크로를 생성을 했다. 더 디테일하게 에러 핸들링 및 처리를 해줘야하지만 일단 스터디용으로 작성한 것이므로 넘어가겠다.

#[derive(Test)]
struct DefaultTest {
    #[default = "kim"]
    name: String,
}

위처럼 derive에 넣어주었는데

이런 오류가 발생한다.

이 오류 메시지의 문제점은 LitStr"kim"으로 파싱 하기 때문에 into() 또는 .to_string()을 붙여서 TokenStream을 만들어 줘야 한다.

        quote! {
            #ident: #value.into()
        }
fn main() {
    let res = DefaultTest::new();
    println!("name: {}", res.name);
}

결론

결국 proc_macro는 코드를 구조화된 트리 형태로 파싱 하여 사용자가 원하는 코드를 컴파일 타임에 만들어주는 것 같다.
여러 번 매크로를 생성하여 사용해 봤지만 이번 기회를 통해 정확히 알아보았다.(그래도 잼미니에게 물어봐야함)
위에 작성한 코드의 경우 좀 더 다듬어야 한다. 추가로 여러 속성들이 생길 경우 복잡해질 것이다.
다음에는 좀 더 복잡한 매크로를 생성해 볼 예정이다.

0개의 댓글