Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

매크로 확장

Rust에는 매우 강력한 매크로 시스템이 있습니다. 이전 장에서는 파서가 (임시 플레이스홀더를 사용하여) 확장할 매크로를 어떻게 따로 남겨 두는지 살펴보았습니다. 이 장에서는 확장되지 않은 매크로가 없는 크레이트의 완전한 추상 구문 트리(AST)를 얻을 때까지(또는 컴파일 오류가 날 때까지) 그 매크로들을 반복적으로 확장하는 과정을 다룹니다.

먼저 매크로 출력을 확장하여 AST에 통합하는 알고리즘을 설명합니다. 다음으로 위생(hygiene) 데이터가 어떻게 수집되는지 살펴봅니다. 마지막으로 여러 종류의 매크로를 확장하는 구체적인 방법을 살펴봅니다.

아래에서 설명하는 알고리즘과 자료 구조의 상당수는 rustc_expand에 있으며, 기본 자료 구조는 rustc_expand::base에 있습니다.

또한 cfg와 cfg_attr는 다른 매크로와 달리 특별하게 취급되며, rustc_expand::config에서 처리됩니다.

확장과 AST 통합

우선 확장은 크레이트 수준에서 일어납니다. 크레이트의 원시 소스 코드가 주어지면 컴파일러는 모든 매크로가 확장되고 모든 모듈이 인라인되는 등의 처리를 거친 거대한 AST를 만들어 냅니다. 이 과정의 주된 진입점은 MacroExpander::fully_expand_fragment 메서드입니다. 몇 가지 예외를 제외하면 이 메서드는 크레이트 전체에 사용됩니다(엣지 케이스 확장 문제에 대한 자세한 논의는 아래의 “즉시 확장”을 참조하십시오).

큰 틀에서 보면 fully_expand_fragment는 반복적으로 동작합니다. 해석되지 않은 매크로 호출(즉, 아직 정의를 찾지 못한 매크로)의 큐를 유지합니다. 큐에서 매크로를 하나 골라 해석하고, 확장한 뒤, 다시 통합하는 작업을 반복해서 시도합니다. 한 번의 반복에서 아무런 진전을 이루지 못하면 이는 컴파일 오류를 뜻합니다. 알고리즘은 다음과 같습니다:

  1. 해석되지 않은 매크로들의 queue를 초기화합니다.
  2. queue가 빌 때까지(또는 진전이 없을 때까지 — 이는 오류입니다) 다음을 반복합니다:
    1. 부분적으로 만들어진 크레이트에서 가능한 한 많은 임포트를 해석합니다.
    2. 부분적으로 만들어진 크레이트에서 가능한 한 많은 매크로 Invocation(fn 형태, 속성, derive)을 수집하여 큐에 추가합니다.
    3. 첫 번째 요소를 큐에서 꺼내 해석을 시도합니다.
    4. 해석되었다면:
      1. TokenStream 또는 AST를 받아 (매크로 종류에 따라) TokenStream 또는 AstFragment를 만들어 내는 매크로의 확장 함수를 실행합니다. (TokenStream은 TokenTree의 모음이며, 각각은 토큰(구두점, 식별자, 리터럴)이거나 구분된 그룹(()/[]/{} 안의 모든 것)입니다.)
        • 이 시점에서는 매크로 자체에 대한 모든 것을 알고 있으므로 set_expn_data를 호출하여 전역 데이터에 그 속성을 채워 넣을 수 있습니다. 이것이 ExpnId와 연관된 위생 데이터입니다(아래의 위생을 참고하십시오).
      2. 그 AST 조각을 현재 존재하지만 부분적으로만 만들어진 AST에 통합합니다. 본질적으로 이 단계에서 “토큰 같은 덩어리“가 사이드 테이블을 갖춘, 확정된 온전한 AST가 됩니다. 이 과정은 다음과 같이 진행됩니다:
        • 매크로가 토큰을 만들어 내는 경우(예: 절차적 매크로) 이를 AST로 파싱하며, 이때 파싱 오류가 발생할 수 있습니다.
        • 확장 중에 SyntaxContext(계층 2)를 생성합니다(아래의 위생을 참고하십시오).
        • 매크로에서 새로 확장된 모든 AST 조각에 대해 다음 두 패스가 차례로 수행됩니다:
          • NodeId는 InvocationCollector가 할당합니다. 이 과정에서는 새 AST 조각에 있는 새로운 매크로 호출도 수집하여 큐에 추가합니다.
          • DefCollector는 “Def 경로”를 생성하고, 해당하는 DefId를 할당하며, 축소된 그래프도 빌드합니다(해석기의 관점에서 이름을 모듈에 넣습니다).
      3. 매크로 하나를 확장하고 그 출력을 통합한 후에는 fully_expand_fragment의 다음 반복으로 넘어갑니다.
    5. 해석되지 않은 경우:
      1. 매크로를 다시 큐에 넣습니다.
      2. 다음 반복으로 넘어갑니다…

