トップ画像
KSPで自動生成コードを作る

執筆者: オキリョウ

最終更新: 2023/04/22

導入

プログラマーとして活動していると、コードの自動生成に憧れを抱くことがあるのではないだろうか?
自動生成をすることで作業の効率化を図ることができることはもちろん、技術マウントを取る際にも非常に役に立つ。
しかし、自動生成なんてどうやってやるのだろうか。
例えば静的言語の場合、コンパイル時に依存関係が解決されないと実行すらできない。

つまり「ソースコードを吐き出すアプリ」を作る必要がある。これは面倒である。

より簡単に自動生成をしてみたい。
そんな人にお勧めしたいのが「Kotlin Symbol Processing」、通称KSPである。

概要

Kotlin Symbol Processing(以下KSP)は、軽量なコンパイラプラグイン(もしくはプリプロセッサ)の1つ。
アノテーション情報等を元にコードの生成を行うプリプロセッサ、つまりコンパイルの前に走るプログラムを作成することができる。

大体以下のような流れで処理が進む。
1. KSPで作成したプリプロセッサがアノテーションを読み取り、解析する


2. 解析情報を元に、ソースコード等を生成する


3. 生成されたコードも含めてコンパイルされる

利点

実はKSP以外にもプリプロセッサ作成用ライブラリは存在する。「kapt(JavaのアノテーションプロセッサをKotlinで使えるようにしたもの)」および「kotlinc compiler plugins」である。
しかし、以下の理由からKSPの方がおススメである。

  • 他のコンパイラプラグインと比べて、JVMのバージョンなどに依存しない
  • Javaのプラグインを利用するkaptよりパフォーマンスがいい(Gildeの場合、コンパイル時間を25%削減)
  • kotlinc compiler pluginを使う時よりも簡単にかけて、またAPIの変更がないように努力してくれるらしい
  • JVMに依存しないため、他のプラットフォームでも利用可能
  • 公式がKSPを使うように推奨している


欠点

いいとこ尽くしのように見えるKSPだが、当然欠点も欠点も存在する。

  • ソースコードの編集は不可能
  • ソースコードを式レベルで解析することは不可能
  • Java Annotation Processing APIで書かれたものに対する互換性はない


要は細かいことができない、Javaの遺産が使えないということである。
細かいことをしたいならkotlinc compiler plugins、Javaの遺産を使いたいならkaptを利用する必要がある。

実装方法

KSPについて一通り説明したところでざっくりした使い方を紹介する。
簡単に以下のステップに分かれる。

  1. 依存関係を追加
  2. SymbolProcessorを実装する
  3. SymbolProcessorProviderを実装する
  4. META-INFに登録する

なお、KSPを用いて「コンパイル前に動くライブラリ」を作るため、別のモジュール、もしくは別のプロジェクトに切り出す必要がある。
それでは1つずつ見ていく。

依存関係を追加

まずはKSPを使えるようにライブラリを追加する必要がある。
build.gradle.ktsに以下を記述する。

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

 

SymbolProcessorを実装する。

KSPではSymbolProcessorというインターフェイスが用意されている。
プリプロセッサで処理する内容はここに記述することになる。

 interface SymbolProcessor {
 
     // コード生成時に呼ばれる部分で、コードが生成されなくなるまで呼ばれ続ける
     fun process(resolver: Resolver): List<KSAnnotated>
     
     // 終了時に呼ばれるコード
     fun finish() {}
     
     // processでのエラー時に呼ばれるコード
     fun onError() {}
 }


こちらを実装する。

実装例:プロジェクト内の関数名だけ取り出してファイルに書くコード

class SampleProcessor(
    private val codeGenerator: CodeGenerator,
) : SymbolProcessor {

    override fun process(resolver: Resolver): List<KSAnnotated> {
        val symbols = resolver.getNewFiles()
        symbols.forEach { file ->
            val output = file.declarations
                .filterIsInstance<KSFunctionDeclaration>()
                .joinToString("\n") { it.simpleName.asString() }
            codeGenerator.createNewFile(
                dependencies = Dependencies(false, file),
                packageName = file.packageName.asString(),
                fileName = file.fileName,
                extensionName = "",
            ).write(output.toByteArray())
        }
        return emptyList()
    }
}


SymbolProcessorProviderを実装する

SymbolProcessorProviderというインターフェイスが用意されているため、こちらを実装する。
こちらは2で作ったクラスのFactoryであり、インスタンスの生成方法を書けばいい。

 class SampleProcessorProvider : SymbolProcessorProvider {
     override fun create(environment: SymbolProcessorEnvironment): SymbolProcessor =
        SampleProcessor(
            codeGenerator = environment.codeGenerator,
        )
 }


META-INFに登録する

3で作成したSymbolProcessorProviderの実装クラスをMETA-INFに登録する。
登録場所はresources/META-INF/services/com.google.devtools.ksp.processing.SymbolProcessorProvider
ここに実装クラスのルートモジュールからのパス(qualified name)を記述しておく。

com.example.SampleProcessorProvider


これで完成となる。あとは適応したいプロジェクトで依存関係を追記すればよい。

 plugins {
     id("com.google.devtools.ksp") version "1.8.10-1.0.9"
     kotlin("jvm")
 }
 
 dependencies {
     implementation(project(":sample-processor"))
     ksp(project(":sample-processor"))
 }


最後に

自動生成の中では簡単・・・とはいえ少し難易度が高めではある。
それでも自動生成のコードが書けるようになることでより効率的にプログラムを書けるようになると思う。
是非一度挑戦していただきたい。

参考文献

公式ページ

https://kotlinlang.org/docs/ksp-overview.html

より細かく書いた記事

https://scrapbox.io/wansukolll-82925906/KotlinSymbolProcessing_API%E3%81%AB%E3%81%A4%E3%81%84%E3%81%A6

分かりやすく作ったスライド

https://speakerdeck.com/bugdog24/kspdezi-dong-sheng-cheng-kodowozuo-ru

取得に失敗しました

2020年度 入部

Twitter GitHub