This article has participated in the call for good writing activities, click to view: back end, big front end double track submission, 20,000 yuan prize pool waiting for you to challenge!
Earlier this year, Android released the first Alpha version of Kotlin Symbol Processing (KSP). In the past few months, KSP has been updated to Beta3. Currently, the API is basically stable, and it is believed that a stable release is not far away.
Why KSP?
Many people make fun of Kotlin’s compile speed, and KAPT is one of the main culprits.
Many libraries use annotations to simplify template code, such as Room, Dagger, Retrofit, etc. Kotlin code uses KAPT to handle annotations. KAPT essentially works on APT. APT can only handle Java annotations, so you need to make APT parsible stubs (Java code), which slows down Kotlin’s overall compilation speed.
KSP was born in this context. It is implemented based on the Kotlin Compiler Plugin (KCP for short), does not need to generate additional stubs, and compels more than twice as fast as KAPT
KSP with KCP
Kotlin Compiler Plugin provides hook opportunities during the kotlinc process to parse AST and modify bytecode products. Many of Kotlin’s syntactic sugars are implemented by KCP. For example, data Class, @Parcelize, Kotlin-Android-extension, etc., for example, Compose, which is currently popular, uses KCP for its compile-time work.
In theory, THE capabilities of KCP are supersets of KAPT and can replace KAPT to improve compilation speed. However, the development cost of KCP is too high, which involves the use of Gradle Plugin and Kotlin Plugin, etc. API involves some compiler knowledge, which is difficult for common developers to master.
The development of a standard KCP involves the following:
- Gradle Plugin is used to read Gradle configurations passed to KCP (Kotlin Plugin).
- Subplugin: provides KCP with customized KP’s Maven library address and other configuration information
- CommandLineProcessor: Converts parameters to KP identifiable parameters
- ComponentRegistrar: Register with the various KCP processes
- Extension: Implement the customized KP function
KSP simplifies this process by eliminating the need for developers to understand how the compiler works, and handling annotations is as cheap as KAPT.
KSP and KAPT
KSP, as the name implies, processes Kotlin’s AST at the Symbols level, accessing elements of types such as classes, class members, functions, and related parameters. It’s analogous to the Kotlin AST in PSI
A Kotlin source file parsed by KSP looks like this:
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 variables, etc.
declarations: List<KSDeclaration>
KSPropertyDeclaration // global variable
simpleName: KSName
qualifiedName: KSName
containingFile: String
typeParameters: KSTypeParameter
parentDeclaration: KSDeclaration
extensionReceiver: KSTypeReference?
type: KSTypeReference
getter: KSPropertyGetter
returnType: KSTypeReference
setter: KSPropertySetter
parameter: KSVariableParameter
KSEnumEntryDeclaration
// same as KSClassDeclaration
Copy the code
This is the Kotlin AST abstraction in KSP. Similarly, THERE is an AST abstraction for Java in APT/KAPT, which can find some corresponding relationships. For example, Java uses Element to describe packages, classes, methods or variables, etc., while KSP uses Declaration
Java/APT | Kotlin/KSP | Description |
---|---|---|
PackageElement | KSFile | Represents a package element. Provides access to information about packages and their members |
ExecuteableElement | KSFunctionDeclaration | A method, constructor, or initializer (static or instance) that represents a class or interface, including annotation type elements |
TypeElement | KSClassDeclaration | Represents a class or interface program element. Provides access to information about types and their members. Note that an enumeration type is a class and an annotation type is an interface |
VariableElement | KSVariableParameter / KSPropertyDeclaration | Represents a field, enum constant, method or constructor parameter, local variable, or exception parameter |
Declaration also contains Type information, such as function parameters, return value types, etc. TypeMirror is used to carry Type information in APT. Detailed capabilities in KSP are implemented by KSType.
The development process of KSP is similar to that of KAPT:
- Parsing source AST
- The generated code
- The generated code participates in Kotlin compilation along with the source code
Note that KSP cannot be used to modify the original code, only to generate new code
KSP entry: SymbolProcessorProvider
KSP is implemented through the SymbolProcessor. The SymbolProcessor needs to be created with a SymbolProcessorProvider. So the SymbolProcessorProvider is the entry point for KSP execution
interface SymbolProcessorProvider {
fun create(environment: SymbolProcessorEnvironment): SymbolProcessor
}
Copy the code
SymbolProcessorEnvironment get some KSP runtime dependency, injected into the Processor
interface SymbolProcessor {
fun process(resolver: Resolver): List<KSAnnotated> // Let's focus on this
fun finish(a) {}
fun onError(a){}}Copy the code
Process () provides a Resolver that resolves symbols on the AST. The Resolver uses the visitor pattern to traverse the AST.
Resolver uses FindFunctionsVisitor to find top-level functions and Class member methods in the current KSFile:
class HelloFunctionFinderProcessor : SymbolProcessor() {...val functions = mutableListOf<String>()
val visitor = FindFunctionsVisitor()
override fun process(resolver: Resolver) {
// Use FindFunctionsVisitor to traverse the AST
resolver.getAllFiles().map { it.accept(visitor, Unit)}}inner class FindFunctionsVisitor : KSVisitorVoid() {
override fun visitClassDeclaration(classDeclaration: KSClassDeclaration.data: Unit) {
// Access the Class node
classDeclaration.getDeclaredFunctions().map { it.accept(this.Unit)}}override fun visitFunctionDeclaration(function: KSFunctionDeclaration.data: Unit) {
// Access the function node
functions.add(function)
}
override fun visitFile(file: KSFile.data: Unit) {
/ / access to the file
file.declarations.map { it.accept(this.Unit)}}}... }Copy the code
KSP API sample
Can you give some examples of how the KSP API works
Access all member methods in a class
fun KSClassDeclaration.getDeclaredFunctions(a): List<KSFunctionDeclaration> {
return this.declarations.filterIsInstance<KSFunctionDeclaration>()
}
Copy the code
Determines whether a class or method is a local class or method
fun KSDeclaration.isLocal(a): Boolean {
return this.parentDeclaration ! =null && this.parentDeclaration !is KSClassDeclaration
}
Copy the code
Determines 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 the code
Get annotation information
// Find out suppressed names in a file annotation:
// @file:kotlin.Suppress("Example1", "Example2")
fun KSFile.suppressedNames(a): 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 the code
Examples of code generation
Finally, let’s look at a relatively complete example of code generation to replace APT
@IntSummable
data class Foo(
val bar: Int = 234.val baz: Int = 123
)
Copy the code
We want to process @IntSummable with KSP to generate the following code
public fun Foo.sumInts(a): Int {
val sum = bar + baz
return sum
}
Copy the code
Dependencies
Developing KSP requires adding 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 the code
IntSummableProcessorProvider
We need an inbound Provider to build the 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 the code
Can be injected the options for the Processor, by SymbolProcessorEnvironment CodeGenerator, logger, etc required to rely on
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 { itis KSClassDeclaration && it.validate() }
.forEach { it.accept(IntSummableVisitor(), Unit)}return symbols.toList()
}
}
Copy the code
builtIns.intType
Access to thekotlin.Int
的KSType
, is needed later.getSymbolsWithAnnotation
Get the annotation asIntSummable
The list of symbols- When the symbol is a Class, it is processed using a Visitor
IntSummableVisitor
The interface of the Visitor is generally as follows, with D and R representing 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 the code
Our requirements have no input or output, so we just implement KSVisitorVoid, essentially a KSVisitor
:
inner class Visitor : KSVisitorVoid() {
override fun visitClassDeclaration(classDeclaration: KSClassDeclaration.data: Unit) {
valqualifiedName = classDeclaration.qualifiedName? .asString()//1. Check the validity
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. Parse the Class information
/ /...
//3. Code generation
/ /...
}
private fun KSClassDeclaration.isDataClass(a) = modifiers.contains(Modifier.DATA)
}
Copy the code
Above, we determine whether this Class is a data Class and whether its Class name is valid
Parsing Class information
Next we need to get the relevant information from 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. Check the validity
/ /...
//2. Parse the Class information
valqualifiedName = 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 the code
- through
KSClassDeclaration
To obtain theclassName
.packageName
, as well asProperties
And deposit itsummables
visitPropertyDeclaration
Ensure that the Property must be of type Int, as mentioned earlierintType
Code generation
After collecting the Class information, proceed to code generation. We introduce KotlinPoet to help us generate Kotlin code
dependencies {
implementation("Com. Squareup: kotlinpoet: 1.8.0 comes with.")}Copy the code
override fun visitClassDeclaration(classDeclaration: KSClassDeclaration.data: Unit) {
//1. Check the validity
/ /...
//2. Parse 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 the code
- The use of KotlinPoet
FunSpec
Generating function code - The front SymbolProcessorEnvironment provides
CodeGenerator
Used to create the file and write to the generatedFileSpec
code
conclusion
As can be seen from the IntSummable example, KSP can completely replace APT/KAPT for annotation processing, and the performance is better.
At present, many third-party libraries using APT have added support for KSP
Library | Status | Tracking issue for KSP |
---|---|---|
Room | Experimentally supported | |
Moshi | Experimentally supported | |
Kotshi | Experimentally supported | |
Lyricist | Experimentally supported | |
Auto Factory | Not yet supported | Link |
Dagger | Not yet supported | Link |
Hilt | Not yet supported | Link |
Glide | Not yet supported | Link |
DeeplinkDispatch | Not yet supported | Link |
Replacing KAPT with KSP is also very simple, as Moshi does
Of course, you can also use both KAPT and KSP in a project without affecting each other. KSP is increasingly replacing KAPT. If your project also handles annotation requirements, why not try KSP?
github.com/google/ksp