Is it safe to use global variables while implementing macros?

I was getting bothered that I needed to give context as a parameter to every function that I implemented. But I got the idea of storing it in a global variable. But is it safe to do this?

// Macros are not isolated to anything.
nonisolated(unsafe) var currentContext: (any MacroExpansionContext)!

extension SomeMacro: MemberMacro {
    static func expansion(
        of node: AttributeSyntax,
        providingMembersOf declaration: some DeclGroupSyntax,
        conformingTo protocols: [TypeSyntax],
        in context: some MacroExpansionContext
    ) throws -> [DeclSyntax] {
        currentContext = context
        // Do stuff
    }
}

func something(_ syntax: Syntax) -> Syntax { /* do someting that accesses 'currentContext' */ }

For example, if I have 2 macros implemented in the same module, can/will they run in parallel and cause a race condition, or does each macro runs in its own isolated environment?

Have you considered creating a helper struct/class and using its stored properties to manage state? (And then implementing your logic as functions/methods on the helper type)

5 Likes

That helper struct could even be the struct that conforms to the macro protocols!

2 Likes

That's a good idea, actually. I'm going to try/use this.

Oh yeah, expansion functions are static, also a great idea. Although it may not work for multi-purpose macros, depends on the implementation.


One issue I found with this approach is the expand function. When I call it, if the macro uses the same technique (in the same module), the context gets replaced. But I can work around it by storing context in a temp variable and reassigning it after it's called.

It seems like the main question is if each macro runs in its own isolated environment, or not (excluding expand function). Maybe I should've asked that question from the start instead of this.