🦖

KSPを触ってみよう

2023/10/07に公開

前提

この記事は筆者がKSPを触ってみて知ったことをもとに書きました。

この記事から得られること

  • KSPとは
  • アノテーションプロセッサとは
  • Kspを使ってアノテーションプロセッサをつくる方法
  • 作ったアノテーションプロセッサをプロジェクトで使用する

KSPとは

Kotlin Symbol Processingの略
簡単に言うと、軽量のコンパイラプラグインの開発に使えるAPIです。
Kotlinコードを解析し、自動でコードの生成ができます。

コンパイラプラグインというと解析、生成以外にも変更や置換などができると思われるかと思います。
しかしKSPでは変更や置換はできません。
それには以下のような理由があります。

言語セマンティクスを変更するコンパイラ・プラグインは、時に非常に混乱を招くことがあります。KSPはソース・プログラムを読み取り専用として扱うことで、それを回避しています。
引用: KSP Docs

アノテーションプロセッサとは

アノテーションプロセッサとは、コンパイル時にアノテーションに基づいて、コードを解析、生成する仕組みのことです。

KSPでアノテーションプロセッサを作る

では早速KSPを使ってみましょう。
今回はAndroidProject内で使う想定で作ります。

準備

  1. 適当なAndroid Projectを用意

  2. Project内に新たにmoduleを作成します。このときJava or Kotlin Libraryを選択してください。
    この、新たに作成したmoduleでアノテーションプロセッサのを作成します。

  3. moduleにKSP APIを依存関係として追加します。

build.gradle.kts(module)
dependencies {
    implementation("com.google.devtools.ksp:symbol-processing-api:1.9.10-1.0.13")
}

アノテーションプロセッサの作成

作成に必要なコードを用意します。

.
└─ main
  ├── java
  |  └─ com.example.inject
  |    ├─ CustomProcessor
  |    ├─ CustomProcessorProvider
  |    └─ CustomAnnotation
  └── resource
     └─ META-INF
       └─ services
         └─ com.google.devtools.ksp.processing.SymbolProcessorProvider

まずアノテーションを定義します。
このアノテーションに基づいてコードの解析と生成をします。

annotation.kt
annotation class Inject

次に、SymbolProcessorを実装したクラスを用意します。
ここにコードの生成や、解析といったメインロジックを記述して行きます。

CustomSymbolProcessor.kt
class CustomSymbolProcessor(
    val codeGenerator: CodeGenerator,
    val logger: KSPLogger
) : SymbolProcessor {
    override fun processor(resolver: Resolver): List<KSAnnotated> {
        // ここにメインの処理を書く
    }
}

SymbolProcessorProviderを実装したクラスを用意します。
ここは基本的にはSymbolProcessorを実装したクラスを呼びだします。

CustomSymbolProcessorProvider.kt
class CustomSymbolProcessorProvider() : SymbolProcessorProvider {
    override fun create(environment: SymbolProcessorEnvironment): SymbolProcessor {
        return CustomSymbolProcessor(environment.codeGenerator, environment.logger)
    }
}

ここまでかけたら、

└── resource
   └─ META-INF
     └─ services
       └─ com.google.devtools.ksp.processing.SymbolProcessorProvider

CustomSymbolProcessorProviderクラスの完全修飾子を記述します。
ここまでがアノテーションプロセッサの作成方法です。

今回作成したもの

参考までに今回作成したものを紹介します。

今回は超簡易的なDIライブラリを作成しました。
アノテーションが付けられたプロパティを持つクラスに対して、依存性注入のコードを自動的に生成します。
以下が実際のコードです。

DIProcessor.kt
DIProcessorProvider.kt
annotation.kt

コードの説明

  1. アノテーションの検索
val symbols = resolver.getSymbolsWithAnnotation("com.example.inject.Inject")

processメソッド内で、特定のアノテーション(com.example.inject.Inject)が付けられたシンボルを検索しています。

  1. バリデーション
val ret = symbols.filter { !it.validate() }.toList()

検索されたシンボルをフィルタリングし、バリデーションを通過しないシンボルをリストとして返しています。バリデーションはit.validate()メソッドを使用して行われます。

  1. コードの生成
symbols.forEach { symbol ->
    if (symbol is KSPropertyDeclaration) {
        val className = symbol.parentDeclaration!!.simpleName.asString()
        val propertyName = symbol.simpleName.asString()
        val propertyType = symbol.type.resolve().declaration.qualifiedName!!.asString()
        val fqcn = symbol.parentDeclaration!!.qualifiedName?.asString()

        val file =
            codeGenerator.createNewFile(
                Dependencies.ALL_FILES,
                "com.example.injectannotation",
                "$className\$Injector"
            )
        file.bufferedWriter().use { write ->
            write.appendLine(
                """
                import $fqcn
                fun $className.inject() {
                    this.$propertyName = $propertyType()
                }
                """.trimIndent()
            )
        }
    }
}

検索されたシンボルをループし、各シンボルがKSPropertyDeclarationタイプである場合に、新しいKotlinファイルを生成します。生成されるファイルは、対象のクラスにinjectメソッドを追加し、このメソッド内でプロパティを初期化します。
ここで見て分かる通り、コードは文字列でどういうコードを生成するかを記述します。

これまでの実装をコードに表すと以下のようになります。

// 依存関係を注入したいクラス
class Main {

    // 必要な依存関係
    @Inject
    lateinit var module: Module

    init {
        // ここで依存関係を注入
        this.inject()
    }
}

// 依存関係
class Module {}

// 自動生成の注入関数
fun Main.inject() {
    this.module = Module()
}

プロジェクトで使ってみる

作成したアノテーションプロセッサを実際にプロジェクトで使ってみましょう。
使いたいプロジェクトのモジュールのbuild.gradle.ktsで以下を記述します。

build.gradle.kts
plugins {
    id("com.google.devtools.ksp") version "1.9.10-1.0.13"
}

dependencies {
    implementation(project(":{アノテーションプロセッサのモジュール名}"))
    ksp(project(":{アノテーションプロセッサのモジュール名}"))
}

あとは、依存関係を注入したいクラスのプロパティにアノテーションをつければ、記述したロジックに基づいてコードの生成などがされます。

まとめ

今までKSPというものを「なにかのライブラリを入れるときに必要になるもの」程度の認識でした。
今回KSPを使ってアノテーションを作成したことでKSPの概念や、アノテーションプロセッサを自分で作成する方法などを知ることができました。
また、今回私は簡単なDIライブラリを作成しましたが、世に存在するDIライブラリの偉大さを認識することができました。

紹介した通り、簡単に作成できるので気軽に開発してみてください。

引用・参考サイト

https://kotlinlang.org/docs/ksp-overview.html
https://github.com/google/ksp

Discussion