Save 37% off PRO during our Black Friday Sale! »

Kotlin Symbol Processing (KSP) を使ったコード生成 / DroidKaigi 2021

80a3a3857a55f154d23acb705eff72cc?s=47 star_zero
October 19, 2021

Kotlin Symbol Processing (KSP) を使ったコード生成 / DroidKaigi 2021

80a3a3857a55f154d23acb705eff72cc?s=128

star_zero

October 19, 2021
Tweet

Transcript

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

  2. About me • Kenji Abe • Google Developers Expert for

    Android, Kotlin • AndroidDagashi • DeNA Co., Ltd. • @STAR_ZERO
  3. Kotlin Symbol Processing (KSP) • https://github.com/google/ksp • Kotlinの軽量コンパイラプラグインを開発できるツール • KAPTに似た機能を提供できる(コード生成)

    • KAPTに比べて高速 • Kotlinのコードを直接解析できる • Kotlin Multiplatformのサポート
  4. 基本構成

  5. None
  6. None
  7. None
  8. None
  9. dependencies { implementation("com.google.devtools.ksp:symbol-processing-api:1.5.30-1.0.0") }

  10. None
  11. com.example.processor.HelloWorldProcessorProvider

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

    ksp(project(":processor")) }
  13. 実装詳細

  14. SymbolProcessorProvider

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

    ◦ CodeGenerator ◦ Logger ◦ Option
  16. class HelloWorldProcessorProvider : SymbolProcessorProvider { override fun create( environment: SymbolProcessorEnvironment

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

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

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

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

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

    ): SymbolProcessor { return HelloWorldProcessor( environment.codeGenerator, environment.options, environment.logger ) } } // build.gradle.kts ksp { arg("key", "value") }
  22. 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
  23. SymbolProcessor

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

  25. class HelloWorldProcessor( private val codeGenerator: CodeGenerator, private val options: Map<String,

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

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

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

    String>, private val logger: KSPLogger ) : SymbolProcessor { override fun process(resolver: Resolver): List<KSAnnotated> { // } }
  29. コード解析

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

  31. override fun process(resolver: Resolver): List<KSAnnotated> { val files = resolver.getAllFiles()

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

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

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

    symbols.forEach { when (it) { is KSClassDeclaration -> { } is KSFunctionDeclaration -> { } is KSPropertyDeclaration -> { } } } // ... }
  35. https://github.com/google/ksp/blob/main/README.md#how-ksp-looks-at-source-files

  36. CodeGenerator

  37. override fun process(resolver: Resolver): List<KSAnnotated> { val file = codeGenerator.createNewFile(

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

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

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

    Dependencies(false), "com.example.generated", "Hello" ) val code = """ ... """.trimIndent() file.write(code.toByteArray()) file.close() // ... }
  41. val file = codeGenerator.createNewFile( Dependencies(false), "hoge/foo", "plain", "txt" )

  42. Multiple round processing

  43. // Hoge.kt @Factory class Hoge // Foo.kt @Factory class Foo

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

    // etc... // SymbolProcessor#process resolver.getSymbolsWithAnnotation("...") // => [Hoge, Foo]
  45. // 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 コード生成
  46. // Hoge.kt @Factory class Hoge // Foo.kt @Factory class Foo

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

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

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

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

  51. override fun process(resolver: Resolver): List<KSAnnotated> { val symbols = resolver.getSymbolsWithAnnotation("...")

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

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

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

    val result = symbols.filter { !it.validate() }.toList() symbols.filter { it.validate() }.forEach { symbol -> // コード生成 } return result }
  55. Resolver • getAllFiles ◦ 全てのラウンドでの処理対象となったファイル • getNewFiles ◦ ラウンドごとに処理対象となっているファイル •

    getSymbolsWithAnnotation ◦ ラウンドごとに処理対象となっているものから、該当の Annotationがあるもの • getClassDeclarationByName ◦ クラスパスから指定したクラス名を探す
  56. Incremental processing

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

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

  59. Hoge.kt Foo.kt SymbolProcessor HogeFactory.kt FooFactory.kt Hoge.kt Foo.kt Bar.kt Changed New

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

    New Clean Build 差分 Build
  61. 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
  62. val symbols = resolver.getSymbolsWithAnnotation("...") symbols.forEach { val file = codeGenerator.createNewFile(

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

    Dependencies(aggregating = false, it.containingFile!!), "...", "..." ) // コード生成 }
  64. Dependencies(aggregating = false) Hoge.kt Foo.kt SymbolProcessor

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

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

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

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

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

  70. KSP対応状況

  71. 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
  72. Thank you