執筆者: オキリョウ
最終更新: 2023/04/22
プログラマーとして活動していると、コードの自動生成に憧れを抱くことがあるのではないだろうか?
自動生成をすることで作業の効率化を図ることができることはもちろん、技術マウントを取る際にも非常に役に立つ。
しかし、自動生成なんてどうやってやるのだろうか。
例えば静的言語の場合、コンパイル時に依存関係が解決されないと実行すらできない。
つまり「ソースコードを吐き出すアプリ」を作る必要がある。これは面倒である。
より簡単に自動生成をしてみたい。
そんな人にお勧めしたいのが「Kotlin Symbol Processing」、通称KSPである。
Kotlin Symbol Processing(以下KSP)は、軽量なコンパイラプラグイン(もしくはプリプロセッサ)の1つ。
アノテーション情報等を元にコードの生成を行うプリプロセッサ、つまりコンパイルの前に走るプログラムを作成することができる。
大体以下のような流れで処理が進む。
1. KSPで作成したプリプロセッサがアノテーションを読み取り、解析する
2. 解析情報を元に、ソースコード等を生成する
3. 生成されたコードも含めてコンパイルされる
実はKSP以外にもプリプロセッサ作成用ライブラリは存在する。「kapt(JavaのアノテーションプロセッサをKotlinで使えるようにしたもの)」および「kotlinc compiler plugins」である。
しかし、以下の理由からKSPの方がおススメである。
kapt
よりパフォーマンスがいい(Gildeの場合、コンパイル時間を25%削減)いいとこ尽くしのように見えるKSPだが、当然欠点も欠点も存在する。
要は細かいことができない、Javaの遺産が使えないということである。
細かいことをしたいならkotlinc compiler plugins、Javaの遺産を使いたいならkaptを利用する必要がある。
KSPについて一通り説明したところでざっくりした使い方を紹介する。
簡単に以下のステップに分かれる。
なお、KSPを用いて「コンパイル前に動くライブラリ」を作るため、別のモジュール、もしくは別のプロジェクトに切り出す必要がある。
それでは1つずつ見ていく。
まずはKSPを使えるようにライブラリを追加する必要がある。build.gradle.kts
に以下を記述する。
dependencies {
implementation("com.google.devtools.ksp:symbol-processing-api:1.8.10-1.0.9")
}
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というインターフェイスが用意されているため、こちらを実装する。
こちらは2で作ったクラスのFactoryであり、インスタンスの生成方法を書けばいい。
例
class SampleProcessorProvider : SymbolProcessorProvider {
override fun create(environment: SymbolProcessorEnvironment): SymbolProcessor =
SampleProcessor(
codeGenerator = environment.codeGenerator,
)
}
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
この人が書いた記事