Slide 1

Slide 1 text

Kotlin Symbol Processing API (KSP) を使って Kotlin ア プリケーションの開発を効率化する Kotlin Fest 2022 @takuji31

Slide 2

Slide 2 text

自己紹介 西林 拓志(にしばやし たくじ) Twitter/GitHub takuji31 Android アプリケーションエンジ ニア 株式会社はてな テクノロジーソリューション 本部第 2 グループ マンガアプ リチーム所属 Kotlin 歴 8 年くらい 2

Slide 3

Slide 3 text

Kotlin Symbol Processing API (以下 KSP) 使ってます か? 3

Slide 4

Slide 4 text

今日は KSP のプロセッサーの作り方(≠ 使い方)について話 します 4

Slide 5

Slide 5 text

今日話すこと KSP とは KSP を使う理由 プロセッサーの作り方 インクリメンタル処理 マルチラウンド処理 マルチモジュールでの活用方法 テスト 5

Slide 6

Slide 6 text

今日話さないこと KSP の使い方、セットアップ方法 Gradle 以外で KSP のプロセッサーを開発する方法 Kotlin Multiplatform での使い方 どういった時に KSP を使うとよいか (質問があれば答えます) etc. 6

Slide 7

Slide 7 text

注意点 スライド内に出てくる Gradle script は Kotlin DSL です、Groovy は公式ドキュメントみ てください https://kotlinlang.org/docs/ksp-overview.html スライド内のコードは import や package を省略しています 7

Slide 8

Slide 8 text

KSP とは 8

Slide 9

Slide 9 text

KSP とは Kotlin でシンボルを処理するための軽量コンパイラープラグイン Java の Pluggable Annotation Processing API と同様にアノテーションを使う Kotlin の言語機能に対応している インクリメンタル処理対応 Kotlin Multiplatform 対応 https://github.com/google/ksp 9

Slide 10

Slide 10 text

KSP を使う理由 10

Slide 11

Slide 11 text

KSP を使う理由 - 課題解決 アノテーションを処理してコード生成をすることで普段の開発に潜む問題を解決する 頑張って型作って解決していたことをコード生成でカバーしたり Boilerplate コードを自動生成することでコード内のノイズを減らしたり 規模が大きなアプリケーションでモジュールごとに必要な設定を集約したり 型安全なクエリービルダーを作ったり 型安全な画面遷移を実現したり etc. 11

Slide 12

Slide 12 text

KSP を使う理由 - kapt と比べて kapt と比べて高速 kapt は一旦 Kotlin から Java のスタブコードを生成しているがこれがとても遅い 現在開発しているアプリの場合だいたい全体のビルド時間の 20%くらいがスタブコ ード生成の時間 Kotlin の言語機能に対応した構造を返してくれるので、Pluggable Annotation Processing API で Kotlin のメタデータを頑張って読むみたいなことが不要 kotlinx-metadata-jvm なるものを使えばできるけどまあまあ難しい メンテナンスモードになってるのでこれ以上更新されない 今から Kotlin で新しくプロセッサーを作って使うのは厳しい 12

Slide 13

Slide 13 text

KSP を使う理由 - リフレクションと比べて ビルド時に型のチェックが行われるので安全 より高速 バイナリーサイズに影響なし (Android 的文脈で)難読化に強い 13

Slide 14

Slide 14 text

KSP を使う理由 - コンパイラープラグインと比べて 簡単 Kotlin のバージョンの影響を受けない(KSP のライブラリー自体は Kotlin のバージョンに 合わせる必要がある) 14

Slide 15

Slide 15 text

KSP を使う理由 - その他 Multiplatform 対応 Kotlin Multiplatform Mobile とかやっていると便利そう 私はやっていないので実際の使用感は不明 公式ドキュメントが充実していて始めやすい https://kotlinlang.org/docs/ksp-overview.html KotlinPoet と組み合わせることでプログラムの構造を簡単に作ることができる https://square.github.io/kotlinpoet/ 15

Slide 16

Slide 16 text

プロセッサーの 作り方 16

Slide 17

Slide 17 text

今回作るもの インターフェースにアノテーションをつけるとそのインターフェースを実装した抽象ク ラスを吐き出す 実用性はないが簡単な例ということで // これを @SimpleGeneration interface Hoge // こうだ abstract class AbstractHoge: Hoge 17

Slide 18

Slide 18 text