오류 복구

한 번의 반복에서 아무런 진전이 없다면 컴파일 오류(예: 정의되지 않은 매크로)에 도달한 것입니다. 진단을 생성하기 위해 실패(즉, 해석되지 않은 매크로나 임포트)로부터 복구를 시도합니다. 실패 복구는 해석되지 않은 매크로를 ExprKind::Err로 확장하는 방식으로 이루어집니다. 이렇게 하면 첫 번째 오류 이후에도 컴파일을 계속할 수 있으므로 rustc가 원래의 실패 외에 더 많은 오류를 보고할 수 있습니다.

이름 해석

여기에 이름 해석이 관여한다는 점에 주목하십시오. 위 알고리즘에서는 임포트와 매크로 이름을 해석해야 합니다. 이 작업은 rustc_resolve::macros에서 수행됩니다. 이곳에서는 매크로 경로를 해석하고, 그 해석 결과를 검증하며, 다양한 오류(예: “찾을 수 없음”, “찾았지만 불안정함”, “x를 예상했으나 y를 찾음”)를 보고합니다. 그러나 다른 이름은 아직 해석하려 하지 않습니다. 다른 이름의 해석은 나중에 이루어지며, 이는 이름 해석 장에서 살펴보겠습니다.

즉시 확장

즉시 확장이란 매크로 호출 자체보다 매크로 호출의 인자를 먼저 확장하는 것을 의미합니다. 이는 리터럴을 기대하는 몇몇 특수한 내장 매크로에 대해서만 구현되어 있습니다. 이러한 매크로 중 일부는 인자를 먼저 확장하면 사용자 경험이 더 매끄러워집니다. 예를 들어, 다음을 살펴보십시오.

macro bar($i: ident) { $i }
macro foo($i: ident) { $i }

foo!(bar!(baz));

지연 확장은 foo!를 먼저 확장합니다. 즉시 확장은 bar!를 먼저 확장합니다.

즉시 확장은 Rust에서 일반적으로 사용할 수 있는 기능이 아닙니다. 즉시 확장을 더 일반적으로 구현하기는 어렵기 때문에, 사용자 경험을 위해 몇몇 특수한 내장 매크로에 대해서만 구현합니다. 내장 매크로는 표준 라이브러리 임포트 주입이나 테스트 하네스 생성과 같은 다른 초기 코드 생성 기능과 함께 rustc_builtin_macros에 구현되어 있습니다. rustc_expand::build에는 AST 조각을 만들기 위한 몇 가지 추가 헬퍼가 있습니다. 즉시 확장은 일반적으로 지연(일반) 확장이 수행하는 작업의 일부를 수행합니다. 즉시 확장은 (평소처럼 크레이트 전체가 아니라) 크레이트의 일부에 대해서만 fully_expand_fragment를 호출하는 방식으로 이루어집니다.

기타 자료 구조

