The Beauty of Swift Language — Macro

👍
· Dec 26, 2023 · 6 mins read
The Beauty of Swift Language — Macro

A macro in computer languages is a rule or pattern that can expand the input into output and it is a great way to reuse the code in macro to avoid code duplication.

The Swift Macros was specially designed to generate code for certain purposes and it has no counterparts in C# or Kotlin. However, there are some similarities between it and Annotation in Kotlin. If you are interested in comparing these two, please check out here. Also, I have written a few articles that covered the basics of the Swift Macro, like Set Up to Develop Swift Macros, My Experience in Developing Swift Macros, Develop Freestanding Expression Macros in Swift, Develop Freestanding Declaration Macros in Swift and Develop Attached Swift Macros. Please check them out if you need them.

In this article, we are going to focus on Swift Macro and its latest development.

Different types of Swift Macros

Swift Marco was designed in a very tailored way and we can use it to generate code for certain purposes. While, at the first sight, you might think this approach limits the powers of the macros, Swift Macro can be very powerful especially if we compose different types of Macros together.

There are two major types of Macros in Swift, Freestanding Macro and Attached Macro. Two types of Freestanding macros are Freestanding declaration macro and Freestanding expression macro and they are used to generate declaration and expression respectively like below.

// Freestanding macros generate code based on node and context of the macro  
  
/// Expression macro  
static func expansion(  
    of node: some FreestandingMacroExpansionSyntax,  
    in context: some MacroExpansionContext  
  ) throws -> ExprSyntax  
  
/// Declartation macro  
static func expansion(  
      of node: some FreestandingMacroExpansionSyntax,  
      in context: some MacroExpansionContext  
  ) throws -> \[DeclSyntax\]

Attached macros are attached to existing code and generate code based on code information and context they are attached to like below. Five types of macros are Peer macro, member macro, accessor macro, member attribute macro and extension macro(updated from conformance macro).

/// Peer macro generates peers for the code it's attaches to  
/// For instance, if it attaches to a method,   
/// it's supposed to generate another method at the same level with the attached method  
static func expansion(  
    of node: AttributeSyntax,  
    providingPeersOf declaration: some DeclSyntaxProtocol,  
    in context: some MacroExpansionContext  
) throws -> [DeclSyntax]  
  
/// Member macro generates members to the declaration it's attached to  
/// For instance, use it to generate a new method or variable to the attached declaration  
static func expansion(  
    of node: AttributeSyntax,  
    providingMembersOf declaration: some DeclGroupSyntax,  
    in context: some MacroExpansionContext  
) throws -> [DeclSyntax]  
  
/// Accessor macro adds accessors(getter/setter) to a given declaration.  
static func expansion(  
    of node: AttributeSyntax,  
    providingAccessorsOf declaration: some DeclSyntaxProtocol,  
    in context: some MacroExpansionContext  
) throws -> [AccessorDeclSyntax]  
  
/// Attribute macro generates attributes to the members inside the declaration it's attached to.  
/// which is a way to compose macros together to achieve a collective goal  
static func expansion(  
    of node: AttributeSyntax,  
    attachedTo declaration: some DeclGroupSyntax,  
    providingAttributesFor member: some DeclSyntaxProtocol,  
    in context: some MacroExpansionContext  
) throws -> [AttributeSyntax]  
  
/// Extension macro generate extension to the declaration it's attached to  
/// For instance to add new protocol conformance etc.  
static func expansion(  
    of node: AttributeSyntax,  
    attachedTo declaration: some DeclGroupSyntax,  
    providingExtensionsOf type: some TypeSyntaxProtocol,  
    conformingTo protocols: [TypeSyntax],  
    in context: some MacroExpansionContext  
) throws -> [ExtensionDeclSyntax]

In order to develop new macros, the major work is to implement the above expansion methods. As we can see, in the expansion methods of freestanding macros, we can only access to the node and context . The node contains the information of the macro itself. The context can be used to provide diagnosis information or generate a unique name etc.

For all attached macros, apart from node and context , we have access to declaration as well, which is what the macro is attached to. For Attribute macro and Extension macro, some other values are injected into expansion method to support the code generation.

SwiftSyntax library

SwiftSyntax is an open sourced library that is used in Swift Macro system. In order to develop new macros, some knowledge of this library is needed. All of the above expansion methods are defined in this library.

Luckily, the library provides rich methods to generate code from either string literals or its builders. Please refer to the full documentation of this library here.

When we design and implement a new macro, we know the code it should expand into, which should be the output of the expansion methods. However, we need to express these code pieces in SwiftSyntax types. In order to look up which syntax should be used, we can use this website to check. Just paste the code snippet into the left panel, it will show the syntax tree like below:

Lastly, we can also learn from examples. Good example projects are listed here.

Fully integrated with Xcode

Apple has made great effort on integrating Swift Macro feature into Xcode. We can easily use the context menu on Macro to expand the code in place and we can even add breakpoints into the code generated to debug it like below:

Please check out this repo for more example macros.

The cost of using Swift Macro

As we can imagine, there must be some cost of expanding the macros. We can experience longer pipeline running time because pulling and compiling SwiftSyntax library as well as expanding macros will take some time. It might add one minute or more to your pipeline depending on the speed of the network and computers.

Also, we need to balance the effort of developing the macro with the benefit of using it. It’s certainly doubtful if we develop one macro and only use it once.

However, as macro is a way to avoid duplication, which is great in terms of code maintenance. I would suggest to use it for long term projects.

It’s a big deal

Even though Macro is a relatively new feature in Swift, it is a big deal. Many great features like SwiftData are built upon it and you might already know the awesome @Observable macro. From these example, we can see, if we use it correctly, it can be super powerful!

Conclusion

Swift Macro is a great addition to the language in version 5.9. Its unique design makes this feature really shine.

Comments

Sharing is caring!