From 5fd5e68a0232211dee72929acb649a6e999387c7 Mon Sep 17 00:00:00 2001 From: Alexander Sysoev Date: Tue, 25 Jun 2024 22:47:38 +0200 Subject: [PATCH 1/5] Introduce K2 plugin --- compiler-plugin/build.gradle.kts | 2 + .../compiler-plugin-common/build.gradle.kts | 18 + .../core/kotlinx/rpc/codegen/common/Names.kt | 29 ++ .../compiler-plugin-k2/build.gradle.kts | 27 ++ .../kotlinx/rpc/codegen/FirGenerationKeys.kt | 48 ++ .../rpc/codegen/FirRPCExtensionRegistrar.kt | 19 + .../rpc/codegen/FirRPCServiceGenerator.kt | 436 ++++++++++++++++++ ...erializationFirResolveExtensionDelegate.kt | 31 ++ .../rpc/codegen/serialization/annotation.kt | 44 ++ compiler-plugin/settings.gradle.kts | 3 + .../extension/RPCDeclarationScanner.kt | 21 +- .../rpc/codegen/extension/RPCIrContext.kt | 2 +- .../rpc/codegen/extension/RPCStubGenerator.kt | 16 +- .../kotlinx/rpc/codegen/RPCCompilerPlugin.kt | 2 + core/build.gradle.kts | 2 - .../rpc/internal/WithRPCStubObject.jvm.kt | 2 +- .../compiler-specific-module.gradle.kts | 14 +- .../rpc/KotlinCompilerPluginBuilder.kt | 2 +- .../kotlin/kotlinx/rpc/RPCGradlePlugin.kt | 12 +- .../kotlin/kotlinx/rpc/compilerPlugins.kt | 36 +- gradle.properties | 3 + gradle/libs.versions.toml | 1 + 22 files changed, 714 insertions(+), 56 deletions(-) create mode 100644 compiler-plugin/compiler-plugin-common/build.gradle.kts create mode 100644 compiler-plugin/compiler-plugin-common/src/main/core/kotlinx/rpc/codegen/common/Names.kt create mode 100644 compiler-plugin/compiler-plugin-k2/build.gradle.kts create mode 100644 compiler-plugin/compiler-plugin-k2/src/main/core/kotlinx/rpc/codegen/FirGenerationKeys.kt create mode 100644 compiler-plugin/compiler-plugin-k2/src/main/core/kotlinx/rpc/codegen/FirRPCExtensionRegistrar.kt create mode 100644 compiler-plugin/compiler-plugin-k2/src/main/core/kotlinx/rpc/codegen/FirRPCServiceGenerator.kt create mode 100644 compiler-plugin/compiler-plugin-k2/src/main/core/kotlinx/rpc/codegen/serialization/SerializationFirResolveExtensionDelegate.kt create mode 100644 compiler-plugin/compiler-plugin-k2/src/main/core/kotlinx/rpc/codegen/serialization/annotation.kt diff --git a/compiler-plugin/build.gradle.kts b/compiler-plugin/build.gradle.kts index 196cd9f1..f64434c4 100644 --- a/compiler-plugin/build.gradle.kts +++ b/compiler-plugin/build.gradle.kts @@ -24,6 +24,8 @@ kotlin { dependencies { compileOnly(libs.kotlin.compiler.embeddable) + implementation(projects.compilerPluginK2) + implementation(projects.compilerPluginCommon) } configureMetaTasks("cleanTest", "test") diff --git a/compiler-plugin/compiler-plugin-common/build.gradle.kts b/compiler-plugin/compiler-plugin-common/build.gradle.kts new file mode 100644 index 00000000..5c1f0a66 --- /dev/null +++ b/compiler-plugin/compiler-plugin-common/build.gradle.kts @@ -0,0 +1,18 @@ +/* + * Copyright 2023-2024 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + */ + +import org.jetbrains.kotlin.gradle.dsl.ExplicitApiMode + +plugins { + alias(libs.plugins.conventions.jvm) + alias(libs.plugins.compiler.specific.module) +} + +kotlin { + explicitApi = ExplicitApiMode.Disabled +} + +dependencies { + compileOnly(libs.kotlin.compiler.embeddable) +} diff --git a/compiler-plugin/compiler-plugin-common/src/main/core/kotlinx/rpc/codegen/common/Names.kt b/compiler-plugin/compiler-plugin-common/src/main/core/kotlinx/rpc/codegen/common/Names.kt new file mode 100644 index 00000000..ed1b09e6 --- /dev/null +++ b/compiler-plugin/compiler-plugin-common/src/main/core/kotlinx/rpc/codegen/common/Names.kt @@ -0,0 +1,29 @@ +/* + * Copyright 2023-2024 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.rpc.codegen.common + +import org.jetbrains.kotlin.name.ClassId +import org.jetbrains.kotlin.name.FqName +import org.jetbrains.kotlin.name.Name + +object ClassDeclarations { + val rpcInterface = ClassId(FqName("kotlinx.rpc"), Name.identifier("RPC")) + + val serializableAnnotation = ClassId(FqName("kotlinx.serialization"), Name.identifier("Serializable")) + val contextualAnnotation = ClassId(FqName("kotlinx.serialization"), Name.identifier("Contextual")) + + val flow = ClassId(FqName("kotlinx.coroutines.flow"), Name.identifier("Flow")) + val sharedFlow = ClassId(FqName("kotlinx.coroutines.flow"), Name.identifier("SharedFlow")) + val stateFlow = ClassId(FqName("kotlinx.coroutines.flow"), Name.identifier("StateFlow")) +} + +object RpcNames { + val SERVICE_STUB_NAME: Name = Name.identifier("\$rpcServiceStub") + + const val METHOD_NAME_SUFFIX = "\$rpcMethod" +} + +val Name.rpcMethodClassName: Name get() = Name.identifier("$identifier${RpcNames.METHOD_NAME_SUFFIX}") +val Name.rpcMethodName: Name get() = Name.identifier(identifier.removeSuffix(RpcNames.METHOD_NAME_SUFFIX)) diff --git a/compiler-plugin/compiler-plugin-k2/build.gradle.kts b/compiler-plugin/compiler-plugin-k2/build.gradle.kts new file mode 100644 index 00000000..59768498 --- /dev/null +++ b/compiler-plugin/compiler-plugin-k2/build.gradle.kts @@ -0,0 +1,27 @@ +/* + * Copyright 2023-2024 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + */ + +import org.jetbrains.kotlin.gradle.dsl.ExplicitApiMode +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile + +plugins { + alias(libs.plugins.conventions.jvm) + alias(libs.plugins.compiler.specific.module) +} + +kotlin { + explicitApi = ExplicitApiMode.Disabled +} + +dependencies { + compileOnly(libs.kotlin.compiler.embeddable) + compileOnly("org.jetbrains.kotlin:kotlin-serialization-compiler-plugin:1.9.24") + implementation(projects.compilerPluginCommon) +} + +tasks.withType().configureEach { + kotlinOptions { + freeCompilerArgs += "-Xcontext-receivers" + } +} diff --git a/compiler-plugin/compiler-plugin-k2/src/main/core/kotlinx/rpc/codegen/FirGenerationKeys.kt b/compiler-plugin/compiler-plugin-k2/src/main/core/kotlinx/rpc/codegen/FirGenerationKeys.kt new file mode 100644 index 00000000..ebe25d34 --- /dev/null +++ b/compiler-plugin/compiler-plugin-k2/src/main/core/kotlinx/rpc/codegen/FirGenerationKeys.kt @@ -0,0 +1,48 @@ +/* + * Copyright 2023-2024 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.rpc.codegen + +import org.jetbrains.kotlin.GeneratedDeclarationKey +import org.jetbrains.kotlin.fir.declarations.FirDeclarationOrigin +import org.jetbrains.kotlin.fir.symbols.FirBasedSymbol +import org.jetbrains.kotlin.fir.symbols.impl.FirClassSymbol +import org.jetbrains.kotlin.fir.symbols.impl.FirFunctionSymbol +import org.jetbrains.kotlin.name.Name +import org.jetbrains.kotlinx.serialization.compiler.fir.SerializationPluginKey + +internal class RPCGeneratedStubKey( + private val serviceName: Name, + val functions: List>, +) : GeneratedDeclarationKey() { + override fun toString(): String { + return "RPCGeneratedStubKey.$serviceName" + } +} + +internal val FirBasedSymbol<*>.generatedRpcServiceStubKey: RPCGeneratedStubKey? get() = + (origin as? FirDeclarationOrigin.Plugin)?.key as? RPCGeneratedStubKey + +internal class RPCGeneratedRpcMethodClassKey( + val rpcMethod: FirFunctionSymbol<*>, +) : GeneratedDeclarationKey() { + val isObject = rpcMethod.valueParameterSymbols.isEmpty() + + override fun toString(): String { + return "RPCGeneratedRpcMethodClassKey.${rpcMethod.name}" + } +} + +internal val FirBasedSymbol<*>.generatedRpcMethodClassKey: RPCGeneratedRpcMethodClassKey? get() = + (origin as? FirDeclarationOrigin.Plugin)?.key as? RPCGeneratedRpcMethodClassKey + +internal object FirRpcServiceStubCompanionObject : GeneratedDeclarationKey() { + override fun toString(): String { + return "FirRpcServiceStubCompanionObject" + } +} + +internal val FirClassSymbol<*>.isFromSerializationPlugin: Boolean get() { + return (origin as? FirDeclarationOrigin.Plugin)?.key is SerializationPluginKey +} diff --git a/compiler-plugin/compiler-plugin-k2/src/main/core/kotlinx/rpc/codegen/FirRPCExtensionRegistrar.kt b/compiler-plugin/compiler-plugin-k2/src/main/core/kotlinx/rpc/codegen/FirRPCExtensionRegistrar.kt new file mode 100644 index 00000000..40de0b36 --- /dev/null +++ b/compiler-plugin/compiler-plugin-k2/src/main/core/kotlinx/rpc/codegen/FirRPCExtensionRegistrar.kt @@ -0,0 +1,19 @@ +/* + * Copyright 2023-2024 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.rpc.codegen + +import org.jetbrains.kotlin.cli.common.CLIConfigurationKeys +import org.jetbrains.kotlin.cli.common.messages.MessageCollector +import org.jetbrains.kotlin.config.CompilerConfiguration +import org.jetbrains.kotlin.fir.extensions.FirExtensionRegistrar +import org.jetbrains.kotlin.fir.extensions.FirDeclarationGenerationExtension.Factory as GFactory + +class FirRPCExtensionRegistrar(private val configuration: CompilerConfiguration) : FirExtensionRegistrar() { + override fun ExtensionRegistrarContext.configurePlugin() { + val logger = configuration.get(CLIConfigurationKeys.MESSAGE_COLLECTOR_KEY, MessageCollector.NONE) + + +GFactory { FirRPCServiceGenerator(it, logger) } + } +} diff --git a/compiler-plugin/compiler-plugin-k2/src/main/core/kotlinx/rpc/codegen/FirRPCServiceGenerator.kt b/compiler-plugin/compiler-plugin-k2/src/main/core/kotlinx/rpc/codegen/FirRPCServiceGenerator.kt new file mode 100644 index 00000000..3e066789 --- /dev/null +++ b/compiler-plugin/compiler-plugin-k2/src/main/core/kotlinx/rpc/codegen/FirRPCServiceGenerator.kt @@ -0,0 +1,436 @@ +/* + * Copyright 2023-2024 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.rpc.codegen + +import kotlinx.rpc.codegen.common.ClassDeclarations +import kotlinx.rpc.codegen.common.RpcNames +import kotlinx.rpc.codegen.common.rpcMethodClassName +import kotlinx.rpc.codegen.common.rpcMethodName +import kotlinx.rpc.codegen.serialization.addAnnotation +import kotlinx.rpc.codegen.serialization.generateCompanionDeclaration +import kotlinx.rpc.codegen.serialization.generateSerializerImplClass +import org.jetbrains.kotlin.cli.common.messages.MessageCollector +import org.jetbrains.kotlin.descriptors.ClassKind +import org.jetbrains.kotlin.descriptors.Modality +import org.jetbrains.kotlin.descriptors.Visibilities +import org.jetbrains.kotlin.fir.FirSession +import org.jetbrains.kotlin.fir.containingClassForStaticMemberAttr +import org.jetbrains.kotlin.fir.declarations.utils.isInterface +import org.jetbrains.kotlin.fir.declarations.utils.visibility +import org.jetbrains.kotlin.fir.extensions.FirDeclarationGenerationExtension +import org.jetbrains.kotlin.fir.extensions.MemberGenerationContext +import org.jetbrains.kotlin.fir.extensions.NestedClassGenerationContext +import org.jetbrains.kotlin.fir.moduleData +import org.jetbrains.kotlin.fir.plugin.* +import org.jetbrains.kotlin.fir.scopes.impl.declaredMemberScope +import org.jetbrains.kotlin.fir.scopes.processAllFunctions +import org.jetbrains.kotlin.fir.symbols.impl.* +import org.jetbrains.kotlin.fir.types.* +import org.jetbrains.kotlin.name.CallableId +import org.jetbrains.kotlin.name.ClassId +import org.jetbrains.kotlin.name.Name +import org.jetbrains.kotlin.name.SpecialNames +import org.jetbrains.kotlin.platform.isJs +import org.jetbrains.kotlin.platform.isWasm +import org.jetbrains.kotlin.platform.konan.isNative +import org.jetbrains.kotlinx.serialization.compiler.fir.SerializationFirResolveExtension +import org.jetbrains.kotlinx.serialization.compiler.fir.SerializationFirSupertypesExtension +import org.jetbrains.kotlinx.serialization.compiler.resolve.SerialEntityNames +import org.jetbrains.kotlinx.serialization.compiler.resolve.SerializationPackages + +/** + * What is happening here: + * + * ## General idea + * + * [getNestedClassifiersNames] should return a set of [Name]s to generate for. + * For these names [generateNestedClassLikeDeclaration] can generate some nested classes, + * which is what we need. + * + * But the catch is that we cannot say for sure, if we need to generate a class + * while in [getNestedClassifiersNames], but if we do not return anything + * [generateNestedClassLikeDeclaration] will not be called. + * We need to generate a class if only the current declaration is an RPC interface + * (inherits kotlinx.rpc.RPC). There is no resolved supertypes in [getNestedClassifiersNames], + * But, if the potentially generated class is not referenced anywhere, + * then [generateNestedClassLikeDeclaration] will already have supertypes resolved, + * so we can use this info to check the actual supertypes for RPC interface. + * + * So we always return a class name that may be generated. + * And then, in [generateNestedClassLikeDeclaration] we do the actual check with the resolved supertypes + * and generate a class if needed, otherwise returning null. + * + * ## Usage of kotlinx.serialization plugin + * + * Here is one more tricky part. + * + * We generate classes that are marked `@Serializable`. + * In that case, the serialization plugin will not be able to pick up those classes and process them accordingly. + * + * That's why we have an instance of this plugin, which we call only on our generated classes - [serializationExtension] + * + * Not all the methods that we would like to call are public, so we access them via reflection: + * - [generateCompanionDeclaration] + * - [generateSerializerImplClass] + * + * This is basically copying the behavior of the actual plugin but isolated only for our generated classes. + */ +class FirRPCServiceGenerator( + session: FirSession, + @Suppress("unused") + private val logger: MessageCollector, +) : FirDeclarationGenerationExtension(session) { + private val serializationExtension = SerializationFirResolveExtension(session) + private val isJvmOrMetadata = !session.moduleData.platform.run { isJs() || isWasm() || isNative() } + + /** + * Generates nested classifiers. + * + * They can be of three kinds: + * - Nested Service Stub class. + * In that case [classSymbol] will not have any RPC-generated [FirClassSymbol.origin]. + * Rge only check we do - is we check that the declaration is an interface, + * and return [RpcNames.SERVICE_STUB_NAME]. + * We cannot be sure if the declaration is actually an RPC service, + * because superTypes are not resolved during that stage. + * We postpone this check until [generateNestedClassLikeDeclaration]. + * + * - Companion object of the service stub and method classes. + * If we generate this companion object, we will have [FirClassSymbol.origin] + * of [classSymbol] be set to [RPCGeneratedStubKey], + * because we are inside the previously generated service stub class. + * The same goes for method classes too. + * So we return [SpecialNames.DEFAULT_NAME_FOR_COMPANION_OBJECT] + * and a list of method class names. + * + * - Inside method classes. + * Method classes will too have their nested declarations. + * We detect them by using [RPCGeneratedRpcMethodClassKey]. + * Nested declarations for these classes are provided by the serialization plugin. + * These declarations can be of two types: serializable object and serializable classes. + * In the case of objects, + * the serialization plugin treats them like their own serializers, + * so no extra nested declarations are necessary. + * In the case of classes, serialization requires two additional nested declarations: + * [SpecialNames.DEFAULT_NAME_FOR_COMPANION_OBJECT] and [SerialEntityNames.SERIALIZER_CLASS_NAME]. + */ + override fun getNestedClassifiersNames( + classSymbol: FirClassSymbol<*>, + context: NestedClassGenerationContext, + ): Set { + val rpcServiceStubKey = classSymbol.generatedRpcServiceStubKey + val rpcMethodClassKey = classSymbol.generatedRpcMethodClassKey + + return when { + rpcMethodClassKey != null -> { + when { + !rpcMethodClassKey.isObject -> setOf( + SpecialNames.DEFAULT_NAME_FOR_COMPANION_OBJECT, + SerialEntityNames.SERIALIZER_CLASS_NAME, + ) + + // otherwise an object is generated instead of a class + // serialization plugin has other logic for such declarations + else -> setOf() + } + } + + rpcServiceStubKey != null -> { + rpcServiceStubKey.functions.map { it.name.rpcMethodClassName }.toSet() + + SpecialNames.DEFAULT_NAME_FOR_COMPANION_OBJECT + } + + classSymbol.isInterface -> { + setOf(RpcNames.SERVICE_STUB_NAME) + } + + else -> { + emptySet() + } + } + } + + /** + * Handles class names provided by the [getNestedClassifiersNames] with the same cases. + * For service stub classes checks if the generation is necessary (inside [generateRpcServiceStubClass]). + */ + override fun generateNestedClassLikeDeclaration( + owner: FirClassSymbol<*>, + name: Name, + context: NestedClassGenerationContext + ): FirClassLikeSymbol<*>? { + val rpcServiceStubKey = owner.generatedRpcServiceStubKey + return when { + rpcServiceStubKey != null && name == SpecialNames.DEFAULT_NAME_FOR_COMPANION_OBJECT -> { + generateCompanionObjectForRpcServiceStub(owner) + } + + rpcServiceStubKey != null -> { + generateRpcMethodClass(owner, name, rpcServiceStubKey) + } + + owner.generatedRpcMethodClassKey != null -> { + generateNestedClassLikeDeclarationWithSerialization(owner, name) + } + + name == RpcNames.SERVICE_STUB_NAME -> { + generateRpcServiceStubClass(owner) + } + + else -> { + error("Cannot run generation for ${owner.classId.createNestedClassId(name).asSingleFqName()}") + } + } + } + + private fun generateCompanionObjectForRpcServiceStub( + owner: FirClassSymbol<*>, + ): FirClassLikeSymbol<*> { + return createCompanionObject(owner, FirRpcServiceStubCompanionObject).symbol + } + + /** + * Generates method class for the respectful function. + * Class is marked `@Serializable` for the backend processing. + */ + private fun generateRpcMethodClass( + owner: FirClassSymbol<*>, + name: Name, + rpcServiceStubKey: RPCGeneratedStubKey, + ): FirClassLikeSymbol<*> { + val methodName = name.rpcMethodName + val rpcMethod = rpcServiceStubKey.functions.single { it.name == methodName } + val rpcMethodClassKey = RPCGeneratedRpcMethodClassKey(rpcMethod) + val classKind = if (rpcMethodClassKey.isObject) ClassKind.OBJECT else ClassKind.CLASS + + val rpcMethodClass = createNestedClass( + owner = owner, + name = name, + key = rpcMethodClassKey, + classKind = classKind, + ) { + visibility = owner.visibility + modality = Modality.FINAL + } + + rpcMethodClass.addAnnotation(ClassDeclarations.serializableAnnotation, session) + + /** + * Required to pass isSerializableObjectAndNeedsFactory check + * from [SerializationFirSupertypesExtension]. + */ + if (!isJvmOrMetadata && rpcMethodClassKey.isObject) { + rpcMethodClass.replaceSuperTypeRefs(createSerializationFactorySupertype()) + } + + return rpcMethodClass.symbol + } + + /** + * Instead of [SerializationFirSupertypesExtension] + * + * Also, it is not run for Companion objects, as [serializationExtension] does it when needed. + */ + private fun createSerializationFactorySupertype(): List { + val serializerFactoryClassId = ClassId( + SerializationPackages.internalPackageFqName, + SerialEntityNames.SERIALIZER_FACTORY_INTERFACE_NAME, + ) + + val ref = serializerFactoryClassId + .constructClassLikeType(emptyArray(), false) + .toFirResolvedTypeRef() + + return listOf(ref) + } + + /** + * Mirrors [generateNestedClassLikeDeclaration] from [serializationExtension]. + */ + private fun generateNestedClassLikeDeclarationWithSerialization( + owner: FirClassSymbol<*>, + name: Name, + ): FirClassLikeSymbol<*>? { + if (owner !is FirRegularClassSymbol) { + error("Expected ${owner.name} to be FirRegularClassSymbol") + } + + return when (name) { + SpecialNames.DEFAULT_NAME_FOR_COMPANION_OBJECT -> serializationExtension.generateCompanionDeclaration(owner) + SerialEntityNames.SERIALIZER_CLASS_NAME -> serializationExtension.generateSerializerImplClass(owner) + else -> error("Can't generate class ${owner.classId.createNestedClassId(name).asSingleFqName()}") + } + } + + /** + * Checks whether the [owner] class is actually an RPC service + * (the supertypes are resolved at this stage, + * as the [RpcNames.SERVICE_STUB_NAME] is not references anywhere) + * + * If the [owner] is an RPC service - generates its service stub. + * Scrapes the functions from the [owner] to generate method classes. + */ + private fun generateRpcServiceStubClass(owner: FirClassSymbol<*>): FirRegularClassSymbol? { + owner.resolvedSuperTypes.find { + it.classId == ClassDeclarations.rpcInterface + } ?: return null + + val functions = mutableListOf>() + owner.declaredMemberScope(session, null).processAllFunctions { + functions.add(it) + } + + return createNestedClass(owner, RpcNames.SERVICE_STUB_NAME, RPCGeneratedStubKey(owner.name, functions)) { + visibility = owner.visibility + modality = Modality.FINAL + }.symbol + } + + override fun getCallableNamesForClass(classSymbol: FirClassSymbol<*>, context: MemberGenerationContext): Set { + val rpcMethodClassKey = classSymbol.generatedRpcMethodClassKey + + return when { + rpcMethodClassKey != null -> { + getCallableNamesForRpcMethodClass(classSymbol, context, rpcMethodClassKey) + } + + classSymbol.isFromSerializationPlugin -> { + serializationExtension.getCallableNamesForClass(classSymbol, context) + } + + else -> { + super.getCallableNamesForClass(classSymbol, context) + } + } + } + + /** + * If the method does not have any parameters, it is an object, + * and its only callable names are constructor, and the ones provided by the [serializationExtension]. + * Otherwise, the callable names are the names of the method parameters and the constructor. + */ + private fun getCallableNamesForRpcMethodClass( + classSymbol: FirClassSymbol<*>, + context: MemberGenerationContext, + rpcMethodClassKey: RPCGeneratedRpcMethodClassKey, + ): Set { + return if (rpcMethodClassKey.isObject) { + // add .serializer() method for a serializable object + serializationExtension.getCallableNamesForClass(classSymbol, context) + } else { + rpcMethodClassKey.rpcMethod.valueParameterSymbols.map { it.name }.toSet() + } + SpecialNames.INIT + + // ^ init is necessary either way, as serialization does not add it for a serializable object + } + + override fun generateConstructors(context: MemberGenerationContext): List { + val rpcMethodClassKey = context.owner.generatedRpcMethodClassKey + return when { + rpcMethodClassKey != null -> generateConstructorsForRpcMethodClass(context, rpcMethodClassKey) + context.owner.isFromSerializationPlugin -> serializationExtension.generateConstructors(context) + else -> super.generateConstructors(context) + } + } + + /** + * An object needs only a default private constructor. + * + * A regular class constructor requires also value parameters of the respectful method. + */ + private fun generateConstructorsForRpcMethodClass( + context: MemberGenerationContext, + rpcMethodClassKey: RPCGeneratedRpcMethodClassKey, + ): List { + if (rpcMethodClassKey.isObject) { + return createDefaultPrivateConstructor(context.owner, rpcMethodClassKey).symbol.let(::listOf) + } + + return createConstructor(context.owner, rpcMethodClassKey) { + visibility = Visibilities.Public + + rpcMethodClassKey.rpcMethod.valueParameterSymbols.forEach { valueParam -> + valueParameter( + name = valueParam.name, + type = valueParam.resolvedReturnType, + ) + } + }.also { + it.containingClassForStaticMemberAttr = ConeClassLikeLookupTagImpl(context.owner.classId) + }.symbol.let(::listOf) + } + + override fun generateProperties( + callableId: CallableId, + context: MemberGenerationContext? + ): List { + context ?: return super.generateProperties(callableId, null) + + val owner = context.owner + val rpcMethodClassKey = owner.generatedRpcMethodClassKey + + return when { + rpcMethodClassKey != null -> { + generatePropertiesForRpcMethodClass(callableId, owner, rpcMethodClassKey) + } + + owner.isFromSerializationPlugin -> { + serializationExtension.generateProperties(callableId, context) + } + + else -> { + super.generateProperties(callableId, context) + } + } + } + + private fun generatePropertiesForRpcMethodClass( + callableId: CallableId, + owner: FirClassSymbol<*>, + rpcMethodClassKey: RPCGeneratedRpcMethodClassKey, + ): List { + val valueParam = rpcMethodClassKey.rpcMethod.valueParameterSymbols.find { + it.name == callableId.callableName + } ?: return emptyList() + + return createMemberProperty( + owner = owner, + key = rpcMethodClassKey, + name = callableId.callableName, + returnType = valueParam.resolvedReturnType, + ).apply { + if (valueParam.resolvedReturnType.requiresContextual()) { + addAnnotation(ClassDeclarations.contextualAnnotation, session) + } + }.symbol.let(::listOf) + } + + private fun ConeKotlinType.requiresContextual(): Boolean { + return when (classId) { + ClassDeclarations.flow, ClassDeclarations.sharedFlow, ClassDeclarations.stateFlow -> true + else -> false + } + } + + /** + * Processes serialization related cases from [getCallableNamesForClass]. + */ + override fun generateFunctions( + callableId: CallableId, + context: MemberGenerationContext? + ): List { + val owner = context?.owner ?: return super.generateFunctions(callableId, null) + + return when { + owner.isFromSerializationPlugin || owner.generatedRpcMethodClassKey?.isObject == true -> { + serializationExtension.generateFunctions(callableId, context) + } + + else -> { + super.generateFunctions(callableId, context) + } + } + } +} diff --git a/compiler-plugin/compiler-plugin-k2/src/main/core/kotlinx/rpc/codegen/serialization/SerializationFirResolveExtensionDelegate.kt b/compiler-plugin/compiler-plugin-k2/src/main/core/kotlinx/rpc/codegen/serialization/SerializationFirResolveExtensionDelegate.kt new file mode 100644 index 00000000..45cb79b6 --- /dev/null +++ b/compiler-plugin/compiler-plugin-k2/src/main/core/kotlinx/rpc/codegen/serialization/SerializationFirResolveExtensionDelegate.kt @@ -0,0 +1,31 @@ +/* + * Copyright 2023-2024 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.rpc.codegen.serialization + +import org.jetbrains.kotlin.fir.symbols.impl.FirClassLikeSymbol +import org.jetbrains.kotlin.fir.symbols.impl.FirRegularClassSymbol +import org.jetbrains.kotlinx.serialization.compiler.fir.SerializationFirResolveExtension + +internal fun SerializationFirResolveExtension.generateSerializerImplClass( + owner: FirRegularClassSymbol, +): FirClassLikeSymbol<*> { + return callPrivateMethod("generateSerializerImplClass", owner) +} + +internal fun SerializationFirResolveExtension.generateCompanionDeclaration( + owner: FirRegularClassSymbol, +): FirRegularClassSymbol? { + return callPrivateMethod("generateCompanionDeclaration", owner) +} + +private fun SerializationFirResolveExtension.callPrivateMethod(name: String, arg: Any?): T { + val method = this::class.java.declaredMethods.find { it.name == name } + ?: error("Expected method with name $name in SerializationFirResolveExtension") + + method.isAccessible = true + + @Suppress("UNCHECKED_CAST") + return method.invoke(this, arg) as T +} diff --git a/compiler-plugin/compiler-plugin-k2/src/main/core/kotlinx/rpc/codegen/serialization/annotation.kt b/compiler-plugin/compiler-plugin-k2/src/main/core/kotlinx/rpc/codegen/serialization/annotation.kt new file mode 100644 index 00000000..9f82229e --- /dev/null +++ b/compiler-plugin/compiler-plugin-k2/src/main/core/kotlinx/rpc/codegen/serialization/annotation.kt @@ -0,0 +1,44 @@ +/* + * Copyright 2023-2024 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.rpc.codegen.serialization + +import org.jetbrains.kotlin.fir.FirAnnotationContainer +import org.jetbrains.kotlin.fir.FirSession +import org.jetbrains.kotlin.fir.expressions.buildResolvedArgumentList +import org.jetbrains.kotlin.fir.expressions.builder.buildAnnotationCall +import org.jetbrains.kotlin.fir.references.builder.buildResolvedNamedReference +import org.jetbrains.kotlin.fir.resolve.defaultType +import org.jetbrains.kotlin.fir.resolve.providers.symbolProvider +import org.jetbrains.kotlin.fir.symbols.impl.FirConstructorSymbol +import org.jetbrains.kotlin.fir.symbols.impl.FirRegularClassSymbol +import org.jetbrains.kotlin.fir.types.builder.buildResolvedTypeRef +import org.jetbrains.kotlin.name.ClassId +import org.jetbrains.kotlin.utils.addToStdlib.firstIsInstanceOrNull + +fun FirAnnotationContainer.addAnnotation(annotationId: ClassId, session: FirSession) { + val annotation = session + .symbolProvider + .getClassLikeSymbolByClassId(annotationId) + as? FirRegularClassSymbol ?: return + + val annotationConstructor = annotation + .declarationSymbols + .firstIsInstanceOrNull() ?: return + + val annotationCall = buildAnnotationCall { + argumentList = buildResolvedArgumentList(null, linkedMapOf()) + annotationTypeRef = buildResolvedTypeRef { + type = annotation.defaultType() + } + calleeReference = buildResolvedNamedReference { + name = annotation.name + resolvedSymbol = annotationConstructor + } + + containingDeclarationSymbol = annotationConstructor + } + + replaceAnnotations(annotations + annotationCall) +} diff --git a/compiler-plugin/settings.gradle.kts b/compiler-plugin/settings.gradle.kts index 13d26bde..2d2a6ddb 100644 --- a/compiler-plugin/settings.gradle.kts +++ b/compiler-plugin/settings.gradle.kts @@ -16,3 +16,6 @@ plugins { } includeRootAsPublic() + +includePublic(":compiler-plugin-k2") +includePublic(":compiler-plugin-common") diff --git a/compiler-plugin/src/main/core/kotlinx/rpc/codegen/extension/RPCDeclarationScanner.kt b/compiler-plugin/src/main/core/kotlinx/rpc/codegen/extension/RPCDeclarationScanner.kt index c58a5db4..8d4ebb79 100644 --- a/compiler-plugin/src/main/core/kotlinx/rpc/codegen/extension/RPCDeclarationScanner.kt +++ b/compiler-plugin/src/main/core/kotlinx/rpc/codegen/extension/RPCDeclarationScanner.kt @@ -4,13 +4,13 @@ package kotlinx.rpc.codegen.extension +import kotlinx.rpc.codegen.common.RpcNames import org.jetbrains.kotlin.ir.declarations.IrClass import org.jetbrains.kotlin.ir.declarations.IrDeclaration import org.jetbrains.kotlin.ir.declarations.IrProperty import org.jetbrains.kotlin.ir.declarations.IrSimpleFunction import org.jetbrains.kotlin.ir.types.classOrNull import org.jetbrains.kotlin.ir.util.dumpKotlinLike -import org.jetbrains.kotlin.ir.util.packageFqName /** * This class scans user declared RPC service @@ -21,6 +21,8 @@ import org.jetbrains.kotlin.ir.util.packageFqName */ internal object RPCDeclarationScanner { fun scanServiceDeclaration(service: IrClass, ctx: RPCIrContext): ServiceDeclaration { + var stubClass: IrClass? = null + val declarations = service.declarations.memoryOptimizedMap { declaration -> when (declaration) { is IrSimpleFunction -> { @@ -53,20 +55,27 @@ internal object RPCDeclarationScanner { ServiceDeclaration.FlowField(declaration, flowType) } + is IrClass -> { + if (declaration.name == RpcNames.SERVICE_STUB_NAME) { + stubClass = declaration + return@memoryOptimizedMap null + } + + unsupportedDeclaration(service, declaration) + } + else -> { unsupportedDeclaration(service, declaration) } } } - val stubClass = ctx.getIrClassSymbol( - packageName = service.packageFqName!!.asString(), - name = "${service.name.asString()}$STUB_SUFFIX", - ) + val stubClassNotNull = stubClass + ?: error("Expected ${RpcNames.SERVICE_STUB_NAME} nested declaration in ${service.name}") return ServiceDeclaration( service = service, - stubClass = stubClass.owner, + stubClass = stubClassNotNull, methods = declarations.filterIsInstance(), fields = declarations.filterIsInstance(), ) diff --git a/compiler-plugin/src/main/core/kotlinx/rpc/codegen/extension/RPCIrContext.kt b/compiler-plugin/src/main/core/kotlinx/rpc/codegen/extension/RPCIrContext.kt index 27a4ce03..b8e36e5b 100644 --- a/compiler-plugin/src/main/core/kotlinx/rpc/codegen/extension/RPCIrContext.kt +++ b/compiler-plugin/src/main/core/kotlinx/rpc/codegen/extension/RPCIrContext.kt @@ -225,7 +225,7 @@ internal class RPCIrContext( return getIrClassSymbol("kotlinx.rpc$suffix", name) } - fun getIrClassSymbol(packageName: String, name: String): IrClassSymbol { + private fun getIrClassSymbol(packageName: String, name: String): IrClassSymbol { return versionSpecificApi.referenceClass(pluginContext, packageName, name) ?: error("Unable to find symbol. Package: $packageName, name: $name") } diff --git a/compiler-plugin/src/main/core/kotlinx/rpc/codegen/extension/RPCStubGenerator.kt b/compiler-plugin/src/main/core/kotlinx/rpc/codegen/extension/RPCStubGenerator.kt index 7c95932f..30a11ee0 100644 --- a/compiler-plugin/src/main/core/kotlinx/rpc/codegen/extension/RPCStubGenerator.kt +++ b/compiler-plugin/src/main/core/kotlinx/rpc/codegen/extension/RPCStubGenerator.kt @@ -5,6 +5,7 @@ package kotlinx.rpc.codegen.extension import kotlinx.rpc.codegen.VersionSpecificApi +import kotlinx.rpc.codegen.common.rpcMethodClassName import org.jetbrains.kotlin.backend.common.lower.DeclarationIrBuilder import org.jetbrains.kotlin.backend.jvm.functionByName import org.jetbrains.kotlin.cli.common.messages.CompilerMessageSeverity @@ -33,10 +34,8 @@ import org.jetbrains.kotlin.types.Variance import org.jetbrains.kotlin.util.OperatorNameConventions import kotlin.properties.Delegates -const val STUB_SUFFIX = "Stub" private const val CLIENT_PROPERTY = "__rpc_client" private const val STUB_ID_PROPERTY = "__rpc_stub_id" -private const val METHOD_CLASS_SUFFIX_PROPERTY = "RPCData" private const val METHOD_NAMES_PROPERTY = "methodNames" private const val METHOD_TYPE_OF_FUNCTION = "methodTypeOf" private const val WITH_CLIENT_METHOD = "withClient" @@ -539,10 +538,13 @@ internal class RPCStubGenerator( ) private fun IrClass.generateRpcMethod(method: ServiceDeclaration.Method) { val isMethodObject = method.argumentTypes.isEmpty() - val methodClassName = "${method.function.name.asString().capitalized()}_" + METHOD_CLASS_SUFFIX_PROPERTY - val methodClass: IrClass = findDeclaration { it.name.asString() == methodClassName } - ?: error("Expected $methodClassName class to be present in stub class ${declaration.simpleName}") + val methodClassName = method.function.name.rpcMethodClassName + val methodClass: IrClass = findDeclaration { it.name == methodClassName } + ?: error( + "Expected $methodClassName class to be present in stub class " + + "${declaration.service.name}${declaration.stubClass.name}" + ) methodClasses.add(methodClass) @@ -596,7 +598,7 @@ internal class RPCStubGenerator( parent = declaredFunction body = irBuilder(symbol).irBlockBody { - val call = irRPCMethodClientCall( + val call = irRpcMethodClientCall( method = method, functionThisReceiver = functionThisReceiver, isMethodObject = isMethodObject, @@ -646,7 +648,7 @@ internal class RPCStubGenerator( "detekt.NestedBlockDepth", "detekt.MagicNumber", ) - private fun IrBlockBodyBuilder.irRPCMethodClientCall( + private fun IrBlockBodyBuilder.irRpcMethodClientCall( method: ServiceDeclaration.Method, functionThisReceiver: IrValueParameter, isMethodObject: Boolean, diff --git a/compiler-plugin/src/main/latest/kotlinx/rpc/codegen/RPCCompilerPlugin.kt b/compiler-plugin/src/main/latest/kotlinx/rpc/codegen/RPCCompilerPlugin.kt index 1fe7c5d0..e7c1d9a7 100644 --- a/compiler-plugin/src/main/latest/kotlinx/rpc/codegen/RPCCompilerPlugin.kt +++ b/compiler-plugin/src/main/latest/kotlinx/rpc/codegen/RPCCompilerPlugin.kt @@ -10,6 +10,7 @@ import org.jetbrains.kotlin.compiler.plugin.CommandLineProcessor import org.jetbrains.kotlin.compiler.plugin.CompilerPluginRegistrar import org.jetbrains.kotlin.compiler.plugin.ExperimentalCompilerApi import org.jetbrains.kotlin.config.CompilerConfiguration +import org.jetbrains.kotlin.fir.extensions.FirExtensionRegistrarAdapter @OptIn(ExperimentalCompilerApi::class) class RPCCommandLineProcessor : CommandLineProcessor { @@ -30,5 +31,6 @@ class RPCCompilerPlugin : CompilerPluginRegistrar() { val extension = RPCCompilerPluginCore.provideExtension(configuration) IrGenerationExtension.registerExtension(extension) + FirExtensionRegistrarAdapter.registerExtension(FirRPCExtensionRegistrar(configuration)) } } diff --git a/core/build.gradle.kts b/core/build.gradle.kts index 6731b095..0ff93535 100644 --- a/core/build.gradle.kts +++ b/core/build.gradle.kts @@ -4,9 +4,7 @@ plugins { alias(libs.plugins.conventions.kmp) - alias(libs.plugins.serialization) alias(libs.plugins.ksp) - alias(libs.plugins.kotlinx.rpc) alias(libs.plugins.atomicfu) } diff --git a/core/src/jvmMain/kotlin/kotlinx/rpc/internal/WithRPCStubObject.jvm.kt b/core/src/jvmMain/kotlin/kotlinx/rpc/internal/WithRPCStubObject.jvm.kt index 941fbc4b..8d31712d 100644 --- a/core/src/jvmMain/kotlin/kotlinx/rpc/internal/WithRPCStubObject.jvm.kt +++ b/core/src/jvmMain/kotlin/kotlinx/rpc/internal/WithRPCStubObject.jvm.kt @@ -13,7 +13,7 @@ import kotlin.reflect.full.companionObjectInstance public actual fun findRPCProviderInCompanion(kClass: KClass<*>): R { @Suppress("UNCHECKED_CAST") return kClass.java.classLoader - .loadClass("${kClass.qualifiedName}Stub") + .loadClass("${kClass.qualifiedName}\$\$rpcServiceStub") ?.kotlin ?.companionObjectInstance as? R ?: internalError("unable to find $kClass rpc client object") diff --git a/gradle-conventions/compiler-specific-module/src/main/kotlin/compiler-specific-module.gradle.kts b/gradle-conventions/compiler-specific-module/src/main/kotlin/compiler-specific-module.gradle.kts index 96b8ac19..d03b310d 100644 --- a/gradle-conventions/compiler-specific-module/src/main/kotlin/compiler-specific-module.gradle.kts +++ b/gradle-conventions/compiler-specific-module/src/main/kotlin/compiler-specific-module.gradle.kts @@ -62,15 +62,17 @@ fun KotlinSourceSet.configureResources(sourceSetPath: Path, vararg versionNames: // 'resources' property does not work alone in gradle 7.5.1 with kotlin 1.7.0 (no idea why), // so we adjust task contents as well - tasks.withType().configureEach { - duplicatesStrategy = DuplicatesStrategy.EXCLUDE - - from(versionNames.map { vsResources.resolve(it) }) - include { it.file.parentInAllowList(versionNames) } - } + // todo duplicate (or to many resources are copied, should update the algo) +// tasks.withType().configureEach { +// duplicatesStrategy = DuplicatesStrategy.EXCLUDE +// +// from(versionNames.map { vsResources.resolve(it) }) +// include { it.file.parentInAllowList(versionNames) } +// } } fun File.parentInAllowList(allowList: Array): Boolean { +// println("decide: $absolutePath") val parent = toPath().parent?.toFile() // will skip v_1_7 for 1.7.0, as it's parent is resources // but will allow META-INF, as it's parent is v_1_7 diff --git a/gradle-plugin/src/main/kotlin/kotlinx/rpc/KotlinCompilerPluginBuilder.kt b/gradle-plugin/src/main/kotlin/kotlinx/rpc/KotlinCompilerPluginBuilder.kt index 2b9a4870..79668135 100644 --- a/gradle-plugin/src/main/kotlin/kotlinx/rpc/KotlinCompilerPluginBuilder.kt +++ b/gradle-plugin/src/main/kotlin/kotlinx/rpc/KotlinCompilerPluginBuilder.kt @@ -63,6 +63,6 @@ class KotlinCompilerPluginBuilder { } } -fun compilerPlugin(builder: KotlinCompilerPluginBuilder.() -> Unit): KotlinCompilerPluginSupportPlugin { +fun compilerPlugin(builder: KotlinCompilerPluginBuilder.() -> Unit = {}): KotlinCompilerPluginSupportPlugin { return KotlinCompilerPluginBuilder().apply(builder).build() } diff --git a/gradle-plugin/src/main/kotlin/kotlinx/rpc/RPCGradlePlugin.kt b/gradle-plugin/src/main/kotlin/kotlinx/rpc/RPCGradlePlugin.kt index 4073a5a4..10724177 100644 --- a/gradle-plugin/src/main/kotlin/kotlinx/rpc/RPCGradlePlugin.kt +++ b/gradle-plugin/src/main/kotlin/kotlinx/rpc/RPCGradlePlugin.kt @@ -26,13 +26,19 @@ class RPCGradlePlugin : Plugin { val config = RPCConfig(target.isInternalDevelopment) applyPlatformConfiguration(target, config) - applyKspPlugin(target, config) + + // TODO languageVersion parameter check + if (kotlinVersion.first() < '2') { // for 1.*.* versions + applyKspPlugin(target, config) + } + applyCompilerPlugin(target) } private fun applyCompilerPlugin(target: Project) { - target.plugins.apply(CompilerPluginCore::class.java) -// target.plugins.apply(compilerPluginForKotlin(kotlinVersion)) + target.plugins.apply(CompilerPlugin::class.java) + target.plugins.apply(CompilerPluginK2::class.java) + target.plugins.apply(CompilerPluginCommon::class.java) } private fun applyKspPlugin(target: Project, config: RPCConfig) { diff --git a/gradle-plugin/src/main/kotlin/kotlinx/rpc/compilerPlugins.kt b/gradle-plugin/src/main/kotlin/kotlinx/rpc/compilerPlugins.kt index dff89ba9..b19ad08e 100644 --- a/gradle-plugin/src/main/kotlin/kotlinx/rpc/compilerPlugins.kt +++ b/gradle-plugin/src/main/kotlin/kotlinx/rpc/compilerPlugins.kt @@ -8,34 +8,12 @@ package kotlinx.rpc import org.jetbrains.kotlin.gradle.plugin.KotlinCompilerPluginSupportPlugin -class CompilerPluginCore : KotlinCompilerPluginSupportPlugin by compilerPlugin({ -// pluginSuffix = "-core" -}) +class CompilerPlugin : KotlinCompilerPluginSupportPlugin by compilerPlugin() -//class CompilerPlugin1_9 : KotlinCompilerPluginSupportPlugin by compilerPlugin({ -// pluginSuffix = "-1_9" -//}) -// -//class CompilerPlugin1_8 : KotlinCompilerPluginSupportPlugin by compilerPlugin({ -// pluginSuffix = "-1_8" -//}) -// -//class CompilerPlugin1_7_2 : KotlinCompilerPluginSupportPlugin by compilerPlugin({ -// pluginSuffix = "-1_7_2" -//}) -// -//class CompilerPlugin1_7 : KotlinCompilerPluginSupportPlugin by compilerPlugin({ -// pluginSuffix = "-1_7" -//}) +class CompilerPluginK2 : KotlinCompilerPluginSupportPlugin by compilerPlugin({ + pluginSuffix = "-k2" +}) -//// Transitive dependencies do not work for Kotlin/Native -//// https://youtrack.jetbrains.com/issue/KT-53477/Native-Gradle-plugin-doesnt-add-compiler-plugin-transitive-dependencies-to-compiler-plugin-classpath -//fun compilerPluginForKotlin(kotlin: String): Class> { -// return when { -// kotlin == "1.7.0" || kotlin == "1.7.10" -> CompilerPlugin1_7::class.java -// kotlin.startsWith("1.7.2") -> CompilerPlugin1_7_2::class.java -// kotlin.startsWith("1.8") -> CompilerPlugin1_8::class.java -// kotlin.startsWith("1.9") -> CompilerPlugin1_9::class.java -// else -> error("Unsupported kotlin version: $kotlin") -// } -//} +class CompilerPluginCommon : KotlinCompilerPluginSupportPlugin by compilerPlugin({ + pluginSuffix = "-common" +}) diff --git a/gradle.properties b/gradle.properties index a35dece9..9eceabf4 100644 --- a/gradle.properties +++ b/gradle.properties @@ -28,3 +28,6 @@ kotlinx.rpc.plugin.internalDevelopment=true # https://github.com/gradle/gradle/issues/20416 systemProp.org.gradle.kotlin.dsl.precompiled.accessors.strict=true + +# uncomment to debug compilation process +#kotlin.compiler.execution.strategy=in-process diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 1a66f82b..52422672 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -35,6 +35,7 @@ kotlin-test-junit = { module = "org.jetbrains.kotlin:kotlin-test-junit", version kotlin-gradle-plugin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin-lang" } kotlin-compiler-embeddable = { group = "org.jetbrains.kotlin", name = "kotlin-compiler-embeddable" } +serialization-plugin = { module = "org.jetbrains.kotlin:kotlin-serialization-compiler-plugin", version.ref = "kotlin-lang" } # serialization serialization-core = { module = "org.jetbrains.kotlinx:kotlinx-serialization-core", version.ref = "serialization" } From 1f06ebe8c0234363ae8c0191616c11ab59fc67c5 Mon Sep 17 00:00:00 2001 From: Alexander Sysoev Date: Wed, 26 Jun 2024 22:00:24 +0200 Subject: [PATCH 2/5] Update IR backend --- .../core/kotlinx/rpc/codegen/common/Names.kt | 6 +- .../kotlinx/rpc/codegen/VersionSpecificApi.kt | 3 +- .../extension/RPCDeclarationScanner.kt | 2 +- .../rpc/codegen/extension/RPCIrContext.kt | 17 ++ .../extension/RPCIrServiceProcessor.kt | 11 +- .../rpc/codegen/extension/RPCStubGenerator.kt | 228 +++++++++++++----- .../codegen/extension/ServiceDeclaration.kt | 2 +- .../rpc/codegen/VersionSpecificApiImpl.kt | 10 +- .../rpc/codegen/VersionSpecificApiImpl.kt | 2 +- .../rpc/codegen/VersionSpecificApiImpl.kt | 2 +- .../rpc/codegen/VersionSpecificApiImpl.kt | 2 +- .../kotlinx/rpc/codegen/RPCCompilerPlugin.kt | 36 +++ .../rpc/codegen/VersionSpecificApiImpl.kt | 73 ++++++ .../rpc/internal/RPCMethodClassArguments.kt | 2 +- 14 files changed, 316 insertions(+), 80 deletions(-) create mode 100644 compiler-plugin/src/main/v_1_9/kotlinx/rpc/codegen/RPCCompilerPlugin.kt create mode 100644 compiler-plugin/src/main/v_1_9/kotlinx/rpc/codegen/VersionSpecificApiImpl.kt diff --git a/compiler-plugin/compiler-plugin-common/src/main/core/kotlinx/rpc/codegen/common/Names.kt b/compiler-plugin/compiler-plugin-common/src/main/core/kotlinx/rpc/codegen/common/Names.kt index ed1b09e6..f01c7bc8 100644 --- a/compiler-plugin/compiler-plugin-common/src/main/core/kotlinx/rpc/codegen/common/Names.kt +++ b/compiler-plugin/compiler-plugin-common/src/main/core/kotlinx/rpc/codegen/common/Names.kt @@ -22,8 +22,8 @@ object ClassDeclarations { object RpcNames { val SERVICE_STUB_NAME: Name = Name.identifier("\$rpcServiceStub") - const val METHOD_NAME_SUFFIX = "\$rpcMethod" + const val METHOD_CLASS_NAME_SUFFIX = "\$rpcMethod" } -val Name.rpcMethodClassName: Name get() = Name.identifier("$identifier${RpcNames.METHOD_NAME_SUFFIX}") -val Name.rpcMethodName: Name get() = Name.identifier(identifier.removeSuffix(RpcNames.METHOD_NAME_SUFFIX)) +val Name.rpcMethodClassName: Name get() = Name.identifier("$identifier${RpcNames.METHOD_CLASS_NAME_SUFFIX}") +val Name.rpcMethodName: Name get() = Name.identifier(identifier.removeSuffix(RpcNames.METHOD_CLASS_NAME_SUFFIX)) diff --git a/compiler-plugin/src/main/core/kotlinx/rpc/codegen/VersionSpecificApi.kt b/compiler-plugin/src/main/core/kotlinx/rpc/codegen/VersionSpecificApi.kt index 2d105c97..95f0da88 100644 --- a/compiler-plugin/src/main/core/kotlinx/rpc/codegen/VersionSpecificApi.kt +++ b/compiler-plugin/src/main/core/kotlinx/rpc/codegen/VersionSpecificApi.kt @@ -5,7 +5,6 @@ package kotlinx.rpc.codegen import org.jetbrains.kotlin.backend.common.extensions.IrPluginContext -import org.jetbrains.kotlin.descriptors.Modality import org.jetbrains.kotlin.ir.builders.declarations.IrFieldBuilder import org.jetbrains.kotlin.ir.declarations.IrDeclarationOrigin import org.jetbrains.kotlin.ir.declarations.IrFunction @@ -39,7 +38,7 @@ interface VersionSpecificApi { origin: IrDeclarationOrigin = IrDeclarationOrigin.DEFINED, ): IrValueParameter - var IrFieldBuilder.modalityVS: Modality + var IrFieldBuilder.isFinalVS: Boolean var IrCall.originVS: IrStatementOrigin? diff --git a/compiler-plugin/src/main/core/kotlinx/rpc/codegen/extension/RPCDeclarationScanner.kt b/compiler-plugin/src/main/core/kotlinx/rpc/codegen/extension/RPCDeclarationScanner.kt index 8d4ebb79..e790bc3b 100644 --- a/compiler-plugin/src/main/core/kotlinx/rpc/codegen/extension/RPCDeclarationScanner.kt +++ b/compiler-plugin/src/main/core/kotlinx/rpc/codegen/extension/RPCDeclarationScanner.kt @@ -32,7 +32,7 @@ internal object RPCDeclarationScanner { ServiceDeclaration.Method( function = declaration, - argumentTypes = declaration.valueParameters.memoryOptimizedMap { param -> + arguments = declaration.valueParameters.memoryOptimizedMap { param -> ServiceDeclaration.Method.Argument(param, param.type) }, ) diff --git a/compiler-plugin/src/main/core/kotlinx/rpc/codegen/extension/RPCIrContext.kt b/compiler-plugin/src/main/core/kotlinx/rpc/codegen/extension/RPCIrContext.kt index b8e36e5b..5bc5b442 100644 --- a/compiler-plugin/src/main/core/kotlinx/rpc/codegen/extension/RPCIrContext.kt +++ b/compiler-plugin/src/main/core/kotlinx/rpc/codegen/extension/RPCIrContext.kt @@ -17,6 +17,7 @@ import org.jetbrains.kotlin.ir.util.functions import org.jetbrains.kotlin.ir.util.isVararg import org.jetbrains.kotlin.ir.util.properties import org.jetbrains.kotlin.platform.konan.isNative +import org.jetbrains.kotlin.types.Variance internal class RPCIrContext( val pluginContext: IrPluginContext, @@ -28,6 +29,10 @@ internal class RPCIrContext( irBuiltIns.anyType.makeNullable() } + val arrayOfAnyNullable by lazy { + irBuiltIns.arrayClass.typeWith(anyNullable, Variance.OUT_VARIANCE) + } + val coroutineScope by lazy { getIrClassSymbol("kotlinx.coroutines", "CoroutineScope") } @@ -114,6 +119,10 @@ internal class RPCIrContext( getRpcIrClassSymbol("RPCDeferredField", "internal") } + val rpcMethodClassArguments by lazy { + getRpcIrClassSymbol("RPCMethodClassArguments", "internal") + } + fun isJsTarget(): Boolean { return versionSpecificApi.isJs(pluginContext.platform) } @@ -145,10 +154,18 @@ internal class RPCIrContext( rpcClient.namedFunction("provideStubContext") } + val asArray by lazy { + rpcMethodClassArguments.namedFunction("asArray") + } + val typeOf by lazy { namedFunction("kotlin.reflect", "typeOf") } + val emptyArray by lazy { + namedFunction("kotlin", "emptyArray") + } + val scopedClientCall by lazy { namedFunction("kotlinx.rpc.internal", "scopedClientCall") } diff --git a/compiler-plugin/src/main/core/kotlinx/rpc/codegen/extension/RPCIrServiceProcessor.kt b/compiler-plugin/src/main/core/kotlinx/rpc/codegen/extension/RPCIrServiceProcessor.kt index 6f7e1e3b..6a00fca6 100644 --- a/compiler-plugin/src/main/core/kotlinx/rpc/codegen/extension/RPCIrServiceProcessor.kt +++ b/compiler-plugin/src/main/core/kotlinx/rpc/codegen/extension/RPCIrServiceProcessor.kt @@ -7,8 +7,7 @@ package kotlinx.rpc.codegen.extension import org.jetbrains.kotlin.cli.common.messages.MessageCollector import org.jetbrains.kotlin.ir.IrStatement import org.jetbrains.kotlin.ir.declarations.IrClass -import org.jetbrains.kotlin.ir.types.classFqName -import org.jetbrains.kotlin.ir.types.typeWith +import org.jetbrains.kotlin.ir.types.defaultType import org.jetbrains.kotlin.ir.util.isInterface import org.jetbrains.kotlin.ir.visitors.IrElementTransformer @@ -18,19 +17,13 @@ internal class RPCIrServiceProcessor( ) : IrElementTransformer { @Suppress("PARAMETER_NAME_CHANGED_ON_OVERRIDE") override fun visitClass(declaration: IrClass, context: RPCIrContext): IrStatement { - if (declaration.isInterface && - // context.rpc is resolved lazily, so first check is rather heuristic - declaration.maybeRPC() && - declaration.superTypes.contains(context.rpc.typeWith()) - ) { + if (declaration.isInterface && declaration.superTypes.contains(context.rpc.defaultType)) { processService(declaration, context) } return super.visitClass(declaration, context) } - private fun IrClass.maybeRPC() = superTypes.any { it.classFqName?.asString()?.contains("RPC") == true } - private fun processService(service: IrClass, context: RPCIrContext) { val declaration = RPCDeclarationScanner.scanServiceDeclaration(service, context) RPCStubGenerator(declaration, context, logger).generate() diff --git a/compiler-plugin/src/main/core/kotlinx/rpc/codegen/extension/RPCStubGenerator.kt b/compiler-plugin/src/main/core/kotlinx/rpc/codegen/extension/RPCStubGenerator.kt index 30a11ee0..4b07e2d3 100644 --- a/compiler-plugin/src/main/core/kotlinx/rpc/codegen/extension/RPCStubGenerator.kt +++ b/compiler-plugin/src/main/core/kotlinx/rpc/codegen/extension/RPCStubGenerator.kt @@ -2,23 +2,22 @@ * Copyright 2023-2024 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. */ +@file:OptIn(UnsafeDuringIrConstructionAPI::class) + package kotlinx.rpc.codegen.extension import kotlinx.rpc.codegen.VersionSpecificApi import kotlinx.rpc.codegen.common.rpcMethodClassName import org.jetbrains.kotlin.backend.common.lower.DeclarationIrBuilder import org.jetbrains.kotlin.backend.jvm.functionByName -import org.jetbrains.kotlin.cli.common.messages.CompilerMessageSeverity import org.jetbrains.kotlin.cli.common.messages.MessageCollector import org.jetbrains.kotlin.descriptors.DescriptorVisibilities +import org.jetbrains.kotlin.descriptors.DescriptorVisibility import org.jetbrains.kotlin.descriptors.Modality import org.jetbrains.kotlin.ir.UNDEFINED_OFFSET import org.jetbrains.kotlin.ir.builders.* import org.jetbrains.kotlin.ir.builders.declarations.* -import org.jetbrains.kotlin.ir.declarations.IrClass -import org.jetbrains.kotlin.ir.declarations.IrDeclarationOrigin -import org.jetbrains.kotlin.ir.declarations.IrProperty -import org.jetbrains.kotlin.ir.declarations.IrValueParameter +import org.jetbrains.kotlin.ir.declarations.* import org.jetbrains.kotlin.ir.expressions.IrCall import org.jetbrains.kotlin.ir.expressions.IrExpression import org.jetbrains.kotlin.ir.expressions.IrStatementOrigin @@ -26,6 +25,7 @@ import org.jetbrains.kotlin.ir.expressions.impl.* import org.jetbrains.kotlin.ir.symbols.IrClassSymbol import org.jetbrains.kotlin.ir.symbols.IrSymbol import org.jetbrains.kotlin.ir.symbols.IrValueSymbol +import org.jetbrains.kotlin.ir.symbols.UnsafeDuringIrConstructionAPI import org.jetbrains.kotlin.ir.types.* import org.jetbrains.kotlin.ir.util.* import org.jetbrains.kotlin.name.Name @@ -45,6 +45,7 @@ private const val RPC_FIELDS_METHOD = "rpcFields" internal class RPCStubGenerator( private val declaration: ServiceDeclaration, private val ctx: RPCIrContext, + @Suppress("unused") private val logger: MessageCollector, ) { private fun irBuilder(symbol: IrSymbol): DeclarationIrBuilder = @@ -57,8 +58,6 @@ internal class RPCStubGenerator( generateStubClass() addAssociatedObjectAnnotationIfPossible() - - logger.report(CompilerMessageSeverity.WARNING, declaration.service.dump()) } private fun generateStubClass() { @@ -102,16 +101,7 @@ internal class RPCStubGenerator( type = ctx.rpcClient.defaultType } - // default constructor implementation - body = irBuilder(symbol).irBlockBody { - +irDelegatingConstructorCall(context.irBuiltIns.anyClass.owner.constructors.single()) - +IrInstanceInitializerCallImpl( - startOffset = startOffset, - endOffset = endOffset, - classSymbol = this@generateStubConstructor.symbol, - type = context.irBuiltIns.unitType, - ) - } + addDefaultConstructor(this) } } @@ -147,7 +137,7 @@ internal class RPCStubGenerator( * ``` */ private fun IrClass.stubIdProperty() { - stubIdProperty = constructorProperty(STUB_ID_PROPERTY, ctx.irBuiltIns.longType, stubIdValueParameter) + stubIdProperty = addConstructorProperty(STUB_ID_PROPERTY, ctx.irBuiltIns.longType, stubIdValueParameter) } private var clientProperty: IrProperty by Delegates.notNull() @@ -160,17 +150,27 @@ internal class RPCStubGenerator( * ``` */ private fun IrClass.clientProperty() { - clientProperty = constructorProperty(CLIENT_PROPERTY, ctx.rpcClient.defaultType, clientValueParameter) + clientProperty = addConstructorProperty(CLIENT_PROPERTY, ctx.rpcClient.defaultType, clientValueParameter) } - private fun IrClass.constructorProperty( + private fun IrClass.addConstructorProperty( propertyName: String, propertyType: IrType, valueParameter: IrValueParameter, + propertyVisibility: DescriptorVisibility = DescriptorVisibilities.PRIVATE + ): IrProperty { + return addConstructorProperty(Name.identifier(propertyName), propertyType, valueParameter, propertyVisibility) + } + + private fun IrClass.addConstructorProperty( + propertyName: Name, + propertyType: IrType, + valueParameter: IrValueParameter, + propertyVisibility: DescriptorVisibility = DescriptorVisibilities.PRIVATE ): IrProperty { return addProperty { - name = Name.identifier(propertyName) - visibility = DescriptorVisibilities.PRIVATE + name = propertyName + visibility = propertyVisibility }.apply { addBackingFieldUtil { visibility = DescriptorVisibilities.PRIVATE @@ -188,8 +188,8 @@ internal class RPCStubGenerator( ) } - addDefaultGetter(this@constructorProperty, ctx.irBuiltIns) { - visibility = DescriptorVisibilities.PRIVATE + addDefaultGetter(this@addConstructorProperty, ctx.irBuiltIns) { + visibility = propertyVisibility } } } @@ -285,8 +285,7 @@ internal class RPCStubGenerator( private fun IrClass.rpcFlowField(field: ServiceDeclaration.FlowField) { val isLazy = !field.property.hasAnnotation(ctx.rpcEagerFieldAnnotation) - val servicePropertyGetter = field.property.getter - ?: error("RPC field declared in service interface expected to have getters: ${field.property.dump()}") + val servicePropertyGetter = field.property.getterOrFail addProperty { name = field.property.name @@ -537,16 +536,10 @@ internal class RPCStubGenerator( "detekt.LongMethod", ) private fun IrClass.generateRpcMethod(method: ServiceDeclaration.Method) { - val isMethodObject = method.argumentTypes.isEmpty() + val isMethodObject = method.arguments.isEmpty() val methodClassName = method.function.name.rpcMethodClassName - val methodClass: IrClass = findDeclaration { it.name == methodClassName } - ?: error( - "Expected $methodClassName class to be present in stub class " + - "${declaration.service.name}${declaration.stubClass.name}" - ) - - methodClasses.add(methodClass) + val methodClass: IrClass = initiateAndGetMethodClass(methodClassName, method) addFunction { name = method.function.name @@ -564,7 +557,7 @@ internal class RPCStubGenerator( val declaredFunction = this - val arguments = method.argumentTypes.memoryOptimizedMap { arg -> + val arguments = method.arguments.memoryOptimizedMap { arg -> addValueParameter { name = arg.value.name type = arg.type @@ -629,6 +622,123 @@ internal class RPCStubGenerator( } } + /** + * Frontend plugins generate the following: + * ```kotlin + * // Given rpc method: + * suspend fun hello(arg1: String, arg2: Int) + * + * // Frontend generates: + * @Serializable + * class hello$rpcMethod { + * constructor(arg1: String, arg2: String) + * + * val arg1: String + * val arg2: Int + * } + * ``` + * + * This method generates missing getters and backing fields' values. + * And adds RPCMethodClassArguments supertype with `asArray` method implemented. + * + * Resulting class: + * ```kotlin + * @Serializable + * class hello$rpcMethod( + * val arg1: String, + * val arg2: Int, + * ) : RPCMethodClassArguments { + * // or emptyArray when no arguments + * override fun asArray(): Array = arrayOf(arg1, arg2) + * } + * ``` + */ + private fun IrClass.initiateAndGetMethodClass(methodClassName: Name, method: ServiceDeclaration.Method): IrClass { + val methodClass = findDeclaration { it.name == methodClassName } + ?: error( + "Expected $methodClassName class to be present in stub class " + + "${declaration.service.name}${declaration.stubClass.name}" + ) + + methodClasses.add(methodClass) + + val methodClassThisReceiver = methodClass.thisReceiver + ?: error("Expected $methodClassName of ${declaration.stubClass.name} to have a thisReceiver") + + val properties = if (methodClass.isClass) { + val argNames = method.arguments.memoryOptimizedMap { it.value.name }.toSet() + + // remove frontend generated properties + // new ones will be used instead + methodClass.declarations.removeAll { it is IrProperty && it.name in argNames } + + // primary constructor, serialization may add another + val constructor = methodClass.constructors.single { + method.arguments.size == it.valueParameters.size + } + + constructor.isPrimary = true + methodClass.addDefaultConstructor(constructor) + + constructor.valueParameters.memoryOptimizedMap { valueParam -> + methodClass.addConstructorProperty( + propertyName = valueParam.name, + propertyType = valueParam.type, + valueParameter = valueParam, + propertyVisibility = DescriptorVisibilities.PUBLIC, + ) + } + } else { + emptyList() + } + + methodClass.superTypes = listOf(ctx.rpcMethodClassArguments.defaultType) + + methodClass.addFunction { + name = ctx.functions.asArray.name + visibility = DescriptorVisibilities.PUBLIC + returnType = ctx.arrayOfAnyNullable + modality = Modality.OPEN + }.apply { + overriddenSymbols = listOf(ctx.functions.asArray.symbol) + + val asArrayThisReceiver = vsApi { + methodClassThisReceiver.copyToVS(this@apply, origin = IrDeclarationOrigin.DEFINED) + }.also { + dispatchReceiverParameter = it + } + + body = irBuilder(symbol).irBlockBody { + val callee = if (methodClass.isObject) { + ctx.functions.emptyArray + } else { + ctx.irBuiltIns.arrayOf + } + + val arrayOfCall = irCall(callee, type = ctx.arrayOfAnyNullable).apply arrayOfCall@{ + putTypeArgument(0, ctx.anyNullable) + + if (methodClass.isObject) { + return@arrayOfCall + } + + val vararg = irVararg( + elementType = ctx.anyNullable, + values = properties.memoryOptimizedMap { property -> + irCallProperty(methodClass, property, symbol = asArrayThisReceiver.symbol) + }, + ) + + putValueArgument(0, vararg) + } + + +irReturn(arrayOfCall) + } + } + + return methodClass + } + /** * Part of [generateRpcMethod] that generates next call: * @@ -704,7 +814,7 @@ internal class RPCStubGenerator( irCallConstructor( // serialization plugin adds additional constructor with more arguments callee = methodClass.constructors.single { - it.valueParameters.size == method.argumentTypes.size + it.valueParameters.size == method.arguments.size }.symbol, typeArguments = emptyList(), ).apply { @@ -760,8 +870,7 @@ internal class RPCStubGenerator( } private fun irCallProperty(receiver: IrExpression, property: IrProperty): IrCall { - val getter = property.getter - ?: error("Expected property getter to call it: ${property.dump()}") + val getter = property.getterOrFail return IrCallImpl( startOffset = UNDEFINED_OFFSET, @@ -813,15 +922,7 @@ internal class RPCStubGenerator( isPrimary = true visibility = DescriptorVisibilities.PRIVATE }.apply { - body = irBuilder(symbol).irBlockBody { - +irDelegatingConstructorCall(context.irBuiltIns.anyClass.owner.constructors.single()) - +IrInstanceInitializerCallImpl( - startOffset = startOffset, - endOffset = endOffset, - classSymbol = this@generateCompanionObjectConstructor.symbol, - type = context.irBuiltIns.unitType, - ) - } + addDefaultConstructor(this) } } @@ -870,7 +971,7 @@ internal class RPCStubGenerator( addBackingFieldUtil { type = mapType isFinal = true - vsApi { modalityVS = Modality.FINAL } + vsApi { isFinalVS = true } visibility = DescriptorVisibilities.PRIVATE }.apply { val isEmpty = declaration.methods.isEmpty() @@ -1090,12 +1191,10 @@ internal class RPCStubGenerator( return@listApply } - val anyArrayType = ctx.irBuiltIns.arrayClass.typeWith(ctx.anyNullable, Variance.OUT_VARIANCE) - val vararg = IrVarargImpl( startOffset = UNDEFINED_OFFSET, endOffset = UNDEFINED_OFFSET, - type = anyArrayType, + type = ctx.arrayOfAnyNullable, varargElementType = ctx.anyNullable, elements = declaration.fields.memoryOptimizedMap { irCallProperty(irGet(service), it.property) @@ -1144,10 +1243,23 @@ internal class RPCStubGenerator( } } + // default constructor implementation + private fun IrClass.addDefaultConstructor(constructor: IrConstructor) { + constructor.body = irBuilder(constructor.symbol).irBlockBody { + +irDelegatingConstructorCall(context.irBuiltIns.anyClass.owner.constructors.single()) + +IrInstanceInitializerCallImpl( + startOffset = startOffset, + endOffset = endOffset, + classSymbol = this@addDefaultConstructor.symbol, + type = context.irBuiltIns.unitType, + ) + } + } + // adds fake overrides for toString(), equals(), hashCode() for a class private fun IrClass.addAnyOverrides(parent: IrClass? = null) { val anyClass = ctx.irBuiltIns.anyClass.owner - val overridenClass = parent ?: anyClass + val overriddenClass = parent ?: anyClass addFunction { name = OperatorNameConventions.EQUALS @@ -1159,7 +1271,9 @@ internal class RPCStubGenerator( isFakeOverride = true returnType = ctx.irBuiltIns.booleanType }.apply { - overriddenSymbols += overridenClass.functions.single { it.name == OperatorNameConventions.EQUALS }.symbol + overriddenSymbols += overriddenClass.functions.single { + it.name == OperatorNameConventions.EQUALS + }.symbol dispatchReceiverParameter = anyClass.thisReceiver @@ -1178,7 +1292,9 @@ internal class RPCStubGenerator( isFakeOverride = true returnType = ctx.irBuiltIns.intType }.apply { - overriddenSymbols += overridenClass.functions.single { it.name == OperatorNameConventions.HASH_CODE }.symbol + overriddenSymbols += overriddenClass.functions.single { + it.name == OperatorNameConventions.HASH_CODE + }.symbol dispatchReceiverParameter = anyClass.thisReceiver } @@ -1192,7 +1308,9 @@ internal class RPCStubGenerator( isFakeOverride = true returnType = ctx.irBuiltIns.stringType }.apply { - overriddenSymbols += overridenClass.functions.single { it.name == OperatorNameConventions.TO_STRING }.symbol + overriddenSymbols += overriddenClass.functions.single { + it.name == OperatorNameConventions.TO_STRING + }.symbol dispatchReceiverParameter = anyClass.thisReceiver } diff --git a/compiler-plugin/src/main/core/kotlinx/rpc/codegen/extension/ServiceDeclaration.kt b/compiler-plugin/src/main/core/kotlinx/rpc/codegen/extension/ServiceDeclaration.kt index 81a8ab32..042cc6e3 100644 --- a/compiler-plugin/src/main/core/kotlinx/rpc/codegen/extension/ServiceDeclaration.kt +++ b/compiler-plugin/src/main/core/kotlinx/rpc/codegen/extension/ServiceDeclaration.kt @@ -22,7 +22,7 @@ class ServiceDeclaration( class Method( val function: IrSimpleFunction, - val argumentTypes: List, + val arguments: List, ) { class Argument( val value: IrValueParameter, diff --git a/compiler-plugin/src/main/latest/kotlinx/rpc/codegen/VersionSpecificApiImpl.kt b/compiler-plugin/src/main/latest/kotlinx/rpc/codegen/VersionSpecificApiImpl.kt index 7898e1f0..02eebd9e 100644 --- a/compiler-plugin/src/main/latest/kotlinx/rpc/codegen/VersionSpecificApiImpl.kt +++ b/compiler-plugin/src/main/latest/kotlinx/rpc/codegen/VersionSpecificApiImpl.kt @@ -6,7 +6,6 @@ package kotlinx.rpc.codegen import org.jetbrains.kotlin.backend.common.extensions.IrPluginContext import org.jetbrains.kotlin.backend.common.ir.addExtensionReceiver -import org.jetbrains.kotlin.descriptors.Modality import org.jetbrains.kotlin.ir.builders.declarations.IrFieldBuilder import org.jetbrains.kotlin.ir.declarations.IrDeclarationOrigin import org.jetbrains.kotlin.ir.declarations.IrFunction @@ -17,7 +16,6 @@ import org.jetbrains.kotlin.ir.expressions.IrStatementOrigin import org.jetbrains.kotlin.ir.symbols.IrClassSymbol import org.jetbrains.kotlin.ir.symbols.IrSimpleFunctionSymbol import org.jetbrains.kotlin.ir.types.IrType -import org.jetbrains.kotlin.ir.types.impl.IrErrorClassImpl.modality import org.jetbrains.kotlin.ir.util.copyTo import org.jetbrains.kotlin.name.CallableId import org.jetbrains.kotlin.name.ClassId @@ -31,9 +29,11 @@ object VersionSpecificApiImpl : VersionSpecificApi { return platform.isJs() } - override var IrFieldBuilder.modalityVS: Modality - get() = modality - set(value) { modality = value } + override var IrFieldBuilder.isFinalVS: Boolean + get() = isFinal + set(value) { + isFinal = value + } override var IrCall.originVS: IrStatementOrigin? get() = origin diff --git a/compiler-plugin/src/main/v_1_7/kotlinx/rpc/codegen/VersionSpecificApiImpl.kt b/compiler-plugin/src/main/v_1_7/kotlinx/rpc/codegen/VersionSpecificApiImpl.kt index 0aa1ffb9..411eccd9 100644 --- a/compiler-plugin/src/main/v_1_7/kotlinx/rpc/codegen/VersionSpecificApiImpl.kt +++ b/compiler-plugin/src/main/v_1_7/kotlinx/rpc/codegen/VersionSpecificApiImpl.kt @@ -26,7 +26,7 @@ object VersionSpecificApiImpl : VersionSpecificApi { override fun isJs(platform: TargetPlatform?): Boolean { return platform.isJs() } - override var IrFieldBuilder.modalityVS: Modality + override var IrFieldBuilder.isFinalVS: Boolean get() = undefinedAPI() set(_) {} diff --git a/compiler-plugin/src/main/v_1_7_2/kotlinx/rpc/codegen/VersionSpecificApiImpl.kt b/compiler-plugin/src/main/v_1_7_2/kotlinx/rpc/codegen/VersionSpecificApiImpl.kt index 5373cc6f..1823b456 100644 --- a/compiler-plugin/src/main/v_1_7_2/kotlinx/rpc/codegen/VersionSpecificApiImpl.kt +++ b/compiler-plugin/src/main/v_1_7_2/kotlinx/rpc/codegen/VersionSpecificApiImpl.kt @@ -29,7 +29,7 @@ object VersionSpecificApiImpl : VersionSpecificApi { override fun isJs(platform: TargetPlatform?): Boolean { return platform.isJs() } - override var IrFieldBuilder.modalityVS: Modality + override var IrFieldBuilder.isFinalVS: Boolean get() = undefinedAPI() set(_) {} diff --git a/compiler-plugin/src/main/v_1_8/kotlinx/rpc/codegen/VersionSpecificApiImpl.kt b/compiler-plugin/src/main/v_1_8/kotlinx/rpc/codegen/VersionSpecificApiImpl.kt index 0ec5b4fa..132e21e5 100644 --- a/compiler-plugin/src/main/v_1_8/kotlinx/rpc/codegen/VersionSpecificApiImpl.kt +++ b/compiler-plugin/src/main/v_1_8/kotlinx/rpc/codegen/VersionSpecificApiImpl.kt @@ -29,7 +29,7 @@ object VersionSpecificApiImpl : VersionSpecificApi { override fun isJs(platform: TargetPlatform?): Boolean { return platform.isJs() } - override var IrFieldBuilder.modalityVS: Modality + override var IrFieldBuilder.isFinalVS: Boolean get() = undefinedAPI() set(_) {} diff --git a/compiler-plugin/src/main/v_1_9/kotlinx/rpc/codegen/RPCCompilerPlugin.kt b/compiler-plugin/src/main/v_1_9/kotlinx/rpc/codegen/RPCCompilerPlugin.kt new file mode 100644 index 00000000..e7c1d9a7 --- /dev/null +++ b/compiler-plugin/src/main/v_1_9/kotlinx/rpc/codegen/RPCCompilerPlugin.kt @@ -0,0 +1,36 @@ +/* + * Copyright 2023-2024 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.rpc.codegen + +import org.jetbrains.kotlin.backend.common.extensions.IrGenerationExtension +import org.jetbrains.kotlin.compiler.plugin.CliOption +import org.jetbrains.kotlin.compiler.plugin.CommandLineProcessor +import org.jetbrains.kotlin.compiler.plugin.CompilerPluginRegistrar +import org.jetbrains.kotlin.compiler.plugin.ExperimentalCompilerApi +import org.jetbrains.kotlin.config.CompilerConfiguration +import org.jetbrains.kotlin.fir.extensions.FirExtensionRegistrarAdapter + +@OptIn(ExperimentalCompilerApi::class) +class RPCCommandLineProcessor : CommandLineProcessor { + override val pluginId = "kotlinx.rpc.codegen" + + override val pluginOptions = emptyList() +} + +@OptIn(ExperimentalCompilerApi::class) +class RPCCompilerPlugin : CompilerPluginRegistrar() { + init { + VersionSpecificApi.INSTANCE = VersionSpecificApiImpl + } + + override val supportsK2: Boolean = true + + override fun ExtensionStorage.registerExtensions(configuration: CompilerConfiguration) { + val extension = RPCCompilerPluginCore.provideExtension(configuration) + + IrGenerationExtension.registerExtension(extension) + FirExtensionRegistrarAdapter.registerExtension(FirRPCExtensionRegistrar(configuration)) + } +} diff --git a/compiler-plugin/src/main/v_1_9/kotlinx/rpc/codegen/VersionSpecificApiImpl.kt b/compiler-plugin/src/main/v_1_9/kotlinx/rpc/codegen/VersionSpecificApiImpl.kt new file mode 100644 index 00000000..dbbc03a4 --- /dev/null +++ b/compiler-plugin/src/main/v_1_9/kotlinx/rpc/codegen/VersionSpecificApiImpl.kt @@ -0,0 +1,73 @@ +/* + * Copyright 2023-2024 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.rpc.codegen + +import org.jetbrains.kotlin.backend.common.extensions.IrPluginContext +import org.jetbrains.kotlin.backend.common.ir.addExtensionReceiver +import org.jetbrains.kotlin.descriptors.Modality +import org.jetbrains.kotlin.ir.builders.declarations.IrFieldBuilder +import org.jetbrains.kotlin.ir.declarations.IrDeclarationOrigin +import org.jetbrains.kotlin.ir.declarations.IrFunction +import org.jetbrains.kotlin.ir.declarations.IrSimpleFunction +import org.jetbrains.kotlin.ir.declarations.IrValueParameter +import org.jetbrains.kotlin.ir.expressions.IrCall +import org.jetbrains.kotlin.ir.expressions.IrStatementOrigin +import org.jetbrains.kotlin.ir.symbols.IrClassSymbol +import org.jetbrains.kotlin.ir.symbols.IrSimpleFunctionSymbol +import org.jetbrains.kotlin.ir.types.IrType +import org.jetbrains.kotlin.ir.util.copyTo +import org.jetbrains.kotlin.name.CallableId +import org.jetbrains.kotlin.name.ClassId +import org.jetbrains.kotlin.name.FqName +import org.jetbrains.kotlin.name.Name +import org.jetbrains.kotlin.platform.TargetPlatform +import org.jetbrains.kotlin.platform.isJs + +object VersionSpecificApiImpl : VersionSpecificApi { + override fun isJs(platform: TargetPlatform?): Boolean { + return platform.isJs() + } + + override var IrFieldBuilder.isFinalVS: Boolean + get() = modality == Modality.FINAL + set(value) { + modality = if (value) Modality.FINAL else Modality.OPEN + } + + override var IrCall.originVS: IrStatementOrigin? + get() = origin + set(value) { origin = value } + + override fun referenceClass(context: IrPluginContext, packageName: String, name: String): IrClassSymbol? { + return context.referenceClass( + ClassId( + FqName(packageName), + FqName(name), + false + ) + ) + } + + override fun referenceFunctions( + context: IrPluginContext, + packageName: String, + name: String + ): Collection { + return context.referenceFunctions( + CallableId( + FqName(packageName), + Name.identifier(name), + ) + ) + } + + override fun IrValueParameter.copyToVS(irFunction: IrFunction, origin: IrDeclarationOrigin): IrValueParameter { + return copyTo(irFunction, origin) + } + + override fun IrSimpleFunction.addExtensionReceiverVS(type: IrType, origin: IrDeclarationOrigin): IrValueParameter { + return addExtensionReceiver(type, origin) + } +} diff --git a/core/src/commonMain/kotlin/kotlinx/rpc/internal/RPCMethodClassArguments.kt b/core/src/commonMain/kotlin/kotlinx/rpc/internal/RPCMethodClassArguments.kt index ce3d09bc..57657334 100644 --- a/core/src/commonMain/kotlin/kotlinx/rpc/internal/RPCMethodClassArguments.kt +++ b/core/src/commonMain/kotlin/kotlinx/rpc/internal/RPCMethodClassArguments.kt @@ -6,5 +6,5 @@ package kotlinx.rpc.internal @InternalRPCApi public interface RPCMethodClassArguments { - public fun asArray(): Array + public fun asArray(): Array } From c34f34c6650f551e2de5ada28f9b0d9da2b83874 Mon Sep 17 00:00:00 2001 From: Alexander Sysoev Date: Mon, 29 Jul 2024 17:25:50 +0200 Subject: [PATCH 3/5] Update to 2.0 --- .gitignore | 1 + compiler-plugin/compiler-plugin-k2/build.gradle.kts | 13 +++++-------- .../kotlin/kotlinx/rpc/internal/ExceptionUtils.kt | 4 +++- .../kotlinx/rpc/internal/transport/RPCPluginKey.kt | 2 +- .../kotlinx/rpc/internal/ExceptionUtils.js.kt | 2 +- .../kotlinx/rpc/internal/ExceptionUtils.jvm.kt | 2 +- .../kotlinx/rpc/internal/ExceptionUtils.native.kt | 2 +- .../src/main/kotlin/util/TargetUtils.kt | 9 +++------ .../conventions-kotlin-version-jvm.gradle.kts | 5 ++++- .../conventions-kotlin-version-kmp.gradle.kts | 5 ++++- .../src/main/kotlin/util/CompilerOptions.kt | 11 ++++++++--- .../conventions-kotlin-version-jvm.gradle.kts | 5 ++++- .../conventions-kotlin-version-kmp.gradle.kts | 5 ++++- .../src/main/kotlin/util/CompilerOptions.kt | 11 ++++++++--- gradle.properties | 3 +++ gradle/kotlin-versions-lookup.csv | 1 + gradle/libs.versions.toml | 2 +- 17 files changed, 53 insertions(+), 30 deletions(-) diff --git a/.gitignore b/.gitignore index 69965f44..caac4238 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ # Ignore Gradle build output directory build +.kotlin # idea files .idea/* diff --git a/compiler-plugin/compiler-plugin-k2/build.gradle.kts b/compiler-plugin/compiler-plugin-k2/build.gradle.kts index 59768498..342cf67c 100644 --- a/compiler-plugin/compiler-plugin-k2/build.gradle.kts +++ b/compiler-plugin/compiler-plugin-k2/build.gradle.kts @@ -3,7 +3,6 @@ */ import org.jetbrains.kotlin.gradle.dsl.ExplicitApiMode -import org.jetbrains.kotlin.gradle.tasks.KotlinCompile plugins { alias(libs.plugins.conventions.jvm) @@ -12,16 +11,14 @@ plugins { kotlin { explicitApi = ExplicitApiMode.Disabled + + compilerOptions { + freeCompilerArgs.add("-Xcontext-receivers") + } } dependencies { compileOnly(libs.kotlin.compiler.embeddable) - compileOnly("org.jetbrains.kotlin:kotlin-serialization-compiler-plugin:1.9.24") + compileOnly(libs.serialization.plugin) implementation(projects.compilerPluginCommon) } - -tasks.withType().configureEach { - kotlinOptions { - freeCompilerArgs += "-Xcontext-receivers" - } -} diff --git a/core/src/commonMain/kotlin/kotlinx/rpc/internal/ExceptionUtils.kt b/core/src/commonMain/kotlin/kotlinx/rpc/internal/ExceptionUtils.kt index 30e849a3..9c67a0b5 100644 --- a/core/src/commonMain/kotlin/kotlinx/rpc/internal/ExceptionUtils.kt +++ b/core/src/commonMain/kotlin/kotlinx/rpc/internal/ExceptionUtils.kt @@ -28,4 +28,6 @@ internal expect class DeserializedException( stacktrace: List, cause: SerializedException?, className: String -) : Throwable +) : Throwable { + override val message: String +} diff --git a/core/src/commonMain/kotlin/kotlinx/rpc/internal/transport/RPCPluginKey.kt b/core/src/commonMain/kotlin/kotlinx/rpc/internal/transport/RPCPluginKey.kt index d6d47fa6..2eeb3562 100644 --- a/core/src/commonMain/kotlin/kotlinx/rpc/internal/transport/RPCPluginKey.kt +++ b/core/src/commonMain/kotlin/kotlinx/rpc/internal/transport/RPCPluginKey.kt @@ -51,7 +51,7 @@ public enum class RPCPluginKey(override val uniqueIndex: Int, private val associ init { require(ordinal == 0 || associatedPlugin != RPCPlugin.UNKNOWN) { error("associatedPlugin must not be $RPCPlugin.${RPCPlugin.UNKNOWN} " + - "for anything other than $RPCPluginKey.${UNKNOWN}") + "for anything other than $RPCPluginKey.UNKNOWN") } } diff --git a/core/src/jsMain/kotlin/kotlinx/rpc/internal/ExceptionUtils.js.kt b/core/src/jsMain/kotlin/kotlinx/rpc/internal/ExceptionUtils.js.kt index b9b43951..cbd4c3cf 100644 --- a/core/src/jsMain/kotlin/kotlinx/rpc/internal/ExceptionUtils.js.kt +++ b/core/src/jsMain/kotlin/kotlinx/rpc/internal/ExceptionUtils.js.kt @@ -11,7 +11,7 @@ import kotlinx.rpc.internal.transport.StackElement internal actual class DeserializedException actual constructor( private val toStringMessage: String, - override val message: String, + actual override val message: String, stacktrace: List, cause: SerializedException?, className: String diff --git a/core/src/jvmMain/kotlin/kotlinx/rpc/internal/ExceptionUtils.jvm.kt b/core/src/jvmMain/kotlin/kotlinx/rpc/internal/ExceptionUtils.jvm.kt index b4539d49..982afdc3 100644 --- a/core/src/jvmMain/kotlin/kotlinx/rpc/internal/ExceptionUtils.jvm.kt +++ b/core/src/jvmMain/kotlin/kotlinx/rpc/internal/ExceptionUtils.jvm.kt @@ -13,7 +13,7 @@ private val throwableFields = Throwable::class.java.fieldsCountOrDefault(-1) internal actual class DeserializedException actual constructor( private val toStringMessage: String, - override val message: String, + actual override val message: String, stacktrace: List, cause: SerializedException?, private val className: String diff --git a/core/src/nativeMain/kotlin/kotlinx/rpc/internal/ExceptionUtils.native.kt b/core/src/nativeMain/kotlin/kotlinx/rpc/internal/ExceptionUtils.native.kt index dab60184..fa5d0f33 100644 --- a/core/src/nativeMain/kotlin/kotlinx/rpc/internal/ExceptionUtils.native.kt +++ b/core/src/nativeMain/kotlin/kotlinx/rpc/internal/ExceptionUtils.native.kt @@ -11,7 +11,7 @@ import kotlinx.rpc.internal.transport.StackElement internal actual class DeserializedException actual constructor( private val toStringMessage: String, - override val message: String, + actual override val message: String, stacktrace: List, cause: SerializedException?, className: String diff --git a/gradle-conventions/conventions-utils/src/main/kotlin/util/TargetUtils.kt b/gradle-conventions/conventions-utils/src/main/kotlin/util/TargetUtils.kt index af76fda5..7f21f783 100644 --- a/gradle-conventions/conventions-utils/src/main/kotlin/util/TargetUtils.kt +++ b/gradle-conventions/conventions-utils/src/main/kotlin/util/TargetUtils.kt @@ -8,7 +8,6 @@ import groovy.json.JsonSlurper import io.gitlab.arturbosch.detekt.extensions.DetektExtension import org.gradle.api.Action import org.gradle.api.Project -import org.gradle.jvm.toolchain.JavaLanguageVersion import org.gradle.kotlin.dsl.the import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension import org.jetbrains.kotlin.gradle.plugin.KotlinTarget @@ -82,11 +81,7 @@ private fun KotlinMultiplatformExtension.configureTargets( } if (jvm && isIncluded("jvm", kotlinVersion, targetsLookup)) { - jvm { - jvmToolchain { - languageVersion.set(JavaLanguageVersion.of(8)) - } - }.also { targets.add(it) } + jvm().also { targets.add(it) } } if (js && isIncluded("js", kotlinVersion, targetsLookup)) { @@ -131,6 +126,8 @@ fun Project.configureKotlin( configureDetekt(includedTargets) + jvmToolchain(8) + action.execute(this) } } diff --git a/gradle-conventions/kotlin-version-new/src/main/kotlin/conventions-kotlin-version-jvm.gradle.kts b/gradle-conventions/kotlin-version-new/src/main/kotlin/conventions-kotlin-version-jvm.gradle.kts index b5a13821..c41495ac 100644 --- a/gradle-conventions/kotlin-version-new/src/main/kotlin/conventions-kotlin-version-jvm.gradle.kts +++ b/gradle-conventions/kotlin-version-new/src/main/kotlin/conventions-kotlin-version-jvm.gradle.kts @@ -3,8 +3,11 @@ */ import org.jetbrains.kotlin.gradle.dsl.KotlinJvmProjectExtension +import util.optionalProperty import util.projectLanguageVersion +val useK2Plugin: Boolean by optionalProperty() + configure { - compilerOptions(projectLanguageVersion) + compilerOptions(projectLanguageVersion(useK2Plugin)) } diff --git a/gradle-conventions/kotlin-version-new/src/main/kotlin/conventions-kotlin-version-kmp.gradle.kts b/gradle-conventions/kotlin-version-new/src/main/kotlin/conventions-kotlin-version-kmp.gradle.kts index 6659ba87..6a3d3143 100644 --- a/gradle-conventions/kotlin-version-new/src/main/kotlin/conventions-kotlin-version-kmp.gradle.kts +++ b/gradle-conventions/kotlin-version-new/src/main/kotlin/conventions-kotlin-version-kmp.gradle.kts @@ -4,9 +4,12 @@ import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension +import util.optionalProperty import util.projectLanguageVersion +val useK2Plugin: Boolean by optionalProperty() + @OptIn(ExperimentalKotlinGradlePluginApi::class) configure { - compilerOptions(projectLanguageVersion) + compilerOptions(projectLanguageVersion(useK2Plugin)) } diff --git a/gradle-conventions/kotlin-version-new/src/main/kotlin/util/CompilerOptions.kt b/gradle-conventions/kotlin-version-new/src/main/kotlin/util/CompilerOptions.kt index 5faaf361..5d63bb2f 100644 --- a/gradle-conventions/kotlin-version-new/src/main/kotlin/util/CompilerOptions.kt +++ b/gradle-conventions/kotlin-version-new/src/main/kotlin/util/CompilerOptions.kt @@ -12,7 +12,12 @@ import org.jetbrains.kotlin.gradle.dsl.KotlinVersion * * This should be lined up with the minimal supported compiler plugin version */ -val projectLanguageVersion: KotlinCommonCompilerOptions.() -> Unit = { - languageVersion.set(KotlinVersion.KOTLIN_1_7) - apiVersion.set(KotlinVersion.KOTLIN_1_7) +fun projectLanguageVersion(useK2Plugin: Boolean): KotlinCommonCompilerOptions.() -> Unit = { + val kotlinVersion = when { + useK2Plugin -> KotlinVersion.KOTLIN_2_0 + else -> KotlinVersion.KOTLIN_1_7 + } + + languageVersion.set(kotlinVersion) + apiVersion.set(kotlinVersion) } diff --git a/gradle-conventions/kotlin-version-old/src/main/kotlin/conventions-kotlin-version-jvm.gradle.kts b/gradle-conventions/kotlin-version-old/src/main/kotlin/conventions-kotlin-version-jvm.gradle.kts index c83983d7..fcbf1600 100644 --- a/gradle-conventions/kotlin-version-old/src/main/kotlin/conventions-kotlin-version-jvm.gradle.kts +++ b/gradle-conventions/kotlin-version-old/src/main/kotlin/conventions-kotlin-version-jvm.gradle.kts @@ -2,6 +2,9 @@ * Copyright 2023-2024 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. */ +import util.optionalProperty import util.setLanguageVersion -setLanguageVersion() +val useK2Plugin: Boolean by optionalProperty() + +setLanguageVersion(useK2Plugin) diff --git a/gradle-conventions/kotlin-version-old/src/main/kotlin/conventions-kotlin-version-kmp.gradle.kts b/gradle-conventions/kotlin-version-old/src/main/kotlin/conventions-kotlin-version-kmp.gradle.kts index c83983d7..fcbf1600 100644 --- a/gradle-conventions/kotlin-version-old/src/main/kotlin/conventions-kotlin-version-kmp.gradle.kts +++ b/gradle-conventions/kotlin-version-old/src/main/kotlin/conventions-kotlin-version-kmp.gradle.kts @@ -2,6 +2,9 @@ * Copyright 2023-2024 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. */ +import util.optionalProperty import util.setLanguageVersion -setLanguageVersion() +val useK2Plugin: Boolean by optionalProperty() + +setLanguageVersion(useK2Plugin) diff --git a/gradle-conventions/kotlin-version-old/src/main/kotlin/util/CompilerOptions.kt b/gradle-conventions/kotlin-version-old/src/main/kotlin/util/CompilerOptions.kt index cbb06182..18867b3c 100644 --- a/gradle-conventions/kotlin-version-old/src/main/kotlin/util/CompilerOptions.kt +++ b/gradle-conventions/kotlin-version-old/src/main/kotlin/util/CompilerOptions.kt @@ -8,11 +8,16 @@ import org.gradle.api.Project import org.gradle.kotlin.dsl.withType import org.jetbrains.kotlin.gradle.tasks.KotlinCompile -fun Project.setLanguageVersion() { +fun Project.setLanguageVersion(useK2Plugin: Boolean) { + val kotlinVersion = when { + useK2Plugin -> "2.0" + else -> "1.7" + } + tasks.withType().all { kotlinOptions { - freeCompilerArgs += "-language-version=1.7" - freeCompilerArgs += "-api-version=1.7" + freeCompilerArgs += "-language-version=$kotlinVersion" + freeCompilerArgs += "-api-version=$kotlinVersion" } } } diff --git a/gradle.properties b/gradle.properties index 9eceabf4..e14ead2f 100644 --- a/gradle.properties +++ b/gradle.properties @@ -31,3 +31,6 @@ systemProp.org.gradle.kotlin.dsl.precompiled.accessors.strict=true # uncomment to debug compilation process #kotlin.compiler.execution.strategy=in-process + +# set to true to use K2 RPC compiler plugin to build and run the project (including tests) +kotlinx.rpc.useK2Plugin=false diff --git a/gradle/kotlin-versions-lookup.csv b/gradle/kotlin-versions-lookup.csv index 17c1edbf..bccaa716 100644 --- a/gradle/kotlin-versions-lookup.csv +++ b/gradle/kotlin-versions-lookup.csv @@ -1,4 +1,5 @@ Kotlin,ksp,atomicfu,serialization,detekt-gradle-plugin,gradle-kotlin-dsl,binary-compatibility-validator,kover +2.0.0,1.0.23,0.22.0,1.6.1,1.23.6,4.1.0,0.14.0,0.8.0 1.9.24,1.0.20,0.22.0,1.6.1,1.23.6,4.1.0,0.14.0,0.8.0 1.9.23,1.0.19,0.22.0,1.6.1,1.23.6,4.1.0,0.14.0,0.8.0 1.9.22,1.0.17,0.22.0,1.6.1,1.23.6,4.1.0,0.14.0,0.8.0 diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 52422672..719e0960 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -3,7 +3,7 @@ kotlinx-rpc = "0.2.1" # kotlin -kotlin-lang = "1.9.24" +kotlin-lang = "2.0.0" # kotlin independent versions detekt-analyzer = "1.23.6" From 93016e65e7861aa8243f24b21eea63ebba8da018 Mon Sep 17 00:00:00 2001 From: Alexander Sysoev Date: Mon, 29 Jul 2024 17:28:59 +0200 Subject: [PATCH 4/5] Fix for KT-70132 --- core/build.gradle.kts | 7 +++ .../kotlinx/rpc/internal/WithRPCStubObject.kt | 2 +- .../rpc/internal/WithRPCStubObject.js.kt | 53 +++++++++++++++++-- .../rpc/internal/WithRPCStubObject.jvm.kt | 15 ++++-- .../rpc/internal/WithRPCStubObject.native.kt | 16 ++++-- gradle/libs.versions.toml | 2 + .../kotlinx/rpc/client/RPCClientUtils.kt | 9 +++- .../rpc/client/awaitFieldInitialization.kt | 5 +- .../rpc/server/internal/RPCServiceUtils.kt | 5 +- .../codegen-tests-mpp/build.gradle.kts | 14 +++-- .../src/jsMain/resources/index.html | 14 +++++ .../kotlinx/rpc/internal/kClassSafeCast.kt | 10 ++++ 12 files changed, 128 insertions(+), 24 deletions(-) create mode 100644 tests/codegen-tests/codegen-tests-mpp/src/jsMain/resources/index.html create mode 100644 utils/src/commonMain/kotlin/kotlinx/rpc/internal/kClassSafeCast.kt diff --git a/core/build.gradle.kts b/core/build.gradle.kts index 0ff93535..cd9712e8 100644 --- a/core/build.gradle.kts +++ b/core/build.gradle.kts @@ -6,6 +6,7 @@ plugins { alias(libs.plugins.conventions.kmp) alias(libs.plugins.ksp) alias(libs.plugins.atomicfu) + alias(libs.plugins.serialization) } kotlin { @@ -22,5 +23,11 @@ kotlin { implementation(projects.krpc.krpcLogging) } } + + jsMain { + dependencies { + implementation(libs.kotlin.js.wrappers) + } + } } } diff --git a/core/src/commonMain/kotlin/kotlinx/rpc/internal/WithRPCStubObject.kt b/core/src/commonMain/kotlin/kotlinx/rpc/internal/WithRPCStubObject.kt index 0d74fc4b..59bc204a 100644 --- a/core/src/commonMain/kotlin/kotlinx/rpc/internal/WithRPCStubObject.kt +++ b/core/src/commonMain/kotlin/kotlinx/rpc/internal/WithRPCStubObject.kt @@ -7,4 +7,4 @@ package kotlinx.rpc.internal import kotlin.reflect.KClass @InternalRPCApi -public expect fun findRPCProviderInCompanion(kClass: KClass<*>): R +public expect fun findRPCStubProvider(kClass: KClass<*>, resultKClass: KClass): R diff --git a/core/src/jsMain/kotlin/kotlinx/rpc/internal/WithRPCStubObject.js.kt b/core/src/jsMain/kotlin/kotlinx/rpc/internal/WithRPCStubObject.js.kt index 9e4f061a..2a6098e6 100644 --- a/core/src/jsMain/kotlin/kotlinx/rpc/internal/WithRPCStubObject.js.kt +++ b/core/src/jsMain/kotlin/kotlinx/rpc/internal/WithRPCStubObject.js.kt @@ -6,6 +6,7 @@ package kotlinx.rpc.internal +import js.objects.Object import kotlinx.rpc.RPC import kotlin.reflect.AssociatedObjectKey import kotlin.reflect.ExperimentalAssociatedObjects @@ -22,9 +23,51 @@ public annotation class WithRPCStubObject( ) @InternalRPCApi -@OptIn(ExperimentalAssociatedObjects::class) -public actual fun findRPCProviderInCompanion(kClass: KClass<*>): R { - @Suppress("UNCHECKED_CAST") - return kClass.findAssociatedObject() as? R - ?: internalError("unable to find $kClass rpc stub object") +public actual fun findRPCStubProvider(kClass: KClass<*>, resultKClass: KClass): R { + val associatedObject = kClass.findAssociatedObjectImpl(WithRPCStubObject::class, resultKClass) + ?: internalError("Unable to find $kClass associated object") + + if (resultKClass.isInstance(associatedObject)) { + @Suppress("UNCHECKED_CAST") + return associatedObject as R + } + + internalError( + "Located associated object is not of desired type $resultKClass, " + + "instead found $associatedObject of class " + + (associatedObject::class.qualifiedClassNameOrNull ?: associatedObject::class.js.name) + ) +} + +private val KClass<*>.jClass get(): JsClass<*> = asDynamic().jClass_1.unsafeCast>() + +/** + * Workaround for bugs in [findAssociatedObject] + * See KT-70132 for more info. + * + * This function uses std-lib's implementation and accounts for the bug in the compiler + */ +@Suppress("detekt.ReturnCount") +internal fun KClass<*>.findAssociatedObjectImpl( + annotationClass: KClass, + resultKClass: KClass, +): Any? { + val key = annotationClass.jClass.asDynamic().`$metadata$`?.associatedObjectKey?.unsafeCast() ?: return null + val map = jClass.asDynamic().`$metadata$`?.associatedObjects ?: return null + val factory = map[key] ?: return fallbackFindAssociatedObjectImpl(map, resultKClass) + return factory() +} + +private fun fallbackFindAssociatedObjectImpl(map: dynamic, resultKClass: KClass): R? { + return Object.entries(map as Any) + .mapNotNull { (_, factory) -> + val unsafeFactory = factory.asDynamic() + val maybeObject = unsafeFactory() + if (resultKClass.isInstance(maybeObject)) { + maybeObject.unsafeCast() + } else { + null + } + } + .singleOrNull() } diff --git a/core/src/jvmMain/kotlin/kotlinx/rpc/internal/WithRPCStubObject.jvm.kt b/core/src/jvmMain/kotlin/kotlinx/rpc/internal/WithRPCStubObject.jvm.kt index 8d31712d..9ffc595d 100644 --- a/core/src/jvmMain/kotlin/kotlinx/rpc/internal/WithRPCStubObject.jvm.kt +++ b/core/src/jvmMain/kotlin/kotlinx/rpc/internal/WithRPCStubObject.jvm.kt @@ -10,11 +10,16 @@ import kotlin.reflect.KClass import kotlin.reflect.full.companionObjectInstance @InternalRPCApi -public actual fun findRPCProviderInCompanion(kClass: KClass<*>): R { - @Suppress("UNCHECKED_CAST") - return kClass.java.classLoader +public actual fun findRPCStubProvider(kClass: KClass<*>, resultKClass: KClass): R { + val candidate = kClass.java.classLoader .loadClass("${kClass.qualifiedName}\$\$rpcServiceStub") ?.kotlin - ?.companionObjectInstance as? R - ?: internalError("unable to find $kClass rpc client object") + ?.companionObjectInstance + + @Suppress("UNCHECKED_CAST") + if (resultKClass.isInstance(candidate)) { + return candidate as R + } + + internalError("unable to find $kClass rpc client object") } diff --git a/core/src/nativeMain/kotlin/kotlinx/rpc/internal/WithRPCStubObject.native.kt b/core/src/nativeMain/kotlin/kotlinx/rpc/internal/WithRPCStubObject.native.kt index 9e4f061a..638bfa78 100644 --- a/core/src/nativeMain/kotlin/kotlinx/rpc/internal/WithRPCStubObject.native.kt +++ b/core/src/nativeMain/kotlin/kotlinx/rpc/internal/WithRPCStubObject.native.kt @@ -23,8 +23,18 @@ public annotation class WithRPCStubObject( @InternalRPCApi @OptIn(ExperimentalAssociatedObjects::class) -public actual fun findRPCProviderInCompanion(kClass: KClass<*>): R { +public actual fun findRPCStubProvider(kClass: KClass<*>, resultKClass: KClass): R { + val associatedObject = kClass.findAssociatedObject() + ?: internalError("Unable to find $kClass associated object") + @Suppress("UNCHECKED_CAST") - return kClass.findAssociatedObject() as? R - ?: internalError("unable to find $kClass rpc stub object") + if (resultKClass.isInstance(associatedObject)) { + return associatedObject as R + } + + internalError( + "Located associated object is not of desired type $resultKClass, " + + "instead found $associatedObject of class " + + (associatedObject::class.qualifiedClassNameOrNull ?: associatedObject::class.simpleName) + ) } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 719e0960..14e2853a 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -13,6 +13,7 @@ kotlin-logging = "6.0.9" slf4j = "2.0.13" logback = "1.3.14" gradle-plugin-publish = "1.2.1" +kotlin-wrappers = "1.0.0-pre.781" # stub versions - relpaced based on kotlin, mostly for gradle-related (plugins) dependencies # but also for dependencies for compiler specific modules @@ -67,6 +68,7 @@ coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", ve coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "coroutines" } detekt-gradle-plugin = { module = "io.gitlab.arturbosch.detekt:detekt-gradle-plugin", version.ref = "detekt-gradle-plugin" } kover-gradle-plugin = { module = "org.jetbrains.kotlinx:kover-gradle-plugin", version.ref = "kover" } +kotlin-js-wrappers = { module = "org.jetbrains.kotlin-wrappers:kotlin-js", version.ref = "kotlin-wrappers" } [plugins] kotlin-multiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin-lang" } diff --git a/krpc/krpc-client/src/commonMain/kotlin/kotlinx/rpc/client/RPCClientUtils.kt b/krpc/krpc-client/src/commonMain/kotlin/kotlinx/rpc/client/RPCClientUtils.kt index 29bffe22..04e827f9 100644 --- a/krpc/krpc-client/src/commonMain/kotlin/kotlinx/rpc/client/RPCClientUtils.kt +++ b/krpc/krpc-client/src/commonMain/kotlin/kotlinx/rpc/client/RPCClientUtils.kt @@ -8,8 +8,9 @@ import kotlinx.atomicfu.atomic import kotlinx.rpc.RPC import kotlinx.rpc.RPCClient import kotlinx.rpc.internal.RPCStubServiceProvider -import kotlinx.rpc.internal.findRPCProviderInCompanion +import kotlinx.rpc.internal.findRPCStubProvider import kotlinx.rpc.internal.kClass +import kotlinx.rpc.internal.safeCast import kotlin.reflect.KClass import kotlin.reflect.KType @@ -54,7 +55,11 @@ private val SERVICE_ID = atomic(0L) * @return instance of the generated service. */ public fun RPCClient.withService(serviceKClass: KClass): T { - val provider = findRPCProviderInCompanion>(serviceKClass) + val provider = findRPCStubProvider>( + kClass = serviceKClass, + resultKClass = RPCStubServiceProvider::class.safeCast(), + ) + val id = SERVICE_ID.incrementAndGet() return provider.withClient(id, this) diff --git a/krpc/krpc-client/src/commonMain/kotlin/kotlinx/rpc/client/awaitFieldInitialization.kt b/krpc/krpc-client/src/commonMain/kotlin/kotlinx/rpc/client/awaitFieldInitialization.kt index ac4dec61..7483218f 100644 --- a/krpc/krpc-client/src/commonMain/kotlin/kotlinx/rpc/client/awaitFieldInitialization.kt +++ b/krpc/krpc-client/src/commonMain/kotlin/kotlinx/rpc/client/awaitFieldInitialization.kt @@ -7,7 +7,8 @@ package kotlinx.rpc.client import kotlinx.rpc.RPC import kotlinx.rpc.internal.RPCDeferredField import kotlinx.rpc.internal.RPCServiceFieldsProvider -import kotlinx.rpc.internal.findRPCProviderInCompanion +import kotlinx.rpc.internal.findRPCStubProvider +import kotlinx.rpc.internal.safeCast import kotlin.reflect.KClass /** @@ -78,7 +79,7 @@ public suspend inline fun T.awaitFieldInitialization(): T { * @return specified service, after all of it's field were initialized. */ public suspend fun T.awaitFieldInitialization(kClass: KClass): T { - findRPCProviderInCompanion>(kClass) + findRPCStubProvider>(kClass, RPCServiceFieldsProvider::class.safeCast()) .rpcFields(this) .forEach { field -> field.await() diff --git a/krpc/krpc-server/src/commonMain/kotlin/kotlinx/rpc/server/internal/RPCServiceUtils.kt b/krpc/krpc-server/src/commonMain/kotlin/kotlinx/rpc/server/internal/RPCServiceUtils.kt index aa49ff0d..5e9d4594 100644 --- a/krpc/krpc-server/src/commonMain/kotlin/kotlinx/rpc/server/internal/RPCServiceUtils.kt +++ b/krpc/krpc-server/src/commonMain/kotlin/kotlinx/rpc/server/internal/RPCServiceUtils.kt @@ -7,7 +7,7 @@ package kotlinx.rpc.server.internal import kotlinx.rpc.RPC import kotlinx.rpc.internal.InternalRPCApi import kotlinx.rpc.internal.RPCServiceMethodSerializationTypeProvider -import kotlinx.rpc.internal.findRPCProviderInCompanion +import kotlinx.rpc.internal.findRPCStubProvider import kotlinx.rpc.internal.kClass import kotlin.reflect.KClass import kotlin.reflect.KType @@ -33,5 +33,6 @@ public fun rpcServiceMethodSerializationTypeOf(serviceType: KType, methodName: S */ @InternalRPCApi public fun rpcServiceMethodSerializationTypeOf(serviceKClass: KClass<*>, methodName: String): KType? { - return findRPCProviderInCompanion(serviceKClass).methodTypeOf(methodName) + return findRPCStubProvider(serviceKClass, RPCServiceMethodSerializationTypeProvider::class) + .methodTypeOf(methodName) } diff --git a/tests/codegen-tests/codegen-tests-mpp/build.gradle.kts b/tests/codegen-tests/codegen-tests-mpp/build.gradle.kts index ebfcf33f..b1b4b040 100644 --- a/tests/codegen-tests/codegen-tests-mpp/build.gradle.kts +++ b/tests/codegen-tests/codegen-tests-mpp/build.gradle.kts @@ -3,12 +3,12 @@ */ import org.jetbrains.kotlin.gradle.dsl.ExplicitApiMode -import org.jetbrains.kotlin.gradle.dsl.KotlinVersion plugins { alias(libs.plugins.conventions.kmp) alias(libs.plugins.ksp) alias(libs.plugins.kotlinx.rpc) + alias(libs.plugins.serialization) } kotlin { @@ -39,9 +39,15 @@ kotlin { } } - explicitApi = ExplicitApiMode.Disabled - compilerOptions { - languageVersion.set(KotlinVersion.KOTLIN_2_0) + js { + binaries.executable() + browser { + commonWebpackConfig { + this.sourceMaps = true + } + } } + + explicitApi = ExplicitApiMode.Disabled } diff --git a/tests/codegen-tests/codegen-tests-mpp/src/jsMain/resources/index.html b/tests/codegen-tests/codegen-tests-mpp/src/jsMain/resources/index.html new file mode 100644 index 00000000..1134dd1d --- /dev/null +++ b/tests/codegen-tests/codegen-tests-mpp/src/jsMain/resources/index.html @@ -0,0 +1,14 @@ + + + + + + + Kotlin JS kRPC Sample + + + + + diff --git a/utils/src/commonMain/kotlin/kotlinx/rpc/internal/kClassSafeCast.kt b/utils/src/commonMain/kotlin/kotlinx/rpc/internal/kClassSafeCast.kt new file mode 100644 index 00000000..d567f267 --- /dev/null +++ b/utils/src/commonMain/kotlin/kotlinx/rpc/internal/kClassSafeCast.kt @@ -0,0 +1,10 @@ +/* + * Copyright 2023-2024 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.rpc.internal + +import kotlin.reflect.KClass + +@Suppress("UNCHECKED_CAST") +fun KClass<*>.safeCast() = this as R From 3b9a6f2671f410d6be106bc5ad9178f90b31d95c Mon Sep 17 00:00:00 2001 From: Alexander Sysoev Date: Tue, 6 Aug 2024 15:44:44 +0200 Subject: [PATCH 5/5] fir --- .../rpc/codegen/FirRPCServiceGenerator.kt | 29 +++++++++---------- 1 file changed, 13 insertions(+), 16 deletions(-) diff --git a/compiler-plugin/compiler-plugin-k2/src/main/core/kotlinx/rpc/codegen/FirRPCServiceGenerator.kt b/compiler-plugin/compiler-plugin-k2/src/main/core/kotlinx/rpc/codegen/FirRPCServiceGenerator.kt index 3e066789..136a0709 100644 --- a/compiler-plugin/compiler-plugin-k2/src/main/core/kotlinx/rpc/codegen/FirRPCServiceGenerator.kt +++ b/compiler-plugin/compiler-plugin-k2/src/main/core/kotlinx/rpc/codegen/FirRPCServiceGenerator.kt @@ -16,7 +16,7 @@ import org.jetbrains.kotlin.descriptors.ClassKind import org.jetbrains.kotlin.descriptors.Modality import org.jetbrains.kotlin.descriptors.Visibilities import org.jetbrains.kotlin.fir.FirSession -import org.jetbrains.kotlin.fir.containingClassForStaticMemberAttr +import org.jetbrains.kotlin.fir.declarations.FirFunction import org.jetbrains.kotlin.fir.declarations.utils.isInterface import org.jetbrains.kotlin.fir.declarations.utils.visibility import org.jetbrains.kotlin.fir.extensions.FirDeclarationGenerationExtension @@ -24,8 +24,7 @@ import org.jetbrains.kotlin.fir.extensions.MemberGenerationContext import org.jetbrains.kotlin.fir.extensions.NestedClassGenerationContext import org.jetbrains.kotlin.fir.moduleData import org.jetbrains.kotlin.fir.plugin.* -import org.jetbrains.kotlin.fir.scopes.impl.declaredMemberScope -import org.jetbrains.kotlin.fir.scopes.processAllFunctions +import org.jetbrains.kotlin.fir.symbols.SymbolInternals import org.jetbrains.kotlin.fir.symbols.impl.* import org.jetbrains.kotlin.fir.types.* import org.jetbrains.kotlin.name.CallableId @@ -133,7 +132,7 @@ class FirRPCServiceGenerator( // otherwise an object is generated instead of a class // serialization plugin has other logic for such declarations - else -> setOf() + else -> emptySet() } } @@ -277,10 +276,10 @@ class FirRPCServiceGenerator( it.classId == ClassDeclarations.rpcInterface } ?: return null - val functions = mutableListOf>() - owner.declaredMemberScope(session, null).processAllFunctions { - functions.add(it) - } + @OptIn(SymbolInternals::class) + val functions = owner.fir.declarations + .filterIsInstance() + .map { it.symbol } return createNestedClass(owner, RpcNames.SERVICE_STUB_NAME, RPCGeneratedStubKey(owner.name, functions)) { visibility = owner.visibility @@ -301,7 +300,7 @@ class FirRPCServiceGenerator( } else -> { - super.getCallableNamesForClass(classSymbol, context) + emptySet() } } } @@ -331,7 +330,7 @@ class FirRPCServiceGenerator( return when { rpcMethodClassKey != null -> generateConstructorsForRpcMethodClass(context, rpcMethodClassKey) context.owner.isFromSerializationPlugin -> serializationExtension.generateConstructors(context) - else -> super.generateConstructors(context) + else -> emptyList() } } @@ -357,8 +356,6 @@ class FirRPCServiceGenerator( type = valueParam.resolvedReturnType, ) } - }.also { - it.containingClassForStaticMemberAttr = ConeClassLikeLookupTagImpl(context.owner.classId) }.symbol.let(::listOf) } @@ -366,7 +363,7 @@ class FirRPCServiceGenerator( callableId: CallableId, context: MemberGenerationContext? ): List { - context ?: return super.generateProperties(callableId, null) + context ?: return emptyList() val owner = context.owner val rpcMethodClassKey = owner.generatedRpcMethodClassKey @@ -381,7 +378,7 @@ class FirRPCServiceGenerator( } else -> { - super.generateProperties(callableId, context) + emptyList() } } } @@ -421,7 +418,7 @@ class FirRPCServiceGenerator( callableId: CallableId, context: MemberGenerationContext? ): List { - val owner = context?.owner ?: return super.generateFunctions(callableId, null) + val owner = context?.owner ?: return emptyList() return when { owner.isFromSerializationPlugin || owner.generatedRpcMethodClassKey?.isObject == true -> { @@ -429,7 +426,7 @@ class FirRPCServiceGenerator( } else -> { - super.generateFunctions(callableId, context) + emptyList() } } }