다음은 확장과 통합에 관여하는 그 밖의 주목할 만한 자료 구조입니다.

  • ResolverExpand - 크레이트 의존성을 끊는 데 사용되는 trait입니다. 이를 통해 rustc_resolve를 비롯한 거의 모든 것이 rustc_ast에 의존함에도 불구하고 rustc_ast에서 리졸버 서비스를 사용할 수 있습니다.
  • ExtCtxt/ExpansionData - 확장에 쓰이는 여러 중간 인프라 데이터를 담습니다.
  • Annotatable - 속성의 대상이 될 수 있는 AST 조각입니다. 매크로로 생성할 수는 있지만 속성을 붙일 수 없는 타입과 패턴을 제외하면 AstFragment와 거의 같습니다.
  • MacResult - “다형적인” AST 조각으로, AstFragmentKind(즉 아이템, 표현식, 패턴 등)에 따라 서로 다른 AstFragment로 바뀔 수 있습니다.

위생성과 계층 구조

C/C++ 전처리기 매크로를 써 본 적이 있다면, 성가시고 디버깅하기 어려운 함정이 있다는 것을 아실 것입니다! 예를 들어 다음 C 코드를 살펴보십시오.

#define DEFINE_FOO struct Bar {int x;}; struct Foo {Bar bar;};

// Then, somewhere else
struct Bar {
    ...
};

DEFINE_FOO

대부분의 사람들은 C를 이렇게 작성하지 않으며, 거기에는 그럴 만한 이유가 있습니다. 컴파일되지 않기 때문입니다. 매크로가 정의한 struct Bar의 이름이 코드에서 정의한 struct Bar와 충돌합니다. 다음 예제도 살펴보십시오.

#define DO_FOO(x) {\
    int y = 0;\
    foo(x, y);\
    }

// Then elsewhere
int y = 22;
DO_FOO(y);

문제가 보이십니까? 우리는 foo(22, 0) 호출을 생성하려 했지만, 매크로가 자체적으로 y를 정의했기 때문에 foo(0, 0)이 생성되었습니다!

두 가지 모두 매크로 위생성 문제의 예입니다. 위생성은 매크로 안에서 정의된 이름을 어떻게 처리하는지와 관련이 있습니다. 특히 위생적인 매크로 시스템은 매크로 안에서 도입된 이름 때문에 발생하는 오류를 방지합니다. Rust 매크로는 위와 같은 종류의 버그를 작성할 수 없게 한다는 점에서 위생적입니다.

큰 틀에서 보면, Rust 컴파일러의 위생성은 이름이 도입되고 사용되는 문맥을 추적하는 방식으로 이루어집니다. 그런 다음 그 문맥을 바탕으로 이름의 모호성을 해소할 수 있습니다. 앞으로 매크로 시스템이 발전하면 매크로 작성자가 그 문맥을 더 세밀하게 제어하여 사용할 수 있게 될 것입니다. 예를 들어 매크로 작성자는 매크로가 호출된 문맥에 새 이름을 도입하고 싶을 수 있습니다. 또는 매크로 작성자가 매크로 안에서만 사용할 변수를 정의하는 경우도 있습니다(즉 그 변수는 매크로 밖에서 보이지 않아야 합니다).

문맥은 AST 노드에 붙어 있습니다. 매크로가 생성한 모든 AST 노드에는 문맥이 붙어 있습니다. 그 밖에도 일부 디슈가링된 구문처럼 문맥이 붙어 있는 다른 노드가 있을 수 있습니다(매크로 확장으로 생성되지 않은 노드는 아래에서 설명하듯이 “루트” 문맥만 가진 것으로 간주합니다). 컴파일러 전반에서 코드 위치를 가리킬 때는 rustc_span::Span을 사용합니다. 뒤에서 보겠지만, 이 구조체에도 위생성 정보가 붙어 있습니다.

매크로 호출과 정의는 중첩될 수 있으므로 노드의 구문 문맥은 계층 구조여야 합니다. 예를 들어 매크로를 확장했는데 생성된 출력에 또 다른 매크로 호출이나 정의가 있다면, 구문 문맥은 그 중첩을 반영해야 합니다.

그런데 실제로는 목적에 따라 추적해야 할 문맥의 종류가 몇 가지 있습니다. 따라서 확장 계층 구조는 하나가 아니라 세 가지이며, 이들이 함께 크레이트의 위생성 정보를 구성합니다.

