티스토리 뷰

🍏/Swift

[Swift] 매크로 (Macro)

eungding 2024. 12. 7. 23:35
728x90
반응형

- 문서 

- 문서 ⭐️

- WWDC 23 > Write Swift macros

 

 

📝 Assisted by GPT 

 

 

[1] 매크로 템플릿 만들기 

 

Xcode > New > Pacakge > Swift Macro 선택해서 매크로 템플릿을 만들 수 있음.  

커맨드라인으로도 가능한데, 아래 명령어를 실행시키면 됨. 

swift package init --type macro

 

 

만들어보면 기본적으로 

ㄴ dependency 로 swift-syntax 가 걸려있음.
ㄴ WWDC 에서 소개하는  stringify 매크로의 선언, 구현, 예제, 테스트 코드 들어가 있음. 

 

 

 

[2] 매크로 유형 

 

일단 크게 두가지 

 

1️⃣  Freestanding Macros
• 코드 블록 외부에서 동작.
• 예: 디버깅 정보를 자동으로 추가.
• 사용 예: 

#debugLog("Message")

 

 

2️⃣ Attached Macros
• 클래스, 구조체, 열거형 등에 속성처럼 부착되어 동작.
• 예: @Codable 매크로로 Codable 구현 자동 생성.
• 사용 예:

@Codable
struct User {
    let name: String
    let age: Int
}

 

 

그리고 세부적으로는 freestanding 2개, attached 5개. 

 

 

 

위 유형들은 프로토콜과도 매핑됨. 

Macro 프로토콜이 있고

 

 

 

이걸 채택하는 FreestandingMacro, AttachedMacro 프로토콜이 있고 

 

 

 

Freestanding 의 하위 유형들  (CodeItem 이 하나 더 추가되었나봄) 

 

 

AttachedMacro 의 하위 유형들 이 있음  (여기도 더 추가되었네..)  

 

 

 


 

[3] Freestanding 매크로 만들어보기 

 

1) ExpressionMacro 

 

이건 기본템플릿에 구현된 예제로 퉁.  

 

# 선언 

 

 

# 구현

 

 

 

# 사용

let result = #stringify(1 + 2)

 

 

 

2) DeclarationMacro

 

repeat 함수를 매크로를 통해 선언하는 예제를 작성. (조금 안와닿을 수 있는 예제이지만,  파라미터 두개를 받아보기 위해..)

 

 

# 선언

@freestanding(declaration, names: named(printRepeat))
public macro printRepeatFunction(_ value: String, _ count: Int) = #externalMacro(module: "MyMacroMacros", type: "PrintRepeatFunctionMacro")

 

 

# 구현

public struct PrintRepeatFunctionMacro: DeclarationMacro {

    public static func expansion(
        of node: some FreestandingMacroExpansionSyntax,
        in context: some MacroExpansionContext
    ) -> [DeclSyntax] {
        guard let value = node.arguments.first?.expression else {
            fatalError("The macro requires a value")
        }

        guard let repeatCount = node.arguments.last?.expression else {
            fatalError("The macro requires a repeat count")
        }

        return [
          """
            func printRepeat() {
                for _ in 0..<\(repeatCount) {
                    print(\(value))
                }
            }
            """
        ]
    }
}

 

 

# 사용

struct SomeStruct {
    #printRepeatFunction("HELLO", 3)
}

SomeStruct().printRepeat()

 

참고로

객체 안에서 매크로로 선언한 function 은 호출가능이지만, 

 

 

 

전역 스코프에서 호출하면 에러 발생 (in 템플릿이 제공하는 main 파일) 

 

 

 

 

3) CodeItemMacro (experimental feature)

 

아직 실험기능이라 써볼 수는 없었음 (swift-syntax 패키지 버전 바꾸면 가능할지도..)

 

DeclarationMacro 는 class, function 등이 아니라 코드 블럭을 선언하면 에러가 발생하는데, 

