之前了解的宏

写过C++和OC的同学可能比较了解宏,一般运用场景是界说重复逻辑,最终在编译时进行文本替换。

#ifdef CCORE_IOS
    // ...
#endif
#define HasFlag(flag, type) (((flag) == 0) || ((flag) & ((uint64_t)1 << (type))))
if (HasFlag(flag, value)) {
    // ...
}

可是Swift在此之前并没有宏的概念,Swift中常见条件编译#if-#else-#end也不属于宏,仅仅预处理指令。想要完成相似C++和OC宏相似的才能只能经过界说大局变量和大局函数的方法完成。


什么是Swift Macros

Swift Marcos是WWDC23推出的新特性,在Swift5.9(Xcode15)中完成的才能,没有系统版本约束。Swift Marcos除了支撑上述才能外还能经过批改语法树的元编程方法完成更多有意思的才能(削减体力活)。

下面是官方给出的一个Swift宏运用运用比如,咱们希望一起得到表达式的成果以及表达式自身的字符串的元组,在此之前的Swift只能经过手动的方法一个个去写表达式的字符串,而经过#stringify这个宏很轻松的完成这种才能,经过宏打开能够看到主动帮咱们完成字符串的填写,当然这个宏是自己完成的并非规范库内置的。

探索Swift Macros

界说Swift Macro

开始可能会比较懵,但写过一遍后理解规矩就很简略了,仍是官方比如#stringify,界说宏有点像界说函数都有称号、入参和回来值,后边紧跟宏完成地点的模块和类名。

探索Swift Macros

完成Swift Macro

完成Swift宏只需求完成相关协议函数即可,这个协议方法入参和回来值类型都有一个Syntax单词,是由于宏的才能是基于语法树去完成的,所谓的宏打开实质也是回来一个新的语法树去生成必要的代码。

再看控制台打印入参的内容,发现调用宏时的a+b会被拆分成语法树中的每一个Syntax,然后咱们能够经过特点的方法获取这些信息。

探索Swift Macros

最终回来值看似回来的是字符串,其实仅仅回来值的类型支撑了字符串字面量创建的方法,而字符串内容会再次被拆分成各个Syntax,咱们能够界说成变量po一下回来值的语法树结构,所以宏打开就能得到咱们所希望的元组,看起来跟写脚本相同。

探索Swift Macros

Swift** Macro编译进程**

When Swift sees you call a macro in your code, like the “stringify” macro from the Xcode macro package template, it extracts that use from the code and sends it to a special compiler plug-in that contains the implementation for that macro. The plug-in runs as a separate process in a secure sandbox, and it contains custom Swift code written by the macro’s author. It processes the macro use and returns an “expansion,” a new fragment of code created by the macro. The Swift compiler then adds that expansion to your program and compiles your code and the expansion together. So when you run the program, it works just as though you wrote the expansion yourself instead of calling the macro.

上面引用WWDC23原文,宏编译进程首要阅历以下几个进程:

  1. 将运用到宏的代码发送给包括该宏完成的特殊的编译器插件。

  2. 插件在沙盒安全的独立进程中履行宏的完成逻辑。

  3. 宏完成逻辑将回来一段新的代码块(宏打开)。

  4. Swift编译器再将代码块增加回源码中一起参加编译。

(这儿独立进程能够说明无法访问宏运用方的App沙盒)

探索Swift Macros

再看语法树结构

现在咱们了解宏的实质便是写一段新的语法树增加会源码中,那么了解语法树结构是整个宏完成进程的核心。咱们再以一个struct Person类型了解下语法树结构,这儿补充说明下@DictionaryStorage也是宏,用法后边再打开说明。

  • Person是个struct类型,因而语法树顶层是以Struct开头的StructDeclSyntax,当然也有对应其他类型的DeclSyntax,例如ClassDeclSyntax、EnumDeclSyntax、FuncDeclSyntax等。所以当咱们编写宏时只需求关注宏作用在什么类型上,就转成对应的DeclSyntax即可。

  • Struct的内容会被拆分成特定的Syntax,然后能够经过特点的方法进行访问

探索Swift Macros

(当然这儿只举例了小部分的特点,实践开发时咱们自行看下源码和调试时po下结构就知道有什么内容。)

Swift Macro种类

探索Swift Macros

  • freestanding:独立宏,能够像函数调用的方法直接运用,不依赖上下文

  • attached:附加宏,不能直接运用,有必要附加到其他声明代码上,给被符号的声明代码增加才能,留心附加宏打开后都是增加新的代码块,而不会批改原有的代码块。

Swift Macro指定命名

咱们能够看到在界说宏时不管是freestanding宏仍是attached宏都有一个names的参数,由于在宏打开后得到的新代码块会包括各种特点和函数,特点名和函数名可能会跟本来的代码中有命名抵触,能够经过names参数来指定命名。

