Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Kotlin Symbol Processing API (KSP) を使って Kotlin ...

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

Takuji Nishibayashi

December 10, 2022
Tweet

More Decks by Takuji Nishibayashi

Other Decks in Technology

Transcript

  1. 自己紹介 西林 拓志(にしばやし たくじ) Twitter/GitHub takuji31 Android アプリケーションエンジ ニア 株式会社はてな

    テクノロジーソリューション 本部第 2 グループ マンガアプ リチーム所属 Kotlin 歴 8 年くらい 2
  2. 注意点 スライド内に出てくる Gradle script は Kotlin DSL です、Groovy は公式ドキュメントみ てください

    https://kotlinlang.org/docs/ksp-overview.html スライド内のコードは import や package を省略しています 7
  3. KSP とは Kotlin でシンボルを処理するための軽量コンパイラープラグイン Java の Pluggable Annotation Processing API

    と同様にアノテーションを使う Kotlin の言語機能に対応している インクリメンタル処理対応 Kotlin Multiplatform 対応 https://github.com/google/ksp 9
  4. KSP を使う理由 - kapt と比べて kapt と比べて高速 kapt は一旦 Kotlin

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

    私はやっていないので実際の使用感は不明 公式ドキュメントが充実していて始めやすい https://kotlinlang.org/docs/ksp-overview.html KotlinPoet と組み合わせることでプログラムの構造を簡単に作ることができる https://square.github.io/kotlinpoet/ 15
  6. Gradle モジュールを作る 好きな IDE やツールでプロジェクトに Kotlin モジュールを新しく作りましょう 名前は自由 compiler processor

    ksp-hogehoge 処理に使うアノテーションや生成されるコードで使うクラスは別のモジュールに分けて もよい 分かれている方が無駄なクラスがアプリケーションの classpath に含まれない etc. 20
  7. 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
  8. SymbolProcessor class ExampleSymbolProcessor( private val codeGenerator: CodeGenerator, private val logger:

    KSPLogger ) : SymbolProcessor { override fun process(resolver: Resolver): List<KSAnnotated> { resolver .getSymbolsWithAnnotation(SimpleGeneration::class.qualifiedName!!) .filterIsInstance<KSClassDeclaration>() .forEach { it.accept(SimpleGenerationVisitor(codeGenerator, logger), Unit) } return emptyList() } } 23
  9. 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
  10. SymbolProcessorProvider class ExampleSymbolProcessorProvider : SymbolProcessorProvider { override fun create(environment: SymbolProcessorEnvironment):

    SymbolProcessor { return ExampleSymbolProcessor(environment.codeGenerator, environment.logger) } } 26
  11. インクリメンタル処理 KSP はインクリメンタル処理に対応している インクリメンタル処理のモードは 2 つ Isolated (分離) Aggregated (集約)

    ファイルを書き出すときに Dependencies で依存を指定すれば、依存に変更があった時 に再処理される 生成されるファイルごとにモードが変えられる 30
  12. Aggregated (集約) 生成されたファイルが不特定の複数のファイルと紐付いている時のモード 依存に指定されたファイル以外のものが変更されると依存に指定されたファイルとまと めて再処理される 基本的に変更があれば毎回処理されるイメージ 1 つのアノテーションで複数のファイルをまとめたファイルを作る時に使われる 例 1)

    アノテーションがつけられたインターフェースをまとめたインターフェースを 作る 例 2) DI コンテナーで DI したいインスタンスの情報を収集する 例 3) 同じアノテーションがつけられた複数のクラスから特定のクラスだけを選んで 何か処理する 33
  13. 処理の遅延方法 override fun process(resolver: Resolver): List<KSAnnotated> { val symbolsWithAnnotation =

    resolver .getSymbolsWithAnnotation(SimpleGeneration::class.qualifiedName!!) .filterIsInstance<KSClassDeclaration>() symbolsWithAnnotation .filter { it.validate() } .forEach { it.accept(SimpleGenerationVisitor(codeGenerator, logger), Unit) } return symbolsWithAnnotation.filter { !it.validate() }.toList() } 38
  14. テストコード 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
  15. まとめ SymbolProcessor、Visitor で KSP のプロセッサーは作れる Kotlin のコード生成は KotlinPoet でやると楽 インクリメンタル処理のための依存は

    Dependencies で指定 マルチラウンド処理をするためには process メソッドで遅延したいシンボルを渡す 複数モジュールのコードを集約したい時は getDeclarationsFromPackage プロセッサーのテストは kotlin-compile-testing で 48