Swift Macros: Detailed & Practical Understanding

Can Akyıldız
11 min readDec 15, 2023

--

Hey guys, today I will be talking about Swift Macros.

Macros before June 2023, were built-in and not modifiable or buildable.
They were source level components that if you were to create a macro, you had to go through a certain scenario with apple where you need to make proposals, prove why it’s needed and what it does and then get approved by them to get it added.

Now, after introduction of macros in June 2023 as a beta software and by September it’s available for everyone to use at home. You can declare a macro without any approval from anyone.

What are Swift Macros?

Swift macros are like smart shortcuts that add extra code for you when you’re building your app. Everything happens at compile time so This helps make coding easier by being less repetitive, and less error prone.

There are a two primary types of macros that are Freestanding and Attached Macros.

I would like to begin with introductions of these macros, then I will dive deep in their inner types and give building examples.

Freestanding Macros

We are not very unfamiliar with freestanding macros as we have seen them and used them before

Like in this example, the method is using #warning and #function macros.

They are created like this above and we will learn more about its creation in a bit

You might think of @inlinable as somewhat similar. @inlinable lets the Swift compiler decide if a function’s code should be inserted directly at each place it’s used (this is called inlining) for efficiency.

But with Freestanding Expression Macros, this inlining happens every time, automatically.

Attached Macros

They only modify the declaration they are attached to. Those are the ones that start with the ‘at sign @’ before declaration.

Like SwiftData’s @Model macro or @Observable from reactive programming:

Get Started: Setting up things

Firstly, open Xcode and go to File > New > Package

This package will be available for us to use in our projects, but initially we can create them separately.

Select SwiftMacro template from multi platform section.

Then we will be presented with this default set up:

Please note that we will be using these folders so I might say “hey let’s go back to main file”.

Understanding Different Cases of Macros

Expression Macros (Freestanding)

Expression macros are used for generating values or performing actions at compile time.

We have an example which is going to help us get the square of number we pass in.

Let’s see how it’s actually being created.

First, just like in our default set up from Xcode when we first created our Swift Macro, we will need to create a macro like this in MyMacroMacro file.

After we define our Macro, we get an error that yells us with the needed set up we need to do.

As we conform to protocol and get needed method there. The expansion so-called method is going to be seen in all of the macro creations, it basically represents expanded state of a macro. You also see node and a return of SwiftSyntax.ExprSyntax.

What is node?
Node will be helpful with retrieving the passed information or lets say the information that is passed to macro. Which will contain arguments, descriptions, types and so on. It will be more clear in a moment when we do the implementation of ComputeSquareMacro.

Why are we returning SwiftSyntax.ExprSyntax?
Well what we are doing is basically writing low level code to create syntax, there are lower levels obviously but this environment or framework that we see is basically a wrap up of lower logic so we can understand.

To continue with our ComputeSquare ExpressionMacro, we can do the implementation like this:

Again, node is allowing us to access arguments passed to macro, and first argument from argumentList is the number that we passed earlier.

  • argumentList first is number parameter in the method, imagine we had other parameters in there. So to specify or access a specific parameter, we work with argumentLists.
  • literalValue is how we obtain this in the boundaries of integer syntax by telling compiler this argument is a type of IntegerLiteralExprSyntax and we obtain the literal text, then we wrap it in a Int type.

Now let’s get back to main file and then select the right target as seen on the screenshot also make sure you select Mac for testing purposes, you can see the desired effect being display on the debugger.

Declaration Macros (Freestanding)

Declaration macros basically automate the creation of data structures or other declarations.

In our example we want to create an struct from arguments. Let’s say this structure has a value and we want to access it.

We want this to be our result when Macro is expanded.

Make sure to update your CompilerPlugin, you can either add this new one to the array or remove previous example like I did.

And here is macro declaration as a freestanding declaration macro.

The “names” parameter you see here is basically what names the compiler needs to pay attention to. You need to pass names and make sure you match with something because this is not a runtime operation and we need to have things pre defined before we get the code working.

Now, looking at the implementation of DecloMacroExample struct, we could see that the returned syntax is literally how you would create a structure. Only difference you could see is the argument that is being passed which is what we pass as value from our client

An important note that you need to make sure struct’s name must match with macro like this:

Now if you go back to the main file and add the line of #createStructWithValue(“”) and pass a value in the parameter, and also add a assert testing assert(DecloMacroStruct.value == “Cancodeswift”). And it should work fine.

To see the details of the macro you can tap on the macro and click on expand

And you will see that the source is there.

Attached Macro Types

Now we can continue with Attached macros. Attached macros have more types compared to freestanding ones. And I would say some of them are more complicated as you need to dig into types more to achieve desired results for macros like Peer Macro.

