IOS/WWDC24

Swift Macro - implement

jjunbbang 2025. 6. 1. 23:32

1. 패키지 생성

Xcode -> File -> New -> Package 선택 시 Swift macro가 있습니다.

2. 구성 보기

패키지 생성 시 기본적으로 #stringfy 메크로 선언, 구현, 단위 테스트 코드가 존재합니다.

3. 메크로 선언

stringify표현에 사용할 매크로 구현체가 있는 모듈을 #externalMacro로 상세한다.

메크로에 사용될 구현체는 @main으로 프로그램 진입 시점에 선언

@main
struct WWDC_MacroPlugin: CompilerPlugin {
    let providingMacros: [Macro.Type] = [
        StringifyMacro.self,
        SlopeSubsetMacro.self
    ]
}
  • 플러그인 메커니즘
    • 플러그인은 호스트 프로세스와 swift enum을 JSON 직렬화 형식으로 메시지를 주고받음
    • Swift 컴파일러가 플러그인에게 명령을 주고 플러그인은 결과를 되돌려줌
    • stdin, stdout 파이프를 통해서 주고받는데, 플러그인은 호스트 프로세스 외 stdin는 막혀있음
    • 즉 이 부분에서 메크로 구현체가 맵핑되는걸 선언해 줌

4. 메크로 구현체

stringifyMacro 구현부를 ExpressionMacro 타입을 구현하기 때문에 다음과 같은 프로토콜을 상속해서 구현체를 구현해야 한다.

한 단계 깊이 들어가면 FreestandingMacroExpansionSyntax 프로토콜로 parse에 의해 sytanx tree로 구성된 토큰들을 추출할 수 있습니다.



// MARK: - FreestandingMacroExpansionSyntax

public protocol FreestandingMacroExpansionSyntax: SyntaxProtocol {
  /// ### Tokens
  /// 
  /// For syntax trees generated by the parser, this is guaranteed to be `#`.
  var pound: TokenSyntax {
    get
    set
  }
  
  /// ### Tokens
  /// 
  /// For syntax trees generated by the parser, this is guaranteed to be `<identifier>`.
  var macroName: TokenSyntax {
    get
    set
  }
  
  var genericArgumentClause: GenericArgumentClauseSyntax? {
    get
    set
  }
  
  /// ### Tokens
  /// 
  /// For syntax trees generated by the parser, this is guaranteed to be `(`.
  var leftParen: TokenSyntax? {
    get
    set
  }
  
  var arguments: LabeledExprListSyntax {
    get
    set
  }
  
  /// ### Tokens
  /// 
  /// For syntax trees generated by the parser, this is guaranteed to be `)`.
  var rightParen: TokenSyntax? {
    get
    set
  }
  
  var trailingClosure: ClosureExprSyntax? {
    get
    set
  }
  
  var additionalTrailingClosures: MultipleTrailingClosureElementListSyntax {
    get
    set
  }
}

 

5. 메크로 테스트

메크로에 사용하는 테스트 코드는 단위 테스트에 용이하도록 설계되어 있습니다.

assertMacroExpansion를 사용해 테스트하는데 메서드 시그니처는 다음과 같습니다.

func assertMacroExpansion(
    _ originalSource: String,
    expandedSource expectedExpandedSource: String,
    diagnostics: [DiagnosticSpec] = [],
    macros: [String : any Macro.Type],
    applyFixIts: [String]? = nil,
    fixedSource expectedFixedSource: String? = nil,
    testModuleName: String = "TestModule",
    testFileName: String = "test.swift",
    indentationWidth: Trivia = .spaces(4),
    file: StaticString = #filePath,
    line: UInt = #line
)

다음은 사용법  코드입니다.

import WWDC_MacroMacros

let testMacros: [String: Macro.Type] = [
    "stringify": StringifyMacro.self, // 메크로 맵핑된 구현체를 dictionary형태로
]
#endif

final class WWDC_MacroTests: XCTestCase {
    func testMacro() throws {
        #if canImport(WWDC_MacroMacros)
        assertMacroExpansion(
            """
            #stringify(a + b) // 매크로 표현식
            """,
            expandedSource: """
            (a + b, "a + b") // 예상되는 확장 코드
            """,
            macros: testMacros
        )
        #else
        throw XCTSkip("macros are only supported when running tests for the host platform")
        #endif
    }
}