이 계층 구조들은 모두 확장 체인의 개별 요소를 식별하기 위한 일종의 “매크로 ID“가 필요합니다. 이 ID가 바로 ExpnId입니다. 모든 매크로는 정수 ID를 받으며, 이 ID는 새 매크로 호출을 발견할 때마다 0부터 차례로 할당됩니다. 모든 계층 구조는 ExpnId::root에서 시작하며, 이것은 자기 자신의 부모입니다.

rustc_span::hygiene 크레이트에는 위생성(hygiene)과 관련된 모든 알고리즘(Resolver::resolve_crate_root에 있는 몇 가지 핵(hack)은 예외입니다)과, 전역 데이터에 보관되는 위생성 및 확장 관련 구조체가 들어 있습니다.

실제 계층 구조는 HygieneData에 저장됩니다. 이는 위생성 및 확장 정보를 담고 있는 전역 데이터로, 어떤 Ident에서든 별도의 컨텍스트 없이 접근할 수 있습니다.

확장 순서 계층 구조

첫 번째 계층 구조는 확장의 순서, 즉 어떤 매크로 호출이 다른 매크로의 출력 안에 있는 경우를 추적합니다.

여기서 계층 구조의 자식은 “가장 안쪽“의 토큰이 됩니다. ExpnData 구조체 자체는 전역 데이터를 통해 사용할 수 있는 매크로 정의와 매크로 호출 양쪽의 속성 중 일부를 담고 있습니다. ExpnData::parent는 이 계층 구조에서 자식에서 부모로 향하는 링크를 추적합니다.

예를 들면 다음과 같습니다.

macro_rules! foo { () => { println!(); } }

fn main() { foo!(); }

이 코드에서 최종적으로 생성되는 AST 노드는 root -> id(foo) -> id(println)이라는 계층 구조를 갖게 됩니다.

매크로 정의 계층 구조

두 번째 계층 구조는 매크로 정의의 순서, 즉 한 매크로를 확장할 때 그 출력에서 다른 매크로 정의가 드러나는 경우를 추적합니다. 이 계층 구조는 다른 두 계층 구조보다 약간 까다롭고 더 복잡합니다.

SyntaxContext는 이 계층 구조의 체인 전체를 ID로 나타냅니다. SyntaxContextData에는 주어진 SyntaxContext와 연관된 데이터가 들어 있으며, 대부분은 해당 체인을 여러 방식으로 필터링한 결과를 담는 캐시입니다. 여기서는 SyntaxContextData::parent가 자식에서 부모로 향하는 링크이고, SyntaxContextData::outer_expns가 체인의 개별 요소입니다. 컴파일러 코드에서 “체이닝 연산자“는 SyntaxContext::apply_mark입니다.

앞서 언급한 Span은 사실 코드 위치와 SyntaxContext를 간결하게 표현한 것에 불과합니다. 마찬가지로 Ident는 인터닝된 Symbol + Span(즉, 인터닝된 문자열 + 위생성 데이터)에 불과합니다.

내장 매크로의 경우 SyntaxContext::empty().apply_mark(expn_id)라는 컨텍스트를 사용하며, 이러한 매크로는 계층 구조의 루트에서 정의된 것으로 간주됩니다. 아직 크레이트 간 위생성을 구현하지 않았기 때문에 proc macro에 대해서도 같은 방식을 사용합니다.

토큰이 매크로에 의해 생성되기 전에 X라는 컨텍스트를 가졌다면, 매크로에 의해 생성된 후에는 X -> macro_id라는 컨텍스트를 갖습니다. 다음은 몇 가지 예시입니다.

예시 0:

macro m() { ident }

m!();

여기서 처음에 SyntaxContext::root 컨텍스트를 가졌던 ident는 m에 의해 생성된 후 ROOT -> id(m) 컨텍스트를 갖습니다.

예시 1:

macro m() { macro n() { ident } }

m!();
n!();

이 예시에서 ident는 처음에 ROOT 컨텍스트를 갖고, 첫 번째 확장 후에는 ROOT -> id(m), 그다음에는 ROOT -> id(m) -> id(n) 컨텍스트를 갖습니다.

