Notice
Recent Posts
Recent Comments
Link
관리 메뉴

김종권의 iOS 앱 개발 알아가기

[iOS - swift] Swift Macro 구현 방법 (swift-syntax, SwiftCompilerPlugin, SwiftSyntax, SwiftSyntaxBuilder, SwiftSyntaxMacros) 본문

Swift Macro

[iOS - swift] Swift Macro 구현 방법 (swift-syntax, SwiftCompilerPlugin, SwiftSyntax, SwiftSyntaxBuilder, SwiftSyntaxMacros)

jake-kim 2023. 9. 8. 01:55

Swift Macro 시작

  • Xcode 15 Beta 이상, 스위프트 5.9이상에서 Xcode -> New -> Package하여 Swift Macro 생성하면 아래처럼 3가지의 파일이 생성 (프로젝트 명을 "MySample"로 생성)
    • main.swift
    • MySample.swift
    • MySampleMacro.swift

  • 지난 포스팅 글에서 알아본 것은 main.swift, MySample.swift파일
    • main.swift: 정의한 매크로를 테스트하는 파일
    • MySample.swift: 직접 정의한 매크로 로직을 외부에서 사용할 수 있도록 인터페이스를 맞춰주는 파일
    • (구체적인 내용은 이전 포스팅 글 참고)
  • MySampleMacro.swift은 매크로 로직이 들어있는 핵심적인 파일

Swift Macro 구현 방법 (MySampleMacro.swift 파일 알아보기)

  • Swift Macro 프로젝트를 생성하면 기본적으로 만들어진 {생성이름}Macro.swift파일에 매크로 로직이 존재

  • 가장 위 코드부터 보면 4가지 import를 수행
    • SwiftCompilerPlugin: 컴파일러의 동작을 사용자 지정하거나 확장하기 위해 사용
      • ex) 특정 Swift 언어 기능을 확장하거나 다른 언어와의 통합을 지원
    • SwiftSyntax: (아래에서 계속)
    • SwiftSyntaxBuilder: result builder 스타일로 swift코드를 generate해주는 역할
    • SwiftSyntaxMacros: syntatic macro를 지원해주는 역할

SwiftSyntax를 사용하여 StringifyMacro구현

  • SwiftSyntax란?
    • SwiftSyntax는 단어 그대로 Swift를 Syntax (구문) 별로 tree 자료구조 형태로 표현한 것을 의미
    • tree 자료구조 형태로 표현하여, parse, inspect, generate, transform를 사용하여 Swift Souce Code로 만들 수 있는 기능
    • SwiftSyntax를 이용하는 이유부터 구체적인 개념은 이 포스팅 글 참고
  • StringfyMacro는 매크로 두 개 중 (freestandingMacro, attachedMacro), 독립적으로 쓰이는 기능이므로 FreestandingMacro 중 ExpressionMacro를 사용

  • 단순히 이 ExpressionMacro를 준수하는 StringifyMacro 구조체를 정의
public struct StringifyMacro: ExpressionMacro {
    public static func expansion(
        of node: some FreestandingMacroExpansionSyntax,
        in context: some MacroExpansionContext
    ) -> ExprSyntax {
        // TODO:
    }
}
  • expansion 함수를 보면 node라는 인수와 반환 타입 ExprSyntax가 중요
    • node: SwiftSyntax의 목적은 tree형태로 구문을 저장하여 각 node들이 모여서 tree가 되는데 이 때의 node가 바로 이 인수를 의미
    • ExprSyntax: 구조체 형태의 expression syntax (표현식 구문)이라고 이해
  • 이 함수가 실행되는 순간에는 이미 SwiftSyntax에 의해 트리 형태로 표현이 되었으며, 구문에 대한 node가 들어오면 node에서 표현식을 가져와서 해당 표현식을 그냥 리턴하면 1+2 값이 계산된 것이 반환되며, description으로하면 "1+2"로 리턴
public static func expansion(
    of node: some FreestandingMacroExpansionSyntax,
    in context: some MacroExpansionContext
) -> ExprSyntax {
    guard let argument = node.argumentList.first?.expression else {
        fatalError("compiler bug: the macro does not have any arguments")
    }

    return "(\(argument), \(literal: argument.description))"
}
  • 반환값을 보면 문자열로 리턴하는데 이 문자열 역시도 ExprSyntax라는 하나의 표현식이므로 내부적으로 알아서 이 구문을 분석하여 사용하는쪽에 반환
return "(\(argument), \(literal: argument.description))"
  • 이렇게하면 매크로 구현이 완료되며 외부에 인터페이스를 열어주는 작업도 필요한데, 이것은 또 다른 파일에서 수행

{생성이름}.swift에 인터페이스 선언

  • 인터페이스 작성
    • 매크로의 타입 @freestanding(expression)을 선언
    • 접근제한자는 public으로 하고 #externalMacro를 사용하여 매크로를 표현 (module에는 로직이 적용된 .swift 파일 이름을 적고, type에는 로직이 적용된 struct 이름을 명시)
// MySample.swift

@freestanding(expression)
public macro stringify<T>(_ value: T) -> (T, String) = #externalMacro(module: "MySampleMacros", type: "StringifyMacro")
  • #externalMacro는 단순히 매크로 인터페이스를 선언할 때 사용하는 것

  • 인터페이스까지 선언했으며 테스트는 main.swift에서 수행

main.swift 파일에서 테스트

  • 인터페이스가 정의된 MySample 모듈을 import한 후 매크로 사용 #stringify
// main.swift

import MySample

let a = 17
let b = 25

let (result, code) = #stringify(a + b)

print("The value \(result) was produced by the code \"\(code)\"")

* 참고

https://developer.apple.com/documentation/swift/externalmacro(module:type:)

https://swiftpackageindex.com/apple/swift-syntax/508.0.1/documentation/swiftsyntax/exprsyntax

https://swiftpackageindex.com/apple/swift-syntax/508.0.1/documentation/swiftsyntax

https://github.com/apple/swift-syntax

https://docs.swift.org/swift-book/documentation/the-swift-programming-language/macros/

Comments