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,还有 irAs、irReturn 等,满足在方法体内改代码的需求。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、挂上 compilerPluginRegistrars 和 commandLineProcessors、传 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 可能报错,需要自己反查。