プロセッサーの作り方 Gradle モジュールを作る SymbolProcessor を実装する Visitor を実装する SymbolProcessorProvider を作る サービスプロバイダーの設定をする 18

Slide 19

Slide 19 text

Gradle モジュールを作る 19

Slide 20

Slide 20 text

Gradle モジュールを作る 好きな IDE やツールでプロジェクトに Kotlin モジュールを新しく作りましょう 名前は自由 compiler processor ksp-hogehoge 処理に使うアノテーションや生成されるコードで使うクラスは別のモジュールに分けて もよい 分かれている方が無駄なクラスがアプリケーションの classpath に含まれない etc. 20

Slide 21

Slide 21 text

build.gradle.kts // ... dependencies { // KSP のAPI implementation("com.google.devtools.ksp:symbol-processing-api:1.7.21-1.0.8") // KotlinPoet implementation("com.squareup:kotlinpoet:1.12.0") // KotlinPoet のKSP 用拡張 implementation("com.squareup:kotlinpoet-ksp:1.12.0") } 21

Slide 22

Slide 22 text

SymbolProcessor を実装する 22

Slide 23

Slide 23 text

SymbolProcessor class ExampleSymbolProcessor( private val codeGenerator: CodeGenerator, private val logger: KSPLogger ) : SymbolProcessor { override fun process(resolver: Resolver): List { resolver .getSymbolsWithAnnotation(SimpleGeneration::class.qualifiedName!!) .filterIsInstance() .forEach { it.accept(SimpleGenerationVisitor(codeGenerator, logger), Unit) } return emptyList() } } 23

Slide 24

Slide 24 text

Visitor class SimpleGenerationVisitor( private val codeGenerator: CodeGenerator, private val logger: KSPLogger ) : KSVisitorVoid() { override fun visitClassDeclaration(classDeclaration: KSClassDeclaration, data: Unit) { if (classDeclaration.classKind != ClassKind.INTERFACE) { logger.error("Only interface allowed", classDeclaration) return } val packageName = classDeclaration.packageName.asString() val className = ClassName(packageName, "Abstract" + classDeclaration.simpleName.asString()) val typeSpec = TypeSpec.classBuilder(className) .addModifiers(KModifier.ABSTRACT) .addSuperinterface(classDeclaration.toClassName()) FileSpec.builder(packageName, className.simpleName) .addType(typeSpec.build()) .build() .writeTo( codeGenerator, Dependencies( aggregating = false, classDeclaration.containingFile!! ) ) } } 24

Slide 25

Slide 25 text

SymbolProcessorProvider を作る 25

Slide 26

Slide 26 text

SymbolProcessorProvider class ExampleSymbolProcessorProvider : SymbolProcessorProvider { override fun create(environment: SymbolProcessorEnvironment): SymbolProcessor { return ExampleSymbolProcessor(environment.codeGenerator, environment.logger) } } 26

Slide 27

Slide 27 text

サービスプロバイダーの設定をする 27

Slide 28

Slide 28 text

サービスプロバイダーの設定 // resources/META-INF/services/com.google.devtools.ksp.processing.SymbolProcessorProvider jp.takuji31.kotlinfest2022.compiler.ExampleSymbolProcessorProvider 28

Slide 29

Slide 29 text

インクリメンタル 処理 29

Slide 30

Slide 30 text

インクリメンタル処理 KSP はインクリメンタル処理に対応している インクリメンタル処理のモードは 2 つ Isolated (分離) Aggregated (集約) ファイルを書き出すときに Dependencies で依存を指定すれば、依存に変更があった時 に再処理される 生成されるファイルごとにモードが変えられる 30

Slide 31

Slide 31 text

Isolated (分離) 生成されたファイルが単一もしくは複数のファイルと紐付いている時のモード 依存に指定されたファイルが変更されると再処理される ↑ 以外のファイルが変更された時は処理されない 1 つのアノテーションで 1 つのファイルを生成したいときによく使う 例) アノテーションがつけられたインターフェースの実装クラスを生成する 31

Slide 32

Slide 32 text

Isolated (分離) Dependencies(aggregating = false, classDeclaration.containingFile!!) 32

Slide 33

Slide 33 text

