07 December 2023
The Kotlin Symbol Processing (KSP) tool provides a high-level API for doing meta-programming in Kotlin. Many tools have been built on KSP, enabling Kotlin code to be generated at compile time. For example, Jetpack Room uses KSP to generate code for accessing the database, based on an interface provided by the developer, like:
@Dao interface UserDao { @Query("SELECT * FROM user") fun getAll(): List<User> }
KSP provides the API to the Kotlin code so that Room in this case can generate the actual implementation of that interface. While KSP has become a core foundation for meta-programing in Kotlin, its current implementation has some gaps which we are aiming to resolve with a new KSP2 architecture. This blog details those architectural changes and the impact for plugins built on KSP.
In addition, KSP2 has preview support for:
After getting feedback on the new architecture and continuing to address gaps we will work towards releasing KSP 2.0 where these changes will be the default.
The new preview changes can be enabled in KSP 1.0.14 or newer using a flag in gradle.properties:
ksp.useKSP2=true
Note: You might need to enlarge the heap size of the Gradle daemon now that KSP and processors run in the Gradle daemon instead of the Kotlin compiler’s daemon (which has larger default heap size), e.g. org.gradle.jvmargs=-Xmx4096M -XX:MaxMetaspaceSize=1024m
Internally KSP2 uses the Beta Kotlin K2 compiler (which will be the default compiler in Kotlin 2.0). You can use KSP2 before switching your Kotlin compiler to K2 (via the languageVersion setting) but if you want to use K2 for compiling your code, check out: Try the K2 compiler in your Android projects.
KSP1 is implemented as a Kotlin 1.x compiler plugin. Running KSP requires running the compiler and specifying KSP and its plugin options. In Gradle, KSP’s tasks are customized compilation tasks, which dispatch real work to KotlinCompileDaemon by default. This makes debugging and testing somewhat difficult, because KotlinCompileDaemon runs in its own process, outside of Gradle.
In KSP2, the implementation can be thought of as a library with a main entry point. Build systems and tools can call KSP with this entry point, without setting up the compiler. This makes it very easy to call KSP programmatically and is very useful especially for debugging and testing. With KSP2 you can set breakpoints in KSP processors without having to perform any other / irregular setup tasks to enable debugging.
Everything becomes much easier because KSP2 now controls its lifecycle and can be called as a standalone program or programmatically, like:
val kspConfig = KSPJvmConfig.Builder().apply { // All configurations happen here. }.build() val exitCode = KotlinSymbolProcessing(kspConfig, listOfProcessors, kspLoggerImpl).execute()
With the new implementation, it is also a great opportunity to introduce some refinements in the API behavior so that developers building on KSP will be more productive, have better debuggability and error recovery. For example, when resolving Map<String, NonExistentType>, KSP1 simply returns an error type. In KSP2, Map<String, ErrorType> will be returned instead. Here is a list of the current API behavior changes we plan on making in KSP2:
interface GrandBaseInterface1 { fun foo(): Unit } interface GrandBaseInterface2 { fun foo(): Unit } interface BaseInterface1 : GrandBaseInterface1 { } interface BaseInterface2 : GrandBaseInterface2 { } class OverrideOrder1 : BaseInterface1, GrandBaseInterface2 { override fun foo() = TODO() } class OverrideOrder2 : BaseInterface2, GrandBaseInterface1 { override fun foo() = TODO() }
When it comes to the processing scheme, i.e. what sources are processed when, the principle of KSP is to be consistent with the build's existing compilation scheme. In other words, what the compiler sees is what processors see, plus the source code that is generated by processors.
What processors see | Kotlin compiler see |
---|---|
ClassA.kt, UtilB.kt, InterfaceC.kt ... | ClassA.kt, UtilB.kt, InterfaceC.kt ... + GeneratedFromA.kt, ... |
In KSP1's current compilation scheme, common / shared source sets are processed and compiled multiple times, with each target. For example, commonMain is processed and compiled 3 times in the following project layout. Being able to process all the sources from dependencies is convenient with one exception: Processors don’t see the sources generated from commonMain when processing jvmMain and jsMain. Everything must be re-processed and that can be inefficient.
tasks |
inputs |
outputs |
kspKotlinCommonMainMetadata |
commonMain |
generatedCommon |
kspKotlinJvm |
commonMain, jvmMain |
generatedCommonJvm |
kspKotlinJs |
commonMain, jsMain |
generatedCommonJs |
compileKotlinCommonMainMetadata |
commonaMain, generatedCommon |
common.klib |
compileKotlinJvm |
commonMain, jvmMain, generatedCommonJvm |
app.jar |
compileKotlinJs |
commonMain, jsMain, generatedCommonJs |
main.js |
In KSP2, we plan to add an experimental mode that tries to align to how source sets are compiled in K2 better. All sources can be processed only once with the available new processing scheme:
tasks |
inputs |
outputs |
Resolvable but not available in getAllFiles / getSymbolsWithAnnotation |
kspKotlinCommonMainMetadata |
commonMain |
generatedCommon |
|
kspKotlinJvm |
jvmMain |
generatedJvm |
commonMain, generatedCommon |
kspKotlinJs |
jsMain |
generatedJs |
commonaMain, generatedCommon |
compileKotlinCommonMainMetadata |
commonaMain, generatedCommon |
common.klib |
|
compileKotlinJvm |
commonMain, jvmMain, generatedCommon, generatedJvm |
app.jar |
|
compileKotlinJs |
commonMain, jsMain, generatedCommon, generatedJs |
main.js |
Please note that Kotlin 2.0 is still in beta and the compilation model is subject to change. Please let us know how this works for you and give us feedback.
KSP2 is in preview but there is still more work to be done before a stable release. We hope these new features will ultimately help you be more productive when using KSP! Please provide us with your feedback so we can make these improvements awesome as they progress towards being stable.