KSPを使ってコード生成してみる

KSP (Kotlin Symbol Processing) を使ってコード生成する方法について簡単に説明していきます。まとまりがない感じになっちゃって長くなりましたが、ぜひ実際に動かしてもらえると良いかなと思います。

注意: これを書いてる時点ではAlphaリリースされたばかりなので変更される可能性が高いです

KSP (Kotlin Symbol Processing) とは?

簡単に言うと、KSPはKAPTと似たような機能を提供しつつビルド速度が向上したものになります。

仕組みとしては、Kotlin Compiler Pluginのサブセットのようなものになっていて、Kotlin Compiler Pluginより簡単に実装できるようになっています。Kotlin Compiler Pluginではコードの改変なども出来たりしますが、KSPでコードファイルを追加するしかできないようになっています。

Multiplatformもサポートしています。(Alphaの段階で動くかはまだ試してないです)

これまでのAnnotation Processorと異なりKotlinのコードを理解できるので、Nullable/NonNullやsuspend functionなのかも簡単に分かるようになっています。

まだ有名ライブラリの対応されていないですが、今後対応されていくと思います。Roomでは2.3.0-beta02から実験的にサポートされました。

セットアップ

KSPでコード生成を実装する側はdependenciesにKSPのライブラリを追加します。

dependencies {
implementation("com.google.devtools.ksp:symbol-processing-api:1.4.30-1.0.0-alpha02")
}

KSPを使う側は以下のような感じで、KSPのプラグインを適用してdependenciesでkspで指定する感じです。

plugins {
id("com.google.devtools.ksp") version "1.4.30-1.0.0-alpha02"
}

dependencies {
ksp(...)
}

詳しくはQuickstartのドキュメントの方を見てもらえると。

SymbolProcessor

KSPを実装するには、 SymbolProcessor を継承したクラスを作成します。

initprocessfinish というメソッドを実装する必要があります。( finish は実装しなくても良くなる予定です。 > PullRequest )

init

Prcessorが初期化処理になります。渡されてくる CodeGenerator や KSPLogger などを変数に設定する感じになると思います。

また、Gradleに設定されたオプションもここで取得できるようになっっています。

ksp {
arg("option1", "value1")
arg("option2", "value2")
}

上のようにGradleにオプションを設定したものを取得するには以下のように取得できます。

process

ここで実際に既存のコードを解析して、それを元に新たにコードを生成していきます。引数で渡されてくる Resolver を使ってコードを取得することが可能です。

いくつか実装例を後述しています。

finish

finishは必要であれば実装する感じになりそうです。あんまり実装する機会はなさそうなので、オプショナルになる予定です。PullRequest

META-INF.services

Annotation Processorと同じように META-INF.services の設定が必要です。

com.google.devtools.ksp.processing.SymbolProcessor を作って、そこに実行するクラスを指定します。

サンプルをいくつか

サンプルをいくつか見ながら少し説明していきたいと思います。

単純なコード生成

これは単純にコードを生成する例です。コードを見ればだいたい分かると思います。

CodeGenerator を使ってコードファイルを生成できます。

注意としては、コード生成した場合は再び process メソッドが呼ばれるので、フラグを使って何度も実行されるのを防ぎます。この制御がないとコードが繰り返し生成されて終了しなくなります。

アノテーションからコード生成する

Resolver.getSymbolsWithAnnotation を使ってアノテーションがついてるコード情報を取得できます。

アノテーション以外にも、すべてのファイルを取得する Resolver.getAllFiles や特定のクラスから取得する Resolver.getClassDeclarationByName もあります。

この例ではクラスをつけられたアノテーションです。そのため filterIsInstance をつかって絞り込んでいます。 KSClassDeclaration はクラスを表すものになります。

何かを元にコードを生成するときは CodeGenerator.createNewFile に渡してる Dependencies が重要になります。

Dependenciesについて

CodeGenerator.createNewFile に渡す Dependencies は Incremental Processing に重要なものになります。これを正しく設定しないと意図した挙動にならないことになります。

第1引数のBooleanについて、すいませんが、まだぼくの理解足らず挙動の違いが分かりませんでした。

第2引数(可変長引数)にはそのコードファイルの生成の元となったファイル(KSFile)を渡します。KSClassDeclaration などの KSDeclaration には containingFile があるので、それから KSFile が取得できます。

例えば、複数のクラスから1つのコードファイルを生成する場合は、その複数クラスの KSFile を指定する必要があります。

ドキュメントがあるのでそちらも見てもらえると。

https://github.com/google/ksp/blob/master/docs/incremental.md

別のコード生成されたものに依存してる場合

例えば、以下のように別のProcessorで生成されたものを使用してるコードを元にコードを生成したい場合です(うまく説明できてないかも)

Processorの処理タイミングでは、Sampleクラスがまだ生成されてない可能性があります。それをうまく処理するには validate メソッドが用意されてるので、それを使用します。

validate で処理しなかったものは戻り値として返すことで、再び呼ばれたときに処理できるようになります。

Visitorパターンで処理する

KSVisitor を使うことでVisitorパターンでコード解析することもできます。(以下の例では KSVistorVoid を使ってる)

順番に定義を訪問していき処理をすることができます。

使用するときは accept を使って呼び出します。

必要に応じて使用していくと良いと思います。

書いてこなかったですが、 KSClassDeclaration.getAllFunctions のように直接メソッドを取得することもできます。

まとめ

うまくまとめられていないですが、個人的にKSPはAnnotation Processorよりだいぶ書きやすい印象です。Kotlinのことも理解してくれてるので、特に難しいことをすることなくコードを解析できます。

ビルド速度も改善されますし、今後KSPが増えてくれると嬉しいです。

Programmer / Gamer / Google Developers Expert for Android, Kotlin / @STAR_ZERO

Programmer / Gamer / Google Developers Expert for Android, Kotlin / @STAR_ZERO