Member Macro

Member macros simply add new declarations inside the type they are applied to.

In our example we want to have a computedProperty member in an enum that will give us websites of cases.

We want to achieve this result:

And when @WebsiteGiver member macro is expanded we will see this:

So basically we want each case to have www at beginning; com at the end and we also don’t want to write website variable.

So first, lets start creating our macro starting from MyMacro file

If you noticed, it’s not freestanding marked anymore, and we also have something called arbitrary that is passed to names, this will make things more generic for us.

And lets see how EnumMemberMacro is created

First we created our struct and conformed to MemberMacro protocol and we will be presented with expansion method that we will need.

Since we have done this part before and you pretty much guess what’s going on with conformance, let’s continue directly with implementation.

We have membersBlock accessed from declaration, which will allow us to work with all cases. We are first compactMap’ping each item to be type of EnumCaseDeclSyntax, this will help compiler understand that we are actually working on Enum cases.

Afterwards, we are mapping members into being ready to use and create our computed property.
Rest of it is not really complicated, just syntax that we are familiar with.

Then you can go back to the main file and be able to access website property that we created as a macro.

Accessor Macro

Accessor macros are usually applied when working with properties, to turn them into computed properties.

As you can see in the example, on the left we have a struct that contains a dictionary and a computed property.

What accessor macro can help us with is, take us from implementation on the left side to right side.

Again, we go to our MyMacro file and create this. The dict that you see in names param is basically to tell compiler to know what our dictionary will be called.

Why it can’t be generic with arbitrary? is a possible and good question that I can only answer with- it has to be compiler waking like freestanding macros, I could not find enough sources to go deeper but more macros are needed for me to use more I will dig deep and make more articles and videos.

If we continue with the StoryingGuyMacro struct implementation, this time we are not going through argumentList or arguments but bindings and declaration.

We declared “varDecl” from declaration value and casted it as VariableDeclSyntax, which then is going to let us access bindings, obtain first one and then get the variable’s naming and look for it in our dictionary or set its dictionary field to the new value.

As our next attached macro, we are continuing with Member Attribute but we will actually be adding on to Accessor Macro and combine them together.

Member Attribute Macro

Member attribute macro is a macro that helps with adding an attributes to your types or extensions. If we look at our previous example , we had this happening.

But imagine now we have another property other than surnameProp or even many more.

So instead of adding this attribute for each property we will have this MemberAttribute macro of @StoringGuyAttributes and it will help getting rid of the repetitive accessor macro of storing guy but putting it in side.

Then when this StoringGuyAttributes macro expands it will look like this:

StoringGuyAttributes member attribute macro will generate properties and Accessor macro of @StoringGuy. Then we will be able to expand those StoringGuy macro that creates computed property work we created before.

So let’s look at how we are actuating building it.

First, go to MyMacro file and add the line of StoringGuyAttributes, type should be same as StoringGuy accessor because we will be working with same struct by extending it to create MemberAttribute macro.

Then, without any modification to Accessor Marco work we did before, just extend the StoringGuyMacro strut and conform to MemberAttribute protocol.

Then our implementation will look like this. What’s happening here is that we first create a guard declaration that says “hey, make sure the property that we will apply this attribute to is a ‘var’ declaration. ” And then by also checking description doesn’t contain “dict” regex, we make sure the dictionary we created in the struct is not applying this StoringGuy attribute.

So you can also see on return part, we are returning AttributeSyntax with the name of identifier called “StoringGuy”. StoringGuy is again the Accessor Macro we created to turn normal variables to computed properties.

So we are essentially saying “hey apply StoringGuy Accessor Macro to variables wherever we apply this MemberAttribute Macro to.” Also as far as .with modifier, it’s just making sure we are on next line after we add @StoringGuy accessory.

That’s pretty much it from Member Attribute macro guys. Next we will continue with Peer macro

Peer Macro

Peer macros are more complicated compared to others, there are not a lot of simple examples to give out and honestly this is another day’s talk to do. So on GitHub I came across a person called Doug Gregor, credits to his due, I will go over his example.

To achieve this result you see of withCheckedThrowingContinuation or withCheckedContinuation depending on marked method’s returning result or not

We would create the macro like this

And implementation of that would be looking like this:

The complexity comes from concurrency and async methods being over crowded on low levels. You can’t just declare the syntax in a string and pass things out. It’s a lot of types and so on.

But I promise I will do a real scenario implementation on this one in another video and article.

Conclusion

That was pretty much it from Swift Macros guys, I will be making a separate article and video for Peer Macros later on. I hope you found it understandable and useful.

Thanks for reading.

Goodbye.

--

--