예시 2:

이러한 체인은 마지막 요소만으로 완전히 결정되지 않는다는 점, 다시 말해 ExpnId가 SyntaxContext와 동형이 아니라는 점에 유의하십시오.

macro m($i: ident) { macro n() { ($i, bar) } }

m!(foo);

모든 확장이 끝난 후 foo는 ROOT -> id(n) 컨텍스트를, bar는 ROOT -> id(m) -> id(n) 컨텍스트를 갖습니다.

현재 매크로 정의를 추적하는 이 계층 구조에는 이른바 “컨텍스트 이식 핵(context transplantation hack)”이 적용되어 있습니다. 최신(즉, 실험적인) 매크로는 기존의 “예제 기반 매크로(Macros By Example, MBE)” 시스템보다 위생성이 더 강하며, 이로 인해 두 시스템 사이에 이상한 상호작용이 발생할 수 있습니다. 이 핵은 당분간 “그냥 동작하도록” 만들기 위한 것입니다.

호출 위치 계층 구조

세 번째이자 마지막 계층 구조는 매크로 호출의 위치를 추적합니다.

이 계층 구조에서 ExpnData::call_site는 child -> parent 링크입니다.

다음은 예시입니다.

macro bar($i: ident) { $i }
macro foo($i: ident) { $i }

foo!(bar!(baz));

최종 출력의 baz AST 노드에서 확장 순서 계층 구조는 ROOT -> id(foo) -> id(bar) -> baz이고, 호출 위치 계층 구조는 ROOT -> baz입니다.

매크로 백트레이스

매크로 백트레이스는 rustc_span::hygiene의 위생(hygiene) 메커니즘을 사용하여 rustc_span에 구현되어 있습니다.

매크로 출력 생성하기

앞에서 매크로의 출력이 크레이트의 AST에 어떻게 통합되는지 살펴보았고, 크레이트의 위생 데이터가 어떻게 생성되는지도 살펴보았습니다. 그렇다면 매크로의 출력은 실제로 어떻게 생성할까요? 이는 매크로의 종류에 따라 다릅니다.

Rust에는 두 종류의 매크로가 있습니다.

  1. macro_rules! 매크로(일명 “예시에 의한 매크로(Macros By Example)”(MBE)), 그리고
  2. 사용자 정의 derive를 포함하는 절차적 매크로(proc macro)입니다.

파싱 단계에서 일반 Rust 파서는 매크로와 그 호출의 내용을 따로 보관해 둡니다. 이후 코드의 이러한 부분을 사용하여 매크로가 확장됩니다.

여기서 중요한 자료 구조/인터페이스는 다음과 같습니다.

  • SyntaxExtension - 낮춰진(lowered) 매크로 표현으로, 확장기 함수를 포함합니다. 확장기 함수는 TokenStream이나 AST를 다른 TokenStream이나 AST로 변환하며, 여기에 안정성이나 매크로 내부에서 허용되는 불안정 기능 목록과 같은 몇 가지 추가 데이터가 더해집니다.
  • SyntaxExtensionKind - 확장기 함수는 여러 가지 다른 시그니처를 가질 수 있습니다(토큰 스트림 하나나 둘, 또는 AST 조각 등을 받습니다). 이는 그 시그니처들을 나열하는 enum입니다.
  • BangProcMacro/TTMacroExpander/AttrProcMacro/MultiItemModifier - 확장기 함수 시그니처를 나타내는 trait들입니다.

예시에 의한 매크로

MBE에는 Rust 파서와 구별되는 자체 파서가 있습니다. 매크로가 확장될 때 매크로를 파싱하고 확장하기 위해 MBE 파서를 호출할 수 있습니다. MBE 파서는 다시 매크로 호출의 내용을 파싱하는 도중 메타변수(예: $my_expr)를 바인딩해야 할 때 Rust 파서를 호출할 수 있습니다. 매크로 확장 코드는 compiler/rustc_expand/src/mbe/에 있습니다.

예시