Aggregated (集約) 生成されたファイルが不特定の複数のファイルと紐付いている時のモード 依存に指定されたファイル以外のものが変更されると依存に指定されたファイルとまと めて再処理される 基本的に変更があれば毎回処理されるイメージ 1 つのアノテーションで複数のファイルをまとめたファイルを作る時に使われる 例 1) アノテーションがつけられたインターフェースをまとめたインターフェースを 作る 例 2) DI コンテナーで DI したいインスタンスの情報を収集する 例 3) 同じアノテーションがつけられた複数のクラスから特定のクラスだけを選んで 何か処理する 33

Slide 34

Slide 34 text

Aggregated (集約) val dependencies: Array = // ... Dependencies(aggregating = true, *dependencies.mapNotNull { it.containingFile }) 34

Slide 35

Slide 35 text

依存をちゃんと指定しないと「なぜかコード生成されな い」みたいな事態になる 35

Slide 36

Slide 36 text

マルチラウンド処理 36

Slide 37

Slide 37 text

マルチラウンド処理 処理できないシンボルの処理を後回しにする仕組み KSP のプロセッサーで生成したコードはすぐにプロセッサーで認識できない ラウンドを複数回繰り返して、そのラウンドで生成されたファイルがなくなったら終わ り 生成されたファイルがなくなったけど遅延されているシンボルがあればエラー マルチラウンド処理で活用するためにシンボルのバリデーションを行う仕組みがある カスタムでバリデーションルールを作れるが今回は説明しません 37

Slide 38

Slide 38 text

処理の遅延方法 override fun process(resolver: Resolver): List { val symbolsWithAnnotation = resolver .getSymbolsWithAnnotation(SimpleGeneration::class.qualifiedName!!) .filterIsInstance() symbolsWithAnnotation .filter { it.validate() } .forEach { it.accept(SimpleGenerationVisitor(codeGenerator, logger), Unit) } return symbolsWithAnnotation.filter { !it.validate() }.toList() } 38

Slide 39

Slide 39 text

マルチモジュールでの活用方法 39

Slide 40

Slide 40 text

依存しているモジュールやライブラリーのシンボルは KSP の getSymbolsWithAnnotation で取得できな い google/ksp#1075 40

Slide 41

Slide 41 text

じゃあどうするのか? 41

Slide 42

Slide 42 text

getDeclarationsFromPackage を使う 42

Slide 43

Slide 43 text

getDeclarationsFromPackage を使う 特定のパッケージに含まれる定義を全部取り出すメソッド 特定のパッケージに何かしらのメタデータを書き出す 集約するモジュールで ↑ のパッケージに書かれた定義を読んでコード生成する 集約に使う用のアノテーションを作って集約したいモジュールで使う 43

Slide 44

Slide 44 text

テスト 44

Slide 45

Slide 45 text

コンパイルのテストってどうやんの? 45

Slide 46

Slide 46 text

tschuchortdev/kotlin-compile-testing 46

Slide 47

Slide 47 text

テストコード val source = SourceFile.kotlin( "ExampleClass.kt", """ package jp.takuji31.kotlinfest2022.compiler import jp.takuji31.kotlinfest2022.compiler.annotation.SimpleGeneration @SimpleGeneration interface SimpleInterface { fun printHelloWorld() } """.trimIndent() ) val compilation = KotlinCompilation().apply { sources = listOf(source) inheritClassPath = true symbolProcessorProviders = listOf(ExampleSymbolProcessorProvider()) kspWithCompilation = true } val result = compilation.compile() assertThat(result.exitCode) .isEqualTo(KotlinCompilation.ExitCode.OK) 47

Slide 48

Slide 48 text

まとめ SymbolProcessor、Visitor で KSP のプロセッサーは作れる Kotlin のコード生成は KotlinPoet でやると楽 インクリメンタル処理のための依存は Dependencies で指定 マルチラウンド処理をするためには process メソッドで遅延したいシンボルを渡す 複数モジュールのコードを集約したい時は getDeclarationsFromPackage プロセッサーのテストは kotlin-compile-testing で 48

Slide 49

Slide 49 text

ドキュメント/サンプルコード https://kotlinlang.org/docs/ksp-overview.html 公式ドキュメント https://github.com/google/ksp/tree/main/examples/playground 公式サンプル https://github.com/takuji31/kotlinfest2022-ksp-example 今回のスライドに出てきたソースコード https://github.com/takuji31/navigation-compose-screen もうちょっと複雑な例 49

Slide 50

Slide 50 text

Enjoy KSP Life! 50