swiftmacrosclosuresswift-package-managerretain-cycle

How to create a macro in Swift that doesn’t return a value but simply substitutes code for execution?


I want to create a Swift macro that doesn’t return a value to be inserted somewhere; I want a macro that simply substitutes code in its place.

Specifically, I want to avoid constantly repeating the following line every time I capture self in a closure:

guard let self = self else { assertionFailure(); return }

and use something like:

let closure = { [weak self] in
            #weakSelf
            // ... actual code
          }

Ideally, I want to be able to pass a value to the macro that should be returned from the function where it’s inserted, but that’s the next step.

What I did: I created a Swift macro using Swift Package Manager. I’ll leave the code below.

Question: Is this macro even possible, and if so, what am I doing wrong?

The actual code:

@freestanding(expression)
public macro weakSelf() = #externalMacro(
    module: "MyMacroMacros",
    type: "SelfGuardMacro"
)

@main
struct MyMacroPlugin: CompilerPlugin {
    let providingMacros: [Macro.Type] = [
        SelfGuardMacro.self,
    ]
}

public struct SelfGuardMacro: ExpressionMacro {
    public static func expansion(
        of node: some SwiftSyntax.FreestandingMacroExpansionSyntax,
        in context: some SwiftSyntaxMacros.MacroExpansionContext
    ) throws -> ExprSyntax {
        return ExprSyntax(stringLiteral: """
        guard let self = self else {
            assertionFailure()
            return
        }
        """)
    }
}

Using:

class MyClass {
    var closure: (() -> Void)!
    
    func function() {
        self.closure = { [weak self] in
            #weakSelf
            self.printString()            
        }
    }
    
    func printString() {
        print("printing")
    }
}

On self.printString() I'm getting error:

Value of optional type 'MyClass?' must be unwrapped to refer to member 'printString' of wrapped base type 'MyClass'

like #weakSelf does nothing.


Solution

  • Applying a macro to a closure is listed as one of the "future directions" of function body macros, but it is currently not implemented.

    A workaround is to create an expression macro that expands to a closure, so that you can do something like:

    doWork(completionHandler: #WeakSelfClosure { result in
        // ...
    })
    

    This will then expand to

    doWork(completionHandler: { [weak self] result in
        guard let self else {
            return
        }
        // ...
    })
    

    Here is an example implementation:

    // declaration
    @freestanding(expression)
    public macro WeakSelfClosure<each P>(
        closure: (repeat each P) -> Void
    ) -> (repeat each P) -> Void = #externalMacro(module: "...", type: "WeakSelfClosure")
    
    
    // implementation
    enum WeakSelfClosure: ExpressionMacro {
        static func expansion(of node: some FreestandingMacroExpansionSyntax, in context: some MacroExpansionContext) throws -> ExprSyntax {
            guard var closure = node.trailingClosure else {
                throw SomeError()
            }
            var signature = closure.signature
            let weakSelfCapture = ClosureCaptureSyntax(
                specifier: .init(specifier: "weak"),
                expression: DeclReferenceExprSyntax(baseName: "self")
            )
            if var signature = closure.signature {
                signature.capture = .init(items: .init {
                    for capture in signature.capture?.items ?? [] {
                        capture
                    }
                    weakSelfCapture
                })
                closure.signature = signature
            } else {
                closure.signature = ClosureSignatureSyntax(capture: .init(items: [weakSelfCapture]))
            }
            closure.statements = .init {
                "guard let self else { return }"
                for stmt in closure.statements {
                    stmt
                }
            }
            return ExprSyntax(closure)
        }
    }
    

    This example only works for closures that return Void. It is easy to extend this to closures that return any type. The macro can take the value that should be retuned as its parameter.

    // closure will return nil when self has been deallocated
    let closure: () -> Int? = #WeakSelfClosure(nil) { ... }