1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

KSPことはじめ

Posted at

KSPについて導入を学んだので記事にします。
公式ドキュメントを読んでのまとめです。KotlinPoetとの併用を一部書きますが、詳しくはドキュメントをみてくださいm(_ _)m

概要

軽量なコンパイラ・プラグインを開発するためのAPI

クラス、クラス・メンバー、関数、関連するパラメータにアクセスし、解析してコードを生成することができる。

メリット

  1. KSPはKotlinのコンパイラと直接統合されいるため、Javaのアノテーションプロセッサやkaptよりも高速に動作
  2. Kotlinの型システムにより、型の安全性を保持しながらコード生成や解析を行うことができるとのこと。

デメリット

  • コンパイラの背景知識や、特定のコンパイラの実装の詳細にある程度精通している必要がある
  • プラグインは特定のコンパイラのバージョンと密接に結びついていることが多いため、コンパイラのバージョンアップは都度プラグインを更新を必要とすることが多い

( が、KSPはコンパイラの変更を隠蔽するように設計されているとのことなので、メンテのコストは他のコンパイラプラグインより低いことを謳っている )

他のコンパイラプラグインとの比較

  • kotlinc

kotlincとの比較。KSPはkotlincのサブセット。kotlincではできてKSPではできないことがある代わりに、一般的なユースケースを満たそうとしている。

例えば、kotlincでは、コードの改変ができるがKSPではできない。KSPで提供している主な機能は、アノテーションなどのシンボルからコードを生成することに絞られている

KSPはソースコードの式レベルの情報を調査したり、ソースコードを動的に変更したりすることはできず、そもそもそれを目的にしてない。

  • reflection

reflectionとの比較 。KSPは型参照を明示的に解決する必要がある。reflectionより型安全によっている。

  • kapt

kaptとの比較。kaptはJavaアノテーションプロセッサを使用しているのでJVMに縛られる。KSPはJVMに縛られないビルドパフォーマンスの向上、より慣用的なKotlin API、Kotlin専用のシンボルを理解できる。

要するに、kaptよりもKotlinに最適化されている。

使用方法

エントリポイントとなるSymbolProcessorProviderを定義

interface SymbolProcessorProvider {
    fun create(environment: SymbolProcessorEnvironment): SymbolProcessor
}

このProviderはMETAINFを定義してコンパイラに伝えるとコード生成を行なってくれる。

SymbolProcessorを実装する

interface SymbolProcessor {
    fun process(resolver: Resolver): List<KSAnnotated>
    fun finish() {}
    fun onError() {}
}

processに処理を記述する

  • Resolverはシンボル、ソースコード内の各要素(クラス、関数、プロパティ、型パラメータ、コンストラクタなど。)にアクセスするためのインターフェース
  • 以下のように、MyAnnotaionがついたシンボル( KSAnnotated)を取得したりできる
override fun process(resolver: Resolver): List<KSAnnotated> {
    val symbols = resolver.getSymbolsWithAnnotation("com.example.MyAnnotation")
    symbols.forEach { symbol ->
        // 何らかの処理
    }
    return emptyList() // この例では空のリストを返していますが、実際には処理すべきアノテーションを含むシンボルのリストを返すことができます。
}

シンボルはKSNode という基底インターフェースがあり、

interface KSNode {
    val origin: Origin
    val location: Location
    val parent: KSNode?
    fun <D, R> accept(visitor: KSVisitor<D, R>, data: D): R
}

対象のsymbolをKSVisiterを用いてコード生成処理をacceptできる。これは以下サンプルのBuilderVisitorが良い例で、サンプルではクラスのビルダーをKSPで生成している。

ちなみに、ここの設計思想はVisiterパターン  というGofのものがベース

KSVisiterの訪問処理をざっくりまとめると以下になる

defaultのハンドラ。
defaultHandler(node: KSNode, data: D): R

アノテションが付与されたシンボルを訪問する際の処理。
visitAnnotated(annotated: KSAnnotated, data: D): R

クラス宣言を訪問する際の処理。
visitClassDeclaration(classDeclaration: KSClassDeclaration, data: D): R

関数宣言を訪問する際の処理。
visitFunctionDeclaration(function: KSFunctionDeclaration, data: D): R

プロパティ宣言を訪問する際の処理。
visitPropertyDeclaration(property: KSPropertyDeclaration, data: D): R

タイプエイリアスを訪問する際の処理。
visitTypeAlias(typeAlias: KSTypeAlias, data: D): R

タイプパラメタを訪問する際の処理。
visitTypeParameter(typeParameter: KSTypeParameter, data: D): R

関数やコンストラクタの値パラメタを訪問する際の処理。
visitValueParameter(valueParameter: KSValueParameter, data: D): R

各シンボルへの参照方法は、公式でダイアグラムをまとめてくれているので参考になる

Something went wrong

Kotlin Poet

KotlinPoet - KotlinPoet

というKotlinのコードをプログラム的に生成するための強力なライブラリがあり、以下のようなに(比較的)型安全にコード生成用のコードが書ける

val helloWorld = TypeSpec.classBuilder("HelloWorld")
    .addFunction(FunSpec.builder("printHello")
        .addStatement("println(%S)", "Hello, KotlinPoet!")
        .build())
    .build()

val file = FileSpec.builder("com.example", "HelloWorld")
    .addType(helloWorld)
    .build()

file.writeTo(System.out)

// 上記から以下を生成できる

package com.example

class HelloWorld {
    fun printHello() {
        println("Hello, KotlinPoet!")
    }
}

これと組み合わせると、Visiter内を型安全に生成コードを書きつつ、可読性が上がる。

サンプルのBuilderVisitorのvisitFunctionDeclarationをKotlin Poetで書き直してみると以下になる

val parent = function.parentDeclaration as KSClassDeclaration
val packageName = parent.containingFile!!.packageName.asString()
val className = "${parent.simpleName.asString()}Builder"

val fileSpecBuilder = FileSpec.builder(packageName, className)
    .addImport("HELLO", "")

val classBuilder = TypeSpec.classBuilder(className)

function.parameters.forEach { parameter ->
    val name = parameter.name!!.asString()
    val typeName = parameter.type.resolve().toTypeName()

    // Property
    classBuilder.addProperty(
        PropertySpec.builder(name, typeName.copy(nullable = true))
            .mutable(true)
            .initializer("null")
            .build()
    )

    // Function
    classBuilder.addFunction(
        FunSpec.builder("with${name.replaceFirstChar { it.uppercase() }}")
            .returns(ClassName(packageName, className))
            .addParameter(name, typeName)
            .addStatement("this.$name = $name")
            .addStatement("return this")
            .build()
    )
}

// Build function
classBuilder.addFunction(
    FunSpec.builder("build")
        .returns(ClassName(packageName, parent.simpleName.asString()))
        .addStatement(
            "return %T(${function.parameters.joinToString { "${it.name!!.asString()}!!" }})",
            ClassName(packageName, parent.simpleName.asString())
        )
        .build()
)

fileSpecBuilder.addType(classBuilder.build())

val fileSpec = fileSpecBuilder.build()
fileSpec.writeTo(codeGenerator, Dependencies(true, function.containingFile!!))

終わり

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?