Kotlin Compiler Plugin
Kotlin提供了编译器插件Compiler Plugin,可以在编译期通过分析和修改AST修改最终生成的字节码。相对于APT或者Transform等方式效率更高。Kotlin很多语法关键字以及注解都是基于KotlinCompilerPlugin实现的,例如data class
、@Parcelize
、
、kotlin-android-extension
等。
开发Compiler Plugin需要涉及一些编译器知识:例如需要了解编译器前、后端产物及相关API,有时需要还需要Gradle Plugin、IDEA Plugin等的配合,学习门槛较高,API也不够完善。普通开发者很难开发出自己的Compiler Plugin。
一个的Compiler Plugin的开发需要若干过程:
KSP API 的出现可以帮助开发者降低开发Compiler Plugin的门槛。
Kotlin Symbol Processing API
KSP API 是一套可用来开发轻量级Kotlin Compiler Plugin 的API, 他屏蔽了很多编译器细节,大大降低了使用门槛。KSP甚至不用JVM强相关,理论上开发出的编译器插件可以跨平台使用。
KSP API 可以基于Kotlin访问AST上的元素节点,例如classes、class members、functions、parameters等,如同访问 Kotlin reflection的KType一样,基于这些元素节点生成自己的代码,然后一同参与编译。
SymbolProcessor
一般需要继承SymbolProcessor
来创建自己的KSP
interface SymbolProcessor {
fun init(options: Map<String, String>,
kotlinVersion: KotlinVersion,
codeGenerator: CodeGenerator,
logger: KSPLogger)
fun process(resolver: Resolver) // Let's focus on this
fun finish()
}
然后通过访问者模式,处理AST:
class HelloFunctionFinderProcessor : SymbolProcessor() {
...
val functions = mutableListOf<String>()
val visitor = FindFunctionsVisitor()
override fun process(resolver: Resolver) {
resolver.getAllFiles().map { it.accept(visitor, Unit) }
}
inner class FindFunctionsVisitor : KSVisitorVoid() {
override fun visitClassDeclaration(classDeclaration: KSClassDeclaration, data: Unit) {
classDeclaration.getDeclaredFunctions().map { it.accept(this, Unit) }
}
override fun visitFunctionDeclaration(function: KSFunctionDeclaration, data: Unit) {
functions.add(function)
}
override fun visitFile(file: KSFile, data: Unit) {
file.declarations.map { it.accept(this, Unit) }
}
}
...
}
Examples
举几个使用KSP API访问AST的例子
- 访问类中的所有成员方法
fun KSClassDeclaration.getDeclaredFunctions(): List<KSFunctionDeclaration> {
return this.declarations.filterIsInstance<KSFunctionDeclaration>()
}
- 判断一个类或者方法是否是局部类或局部方法
fun KSDeclaration.isLocal(): Boolean {
return this.parentDeclaration != null && this.parentDeclaration !is KSClassDeclaration
}
- 判断一个类成员是否对其他Declaration可见
fun KSDeclaration.isVisibleFrom(other: KSDeclaration): Boolean {
return when {
// locals are limited to lexical scope
this.isLocal() -> this.parentDeclaration == other
// file visibility or member
this.isPrivate() -> {
this.parentDeclaration == other.parentDeclaration
|| this.parentDeclaration == other
|| (
this.parentDeclaration == null
&& other.parentDeclaration == null
&& this.containingFile == other.containingFile
)
}
this.isPublic() -> true
this.isInternal() && other.containingFile != null && this.containingFile != null -> true
else -> false
}
}
- 注解处理
// Find out suppressed names in a file annotation:
// @file:kotlin.Suppress("Example1", "Example2")
fun KSFile.suppressedNames(): List<String> {
val ignoredNames = mutableListOf<String>()
annotations.forEach {
if (it.shortName.asString() == "Suppress" && it.annotationType.resolve()?.declaration?.qualifiedName?.asString() == "kotlin.Suppress") {
it.arguments.forEach {
(it.value as List<String>).forEach { ignoredNames.add(it) }
}
}
}
return ignoredNames
}
Compare To KAPT
KSP API相对于KAPT的主要优势有两个:
- KSP实在kotlinc的同时解析APT,而KAPT需要增加额外处理,所以前者的性能更高
- KAPT让然依托Java的AST,而KSP更加聚焦Kotlin的AST,对Kotlin更友好
为什么KAPT需要额外处理?KAPT发生在javac之前,所以需要基于源码而非字节码进行AST分析,重新写一套AST分析器显然工作量巨大。KAPT的做法是尽量复用APT的注解处理逻辑,APT只接受Java,这就需要增加将Kotlin预编成Java的处理,这个处理将会增加1/3的额外时间开销。
但是KAPT不依托Java的AST,所以不需要上述开销。性能方面的提升将会是KSP相对于KAPT的最大优势。