Slide 1

Slide 1 text

Kotlin Symbol Processing (KSP) を使ったコード生成 Kenji Abe

Slide 2

Slide 2 text

About me ● Kenji Abe ● Google Developers Expert for Android, Kotlin ● AndroidDagashi ● DeNA Co., Ltd. ● @STAR_ZERO

Slide 3

Slide 3 text

Kotlin Symbol Processing (KSP) ● https://github.com/google/ksp ● Kotlinの軽量コンパイラプラグインを開発できるツール ● KAPTに似た機能を提供できる(コード生成) ● KAPTに比べて高速 ● Kotlinのコードを直接解析できる ● Kotlin Multiplatformのサポート

Slide 4

Slide 4 text

基本構成

Slide 5

Slide 5 text

No content

Slide 6

Slide 6 text

No content

Slide 7

Slide 7 text

No content

Slide 8

Slide 8 text

No content

Slide 9

Slide 9 text

dependencies { implementation("com.google.devtools.ksp:symbol-processing-api:1.5.30-1.0.0") }

Slide 10

Slide 10 text

No content

Slide 11

Slide 11 text

com.example.processor.HelloWorldProcessorProvider

Slide 12

Slide 12 text

plugins { id("com.google.devtools.ksp") version "1.5.30-1.0.0" // ... } dependencies { ksp(project(":processor")) }

Slide 13

Slide 13 text

実装詳細

Slide 14

Slide 14 text

SymbolProcessorProvider

Slide 15

Slide 15 text

SymbolProcessorProvider ● エントリーポイント ● ServiceLoaderによって生成される (META-INF/services) ● createメソッドでSymbolProcessorを生成して返す必要がある ● createメソッドの引数から処理に必要なオブジェクトを受け取れる ○ CodeGenerator ○ Logger ○ Option

Slide 16

Slide 16 text

class HelloWorldProcessorProvider : SymbolProcessorProvider { override fun create( environment: SymbolProcessorEnvironment ): SymbolProcessor { return HelloWorldProcessor( environment.codeGenerator, environment.options, environment.logger ) } }

Slide 17

Slide 17 text

class HelloWorldProcessorProvider : SymbolProcessorProvider { override fun create( environment: SymbolProcessorEnvironment ): SymbolProcessor { return HelloWorldProcessor( environment.codeGenerator, environment.options, environment.logger ) } }

Slide 18

Slide 18 text

class HelloWorldProcessorProvider : SymbolProcessorProvider { override fun create( environment: SymbolProcessorEnvironment ): SymbolProcessor { return HelloWorldProcessor( environment.codeGenerator, environment.options, environment.logger ) } }

Slide 19

Slide 19 text

class HelloWorldProcessorProvider : SymbolProcessorProvider { override fun create( environment: SymbolProcessorEnvironment ): SymbolProcessor { return HelloWorldProcessor( environment.codeGenerator, environment.options, environment.logger ) } }

Slide 20

Slide 20 text

class HelloWorldProcessorProvider : SymbolProcessorProvider { override fun create( environment: SymbolProcessorEnvironment ): SymbolProcessor { return HelloWorldProcessor( environment.codeGenerator, environment.options, environment.logger ) } }

Slide 21

Slide 21 text

class HelloWorldProcessorProvider : SymbolProcessorProvider { override fun create( environment: SymbolProcessorEnvironment ): SymbolProcessor { return HelloWorldProcessor( environment.codeGenerator, environment.options, environment.logger ) } } // build.gradle.kts ksp { arg("key", "value") }

Slide 22

Slide 22 text

class HelloWorldProcessorProvider : SymbolProcessorProvider { override fun create( environment: SymbolProcessorEnvironment ): SymbolProcessor { return HelloWorldProcessor( environment.codeGenerator, environment.options, environment.logger ) } } logger.logging("...") logger.info("...") logger.warn("...") logger.error("...") $ ./gradlew build --debug or $ ./gradlew build --info

Slide 23

Slide 23 text

SymbolProcessor

Slide 24

Slide 24 text

SymbolProcessor ● SymbolProcessorProviderで生成される ● processメソッドでコード解析やコード生成を行う ● finish、onErrorメソッドもある

Slide 25

Slide 25 text

class HelloWorldProcessor( private val codeGenerator: CodeGenerator, private val options: Map, private val logger: KSPLogger ) : SymbolProcessor { override fun process(resolver: Resolver): List { // } }

Slide 26

Slide 26 text

class HelloWorldProcessor( private val codeGenerator: CodeGenerator, private val options: Map, private val logger: KSPLogger ) : SymbolProcessor { override fun process(resolver: Resolver): List { // } }

Slide 27

Slide 27 text

class HelloWorldProcessor( private val codeGenerator: CodeGenerator, private val options: Map, private val logger: KSPLogger ) : SymbolProcessor { override fun process(resolver: Resolver): List { // } }

Slide 28

Slide 28 text

class HelloWorldProcessor( private val codeGenerator: CodeGenerator, private val options: Map, private val logger: KSPLogger ) : SymbolProcessor { override fun process(resolver: Resolver): List { // } }

Slide 29

Slide 29 text

コード解析

Slide 30

Slide 30 text

https://github.com/google/ksp/blob/main/docs/ksp-additional-details.md

Slide 31

Slide 31 text

override fun process(resolver: Resolver): List { val files = resolver.getAllFiles() val newFiles = resolver.getNewFiles() val symbols = resolver.getSymbolsWithAnnotation("com.example.Factory") val ksClass = resolver.getClassDeclarationByName("com.example.Hoge") // ... }

Slide 32

Slide 32 text

override fun process(resolver: Resolver): List { val files = resolver.getAllFiles() val newFiles = resolver.getNewFiles() val symbols = resolver.getSymbolsWithAnnotation("com.example.Factory") val ksClass = resolver.getClassDeclarationByName("com.example.Hoge") // ... }

Slide 33

Slide 33 text

override fun process(resolver: Resolver): List { val files = resolver.getAllFiles() val newFiles = resolver.getNewFiles() val symbols = resolver.getSymbolsWithAnnotation("com.example.Factory") val ksClass = resolver.getClassDeclarationByName("com.example.Hoge") // ... }

Slide 34

Slide 34 text

override fun process(resolver: Resolver): List { val symbols = resolver.getSymbolsWithAnnotation("com.example.Factory") symbols.forEach { when (it) { is KSClassDeclaration -> { } is KSFunctionDeclaration -> { } is KSPropertyDeclaration -> { } } } // ... }

Slide 35

Slide 35 text

https://github.com/google/ksp/blob/main/README.md#how-ksp-looks-at-source-files

Slide 36

Slide 36 text

CodeGenerator

Slide 37

Slide 37 text

override fun process(resolver: Resolver): List { val file = codeGenerator.createNewFile( Dependencies(false), "com.example.generated", "Hello" ) val code = """ ... """.trimIndent() file.write(code.toByteArray()) file.close() // ... }

Slide 38

Slide 38 text

override fun process(resolver: Resolver): List { val file = codeGenerator.createNewFile( Dependencies(false), "com.example.generated", "Hello" ) val code = """ ... """.trimIndent() file.write(code.toByteArray()) file.close() // ... }

Slide 39

Slide 39 text

override fun process(resolver: Resolver): List { val file = codeGenerator.createNewFile( Dependencies(false), "com.example.generated", "Hello" ) val code = """ ... """.trimIndent() file.write(code.toByteArray()) file.close() // ... }

Slide 40

Slide 40 text

override fun process(resolver: Resolver): List { val file = codeGenerator.createNewFile( Dependencies(false), "com.example.generated", "Hello" ) val code = """ ... """.trimIndent() file.write(code.toByteArray()) file.close() // ... }

Slide 41

Slide 41 text

val file = codeGenerator.createNewFile( Dependencies(false), "hoge/foo", "plain", "txt" )

Slide 42

Slide 42 text

Multiple round processing

Slide 43

Slide 43 text

// Hoge.kt @Factory class Hoge // Foo.kt @Factory class Foo // etc... // SymbolProcessor#process resolver.getSymbolsWithAnnotation("...") // =>

Slide 44

Slide 44 text

// Hoge.kt @Factory class Hoge // Foo.kt @Factory class Foo // etc... // SymbolProcessor#process resolver.getSymbolsWithAnnotation("...") // => [Hoge, Foo]

Slide 45

Slide 45 text

// Hoge.kt @Factory class Hoge // Foo.kt @Factory class Foo // etc... // SymbolProcessor#process resolver.getSymbolsWithAnnotation("...") // => [Hoge, Foo] // HogeFactory.kt class HogeFactory // FooFactory.kt class FooFactory コード生成

Slide 46

Slide 46 text

// Hoge.kt @Factory class Hoge // Foo.kt @Factory class Foo // etc... // SymbolProcessor#process resolver.getSymbolsWithAnnotation("...") // => // HogeFactory.kt class HogeFactory // FooFactory.kt class FooFactory コード生成

Slide 47

Slide 47 text

// Hoge.kt @Factory class Hoge // Foo.kt @Factory class Foo // etc... // SymbolProcessor#process resolver.getSymbolsWithAnnotation("...") // => [] // HogeFactory.kt class HogeFactory // FooFactory.kt class FooFactory コード生成

Slide 48

Slide 48 text

// Hoge.kt @Factory class Hoge // Foo.kt @Factory class Foo // etc... // SymbolProcessor#process resolver.getSymbolsWithAnnotation("...") // => [] // HogeFactory.kt class HogeFactory // FooFactory.kt class FooFactory コード生成 Finish

Slide 49

Slide 49 text

@Factory class Hoge(hello: Hello) HelloProcessor class Hello FactoryProcessor

Slide 50

Slide 50 text

@Factory class Hoge(hello: Hello) HelloProcessor FactoryProcessor Hoge => Hello🤔

Slide 51

Slide 51 text

override fun process(resolver: Resolver): List { val symbols = resolver.getSymbolsWithAnnotation("...") val result = symbols.filter { !it.validate() }.toList() symbols.filter { it.validate() }.forEach { symbol -> // コード生成 } return result }

Slide 52

Slide 52 text

override fun process(resolver: Resolver): List { val symbols = resolver.getSymbolsWithAnnotation("...") val result = symbols.filter { !it.validate() }.toList() symbols.filter { it.validate() }.forEach { symbol -> // コード生成 } return result }

Slide 53

Slide 53 text

override fun process(resolver: Resolver): List { val symbols = resolver.getSymbolsWithAnnotation("...") val result = symbols.filter { !it.validate() }.toList() symbols.filter { it.validate() }.forEach { symbol -> // コード生成 } return result }

Slide 54

Slide 54 text

override fun process(resolver: Resolver): List { val symbols = resolver.getSymbolsWithAnnotation("...") val result = symbols.filter { !it.validate() }.toList() symbols.filter { it.validate() }.forEach { symbol -> // コード生成 } return result }

Slide 55

Slide 55 text

Resolver ● getAllFiles ○ 全てのラウンドでの処理対象となったファイル ● getNewFiles ○ ラウンドごとに処理対象となっているファイル ● getSymbolsWithAnnotation ○ ラウンドごとに処理対象となっているものから、該当の Annotationがあるもの ● getClassDeclarationByName ○ クラスパスから指定したクラス名を探す

Slide 56

Slide 56 text

Incremental processing

Slide 57

Slide 57 text

Hoge.kt Foo.kt SymbolProcessor HogeFactory.kt FooFactory.kt Clean Build

Slide 58

Slide 58 text

Hoge.kt Foo.kt SymbolProcessor HogeFactory.kt FooFactory.kt Clean Build

Slide 59

Slide 59 text

Hoge.kt Foo.kt SymbolProcessor HogeFactory.kt FooFactory.kt Hoge.kt Foo.kt Bar.kt Changed New Clean Build 差分 Build

Slide 60

Slide 60 text

Hoge.kt Foo.kt SymbolProcessor HogeFactory.kt FooFactory.kt Hoge.kt Foo.kt SymbolProcessor Bar.kt Changed New Clean Build 差分 Build

Slide 61

Slide 61 text

Hoge.kt Foo.kt SymbolProcessor HogeFactory.kt FooFactory.kt Hoge.kt Foo.kt SymbolProcessor HogeFactory.kt FooFactory.kt Bar.kt BarFactory.kt Changed New Clean Build 差分 Build

Slide 62

Slide 62 text

val symbols = resolver.getSymbolsWithAnnotation("...") symbols.forEach { val file = codeGenerator.createNewFile( Dependencies(aggregating = false, it.containingFile!!), "...", "..." ) // コード生成 }

Slide 63

Slide 63 text

val symbols = resolver.getSymbolsWithAnnotation("...") symbols.forEach { val file = codeGenerator.createNewFile( Dependencies(aggregating = false, it.containingFile!!), "...", "..." ) // コード生成 }

Slide 64

Slide 64 text

Dependencies(aggregating = false) Hoge.kt Foo.kt SymbolProcessor

Slide 65

Slide 65 text

Hoge.kt Foo.kt SymbolProcessor Bar.kt New Changed Hoge.kt Bar.kt Dependencies(aggregating = false)

Slide 66

Slide 66 text

Dependencies(aggregating = false, Hoge, Foo) SymbolProcessor Hoge.kt Foo.kt

Slide 67

Slide 67 text

Dependencies(aggregating = false, Hoge, Foo) Hoge.kt Foo.kt SymbolProcessor Changed Hoge.kt Foo.kt

Slide 68

Slide 68 text

Dependencies(aggregating = true, Hoge) Hoge.kt Foo.kt SymbolProcessor

Slide 69

Slide 69 text

Dependencies(aggregating = true, Hoge) Hoge.kt Foo.kt SymbolProcessor Changed Hoge.kt Foo.kt

Slide 70

Slide 70 text

KSP対応状況

Slide 71

Slide 71 text

KSP対応状況 ● Room ○ https://developer.android.com/jetpack/androidx/releases/room#2.3.0-beta02 ● Moshi ○ https://github.com/ZacSweers/MoshiX/tree/main/moshi-ksp ● Kotshi ○ https://github.com/ansman/kotshi

Slide 72

Slide 72 text

Thank you