本文已参与「新人创作礼」活动,一起开启掘金创作之路。
KCP, 是Kotlin Compiler Plugin的缩写, 表示Kotlin编译器插件.
简介
KCP基本上是注解处理器吗?
| 标题 | 注解处理器 | 编译器插件 |
|---|---|---|
| 运行时机 | 编译期 | 编译期 |
| API可达性 | 公开且有文档 | 私有且无文档 |
| 生成代码 | Java源码 | Java字节码或者LLVM IR |
| 工作源码 | Kotlin/Java源码 | Kotlin源码 |
| 跨平台 | 不支持 | 支持 |
为何要写KCP?
- KCP API异常强大;能够修改函数或者类的内部;
- 能够解决新类型的元编译问题;
- 注解处理器只能用于JVM, 但KCP不止于此;
为何不要写KCP?
- 注解处理器更容易写(如果只关注JVM的话);
- KCP工作量很大. 需要写:
-
- IntelliJ插件(如果要创建合成成员的话)
-
- Gradle(Maven或者其它构建工具)插件
-
- 对于JVM, JS或者Native目标扩展稍微不同
KCP例子
- allopen: 修改所有添加过注解的类为open
- noarg: 修改全部添加过注解的类拥有无参构造器
- android-extensions: findViewById(R.id.foo)别名, 通过@Parcelize注解自动生成Parcelable实现
- kotlin-serialization: 自动生成Serializable实现
-
- 首个跨平台就绪的插件(也给Native生成LLVM)
- 所有已存在的KCP都是一方的(位于github.com/JetBrains/kotlin工程中).
- 目录plugins/{name}/... 存放了插件的真实业务逻辑
- 目录libraries/tools/kotlin-{name}/... 存放Gradle wrapper相关
- 目录libraries/tools/kotlin-maven-{name}/... 存放Maven wrapper相关
插件架构
graph TD
Plugin --> Subplugin
Subplugin --> CommandLineProcessor
CommandLineProcessor --> ComponentRegistrar
ComponentRegistrar --> Extension1
ComponentRegistrar --> Extension2
其中, Gradle API包含2部分:
erDiagram
Gradle ||--o{ Plugin : contains
Gradle ||--o{ Subplugin : contains
Kotlin API包含3部分:
erDiagram
Kotlin ||--o{ CommandLineProcessor : contains
Kotlin ||--o{ ComponentRegistrar : contains
Kotlin ||--o{ Extension : contains
Plugin
- 是Gradle API, 与Kotlin完全无关.
- 从build.gradle脚本中提供了入口点.
- 允许通过Gradle扩展提供配置
Subplugin
- 是Gradle API和Kotlin API之间的接口
- 能够读取Gradle扩展选项
- 写出 Kotlin SubpluginOptions
- 定义了KCP的id, 是内部唯一ID
- 定义了KCP的Maven坐标, 由此编译器可以进行下载KCP.
CommandLineProcessor
- 读取kotlinc -Xplugin的参数
- Subplugin选项实际上是通过该管道线传递
- 写入CompilerConfigurationKeys
ComponentRegistrar
- 读取 CompilerConfigurationKeys
- 注册Extension扩展
Extension
- 用于生成代码
- 扩展有多种类型:
-
- ExpressionCodegenExtension
-
- ClassBuilderInterceptorExtension
-
- StorageComponentContainerContributor
-
- IrGenerationExtension (!!)
- 写入字节码(或者LLVM IR)
原理是讲清楚了, 下面我们来写自己的KCP.
自定义KCP
我们要做的事情:
- 写一个KCP用来追踪函数的调用.
- 添加了@DebugLog注解的函数将修改自己的方法体以导入日志功能
- 不能是注解处理器; 且能够修改函数体.
- 现有技术:
-
- 使用了AspectJ 字节码织入技术和Android Gradle Transform API
目标:
fun prime(n: Int): Long {
println("⇢ prime(n=$n)")
val startTime = System.currentTimeMillis()
val result = primeNumberSequence.take(n).last()
val timeToRun = System.currentTimeMillis() - startTime
println("⇠ prime [ran in $timeToRun ms]")
return result
}
其中: 只有1行是原先的函数体内容.
fun prime(n: Int): Long {
println("⇢ prime(n=$n)")
val startTime = System.currentTimeMillis()
**val result = primeNumberSequence.take(n).last()**
val timeToRun = System.currentTimeMillis() - startTime
println("⇠ prime [ran in $timeToRun ms]")
return result
}
我们要做的是将上述代码转化为如下所示:
@DebugLog fun prime(n: Int): Long = primeNumberSequence.take(n).last()
既然已经知道要做什么了, 那就让我们开始吧!
Plugin
在项目的构建文件中, 比如gradle-plugin/build.gradle, 添加如下内容:
// what plugins we use to implement our own KCP
apply plugin: "java-gradle-plugin" // provide Java language for Gradle
apply plugin: "org.jetbrains.kotlin.jvm" // provide Kotlin API for JVM
apply plugin: "kotlin-kapt" // Kotlin APT
gradlePlugin {
plugins {
simplePlugin { // configure for our own plugin, like unique plugin id, implementation class of our own Gradle Plugin
id = "debuglog.plugin" // `apply plugin: "debuglog.plugin"`
implementationClass = "debuglog.DebugLogGradlePlugin" // entry-point class
}
}
}
dependencies {
// Kotlin API
implementation "org.jetbrains.kotlin:kotlin-stdlib:$ktVersion"
// Kotlin API for Gradle Plugin
implementation "org.jetbrains.kotlin:kotlin-gradle-plugin-api:$ktVersion"
// help us generate relavant code to configure annotation processor
compileOnly 'com.google.auto.service:auto-service:1.0-rc4'
kapt 'com.google.auto.service:auto-service:1.0-rc4'
}
Kotlin版本的Gradle插件的实现:
class DebugLogGradlePlugin : org.gradle.api.Plugin<Project> {// 自定义插件及其插件的实现
override fun apply(project: Project) {
project.extensions.create("debugLog",DebugLogGradleExtension::class.java)
}
}
open class DebugLogGradleExtension { // 插件配套使用的扩展
var enabled: Boolean = true
var annotations: List<String> = emptyList()
}
Subplugin
Subplugin是处于Gradle API和Kotlin API之间的接口.
自定义实现及其说明如下:
@AutoService(KotlinGradleSubplugin::class) // don't forget!
class DebugLogGradleSubplugin : KotlinGradleSubplugin<AbstractCompile> {
// 什么时候子插件可用
override fun isApplicable(project: Project, task: AbstractCompile): Boolean = project.plugins.hasPlugin(DebugLogGradlePlugin::class.java)
// 编译器插件的唯一Id
override fun getCompilerPluginId(): String = "debuglog"
// 产物
override fun getPluginArtifact(): SubpluginArtifact = SubpluginArtifact(groupId = "debuglog", artifactId = "kotlin-plugin", version = "0.0.1")
// 应用插件, 写入SubpluginOption
override fun apply(project: Project, /*...*/): List<SubpluginOption> {
// 获取自定义扩展
val extension = project.extensions.findByType<DebugLogGradleExtension>() ?: DebugLogGradleExtension()
// 是否有函数添加了注解
if (extension.enabled && extension.annotations.isEmpty())
error("DebugLog is enabled, but no annotations were set")
// 生成并返回 SubpluginOption
val annotationOptions = extension.annotations.map { SubpluginOption(key = "debugLogAnnotation", value = it) }
val enabledOption = SubpluginOption(key = "enabled", value = extension.enabled.toString())
return annotationOptions + enabledOption
}
}
CommandLineProcessor
KCP需要在不同于APT的工程中进行构建. 而且它使用了Kotlin API而不再是Gradle API
在KCP项目的构建文件中, 比如kotlin-plugin/build.gradle, 添加了如下内容:
apply plugin: "org.jetbrains.kotlin.jvm"
apply plugin: "kotlin-kapt"
dependencies {
// Kotlin SDK
implementation "org.jetbrains.kotlin:kotlin-stdlib:$ktVersion"
// Kotlin Compiler Embeddable, insert KCP into Kotlin compiler
compileOnly "org.jetbrains.kotlin:kotlin-compiler-embeddable:$ktVersion"
// 使用Google的AutoService来自动生成对插件的配置信息
compileOnly "com.google.auto.service:auto-service:1.0-rc4"
kapt "com.google.auto.service:auto-service:1.0-rc4"
}
KCP的自定义实现及说明如下:
@AutoService(CommandLineProcessor::class)
class DebugLogCommandLineProcessor : CommandLineProcessor {
// 与上述的Subplugin具有相同的ID
override val pluginId: String = "debuglog" // same as ID from subplugin
// 提供Pluglin选项
override val pluginOptions: Collection<CliOption> = listOf(CliOption("enabled", "<true|false>", "whether plugin is enabled"), CliOption("debugLogAnnotation", "<fqname>", "debug-log annotation names",required = true, allowMultipleOccurrences = true))
// 对上述Plugin选项集合遍历处理
override fun processOption(option: CliOption, value: String, configuration: CompilerConfiguration) = when (option.name) { // 以选项的名字进行过滤, 匹配, 分支处理
"enabled" -> configuration.put(KEY_ENABLED, value.toBoolean())
"debugLogAnnotation" -> configuration.appendList(KEY_ANNOTATIONS, value)
else -> error("Unexpected config option ${option.name}")
}
}
ComponentRegistrar
自定义ComponentRegistrar实现及说明
@AutoService(ComponentRegistrar::class)
class DebugLogComponentRegistrar : ComponentRegistrar {
override fun registerProjectComponents(project: MockProject, configuration: CompilerConfiguration){
if (configuration[KEY_ENABLED] == false) {
return
}
ClassBuilderInterceptorExtension.registerExtension(project,
DebugLogClassGenerationInterceptor(debugLogAnnotations = configuration[KEY_ANNOTATIONS] ?: error("debuglog plugin requires at least one annotation class option passed to it")))
}
}
DebugLogClassGenerationInterceptor用于拦截注解类. 它有实现如下:
class DebugLogClassGenerationInterceptor(
val debugLogAnnotations: List<String>
) : ClassBuilderInterceptorExtension {
override fun interceptClassBuilderFactory(interceptedFactory: ClassBuilderFactory, bindingContext: BindingContext, diagnostics: DiagnosticSink): ClassBuilderFactory = object: ClassBuilderFactory by interceptedFactory {
override fun newClassBuilder(origin: JvmDeclarationOrigin) = DebugLogClassBuilder(
annotations = debugLogAnnotations,
delegateBuilder = interceptedFactory.newClassBuilder(origin))
}
}
而DebugLogClassBuilder的实现如下:
private class DebugLogClassBuilder(
val annotations: List<String>,
delegateBuilder: ClassBuilder
) : DelegatingClassBuilder(delegateBuilder) {
override fun newMethod(
origin: JvmDeclarationOrigin, access: Int,
name: String, desc: String,
signature: String?,
exceptions: Array<out String>?
): MethodVisitor {
val original = super.newMethod(origin, ...)
val function = origin.descriptor as? FunctionDescriptor ?: return original
if (annotations.none { descriptor.annotations.hasAnnotation(it) }) {
return original
}
return object : MethodVisitor(Opcodes.ASM5, original) {
override fun visitCode() {
super.visitCode()
InstructionAdapter(this).apply { TODO("on method entry") }
override fun visitInsn(opcode: Int) {
when (opcode) {
RETURN /* void */, ARETURN /* object */, IRETURN /* int */ -> {
InstructionAdapter(this).apply { TODO("on method exit") }
}
}
super.visitInsn(opcode)
}
}
}
}
在类DebugLogClassBuilder#newMethod当中, 我们看到了MethodVisitor, 是的, 通过自定义MethodVisitor实现了对添加了注解函数的字节码插桩.
接下来怎么做?
到了MethodVisitor, 就是在这里实现对函数的插入字节码.
- 写入字节码
- 使用ObjectWeb ASM API
-
- 它既跟ASM(汇编)无关, 也跟Web ASM(wasm)无关
-
- 它是用于修改JVM字节码的API
- JVM是基于栈的机器
-
- 方法运行于栈上
-
- 可以从本地变量数组(Local Variable Array)中读取任意变量
字节码看起来是什么样子?
这部分内容有点多, 我会在下一篇文章里面专门讲述.