Farewell to KAPT! Use KSP to speed up Kotlin compilation

Farewell to KAPT! Use KSP to speed up Kotlin compilation

This article has participated in the Haowen Convening Order activity, click to view: Back-end, big front-end dual-track submissions, 20,000 yuan prize pool waiting for you to challenge!

At the beginning of this year, Android released the first Alpha version of Kotlin Symbol Processing (KSP). A few months later, KSP has been updated to Beta3. The API is basically stable, and I believe it will not be far from the release of the stable version.

Why use KSP?

Many people complain about Kotlin's compilation speed, and KAPT is one of the culprits that slows down compilation.

Many libraries use annotations to simplify template code, such as Room, Dagger, Retrofit, etc. Kotlin code uses KAPT to process annotations. KAPT essentially works based on APT. APT can only process Java annotations, so APT parseable stubs (Java code) need to be generated first, which slows down the overall compilation speed of Kotlin.

KSP was born under this background. It is based on Kotlin Compiler Plugin (KCP) and does not need to generate additional stubs. The compilation speed is more than twice that of KAPT.

KSP and KCP

Kotlin Compiler Plugin provides hook timing during the kotlinc process, which can analyze AST again, modify bytecode products, etc., many of Kotlin's syntactic sugar are implemented by KCP, for example

data class
,
@Parcelize
,
kotlin-android-extension
Wait, now the popular Compose's compile-time work is also done with the help of KCP.

Theoretically, the ability of KCP is a superset of KAPT, which can replace KAPT to improve compilation speed. However, the development cost of KCP is too high, involving the use of Gradle Plugin, Kotlin Plugin, etc. API involves some understanding of compiler knowledge, which is difficult for general developers to master.

The development of a standard KCP involves many things as follows:

  • Plugin: Gradle plugin is used to read Gradle configuration and pass it to KCP (Kotlin Plugin)
  • Subplugin: Provide KCP with configuration information such as the address of the maven library of custom KP
  • CommandLineProcessor: Convert parameters to KP recognizable parameters
  • ComponentRegistrar: Register Extension to different processes of KCP
  • Extension: realize custom KP function

KSP simplifies the above process, developers do not need to understand the working principle of the compiler, and the cost of processing annotations is as low as KAPT.

KSP and KAPT

As the name implies, KSP processes Kotlin's AST at the Symbols level, accessing elements of types, class members, functions, and related parameters. It can be compared to Kotlin AST in PSI

The result of a Kotlin source file after KSP parsing is as follows:

KSFile packageName: KSName fileName: String annotations: List<KSAnnotation> (File annotations) declarations: List<KSDeclaration> KSClassDeclaration // class, interface, object simpleName: KSName qualifiedName: KSName containingFile: String typeParameters: KSTypeParameter parentDeclaration: KSDeclaration classKind: ClassKind primaryConstructor : KSFunctionDeclaration superTypes: List<KSTypeReference> // contains inner classes, member functions, properties, etc. declarations: List<KSDeclaration> KSFunctionDeclaration // top level function simpleName: KSName qualifiedName: KSName containingFile: String typeParameters: KSTypeParameter parentDeclaration: KSDeclaration functionKind: FunctionKind extensionReceiver: KSTypeReference? returnType: KSTypeReference parameters: List< KSVariableParameter> // contains local classes, local functions, local the Variables, etc. Declarations: same asList <KSDeclaration> KSPropertyDeclaration // , Ltd. Free Join variable SimpleName: KSName the qualifiedName: KSName containingFile: String typeParameters: KSTypeParameter parentDeclaration: KSDeclaration extensionReceiver: ? KSTypeReference of the type: KSTypeReference getter: KSPropertyGetter returnType: KSTypeReference setter: KSPropertySetter the Parameter: KSVariableParameter KSEnumEntryDeclaration // KSClassDeclaration copy the code

This is the Kotlin AST abstraction in KSP. Similarly, there is an AST abstraction to Java in APT/KAPT, in which some correspondences can be found, such as the use of Java

Element
Describe packages, classes, methods or variables, etc., used in KSP
Declaration

Java/APTKotlin/KSPDescription
PackageElementKSFileRepresents a package program element. Provide access to information about the package and its members
ExecuteableElementKSFunctionDeclarationRepresents a method, constructor, or initializer (static or instance) of a class or interface, including annotation type elements
TypeElementKSClassDeclarationRepresents a class or interface program element. Provides access to information about the type and its members. Note that the enumeration type is a class, and the annotation type is an interface
VariableElementKSVariableParameter/KSPropertyDeclarationRepresents a field, enum constant, method or constructor parameter, local variable or abnormal parameter

Declaration
There is also Type information below, such as function parameters, return value types, etc., which are used in APT
TypeMirror
Bearer type information, the detailed capabilities in KSP are determined by
KSType
achieve.

