I want to create a macro to protect the initialization process of lazy varibles. For example:
@ThreadSafeLazy(lock: lock)
lazy var varible: Int = {
return 1 + 1
}()
// will expanded to
lazy var varible: Int = {
lock.around {
if let _unique_name_varible { return _unique_name_varible }
_unique_name_varible = {
return 1 + 1
}()
return _unique_name_varible
}
}()
var _unique_name_varible: Int? = nil
I know how to add a _unique_name_varible
varible using PeerMacro
here, but I have no idea how to override the varible
's initiate closure.
Due to the lazy var
generation compiler crash issue, I changed the solution for workaround. now the macro looks like:
@Lazify(name: "internalClassName", lock: "SomeClass.classLock")
func createLazyVariable() -> String {
return "__" + NSStringFromClass(type(of: self))
}
// expanded to
private(set) var _lazy_internalClassName: String? = nil
var internalClassName: String {
if let exsist = self._lazy_internalClassName {
return exsist
}
return SomeClass.classLock.around { [weak self] in
guard let self else {
fatalError()
}
if let exsist = self._lazy_internalClassName {
return exsist
}
let temp = self.createLazyVariable()
self._lazy_internalClassName = temp
return temp
}
}
But there's another problem I encountered, I raised another topic here
You cannot modify existing declarations using a macro, except for adding accessors to a property.
As a workaround, you can make the lazy var declarations a parameter of a freestanding declaration macro. That is,
#ThreadSafeLazy(lock: lock) {
lazy var variable: Int = {
return 1 + 1
}()
}
You would add an additional () -> Void
closure argument to ThreadSafeLazy
, so that users of the macro can declare lazy vars in the closure. The macro can then expand to the two declarations you want.
Here is a simple implementation:
enum ThreadSafeLazy: DeclarationMacro {
static func generatePair(_ varDecl: VariableDeclSyntax, uniqueName: TokenSyntax, lock: ExprSyntax) -> [DeclSyntax] {
guard let binding = varDecl.bindings.first,
let initialiser = binding.initializer?.value,
let type = binding.typeAnnotation?.type,
let name = binding.pattern.as(IdentifierPatternSyntax.self)?.identifier else {
return []
}
let initialiserWithLock: DeclSyntax = """
lazy var \(name): \(type) = {
\(lock).around { () -> \(type) in
if let \(uniqueName) { return \(uniqueName) }
let temp: \(type) = \(initialiser)
\(uniqueName) = temp
return temp
}
}()
"""
let backingProperty: DeclSyntax = """
var \(uniqueName): Optional<\(type)> = nil
"""
return [initialiserWithLock, backingProperty]
}
static func expansion(
of node: some FreestandingMacroExpansionSyntax,
in context: some MacroExpansionContext) throws -> [DeclSyntax]
{
let varDecls = node.trailingClosure!.statements.compactMap { $0.item.as(VariableDeclSyntax.self) }
let lockExpr = node.argumentList.first!.expression
return varDecls.flatMap {
generatePair($0, uniqueName: context.makeUniqueName("backingProperty"), lock: lockExpr)
}
}
}
With this approach, you don't even need to write lazy
explicitly in the variable declaration, because that can also be added by ThreadSafeLazy
. Though IMO that could create confusion.
It is possible to declare multiple lazy vars in the closure, so you can also consider a design like what I did in this answer:
#ThreadSafeLazy {
@Locked(lock1)
lazy var variable`: Int = {
return 1 + 1
}()
@Locked(lock2)
lazy var variable2: Int = {
return 2 + 2
}()
}
where ThreadSafeLazy
expands to all the declaration inside the closure, but if any of the declarations has a @Locked
macro attached, ThreadSafeLazy
generates the thread safe version of the property, along with the additional unique name property. Locked
doesn't need to expand to anything - it only acts as a marker for ThreadSafeLazy
.