如何写一个KCP? - 1

888 阅读5分钟

本文已参与「新人创作礼」活动,一起开启掘金创作之路。

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)
  1. 所有已存在的KCP都是一方的(位于github.com/JetBrains/kotlin工程中).
  2. 目录plugins/{name}/... 存放了插件的真实业务逻辑
  3. 目录libraries/tools/kotlin-{name}/... 存放Gradle wrapper相关
  4. 目录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: Booleantrue
    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)中读取任意变量

字节码看起来是什么样子?

这部分内容有点多, 我会在下一篇文章里面专门讲述.