The development process of KSP is similar to KAPT:

  1. Parse the source code AST
  2. Generate code
  3. The generated code and source code participate in Kotlin compilation

It should be noted that KSP cannot be used to modify the original code, but can only be used to generate new code

KSP entrance: SymbolProcessorProvider

KSP passed

SymbolProcessor
To implement it in detail.
SymbolProcessor
Need to pass a
SymbolProcessorProvider
To create. therefore
SymbolProcessorProvider
Is the entrance to KSP execution

interface SymbolProcessorProvider { fun create (environment: SymbolProcessorEnvironment ) : SymbolProcessor } Copy code

SymbolProcessorEnvironment
Get some KSP runtime dependencies and inject them into the Processor

interface SymbolProcessor { fun process (resolver: Resolver ) : List<KSAnnotated>//Let's focus on this fun finish () {} fun onError () {} } Copy code

process()
Provide a
Resolver
, Analyze the symbols on the AST. Resolver uses the visitor pattern to traverse the AST.

As follows, Resolver uses

FindFunctionsVisitor
Find out the current
KSFile
The top-level functions and Class member methods:

class HelloFunctionFinderProcessor : SymbolProcessor() { ... val functions = mutableListOf<String>() val visitor = FindFunctionsVisitor() override fun process(resolver: Resolver) { // FindFunctionsVisitor AST resolver.getAllFiles().map { it.accept(visitor, Unit) } } inner class FindFunctionsVisitor : KSVisitorVoid() { override fun visitClassDeclaration(classDeclaration: KSClassDeclaration, data: Unit) { // Class classDeclaration.getDeclaredFunctions().map { it.accept(this, Unit) } } override fun visitFunctionDeclaration(function: KSFunctionDeclaration, data: Unit) { // function functions.add(function) } override fun visitFile (file: KSFile , data : Unit ) { //Access file file.declarations.map {it.accept( this , Unit )} } } ... } Copy code

KSP API example

Give a few examples to see how KSP's API works

Access all member methods in the class

fun KSClassDeclaration. getDeclaredFunctions () : List<KSFunctionDeclaration> { return this .declarations.filterIsInstance<KSFunctionDeclaration>() } Copy code

Determine whether a class or method is a local class or local method

fun KSDeclaration. isLocal () : Boolean { return this .parentDeclaration != null && this .parentDeclaration! is KSClassDeclaration } Copy code

Determine whether a class member is visible to other Declarations

fun KSDeclaration. isVisibleFrom (other: KSDeclaration ) : Boolean { return when { //locals are limited to lexical scope this .isLocal() -> this .parentDeclaration == other //file visibility or member this .isPrivate() -> { this .parentDeclaration == other.parentDeclaration || this .parentDeclaration == other || ( this .parentDeclaration == null && other.parentDeclaration == null && this .containingFile == other.containingFile ) } this .isPublic() -> true this .isInternal() && other.containingFile != null && this .containingFile != null -> true else -> false } } Copy code

Get annotation information

//Find out suppressed names in a file annotation: //@file:kotlin.Suppress("Example1", "Example2") fun KSFile. suppressedNames () : List<String> { val ignoredNames = mutableListOf<String>() annotations.forEach { if (it.shortName.asString() == "Suppress" && it.annotationType.resolve()?.declaration?.qualifiedName?.asString() == "kotlin.Suppress" ) { it.arguments.forEach { (it.value as List<String>).forEach {ignoredNames.add(it)} } } } return ignoredNames } Copy code

Example of code generation

Finally, look at a relatively complete example to replace APT code generation

@IntSummable data class Foo ( val bar: Int = 234 , val baz: Int = 123 ) Copy code

We hope to process through KSP

@IntSummable
To generate the following code

public fun Foo. sumInts () : Int { val sum = bar + baz return sum } Copy code

Dependencies

To develop KSP, you need to add dependencies:

plugins { kotlin( "jvm" ) version "1.4.32" } repositories { mavenCentral() google() } dependencies { implementation(kotlin( "stdlib" )) implementation( "com.google.devtools.ksp:symbol-processing-api:1.5.10-1.0.0-beta01" ) } Copy code

IntSummableProcessorProvider

We need an entrance

Provider
To build
Processor

import com.google.devtools.ksp.symbol.* class IntSummableProcessorProvider : SymbolProcessorProvider { override fun create (environment: SymbolProcessorEnvironment ) : SymbolProcessor { return IntSummableProcessor( options = environment.options, codeGenerator = environment.codeGenerator, logger = environment.logger ) } } Copy code

by

SymbolProcessorEnvironment
Can be injected into the Processor
options
,
CodeGenerator
,
logger
Wait for the required dependencies

IntSummableProcessor

class IntSummableProcessor (): SymbolProcessor { private lateinit var intType: KSType override fun process (resolver: Resolver ) : List<KSAnnotated> { intType = resolver.builtIns.intType val symbols = resolver.getSymbolsWithAnnotation(IntSummable:: class .qualifiedName!!).filterNot{ it.validate()} symbols.filter {it is KSClassDeclaration && it.validate()} .forEach {it.accept(IntSummableVisitor(), Unit )} return symbols.toList() } } Copy code
  • builtIns.intType
    Get
    kotlin.Int
    of
    KSType
    , Need to be used later.
  • getSymbolsWithAnnotation
    Get annotations as
    IntSummable
    List of symbols
  • When the symbol is Class, use Visitor to process it

IntSummableVisitor

The interface of Visitor is generally as follows,

D
with
R
Represents the input and output of the Visitor,

interface KSVisitor < D, R > { fun visitNode (node: KSNode , data : D ) : R fun visitAnnotated (annotated: KSAnnotated , data : D ) : R //etc. } Copy code

Our demand has no input and output, so it is realized

KSVisitorVoid
That is, it is essentially a
KSVisitor<Unit, Unit>
:

inner class Visitor : KSVisitorVoid () { override fun visitClassDeclaration (classDeclaration: KSClassDeclaration , data : Unit ) { val qualifiedName = classDeclaration.qualifiedName?.asString() //1. Validity check if (!classDeclaration.isDataClass()) { logger.error( "@IntSummable cannot target non-data class $qualifiedName " , classDeclaration ) return } if (qualifiedName == null ) { logger.error( "@IntSummable must target classes with qualified names" , classDeclaration ) return } //2. Analyze the Class information //... //3. Code generation //... } private fun KSClassDeclaration. isDataClass () = modifiers.contains(Modifier.DATA) } Copy code

As above, we judge whether this Class is

data class
, Whether its class name is legal

Parse Class information

Next, we need to obtain the relevant information in the Class for our code generation:

inner class IntSummableVisitor : KSVisitorVoid () { private lateinit var className: String private lateinit var packageName: String private val summables: MutableList<String> = mutableListOf() override fun visitClassDeclaration (classDeclaration: KSClassDeclaration , data : Unit ) { //1. Validity check //... //2. Parse Class information val qualifiedName = classDeclaration.qualifiedName?.asString() className = qualifiedName packageName = classDeclaration.packageName.asString() classDeclaration.getAllProperties() .forEach { it.accept( this , Unit ) } if (summables.isEmpty()) { return } //3. Code generation //... } override fun visitPropertyDeclaration (property: KSPropertyDeclaration , data : Unit ) { if (property.type.resolve().isAssignableFrom(intType)) { val name = property.simpleName.asString() summables.add(name) } } } Copy code
  • by
    KSClassDeclaration
    Got
    className
    ,
    packageName
    ,as well as
    Properties
    And deposit it in
    summables
  • visitPropertyDeclaration
    Ensure that the Property must be of type Int, where the previously mentioned
    intType

Code generation

After collecting the Class information, proceed to code generation. We introduce

KotlinPoet
Help us generate Kotlin code

dependencies { implementation( "com.squareup:kotlinpoet:1.8.0" ) } Copy code
override fun visitClassDeclaration (classDeclaration: KSClassDeclaration , data : Unit ) { //1. Validity check //... //2. Analyze the Class information //... //3. Code generation if (summables.isEmpty()) { return } val fileSpec = FileSpec.builder( packageName = packageName, fileName = classDeclaration.simpleName.asString() ).apply { addFunction( FunSpec.builder( "sumInts" ) .receiver(ClassName.bestGuess(className)) .returns( Int :: class ) .addStatement( "val sum = ${summables.joinToString( "+" )} " ) .addStatement( "return sum" ) .build() ) }.build() codeGenerator.createNewFile( dependencies = Dependencies(aggregating = false ), packageName = packageName, fileName = classDeclaration.simpleName.asString() ).use {outputStream -> outputStream.writer() .use { fileSpec.writeTo(it) } } } Copy code
  • Use KotlinPoet's
    FunSpec
    Generate function code
  • The previous SymbolProcessorEnvironment provided
    CodeGenerator
    Used to create a file and write the generated
    FileSpec
    Code

summary

by

IntSummable
In the example, you can see that KSP can completely replace APT/KAPT for annotation processing, and the performance is better.

At present, many tripartite libraries using APT have added support for KSP

LibraryStatusTracking issue for KSP
RoomExperimentally supported
MoshiExperimentally supported
KotshiExperimentally supported
LyricistExperimentally supported
Auto FactoryNot yet supportedLink
DaggerNot yet supportedLink
HiltNot yet supportedLink
GlideNot yet supportedLink
DeeplinkDispatchNot yet supportedLink

It is also very simple to replace KAPT with KSP, take Moshi as an example

Of course, you can also use KAPT and KSP at the same time in the project, they do not affect each other. The trend of KSP replacing KAPT is becoming more and more obvious. If your project also deals with the needs of annotations, why not try KSP?

github.com/google/ksp