CodeItemMacro 는 코드블럭을 선언할 수 있는 매크로. 

public struct RepeatMacro: CodeItemMacro {

    public static func expansion(
        of node: some FreestandingMacroExpansionSyntax,
        in context: some MacroExpansionContext
    ) -> [CodeBlockItemSyntax] {
        guard let value = node.arguments.first?.expression else {
            fatalError("The macro requires a value")
        }

        guard let repeatCount = node.arguments.last?.expression else {
            fatalError("The macro requires a repeat count")
        }

        return [
          """
            {
               for _ in 0..<\(repeatCount) {
                    print(\(value))
                }
            }
            """
        ]
    }
}

 


[4] Attached 매크로 만들어보기 

 

1) PeerMacro 

 

PeerMacro 는 적용된 선언과 동등한 수준(peer level)에 새로운 선언을 추가할 때 사용됩니다.
예를 들어, 클래스나 구조체에 @attached(peer) 매크로를 적용하면, 매크로는 해당 클래스나 구조체의 외부에 새로운 선언을 생성합니다.

 

# 선언

@attached(peer, names: prefixed(Mock))
public macro Mock() = #externalMacro(module: "MyMacroMacros", type: "MockMacro")

 

[ names ]

• arbitrary는 이름을 고정하지 않겠다는 선언입니다. (매크로 실행 시 동적으로 결정됩니다)
• 이 경우 매크로가 생성한 선언의 이름은 사용자가 명시적으로 지정하지 않는 한, 컴파일러에 의해 기본 이름 또는 충돌 방지 이름으로 설정될 수 있습니다.
• 사용자로부터 이름을 전달받아 동적으로 생성하는 경우 예시:
@Mock(name: "CustomMockUserService")

 

• prefixed(Mock): 생성된 선언의 이름이 Mock 접두사를 포함해야 합니다.

• 예를 들어, 매크로가 UserService에서 호출되면 MockUserService라는 이름으로 확장됩니다.

 

 

# 구현 

public struct MockMacro: PeerMacro {

    public static func expansion(
        of node: AttributeSyntax,
        providingPeersOf declaration: some DeclSyntaxProtocol,
        in context: some MacroExpansionContext
    ) throws -> [DeclSyntax] {
        // Ensure the macro is applied to a protocol
        guard let protocolDecl = declaration.as(ProtocolDeclSyntax.self) else {
            fatalError("The macro must be applied to a protocol")
        }

        let protocolName = protocolDecl.name.text
        let mockName = "Mock\(protocolName)"

        // 프로퍼티 및 주석에서 기본값 추출
        let members = protocolDecl.memberBlock.members.compactMap { member -> String? in
            if let variable = member.decl.as(VariableDeclSyntax.self) {
                let name = variable.bindings.first?.pattern.description ?? ""
                let type = variable.bindings.first?.typeAnnotation?.type.description ?? "Any"
                let comment = member.leadingTrivia.compactMap { trivia -> String? in
                    if case .lineComment(let text) = trivia {
                        return text.trimmingCharacters(in: .whitespacesAndNewlines)
                    }
                    return nil
                }.first
                let defaultValue = comment?.replacingOccurrences(of: "//", with: "").trimmingCharacters(in: .whitespacesAndNewlines) ?? defaultForType(type)

                return "var \(name): \(type) = \(defaultValue)"
            } else if let function = member.decl.as(FunctionDeclSyntax.self) {
                let name = function.name.text
                let params = function.signature.parameterClause.description
                let returnType = function.signature.returnClause?.type.description ?? "Void"
                return """
                func \(name)\(params) -> \(returnType) {
                    fatalError("Mock implementation for \(name) not provided")
                }
                """
            }
            return nil
        }

        // Mock 클래스 생성
        let mockClass = """
        class \(mockName): \(protocolName) {
            \(members.joined(separator: "\n"))
        }
        """

        // Parse the mock class as DeclSyntax
        return [
            DeclSyntax(stringLiteral: mockClass)
        ]
    }

