Upgrade to Pro — share decks privately, control downloads, hide ads and more …

KSPの話がしたい

 KSPの話がしたい

kk__777

July 17, 2024
Tweet

More Decks by kk__777

Other Decks in Programming

Transcript

  1. kk__777 ( けーけー) ⽊村 洸太 株式会社はてな - 2023年8⽉〜 - マンガアプリ

    GigaViewer for Apps Android(2020〜) Qiita https://qiita.com/kk__777
  2. 話すこと
 - Overview
 - 仕組み
 - コード例
 - なぜ KSPを使うのか


    ~~~ ここ以下は時間があれば
 - With KMP 
 - KSP2

  3. annotation class SampleAnnotation @SampleAnnotation class Greeting { private val platform

    = getPlatform() fun greet(): String = "Hello, ${platform.name}!" }
  4. class TestProcessor( private val codeGenerator: CodeGenerator, private val kspLogger: KSPLogger,

    ) : SymbolProcessor { override fun process(resolver: Resolver): List<KSAnnotated> { resolver .getSymbolsWithAnnotation(SampleAnnotation::class.qualifiedName!!) .filterIsInstance<KSClassDeclaration>() .forEach { ksClassDeclaration -> kspLogger.logging("Found class : ${ksClassDeclaration.qualifiedName}") ksClassDeclaration.getAllProperties().forEach { ksPropertyDeclaration -> kspLogger.logging("Found class property: ${ksPropertyDeclaration.qualifiedName}") } ksClassDeclaration.getAllFunctions().forEach { ksFunctionDeclaration -> kspLogger.logging("Found class function: ${ksFunctionDeclaration.qualifiedName}") } /// ... コード生成ロジック } } } i: [ksp] Found class : Greeting i: [ksp] Found class property: Greeting.platform i: [ksp] Found class function: Greeting.greet
  5. annotation class SampleAnnotation @SampleAnnotation class Greeting { private val platform

    = getPlatform() fun greet(): String = "Hello, ${platform.name}!" }
  6. class TestProcessor( private val codeGenerator: CodeGenerator, private val kspLogger: KSPLogger,

    ) : SymbolProcessor { override fun process(resolver: Resolver): List<KSAnnotated> { resolver .getSymbolsWithAnnotation(SampleAnnotation::class.qualifiedName!!) .filterIsInstance<KSClassDeclaration>() .forEach { ksClassDeclaration -> kspLogger.logging("Found class : ${ksClassDeclaration.qualifiedName}") ksClassDeclaration.getAllProperties().forEach { ksPropertyDeclaration -> kspLogger.logging("Found class property: ${ksPropertyDeclaration.qualifiedName}") } ksClassDeclaration.getAllFunctions().forEach { ksFunctionDeclaration -> kspLogger.logging("Found class function: ${ksFunctionDeclaration.qualifiedName}") } /// ... コード生成ロジック } } }
  7. class TestProcessorProvider : SymbolProcessorProvider { override fun create( environment: SymbolProcessorEnvironment

    ):SymbolProcessor = TestProcessor(environment.codeGenerator, environment.logger) }
  8. plugins { kotlin("jvm") } ... dependencies { implementation(kotlin("stdlib")) implementation("com.squareup:javapoet:1.12.1") implementation("com.google.devtools.ksp:symbol-processing-api:$kspVersion")

    } dependencies { implementation(kotlin("stdlib")) implementation(project(":test-processor")) ksp(project(":test-processor")) } Processor側 Processorを 受け入れる側
  9. class TestProcessor( private val codeGenerator: CodeGenerator, private val kspLogger: KSPLogger,

    ) : SymbolProcessor { override fun process(resolver: Resolver): List<KSAnnotated> { resolver .getSymbolsWithAnnotation(SampleAnnotation::class.qualifiedName!!) .filterIsInstance<KSClassDeclaration>() .forEach { ksClassDeclaration -> kspLogger.logging("Found class : ${ksClassDeclaration.qualifiedName}") ksClassDeclaration.getAllProperties().forEach { ksPropertyDeclaration -> kspLogger.logging("Found class property: ${ksPropertyDeclaration.qualifiedName}") } ksClassDeclaration.getAllFunctions().forEach { ksFunctionDeclaration -> kspLogger.logging("Found class function: ${ksFunctionDeclaration.qualifiedName}") } /// ... コード生成ロジック } } }
  10. KSFile packageName: KSName fileName: String annotations: List<KSAnnotation> (File annotations) declarations:

    List<KSDeclaration> // class, interface, object KSClassDeclaration simpleName: KSName qualifiedName: KSName containingFile: String typeParameters: KSTypeParameter parentDeclaration: KSDeclaration classKind: ClassKind primaryConstructor: KSFunctionDeclaration superTypes: List<KSTypeReference> // contains inner classes, member functions, properties, etc. declarations: List<KSDeclaration> KSFunctionDeclaration // top level function simpleName: KSName qualifiedName: KSName containingFile: String typeParameters: KSTypeParameter parentDeclaration: KSDeclaration functionKind: FunctionKind extensionReceiver: KSTypeReference? returnType: KSTypeReference parameters: List<KSValueParameter> // contains local classes, local functions, local variables, etc. declarations: List<KSDeclaration> KSPropertyDeclaration // global variable simpleName: KSName qualifiedName: KSName containingFile: String typeParameters: KSTypeParameter parentDeclaration: KSDeclaration extensionReceiver: KSTypeReference? type: KSTypeReference getter: KSPropertyGetter returnType: KSTypeReference setter: KSPropertySetter parameter: KSValueParameter
  11. class TestProcessor( private val codeGenerator: CodeGenerator, private val kspLogger: KSPLogger,

    ) : SymbolProcessor { override fun process(resolver: Resolver): List<KSAnnotated> { resolver .getSymbolsWithAnnotation(SampleAnnotation::class.qualifiedName!!) .filterIsInstance<KSClassDeclaration>() .forEach { ksClassDeclaration -> kspLogger.logging("Found class : ${ksClassDeclaration.qualifiedName}") ksClassDeclaration.getAllProperties().forEach { ksPropertyDeclaration -> kspLogger.logging("Found class property: ${ksPropertyDeclaration.qualifiedName}") } ksClassDeclaration.getAllFunctions().forEach { ksFunctionDeclaration -> kspLogger.logging("Found class function: ${ksFunctionDeclaration.qualifiedName}") } /// ... コード生成ロジック } } }
  12. class Greeter(val name: String) { fun greet() { println("""Hello, $name""")

    } } fun main(vararg args: String) { Greeter(args[0]).greet() } val greeterClass = ClassName("", "Greeter") val file = FileSpec.builder("", "HelloWorld") .addType( TypeSpec.classBuilder("Greeter") .primaryConstructor( FunSpec.constructorBuilder() .addParameter("name", String::class) .build() ) .addProperty( PropertySpec.builder("name", String::class) .initializer("name") .build() ) .addFunction( FunSpec.builder("greet") .addStatement("println(%P)", "Hello, \$name") .build() ) .build() ) .addFunction( FunSpec.builder("main") .addParameter("args", String::class, VARARG) .addStatement("%T(args[0]).greet()", greeterClass) .build() ) .build() file.writeTo(System.out)
  13. interface Greeting { fun greet(): String } @Module @InstallIn(SingletonComponent::class) abstract

    class Binder { @BindsOptinalOf abstract fun binds(): Greeting } 対応
 変数
  14. override fun process(resolver: Resolver): List<KSAnnotated> { resolver .getSymbolsWithAnnotation(BindsOptionalHiltModule::class.qualifiedName!!) .filterIsInstance<KSClassDeclaration>() .forEach

    { ksClassDeclaration -> val fileSpec = FileSpec.builder( ksClassDeclaration.packageName.getQualifier(), ksClassDeclaration.simpleName.getShortName() + "Binder", ) ksClassDeclaration.accept(BindsOptionalHiltModuleVisitor(), fileSpec) fileSpec.build().writeTo(codeGenerator, Dependencies(false, ksClassDeclaration.containingFile!!)) } ...
  15. inner class BindsOptionalHiltModuleVisitor : KSDefaultVisitor<FileSpec.Builder, Unit>() { override fun visitClassDeclaration(classDeclaration:

    KSClassDeclaration, data: FileSpec.Builder) { check(classDeclaration.classKind == ClassKind.INTERFACE) { "BindsOptionalHiltModule must annotate interface" } val typeSpec = TypeSpec .classBuilder(classDeclaration.simpleName.asString() + "Binder") .addModifiers(KModifier.ABSTRACT) .addAnnotations(listOf(AnnotationSpec.builder(Module).build())) val installInAnnotationSpecBuilder = AnnotationSpec.builder(InstallIn) // InstallIn Componentの生成 classDeclaration.annotations.forEach { it.arguments.forEach { arg -> val components = arg.value as? List<*> components?.forEach { componentEnum -> val ksType = componentEnum as? KSType ?: return val componentClassName = InstallInComponent.createComponentClassName(ksType) installInAnnotationSpecBuilder .addMember("%T::class", componentClassName) } } } // abstract fun の バインド関数作成 val bindsFunSpec = FunSpec .builder("bindOptional" + classDeclaration.simpleName.asString()) .addModifiers(KModifier.ABSTRACT) .addAnnotation(AnnotationSpec.builder(BindsOptionalOf).build()) .returns(classDeclaration.toClassName()) .build() data.addType( typeSpec .addAnnotation(installInAnnotationSpecBuilder.build()) .addFunction(bindsFunSpec) .build(), ) } … 注釈はInterfaceに限る
 @Module @InstallIn(...) public abstract class GreetingBinder SingletonComponent::class
 @BindsOptionalOf public abstract fun bindOptionalGreeting(): Greeting
  16. KSP
 - compiler の 変更を 隠蔽するようにデザインされている - KSPのAPIを通じてのみ Compiler にアクセス

    - 「できないこと」 とトレードオフに シンプルなmeta programming が可能
  17. Processor は JVMターゲット 
 plugins { alias(libs.plugins.kotlinMultiplatform) } kotlin {

    jvm() sourceSets { jvmMain.dependencies { implementation(libs.ksp.poet) implementation(libs.ksp.poet.extension) implementation(libs.ksp.symbol.processing.api) implementation(projects.annotations) } } }
  18. Processor を受け入れる側は.. plugins { alias(libs.plugins.kotlinMultiplatform) alias(libs.plugins.androidLibrary) alias(libs.plugins.ksp) } kotlin {

    wasmJs { ... } androidTarget { ... } iosTargets.forEach { ... } jvm { ... } sourceSets { commonMain.dependencies { // put your Multiplatform dependencies here implementation(projects.annotations) } } } dependencies { add("kspCommonMainMetadata", projects.testProcessor) add("kspJvm", projects.testProcessor) add("kspAndroid", projects.testProcessor) add("kspWasmJs", projects.testProcessor) add("kspIosX64", projects.testProcessor) add("kspIosArm64", projects.testProcessor) add("kspIosSimulatorArm64", projects.testProcessor) }
  19. AndroidMain iOS JVM WasmJS - Greeting は CommonMainの ソースから生成 -

    *Platformはプラットフォームごと のソースから生成
  20. • CommonMainにコード生成ができな い#567 
 • KotlinNative は 根っこのターゲット の ソースセットで生成される

    #929
 今の所この辺が 制約になっている https://qiita.com/kk__777/items/f6a95932990040586ea2
  21. KSP2 is a new implementation of the KSP API. Unlike

    KSP 1.x, it is no longer a compiler plugin and is built on the same set of Kotlin compiler APIs shared with IntelliJ IDEA, Android Lint, etc. Compared to the compiler-plugin approach, this allows a finer control of the program life cycle, simplifies KSP’s implementation and is more efficient.
 https://github.com/google/ksp/blob/main/docs/ksp2.md
 Compiler Plugin ではなく、AndroidLintやIntellJ IDEA で共有されてる Kotlin compiler APIs 上で ビルドされる とのこと

  22. たとえば main関数から呼び出してみるとこんな感じ fun main() { // Step 1: Load processors

    val classpath = listOf("test-processor/build/classes/kotlin/main") val processorClassloader = URLClassLoader(classpath.map { File(it).toURI().toURL() }.toTypedArray()) val processorProviders = ServiceLoader.load( processorClassloader.loadClass("com.google.devtools.ksp.processing.SymbolProcessorProvider"), processorClassloader ).toList() as List<SymbolProcessorProvider> // Step 2: Implement or use KSPLogger val logger = KspGradleLogger(KspGradleLogger.LOGGING_LEVEL_INFO) // Step 3: Fill KSPConfig val kspConfig = KSPJvmConfig.Builder().apply { moduleName = "mainsrc" sourceRoots = listOf(File("mainsrc/src/main/kotlin")) kotlinOutputDir = File("mainsrc/build/generated/ksp/main/kotlin") javaOutputDir = File("mainsrc/build/generated/ksp/main/java") jvmTarget = "1.8" projectBaseDir = File(".") outputBaseDir = File("mainsrc/build/generated/ksp/main") cachesDir = File("mainsrc/build/generated/ksp/main/caches") classOutputDir = File("mainsrc/build/generated/ksp/main/classes") resourceOutputDir = File("mainsrc/build/generated/ksp/main/resources") apiVersion = "2.0.0-1.0.23" languageVersion = "2.0" }.build() // Step 4: Run KotlinSymbolProcessing val exitCode = KotlinSymbolProcessing(kspConfig, processorProviders, logger).execute() println("KSP finished with exit code: $exitCode") } https://qiita.com/kk__777/items/fd3af8eb11ade9677a4e
  23. - 他にもAPI変更点がある 詳しくは👇 https://github.com/google/ksp/blob/main/docs/ksp2api.md
 - KSPからの移行は 2.0 のうちは 容易 2.1以降は

    KSP を 追従させる よりは KSP2を使ってね といったこ とが書かれている https://github.com/google/ksp/blob/main/docs/ksp2.md#ksp1-deprec ation-schedule