macro_rules! printer {
    (print $mvar:ident) => {
        println!("{}", $mvar);
    };
    (print twice $mvar:ident) => {
        println!("{}", $mvar);
        println!("{}", $mvar);
    };
}

여기서 $mvar를 메타변수라고 부릅니다. 메타변수는 일반 변수와 달리 런타임에 값에 바인딩되지 않고, 컴파일 타임에 토큰 트리에 바인딩됩니다. 토큰은 식별자(예: foo)나 구두점(예: =>)처럼 문법을 이루는 하나의 “단위“입니다. EOF와 같은 특수 토큰도 있습니다. EOF는 더 이상 토큰이 없음을 나타냅니다. 짝을 이루는 괄호류 문자((…), […], {…})로 만들어지는 토큰 트리도 있습니다. 이러한 토큰 트리는 여는 문자와 닫는 문자, 그리고 그 사이의 모든 토큰을 포함합니다(Rust에서는 괄호류 문자의 짝이 반드시 맞아야 합니다). 매크로 확장은 소스 파일의 원시 바이트가 아니라 토큰 스트림을 대상으로 동작합니다. 그 덕분에 많은 복잡성이 추상화됩니다. 매크로 확장기(그리고 컴파일러의 나머지 대부분)는 코드에서 어떤 구문 요소가 놓인 정확한 줄과 열을 고려하지 않습니다. 대신 코드에 어떤 구문 요소가 쓰였는지를 고려합니다. 토큰을 사용하면 어디에 있는지 걱정하지 않고 무엇인지에만 신경 쓸 수 있습니다. 토큰에 대한 자세한 내용은 이 책의 파싱 장을 참고하십시오.

printer!(print foo); // `foo` is a variable

매크로 호출을 구문 트리 println!("{}", foo)로 확장한 다음, 이 구문 트리를 다시 Display::fmt 호출로 확장하는 과정은 매크로 확장의 대표적인 예입니다.

MBE 파서

매크로 파서가 수행하는 MBE 확장은 두 부분으로 이루어집니다.

  1. 정의를 파싱하는 것, 그리고
  2. 호출을 파싱하는 것입니다.

MBE 파서는 Earley 파싱 알고리즘과 원리가 비슷한 알고리즘을 사용하므로, 비결정적 유한 오토마톤(NFA) 기반의 정규식 파서로 볼 수 있습니다. 매크로 파서는 compiler/rustc_expand/src/mbe/macro_parser.rs에 정의되어 있습니다.

매크로 파서의 인터페이스는 다음과 같습니다(약간 단순화했습니다).

fn parse_tt(
    &mut self,
    parser: &mut Cow<'_, Parser<'_>>,
    matcher: &[MatcherLoc]
) -> ParseResult

매크로 파서에서는 다음 항목을 사용합니다.

  • parser 변수는 일반 Rust 파서의 상태를 가리키는 참조입니다. 이 상태에는 토큰 스트림과 파싱 세션이 포함됩니다. 토큰 스트림은 MBE 파서에게 파싱을 맡길 대상입니다. 원시 토큰 스트림을 소비하여, 메타변수와 그에 대응하는 토큰 트리 사이의 바인딩을 출력합니다. 파싱 세션은 파서 오류를 보고하는 데 사용할 수 있습니다.
  • matcher 변수는 토큰 스트림과 매칭하려는 MatcherLoc의 시퀀스입니다. 이들은 매칭하기 전에 매크로 정의의 원래 토큰 트리로부터 변환됩니다.

정규식 파서에 비유하면 토큰 스트림은 입력이고, 이를 매처가 정의한 패턴과 대조하여 매칭합니다. 앞의 예시를 사용하면 토큰 스트림은 예시 호출 print foo의 내부를 담은 토큰 스트림이 될 수 있고, 매처는 토큰 (트리)의 시퀀스인 print $mvar:ident가 될 수 있습니다.

