코드 생성은 대부분의 최신 프로그래밍 언어에서 볼 수 있는 기능입니다. 상용구 코드와 코드 중복을 줄이고, 도메인별 언어(DSL)를 정의하고, 새로운 구문을 구현하는 데 도움이 될 수 있습니다.

Rust는 보다 정교한 프로그래밍을 위해 컴파일 시 코드를 생성할 수 있는 강력한 매크로 시스템을 제공합니다.

Rust 매크로 소개

매크로는 코드를 작성하는 코드를 작성하는 데 활용할 수 있는 메타프로그래밍의 한 유형입니다. Rust에서 매크로는 컴파일 시 다른 코드를 생성하는 코드 조각입니다.

Rust 매크로는 반복적인 작업을 자동화하기 위해 컴파일 시 다른 코드를 생성하는 코드를 작성할 수 있는 강력한 기능입니다. Rust의 매크로는 코드 중복을 줄이고 코드 유지보수성과 가독성을 높이는 데 도움이 됩니다.

매크로를 사용하여 간단한 코드 스니펫부터 라이브러리 및 프레임워크까지 무엇이든 생성할 수 있습니다. 매크로는 런타임에 데이터가 아닌 코드에서 작동하기 때문에 Rust 함수와 다릅니다.

Rust에서 매크로 정의하기

macro_rules! 매크로를 사용하여 매크로를 정의합니다. macro_rules! 매크로는 패턴과 템플릿을 입력으로 받습니다. Rust는 패턴을 입력 코드와 일치시키고 템플릿을 사용하여 출력 코드를 생성합니다.

Rust에서 매크로를 정의하는 방법은 다음과 같습니다:

 macro_rules! say_hello {
    () => {
        println!("Hello, world!");
    };
}

fn main() {
    say_hello!();
}

이 코드는 “Hello, world!”를 인쇄하는 코드를 생성하는 say_hello 매크로를 정의합니다. 이 코드는 빈 입력에 대해 () 구문과 일치하고 println! 매크로가 출력 코드를 생성합니다.

다음은 메인 함수에서 매크로를 실행한 결과입니다:

매크로는 생성된 코드에 대한 입력 인수를 받을 수 있습니다. 다음은 단일 인수를 받아 메시지를 인쇄하는 코드를 생성하는 매크로입니다:

 macro_rules! say_message {
    ($message:expr) => {
        println!("{}", $message);
    };
}

say_message 매크로는 $message 인수를 받아 println! 매크로를 사용하여 인수를 인쇄하는 코드를 생성합니다. expr 구문은 인수를 모든 Rust 표현식과 일치시킵니다.

Rust 매크로 유형

Rust는 세 가지 유형의 매크로를 제공합니다. 각 매크로 유형은 특정 용도로 사용되며 구문과 제한 사항이 있습니다.

프로시저 매크로

프로시저 매크로는 가장 강력하고 다재다능한 유형으로 간주됩니다. 프로시저 매크로를 사용하면 Rust 코드를 동시에 생성하는 사용자 지정 구문을 정의할 수 있습니다. 프로시저럴 매크로를 사용하여 사용자 지정 파생 매크로, 사용자 지정 속성 유사 매크로 및 사용자 지정 함수 유사 매크로를 생성할 수 있습니다.

이 글도 확인해 보세요:  슬랙에서 나만의 사용자 지정 슬래시 명령 만들기

사용자 정의 파생 매크로를 사용하여 구조체와 열거형 특성을 자동으로 구현할 수 있습니다. Serde와 같이 널리 사용되는 패키지는 사용자 정의 파생 매크로를 사용하여 Rust 데이터 구조에 대한 직렬화 및 역직렬화 코드를 생성합니다.

사용자 지정 속성과 유사한 매크로는 Rust 코드에 사용자 지정 주석을 추가할 때 유용합니다. 로켓 웹 프레임워크는 사용자 정의 속성과 유사한 매크로를 사용하여 경로를 간결하고 읽기 쉽게 정의합니다.

사용자 정의 함수형 매크로를 사용하여 새로운 Rust 표현식이나 문을 정의할 수 있습니다. Lazy_static 상자는 사용자 정의 함수와 유사한 매크로를 사용하여 지연 초기화된 정적 변수를 정의합니다.

사용자 정의 파생 매크로를 정의하는 절차적 매크로를 정의하는 방법은 다음과 같습니다:

 use proc_macro::TokenStream;
use quote::quote;
use syn::{DeriveInput, parse_macro_input};

use 지시어는 Rust 절차적 매크로를 작성하는 데 필요한 상자와 유형을 가져옵니다.

 #[proc_macro_derive(MyTrait)]
pub fn my_derive_macro(input: TokenStream) -> TokenStream {
    let ast = parse_macro_input!(input as DeriveInput);
    let name = &ast.ident;

    let gen = quote! {
        impl MyTrait for #name {
            // implementation here
        }
    };

    gen.into()
}

이 프로그램은 구조체 또는 열거형에 대한 특성 구현을 생성하는 절차적 매크로를 정의합니다. 프로그램은 구조체 또는 열거형의 파생 속성에 MyTrait라는 이름을 사용하여 매크로를 호출합니다. 이 매크로는 parse_macro_input! 매크로를 사용하여 추상 구문 트리(AST)로 구문 분석된 코드가 포함된 토큰 스트림 객체를 입력으로 받습니다.

