Attached macro adding arbitrary conformance

I am wondering if I can write something like the following:

protocol MyProtocol {
  var a: String { get }
}

@Wrapper(MyProtocol.self)
struct MyStruct {
  var inner: any MyProtocol
}

let instance = MyStruct(inner: /*imagine a value here*/)

print(instance.a)

What I want to achieve is for a struct to contain a value that conforms to a protocol and have a macro that will implement the protocol for the struct, calling the methods on the inner object.

Is it possible to write a generic macro for this which takes the protocol as an argument? Can I write an extension macro that will output conformance to an arbitrary protocol?

Is it then possible to get the field declarations of the protocol in my macro definition?

That's currently not possible with macros. You can pass the name of the protocol to the macro, but within its implementation there is no way to access the declaration of the protocol. Imagine it being imported from another module ... But even in the same file, the implementation has no way to access context outside of the declaration it was attached to.

1 Like

Macros

You may not be able to attach macro to the wrapper, but you can attach the macro to the protocol. This is how swift-spyable by @Matejkob works. You write:

@Spyable
public protocol ServiceProtocol {
  var name: String { get }
  func fetchConfig(arg: UInt8) async throws -> [String: String]
}

It generates:

public class ServiceProtocolSpy: ServiceProtocol {
  public var name: String {
    get { underlyingName }
    set { underlyingName = newValue }
  }
  public var underlyingName: (String)!

  // And so on…
}

So, technically what you are asking is possible, and you even have a pretty big code base to inspire you. That said macros would not be my 1st choice for this kind of functionality. Enter the…

Sourcery

(I'm not affiliated with @krzysztofzablocki, I just really like this repo.)

Macros have tons of problems with compilation times and they involve "virtual" code - code that you can't debug or add to source control.

Sourcery lets you generate the code and store it in a file, just like any other code. No compilation time cost, it is just a normal Swift code.

I will use the spy/mock example here, so you can compare it with swift-spyable from the above. You write:

// sourcery: generate-fake
protocol FileSystem: Sendable {
  /// Current working directory.
  var cwd: String { get }
}

This is processed by the Sourcery using your template:

@testable import YourModule

{% for type in types.protocols where type|annotated:"generate-fake" %}
class {{ type.name }}Fake: {{ type.name }}, @unchecked Sendable {

  {%- for variable in type.allVariables|!definedInExtension %}
  // Do some stuff for every property.
  {% endfor %}

  {%- for method in type.allMethods|!definedInExtension %}
  // Do some stuff for every method.
  {% endfor %}
}
{% endfor %}

This will generate the following file (that you can check into your source control!):

class FileSystemFake: FileSystem, @unchecked Sendable {

  /// Always return this value.
  var cwdResult: String?
  /// Return specified values in succession.
  var cwdResults = [String]()
  private var cwdResultsIndex = 0
  /// How many times was this property called?
  var cwdCallCount = 0

  var cwd: String {
    self.cwdCallCount += 1

    if let r = self.cwdResult { return r }

    if self.cwdResultsIndex < self.cwdResults.count {
      let r = self.cwdResults[self.cwdResultsIndex]
      self.cwdResultsIndex += 1
      return r
    }

    fatalError("FileSystemFake.cwd - missing mock")
  }
}

If you are an iOS dev then I highly recommend looking at SwiftGen to automate assets/colors/fonts/localization. Very similar to Sourcery.

Other

One minor thing is that when writing struct Wrapper: MyProtocol you need to store an inner: MyProtocol instance. You can store it as any MyProtocol, but that may involve an existential container (TypeLayout -> existential-container-layout).

In theory it may be better to make the wrapper generic (struct Wrapper<Inner: MyProtocol> { let inner: Inner }). Then you would have to use some MyProtocol. This is basically a way of saying: "I'm too lazy so spell the whole type, but the compiler knows it".

The big downside is that you will always need some concrete type, which can get a bit annoying, and the performance gains would be miniscule (unless you are writing the next SwiftUI that needs to be refreshed at 120Hz).

2 Likes

Btw. You may want too look at this thread: Can swift macro create separate implementation of a declaration?

Thank you for that well-explained answer. I have used Sourcery in the past for a different project, indeed a good alternative.