    /// 기본 타입에 따른 기본값 반환
    private static func defaultForType(_ type: String) -> String {
        switch type {
        case "String": return "\"\""
        case "Int", "Float", "Double": return "0"
        case "Bool": return "false"
        case _ where type.hasSuffix("?"): return "nil" // Optional 타입
        default: return "fatalError(\"No default value for type: \(type)\")"
        }
    }
}

 

 

 

# 사용 

@Mock
protocol UserService {
    // "Default Name"
    var name: String { get set }

    // 25
    var age: Int { get set }

    // true
    var isActive: Bool { get set }

    func fetchUser(id: Int) -> String
}

 

 

# Expand Macro

 

 

 

 

2) AccessorMacro 

 

위의 peer 랑 같이 쓰는 예제. 

 

# 선언

@attached(peer, names: arbitrary)
@attached(accessor)
public macro Logged() = #externalMacro(module: "MyMacroMacros", type: "LoggedMacro")

 

 

# 구현

public struct LoggedMacro: AccessorMacro, PeerMacro {

    // PeerMacro: 내부 저장 프로퍼티 생성
    public static func expansion(
        of node: AttributeSyntax,
        providingPeersOf declaration: some DeclSyntaxProtocol,
        in context: some MacroExpansionContext
    ) throws -> [DeclSyntax] {
        // 매크로가 변수에 적용되었는지 확인
        guard let variableDecl = declaration.as(VariableDeclSyntax.self) else {
            fatalError("이 매크로는 변수에만 적용할 수 있습니다.")
        }

        // 프로퍼티 이름과 타입 추출
        guard let variableName = variableDecl.bindings.first?.pattern.description else {
            fatalError("변수 이름을 확인할 수 없습니다.")
        }
        guard let type = variableDecl.bindings.first?.typeAnnotation?.type.description else {
            fatalError("변수 타입을 확인할 수 없습니다.")
        }

        // 내부 저장 프로퍼티 생성
        let internalPropertyCode = """
        private var _\(variableName): \(type) = \(variableDecl.bindings.first?.initializer?.value.description ?? "\(type)()")
        """

        // 문자열을 DeclSyntax로 변환
        return [DeclSyntax(stringLiteral: internalPropertyCode)]
    }

    // AccessorMacro: Getter, Setter 추가
    public static func expansion(
        of node: AttributeSyntax,
        providingAccessorsOf declaration: some DeclSyntaxProtocol,
        in context: some MacroExpansionContext
    ) -> [AccessorDeclSyntax] {
        guard let variableDecl = declaration.as(VariableDeclSyntax.self) else {
            fatalError("이 매크로는 변수에만 적용할 수 있습니다.")
        }
        guard let variableName = variableDecl.bindings.first?.pattern.description else {
            fatalError("변수 이름을 확인할 수 없습니다.")
        }

        let getter = """
        get {
            _\(variableName)
        }
        """
        let setter = """
        set {
            let oldValue = _name
            print("Setting \(variableName) from \\(oldValue) to \\(newValue)")
            _\(variableName) = newValue
        }
        """

        return [
            AccessorDeclSyntax(stringLiteral: getter),
            AccessorDeclSyntax(stringLiteral: setter)
        ]
    }
}

 

 

# 사용 

struct User {
    @Logged
    var name: String
}

var user = User()
user.name = "Bob"   // Setting name from  to Bob
user.name = "Alice" // Setting name from Bob to Alice

 

 

# Expand Macro

 

 

 

 

반응형

'🍏 > Swift' 카테고리의 다른 글

[Swift] @isolated(any)  (4) 2024.11.15
[Swift] @unchecked, @preconcurrency, @retroactive  (9) 2024.11.06
[Swift] Noncopyable (~Copyable)  (3) 2024.10.26
[Swift] Typed throws  (0) 2024.10.13
[Swift] AsyncStream  (1) 2024.08.24
댓글