@freestanding(declaration, names: arbitrary)
macro gyb(String, [Any]) = #externalMacro(module: "MyMacros", type: "GYBMacro")
@attached(peer, names: overloaded)
public macro AddCompletionHandler() =
    #externalMacro(module: "MacroExamplesPlugin", type: "AddCompletionHandlerMacro")

探索Swift Macros

  • overloaded:具有相同的姓名,能够用在函数重载的场景上

  • prefixed:宏增加相同姓名声明,但增加指定的前缀

  • suffixed:宏增加相同姓名声明,但增加指定的后缀

  • named:宏增加指定姓名的声明,能够用在完成某些协议函数,由于咱们比较清晰宏打开后会有哪些姓名

  • arbitrary:宏增加的称号无法用上述规矩描绘,宏打开后的姓名都由宏完本钱身决定,也是咱们最常用


可能的运用场景

现在咱们把Swift宏的元编程才能根本都了解了,事务上以往一些有侵入性逻辑、维护本钱大、迭代时容易遗漏的场景能够考虑经过宏的计划去处理:

  • 主动完成对事务函数做无侵入打点、权限校验、阶段性数据缓存

  • 主动完成线程安全数据结构

  • 主动完成public struct的init方法

  • 主动完成纯数据类型自界说序列化和反序列化


实践一个Swift Macro

目标:尝试完成一个PerformanceTrack宏,能够无侵入打印函数履行的耗时,对调用方无感知,调用本来的函数

// 原函数加上宏
@PerformanceTrack
func add(_ x: Int, _ y: Int) -> Int {
    return x + y
}
print(add(1, 2))
// macro expands 希望宏打开后得到下面函数,调用方无感知
func add(_ x: Int, _ y: Int) -> Int {
    let start = DispatchTime.now()
    defer {
        let end = DispatchTime.now()
        let nanoseconds = end.uptimeNanoseconds - start.uptimeNanoseconds
        let milliseconds = Double(nanoseconds) / 1_000_000
        print("\(#function) 履行时间为 \(milliseconds) 毫秒")
    }
    return x + y
}
print(add(1, 2))

创建Swift Macro工程

image

探索Swift Macros

Xcode新建Package工程挑选Swift Macro,demo工程名比较随意就写了swift-macro,新建完成后得到上图工程文件

  • Sources

  • Tests

  • Dependencies

确定宏类型和宏打开

探索Swift Macros

咱们希望在main.swift经过上述方法增加宏来给add函数增加打印函数履行耗时的才能,首要比较清晰宏是作用在函数上的,因而是个attached宏,宏打开的函数跟原函数在同一层级且没有特点没有协议完成,因而是个attached(peer)宏,命名上咱们没有要求能够直接是arbitrary,最终咱们计划把宏完成的类名定为PerformanceTrackMacro,因而在swift_macro.swift有宏的界说如下:

探索Swift Macros

别的还有个问题是attached宏只会新增代码,对本来的函数不会有任何批改,相当于宏打开后将会有两个函数,而咱们希望调用方仍是无感知的调用本来的函数,宏打开的新函数不能与原函数有相同的函数签名否则会重复界说。

探索Swift Macros

现在看来宏打开的新函数签名就不能与原函数相同,可是又要调用原函数,这种状况换在OC上是经过runtime method swizzling的计划去完成AOP,Swift尽管没有函数交流的才能,可是有函数替换的才能,经过给新函数加上@_dynamicReplacement符号和给原函数加上dynamic润饰符,就能够替换对应函数的完成。

探索Swift Macros

在此之前Swift单独依靠@_dynamicReplacement符号是无法做到AOP的,由于替换的新函数并没有本来函数的完成逻辑,经过宏的元编程才能能够拿到原函数的语法树,相当于编译期拿到函数完成再进行函数替换。

宏完成、单测与调试

咱们在swift_macroMacro.swift文件中完成宏,由于宏类型是**@attached**(peer, names: arbitrary),因而咱们的宏也恪守PeerMacro协议,当然peer是attached的一个子类型,因而PeerMacro协议自身也恪守AttachedMacro协议,而最终AttachedMacro协议恪守Macro协议。接着咱们需求完成PeerMacro协议的expansion函数,函数的回来值当然是个语法树,这儿先回来空数组代表什么宏打开后代码都不生成。

探索Swift Macros

新建工程时swift_macroMacro.swift文件中会主动生成一个swift_macroPlugin结构体,而且该结构体恪守CompilerPlugin协议,回来的providingMacros便是参加编译的宏类型,将咱们的宏类型注册到编译插件中参加Swift Macro编译,这样咱们就能main.swift中运用咱们的宏,宏的打开就能履行咱们的expansion函数,只不过目前打开没有任何新代码生成。

探索Swift Macros

