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.
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) { ... }