目录

Kotlin Compiler Plugin(KCP)开发小结

最近在搞 KMP(Kotlin Multiplatform),框架思路不错,一码多端,但路漫漫:体验有点像用 Kotlin 写 C,提效工具和文档都偏少,得自己啃(国内大厂:研究 + 魔改 = 自研 = 世界领先)。

一、为什么需要 Kotlin Compiler Plugin

Kotlin 早已不只有 JVM backend,还有 kotlin/js、kotlin/native 等。在 Android 里可以用 AGP 的 transform 做插桩或改代码,但若要把 Kotlin 编译成 native binary,中间会经过 Kotlin IR → LLVM IR,插桩就要在编译器里做,这就需要 Kotlin Compiler Plugin(KCP)


二、开发一个 KCP 模块

2.1 基础依赖

implementation(kotlin("gradle-plugin-api"))
implementation(kotlin("compiler-embeddable", "2.0.21-4"))

// 可选:用 Auto Service + KSP 生成 META-INF
ksp("dev.zacsweers.autoservice:auto-service-ksp:1.2.0")
implementation("com.google.devtools.ksp:symbol-processing-api:2.0.21-1.0.25")
compileOnly("com.google.auto.service:auto-service-annotations:1.1.1")

2.2 META-INF 配置

可以手写,也可以用 Gradle 的 gradlePlugin 自动生成:

gradlePlugin {
    plugins {
        create("test") {
            id = rootProject.group.toString()
            implementationClass = "com.example.test.TestPlugin"
        }
    }
}

2.3 Compiler Plugin 注册

通过实现 KotlinCompilerPluginSupportPlugin 把 KCP 挂到编译流程里:

class TestPlugin : KotlinCompilerPluginSupportPlugin {

    override fun apply(target: Project) {
        super.apply(target)
    }

    override fun applyToCompilation(
        kotlinCompilation: KotlinCompilation<*>
    ): Provider<List<SubpluginOption>> {
        val project = kotlinCompilation.target.project
        return project.provider {
            mutableListOf(
                SubpluginOption(KEY_TARGET, kotlinCompilation.target.targetName)
            )
        }
    }

    override fun getCompilerPluginId(): String = BuildConfig.KOTLIN_PLUGIN_ID
    override fun getPluginArtifact(): SubpluginArtifact = SubpluginArtifact(
        groupId = BuildConfig.KOTLIN_PLUGIN_GROUP,
        artifactId = BuildConfig.KOTLIN_PLUGIN_NAME,
        version = BuildConfig.KOTLIN_PLUGIN_VERSION,
    )

    override fun isApplicable(kotlinCompilation: KotlinCompilation<*>): Boolean = true
}

2.4 Extension 能力注册

实现 CompilerPluginRegistrar,在 registerExtensions 里注册 FIR / IR 等能力。FIR 是源码到 IR 的中间状态(K2 用 FIR 解析),IR 再转成多端产物;所以 FIR extension 先执行,IR extension 后执行

@OptIn(ExperimentalCompilerApi::class)
@AutoService(CompilerPluginRegistrar::class)
class TestPluginRegistrar : CompilerPluginRegistrar() {
    override val supportsK2: Boolean get() = true

    override fun ExtensionStorage.registerExtensions(
        configuration: CompilerConfiguration
    ) {
        FirExtensionRegistrarAdapter.registerExtension(FirAllPublicExtensionRegistrar())
        IrGenerationExtension.registerExtension(HotfixIrGenerationExtension())
    }
}

2.5 IR Transform 示例

在 IR 里主要是注册各种 IrElementVisitorVoid 做 transform:

class TestIrGenerationExtension(private val config: HotfixConfig) : IrGenerationExtension {
    override fun generate(moduleFragment: IrModuleFragment, pluginContext: IrPluginContext) {
        val transformers = listOf(TestTransformer(pluginContext))
        transformers.forEach { moduleFragment.acceptChildrenVoid(it) }
    }
}

class TestTransformer(val context: IrPluginContext) : IrElementVisitorVoid {
    override fun visitElement(element: IrElement) {
        super.visitElement(element)
    }
    override fun visitClass(declaration: IrClass) {
        super.visitClass(declaration)
    }
    override fun visitSimpleFunction(declaration: IrSimpleFunction) {
        super.visitSimpleFunction(declaration)
    }
}

IrBuilder 可以构造 IrCall 等,context.reference 拿已有 class/function,还有 irAsirReturn 等,满足在方法体内改代码的需求。IR 语法较多,这里只点到为止。

2.6 Options 与 CommandLineProcessor

Gradle 传下来的参数通过 CommandLineProcessor 解析进 CompilerConfiguration,在 CompilerPluginRegistrar 里能拿到,用来做开关或定制逻辑。示例略(定义 CliOption、在 processOption 里塞到 configuration,再在 Registrar 里 configuration.get(YourKey) 即可)。


三、给 KCP 写单元测试

直接跑 Gradle 发布再引用、再 debug,链路太长。可以用 kotlin-compile-testing 在单测里编译代码并跑 KCP,方便很多。

依赖示例:

testImplementation(project(":test-runtime"))
testImplementation("org.junit.jupiter:junit-jupiter:5.8.2")
testImplementation("junit:junit:4.13.2")
testImplementation("dev.zacsweers.kctfork:core:0.6.0")

测试里读 kt 源文件、构造 KotlinCompilation、挂上 compilerPluginRegistrarscommandLineProcessors、传 pluginOptions,然后 compile() 看结果即可。详见 kotlin-compile-testing


四、FAQ

4.1 日志打不出来

写 KCP 时 println 可能看不到。可以用 CompilerConfiguration 里的 MessageCollector

val messageCollector = configuration.get(
    CommonConfigurationKeys.MESSAGE_COLLECTOR_KEY,
    MessageCollector.NONE
)
// 封装成 log 方法,用 messageCollector.report(CompilerMessageSeverity.INFO, msg)

4.2 FIR 与 IR 顺序

若同时注册了 FIR 和 IR extension,执行顺序是:.kt → FIR → IR → backend。若在 FIR 里强行改东西,后面 IR / backend 可能报错,需要自己反查。