接下来开始编写语法树,即便咱们在前面的内容了解过语法树的结构,咱们仍是会比较懵不知道怎样去写,这时候咱们能够经过调试po的方法看看输入参数中原始函数的语法树结构。想要调试expansion函数,直接履行main.swift是无法断点的,只能经过单元测试的方法才能触发断点,走运的是咱们新建工程时就帮咱们生成好单元测试的文件swift_macroTests.swift,咱们参考模板中#stringify的单测来编写咱们的宏的单测,咱们先把PerformanceTrack先注册到testMacros中,再写咱们的单测函数,这儿调用的assertMacroExpansion第一个参数是打开前的代码,第二个参数是宏打开后的代码,第三个参数是用来查找宏,这儿我只想看看语法树,所以第二个参数先不管。

探索Swift Macros

这时咱们就能断点到宏的expansion函数,前面介绍过宏作用的什么类型上,入参便是对应的类型DeclSyntax,这儿宏作用在函数上,因而能够转成FunctionDeclSyntax类型,接着po下得到语法树就能看到每个Syntax。

探索Swift Macros

po后咱们知道原函数的语法树结构,现在也需求知道宏打开后咱们所希望得到的函数的语法树结构,这样咱们才知道应该怎样从原函数语法树结构转化得到新函数语法树结构,用同样的方法把单测的入参函数换成咱们希望的函数。

探索Swift Macros

经过断点po后咱们看到宏打开函数的语法树结构,下图是两函数语法树的结构比照,能够清晰咱们需求做的工作:

  • attributes需求把PerformanceTrack改成_dynamicReplacement

  • modifiers需求把dynamic设置为nil

  • identifier需求把add改成add_PerformanceTrack

  • body中的statements需求增加两个代码块

探索Swift Macros

动手写代码之前咱们再了解一个东西,由于语法树每个特点都是Syntax类型,都恪守SyntaxProtocol协议,该协议有个快捷的with方法经过keypath的方法给特点赋值,一起回来一个新对象,为咱们编写语法树结构时供给快捷的链式调用。

探索Swift Macros

下面便是完整的完成进程,能够先看最下面的newFunc是经过funcDecl批改而来的,批改的进程经过with函数对特点进行设置,设置的特点便是上面提到要做的工作,最终把newFunc回来出去得到新函数。而所有的特点和值都能够按照po出来的语法树结构进行读取和赋值。别的咱们能够留心到咱们创建的各种Syntax都是经过字符串字面量的方法构建,由于代码自身便是字符串,对字符串内容进行拆分得到Syntax,所以语法树跟字符串是能够互相转化,所以咱们只需求硬编码字符串刺进变量就能够完成咱们大部分的需求,掌握规矩后就很简略了。

探索Swift Macros

最终咱们经过单测看输入输出成果是否契合预期,单测经过后咱们在main.swift试运行下宏,仍是调用本来的add函数,控制台打印出函数耗时,咱们的宏就完成了。

探索Swift Macros

过错抛出和Fix It

一般编写的宏都是为了完成特定功用,而且作用对象是很清晰的,因而当宏被作用在不合符要求的地方时咱们需求经过编译器报错提示过错原因,以及尽可能供给fix计划快速批改,能够运用Diagnostic类型来供给以下才能:

  • node:提示报错节点

  • message:提示error信息,需求完成DiagnosticMessage协议

  • fixIt:供给快速批改计划,需求完成FixItMessage协议

例如PerformanceTrack宏作用在非函数类型或者函数短少dynamic润饰,咱们先界说过错类型,message特点是DiagnosticMessage协议和FixItMessage协议需求完成的特点,用于编译器提示过错。

探索Swift Macros

当宏作用在非函数类型时,宏expansion函数中无法转化成FunctionDeclSyntax类型,这儿咱们直接抛出过错,也不需求任何fix计划。

探索Swift Macros

探索Swift Macros

当宏作用的函数短少dynamic润饰时,能够从语法树中检测到modifiers中没有包括dynamic。

探索Swift Macros

接下来咱们需求供给过错信息,以及fix计划,计划很简略,往本来函数的语法树modifiers特点增加dynamic就好了。而FixIt中的change是替换的是整个函数,是由于考虑到modifiers可选特点可能为nil则无法替换(nil的时候找不到要被替换的地方,也可能有方法做到但暂时没深化看)。

探索Swift Macros

函数短少dynamic润饰时,编译器就会提示过错信息,以及供给增加dynamic的fix计划(这儿看其实是整个函数都被改了,由于咱们上面替换的是函数node),点击fix按钮就会主动增加上dynamic润饰。

探索Swift Macros


总结

Swift宏是一个很不错的元编程东西,iOS开发者能够经过熟悉的Swift语法完成各种东西和模板代码,不需求额外学习和运用其他元编程东西,究竟对已有项目存量代码做批改用其他东西本钱会比较大,一起咱们在完成宏的进程中能够了解到Swift语法树的一些内容,可是需求留心宏是在编译时打开,留心宏带来编译耗时以及包体积的负向影响。


资料

  • Expand on Swift macros

  • Write Swift macros

  • attached-macros

  • freestanding-declaration-macros

  • GitHub swift-macro-examples