파서의 출력은 ParseResult입니다. 이 값은 다음 세 가지 경우 중 어느 것이 발생했는지를 나타냅니다.

  • 성공: 토큰 스트림이 주어진 매처와 일치하며, 메타변수에서 대응하는 토큰 트리로의 바인딩이 생성되었습니다.
  • 실패: 토큰 스트림이 매처와 일치하지 않으며, “No rule expected token …“과 같은 오류 메시지가 발생합니다.
  • 오류: 파서에서 치명적인 오류가 발생했습니다. 예를 들어 패턴 매칭 결과가 둘 이상이면 이 경우가 발생합니다. 이는 매크로가 모호하다는 뜻이기 때문입니다.

전체 인터페이스는 여기에 정의되어 있습니다.

매크로 파서는 한 가지 예외를 제외하면 일반적인 정규식 파서와 거의 똑같이 동작합니다. ident, block, expr 등 다양한 종류의 메타변수를 파싱하려면 매크로 파서가 일반 Rust 파서를 다시 호출해야 합니다.

매크로 정의를 파싱하는 코드는 compiler/rustc_expand/src/mbe/macro_rules.rs에 있습니다. 매크로 파서의 구현에 대한 자세한 내용은 compiler/rustc_expand/src/mbe/macro_parser.rs의 주석을 참고하십시오.

앞의 예제로 설명하면, 호출에서 나온 토큰 스트림 print foo를 앞서 매크로 정의의 규칙에서 추출한 매처 print $mvar:ident 및 print twice $mvar:ident와 매칭해 봅니다. 매크로 파서는 현재 매처에서 비단말(예: $mvar:ident)을 매칭해야 하는 지점에 이르면, 일반 Rust 파서를 다시 호출하여 해당 비단말의 내용을 얻습니다. 이 경우 Rust 파서는 ident 토큰을 찾고, 찾아낸 토큰(foo)을 매크로 파서에 반환합니다. 그런 다음 매크로 파서가 파싱을 계속합니다.

여러 규칙의 매처 중 정확히 하나만 호출과 매칭되어야 한다는 점에 유의하십시오. 둘 이상이 매칭되면 파싱이 모호해지고, 하나도 매칭되지 않으면 구문 오류가 발생합니다.

정확히 하나의 규칙이 매칭된다고 가정하면, 매크로 확장은 좌변과 매칭할 때 포착한 값들로 치환하면서 해당 규칙의 우변을 전사합니다.

절차적 매크로

절차적 매크로 역시 파싱 중에 확장됩니다. 하지만 절차적 매크로는 컴파일러 안에 파서를 두는 대신 사용자 정의 서드파티 크레이트로 구현됩니다. 컴파일러는 절차적 매크로 크레이트와 그 안에서 특별한 어노테이션이 붙은 함수(즉, 절차적 매크로 자체)를 컴파일하고, 이 함수들에 토큰 스트림을 전달합니다. 그러면 절차적 매크로는 토큰 스트림을 변환하여 새 토큰 스트림을 출력할 수 있으며, 이 토큰 스트림은 AST로 합성됩니다.

절차적 매크로가 사용하는 토큰 스트림 타입은 안정화된 타입이므로 rustc는 내부적으로 이 타입을 사용하지 않습니다. 컴파일러의 (불안정한) 토큰 스트림은 rustc_ast::tokenstream::TokenStream에 정의되어 있습니다. 이 토큰 스트림은 rustc_expand::proc_macro와 rustc_expand::proc_macro_server에서 안정화된 proc_macro::TokenStream으로 변환되고 다시 역변환됩니다. Rust ABI는 현재 불안정하므로 이 변환에는 C ABI를 사용합니다.

사용자 정의 derive

사용자 정의 derive는 절차적 매크로의 특수한 한 종류입니다.

예제에 의한 매크로와 매크로 2.0

MBE 시스템에 위생성 관련 기능을 더하고 스코프 및 가시성 규칙을 개선하는 등 MBE 시스템을 개선하려는, 오래되었고 대부분 문서화되지 않은 작업이 있습니다. 내부적으로는 오늘날의 MBE와 같은 메커니즘에 몇 가지 문법적 설탕을 더해 사용하며, 네임스페이스 안에 둘 수 있습니다.