name 변수는 파생된 구조체 또는 열거형 식별자인 따옴표입니다! 이 매크로는 결국 into 메서드를 사용하여 토큰 스트림으로 반환되는 유형에 대한 MyTrait 구현을 나타내는 새 AST를 생성합니다.

매크로를 사용하려면 매크로를 선언한 모듈에서 매크로를 가져와야 합니다:

 // assuming you declared the macro in a my_macro_module module

use my_macro_module::my_derive_macro;

매크로를 사용하는 구조체 또는 열거형을 선언할 때 선언 맨 위에 #[derive(MyTrait)] 속성을 추가합니다.

 #[derive(MyTrait)]
struct MyStruct {
    // fields here
}

속성이 있는 구조체 선언은 구조체에 대한 MyTrait 특성의 구현으로 확장됩니다:

 impl MyTrait for MyStruct {
    // implementation here
}

이 구현을 사용하면 MyStruct 인스턴스에서 MyTrait 특성의 메서드를 사용할 수 있습니다.

속성 매크로

속성 매크로는 구조체, 열거형, 함수, 모듈과 같은 Rust 항목에 적용할 수 있는 매크로입니다. 어트리뷰트 매크로는 어트리뷰트 뒤에 인자 목록이 오는 형태를 취합니다. 매크로는 인수를 구문 분석하여 Rust 코드를 생성합니다.

이 글도 확인해 보세요:  웹 개발을 위한 가장 인기 있는 8가지 백엔드 프레임워크

속성 매크로를 사용하여 코드에 사용자 지정 동작 및 주석을 추가할 수 있습니다.

Rust 구조체에 사용자 정의 어트리뷰트를 추가하는 어트리뷰트 매크로입니다:

 // importing modules for the macro definition
use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, DeriveInput, AttributeArgs};

#[proc_macro_attribute]
pub fn my_attribute_macro(attr: TokenStream, item: TokenStream) -> TokenStream {
    let args = parse_macro_input!(attr as AttributeArgs);
    let input = parse_macro_input!(item as DeriveInput);
    let name = &input.ident;

    let gen = quote! {
        #input
        impl #name {
            // custom behavior here
        }
    };

    gen.into()
}

이 매크로는 인수 목록과 구조체 정의를 받아 정의된 사용자 지정 동작을 가진 수정된 구조체를 생성합니다.

매크로는 두 개의 인수를 입력으로 받습니다: 매크로에 적용된 속성(parse_macro_input! 매크로로 구문 분석됨)과 항목(parse_macro_input! 매크로로 구문 분석됨). 매크로는 인용! 매크로를 사용하여 원래 입력 항목과 사용자 지정 동작을 정의하는 추가 임포트 블록을 포함한 코드를 생성합니다.

마지막으로, 이 함수는 생성된 코드를 into() 메서드를 사용하여 토큰 스트림으로 반환합니다.

매크로 규칙

매크로 규칙은 가장 간단하고 유연한 매크로 유형입니다. 매크로 규칙을 사용하면 컴파일 시 Rust 코드로 확장되는 사용자 지정 구문을 정의할 수 있습니다. 매크로 규칙은 모든 Rust 표현식 또는 문과 일치하는 사용자 지정 매크로를 정의합니다.

매크로 규칙을 사용하여 상용구 코드를 생성하여 낮은 수준의 세부 사항을 추상화할 수 있습니다.

Rust 프로그램에서 매크로 규칙을 정의하고 사용하는 방법은 다음과 같습니다:

 macro_rules! make_vector {
    ( $( $x:expr ),* ) => {
        {
            let mut v = Vec::new();
            $(
                v.push($x);
            )*
            v
        }
    };
}

fn main() {
    let v = make_vector![1, 2, 3];
    println!("{:?}", v); // prints "[1, 2, 3]"
}

이 프로그램은 메인 함수의 쉼표로 구분된 표현식 목록에서 새 벡터를 생성하는 매크로 make_vector! 를 정의합니다.

매크로 내부에서 패턴 정의는 매크로에 전달된 인수와 일치합니다.

확장 코드의 $( ) 구문은 닫는 괄호 뒤에 매크로에 전달된 인수 목록의 각 식을 반복하며, 매크로가 모든 식을 처리할 때까지 반복을 계속해야 함을 나타냅니다.

이 글도 확인해 보세요:  JES를 활용한 흥미로운 사운드 처리 기법 3가지

Rust 프로젝트를 효율적으로 구성하기

Rust 매크로는 재사용 가능한 코드 패턴과 추상화를 정의할 수 있도록 하여 코드 구성을 개선합니다. 매크로를 사용하면 다양한 프로젝트 부분에서 중복 없이 보다 간결하고 표현력이 풍부한 코드를 작성할 수 있습니다.

또한 코드 구성, 재사용성, 다른 크레이트 및 모듈과의 상호 운용성을 개선하기 위해 Rust 프로그램을 크레이트 및 모듈로 구성할 수 있습니다.

By 김민수

안드로이드, 서버 개발을 시작으로 여러 분야를 넘나들고 있는 풀스택(Full-stack) 개발자입니다. 오픈소스 기술과 혁신에 큰 관심을 가지고 있고, 보다 많은 사람이 기술을 통해 꿈꾸던 일을 실현하도록 돕기를 희망하고 있습니다.