KCP 开发

最近,一直在搞 kmp(Kotlin Multiplatform),这个框架出发点倒还可以,可以实现一码多端
但是,路漫漫其修远兮,感觉就是在用 kotlin 写 c
各种提效工具比较落后,文档比较稀少,需要自己去研究(国内大厂:研究 + 魔改 = 自研 = 世界领先)
Kotlin Compiler Plugin
kotlin 如今已经不是简单的将 kotlin 代码编译成 java bytecode 了,kotlin/jvm 只是其中之一的 backend
还有诸如 kotlin/js kotlin/native 等等的 backend
在 android 开发中,我们可以使用 agp 提供的 transform 能力来实现代码的插桩,或者任意的修改
但是,如今,我们的 kotlin 代码还需要编译成 native binary
这其中,kotlin ir 会被转换为 llvm ir,那我们如何插桩呢?
这就需要 kotlin compiler plugin 了
开发一个 KCP 模块
基础依赖
implementation(kotlin("gradle-plugin-api"))
implementation(kotlin("compiler-embeddable", "2.0.21-4"))
// 如果需要 auto service 帮助 kcp services 生成 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")
META-INF 配置
可以选择手写,也可以选择如下方式
这样会自动生成 meta-inf 下的 plugin 配置
gradlePlugin {
plugins {
create("test") {
id = rootProject.group.toString()
implementationClass = "com.example.test.TestPlugin"
}
}
}
Compiler Plugin 注册方式
我们可以通过注册常规的 plugin 来加载 kotlin compiler plugin
class TestPlugin : KotlinCompilerPluginSupportPlugin {
// 和普通的 gradle plugin 一样
// 可以获取 extension 等等
override fun apply(target: Project) {
super.apply(target)
}
// options 用于传递参数
override fun applyToCompilation(
kotlinCompilation: KotlinCompilation<*>
): Provider<List<SubpluginOption>> {
val project = kotlinCompilation.target.project
return project.provider {
val options = mutableListOf<SubpluginOption>()
options.add(SubpluginOption(KEY_TARGET, kotlinCompilation.target.targetName))
options
}
}
// kcp plugin id
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,
)
// 返回 true 才会走到 applyToCompilation
// 可以添加一些特殊逻辑
override fun isApplicable(kotlinCompilation: KotlinCompilation<*>): Boolean {
return true
}
}
Extension 能力注册
需要实现 CompilerPluginRegistrar
在 registerExtensions 中注册自己需要的 extension 能力
常见的,可以注册 fir 和 ir 能力
fir 是 kotlin 源码 和 ir 之间的一种状态, 新版 kotlin compiler 使用 k2,而 k2 就是使用 fir 来解析 kotlin 源码
ir 是 kotlin 转为多端产物的标准的中间表达式,它会通过 fir2ir 将 fir 转为 ir
所以,我们注册的 fir extension 会先执行,ir extension 会后执行
// 这里使用 auto service 就不用手写 meta-inf/services 了
@OptIn(ExperimentalCompilerApi::class)
@AutoService(CompilerPluginRegistrar::class)
class TestPluginRegistrar : CompilerPluginRegistrar() {
override val supportsK2: Boolean
get() = true
override fun ExtensionStorage.registerExtensions(
configuration: CompilerConfiguration
) {
// 注册 fir
FirExtensionRegistrarAdapter.registerExtension(
FirAllPublicExtensionRegistrar()
)
// 注册 ir
IrGenerationExtension.registerExtension(
HotfixIrGenerationExtension()
)
}
}
处理 IR Extention Transform 的例子
kotlin ir extension 的逻辑很简单,主要是注册 transform
class TestIrGenerationExtension(private val config: HotfixConfig) : IrGenerationExtension {
override fun generate(moduleFragment: IrModuleFragment, pluginContext: IrPluginContext) {
val transformers = mutableListOf<IrElementVisitorVoid>()
// transform 逻辑
transformers.add(TestTransformer(pluginContext))
for (transformer in transformers) {
moduleFragment.acceptChildrenVoid(transformer)
}
}
}
我们来看一下 transform 如何实现
class TestTransformer(val context: IrPluginContext) : IrElementVisitorVoid {
override fun visitElement(element: IrElement) {
// 这里选择自己感兴趣的 element
// 调用 element.acceptChildrenVoid(this)
// 后续就会开始遍历 class funtion 等等
super.visitElement(element)
}
// 遍历 class
override fun visitClass(declaration: IrClass) {
super.visitClass(declaration)
}
override fun visitSimpleFunction(declaration: IrSimpleFunction) {
super.visitSimpleFunction(declaration)
}
}
IR 语法较多,我这里了解也比较有限
在 IrBuilder 下,可以使用 IrCall 等等指令执行方法调用,context.reference 方法可以获取已经在编译环境中的 class/function 等信息
还有 irAs/irReturn 等众多方法,满足我们任何修方法体内代码的需求
处理 Options 和 CommanLineProcessor
kotlin gradle plugin 的参数可以通过 commandline processor 解析,传递给 configuration
在 compilerPluginRegistrar 调用 registerExtension 时,会回传 configuration,进而可以被 kotlin compiler plugin 获取到对应参数,可以用做定制逻辑
example:
// commandline processor
TestCommandLineProcessor : CommandLineProcessor {
override val pluginId: String = BuildConfig.KOTLIN_PLUGIN_ID
override val pluginOptions: Collection<AbstractCliOption> =
// 处理哪些参数
listOf(
CliOption(KEY_TARGET, "target name", "platform target", required = false),
CliOption(KEY_DEBUG, "debug mode", "debug mode", required = false)
)
override fun processOption(option: AbstractCliOption, value: String, configuration: CompilerConfiguration) {
// 自己解析,塞到 configuration 中区
when (option.optionName) {
KEY_TARGET -> {
configuration.put(TestConfigurationKeys.TARGET, value)
}
KEY_DEBUG -> {
configuration.put(TestConfigurationKeys.DEBUG, value)
}
else -> {
super.processOption(option, value, configuration)
}
}
}
}
// key 需要是一个 CompilerConfigurationKey
object TestConfigurationKeys {
val TARGET = CompilerConfigurationKey<String>(KEY_TARGET)
val DEBUG = CompilerConfigurationKey<String>(KEY_DEBUG)
}
// CompilerPluginRegistrar 中获取参数
class TestPluginRegistrar : CompilerPluginRegistrar() {
override fun ExtensionStorage.registerExtensions(
configuration: CompilerConfiguration
) {
val target = configuration.get(TestConfigurationKeys.TARGET)
val isDebug = configuration.get(TestConfigurationKeys.DEBUG)
}
}
给 KCP 添加单元测试
开发完一个插件,debug 过程是痛苦的
如果,需要发布、引用插件、gradle debug 那整个路径也太长了
好在,我们可以通过写 kotlin test 的方式,直接像单元测试一样,跑测试
这里我使用的是:dev.zacsweers.kctfork:core:0.6.0 这个开源库
gradle 中添加我们的 test 依赖即可
// 如果需要引用到你其他模块的代码
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")
test 代码示例:
@OptIn(ExperimentalCompilerApi::class)
class CompilerPluginTest {
// 可以直接 run/debug
@Test
fun testExample() {
// 读取要测试的 kt 文件
val sourceFile = mutableListOf<SourceFile>()
listOf("TestEnum.kt").forEach { it->
sourceFile.add(SourceFile.kotlin(it, classLoader.getResourceAsStream(it)!!.bufferedReader().use {
it.readText()
}))
}
// 编译,详见下方 compile 方法
compile(sourceFile)
}
private fun compile(sourceFile: MutableList<SourceFile>) {
val result = KotlinCompilation().apply {
sources = sourceFile
supportsK2 = true
// 注册我们写好的 kcp
compilerPluginRegistrars = listOf(TestPluginRegistrar())
inheritClassPath = true
// 注册我们写好的 command line processor
commandLineProcessors += TestCommandLineProcessor()
// 添加我们的参数
pluginOptions += listOf(PluginOption("your_plugin_id", "target","jvm"), PluginOption("your_plugin_id", "debug","true"))
messageOutputStream = System.out
// 指定最终产物目录
workingDir = File("build", "kcp")
}.compile()
}
}
FAQ
log 无法打印的问题
在写 kcp 的时候,发现使用 println 打印不出日志,还不太清楚原因
总之,目前可以通过 MessageCollector 打印
// CompilerRegistrar 中,可以获取 messageCollector
val messageCollector = configuration.get(CommonConfigurationKeys.MESSAGE_COLLECTOR_KEY, MessageCollector.NONE)
MSG_COLLECTOR = messageCollector
// 随便封装一个 log 方法
const val TAG = "TestPlugin: "
fun logInfo(msg: String) {
MSG_COLLECTOR.report(CompilerMessageSeverity.INFO, TAG + msg)
}
fir 问题
如果,你的 CompilerRegistrar 中不仅注册了 irExtension 还注册了 firextension
那么 fir extension 肯定是优先执行的(因为 kotlin 文件编译流程为: .kt -> fir -> ir -> backend)
当你,强行修改一些 fir 的东西时,后续的 backend ir 阶段可能会报错,需要自己反查