From f5305842e9ad80cba86dac4ca2becfb1d215d06c Mon Sep 17 00:00:00 2001 From: Ryan Nett Date: Tue, 7 Oct 2025 17:40:12 -0700 Subject: [PATCH 1/4] Add a public RpcServiceDescriptor.callableMap property --- core/api/core.api | 1 + core/api/core.klib.api | 2 ++ .../kotlin/kotlinx/rpc/descriptor/RpcServiceDescriptor.kt | 2 ++ 3 files changed, 5 insertions(+) diff --git a/core/api/core.api b/core/api/core.api index f9064281a..41e0be7c3 100644 --- a/core/api/core.api +++ b/core/api/core.api @@ -55,6 +55,7 @@ public abstract interface class kotlinx/rpc/descriptor/RpcParameter { public abstract interface class kotlinx/rpc/descriptor/RpcServiceDescriptor { public abstract fun createInstance (JLkotlinx/rpc/RpcClient;)Ljava/lang/Object; public abstract fun getCallable (Ljava/lang/String;)Lkotlinx/rpc/descriptor/RpcCallable; + public abstract fun getCallableMap ()Ljava/util/Map; public abstract fun getFqName ()Ljava/lang/String; } diff --git a/core/api/core.klib.api b/core/api/core.klib.api index 4af66af8d..f553e289b 100644 --- a/core/api/core.klib.api +++ b/core/api/core.klib.api @@ -28,6 +28,8 @@ abstract interface <#A: kotlin/Any> kotlinx.rpc.descriptor/RpcCallable { // kotl } abstract interface <#A: kotlin/Any> kotlinx.rpc.descriptor/RpcServiceDescriptor { // kotlinx.rpc.descriptor/RpcServiceDescriptor|null[0] + abstract val callableMap // kotlinx.rpc.descriptor/RpcServiceDescriptor.callableMap|{}callableMap[0] + abstract fun (): kotlin.collections/Map> // kotlinx.rpc.descriptor/RpcServiceDescriptor.callableMap.|(){}[0] abstract val fqName // kotlinx.rpc.descriptor/RpcServiceDescriptor.fqName|{}fqName[0] abstract fun (): kotlin/String // kotlinx.rpc.descriptor/RpcServiceDescriptor.fqName.|(){}[0] diff --git a/core/src/commonMain/kotlin/kotlinx/rpc/descriptor/RpcServiceDescriptor.kt b/core/src/commonMain/kotlin/kotlinx/rpc/descriptor/RpcServiceDescriptor.kt index 508fcb777..a1274c24a 100644 --- a/core/src/commonMain/kotlin/kotlinx/rpc/descriptor/RpcServiceDescriptor.kt +++ b/core/src/commonMain/kotlin/kotlinx/rpc/descriptor/RpcServiceDescriptor.kt @@ -44,6 +44,8 @@ public interface RpcServiceDescriptor<@Rpc T : Any> { public fun getCallable(name: String): RpcCallable? + public val callableMap: Map> + public fun createInstance(serviceId: Long, client: RpcClient): T } From a38b875c9b8ae272124fb6eb185731c3686e5989 Mon Sep 17 00:00:00 2001 From: Ryan Nett Date: Tue, 7 Oct 2025 17:57:40 -0700 Subject: [PATCH 2/4] Make the generated callableMap public and override the RpcServiceDescriptor property --- .../kotlinx/rpc/codegen/extension/IrUtils.kt | 14 +++-- .../rpc/codegen/extension/RpcStubGenerator.kt | 52 +++++++++++-------- 2 files changed, 41 insertions(+), 25 deletions(-) diff --git a/compiler-plugin/compiler-plugin-backend/src/main/kotlin/kotlinx/rpc/codegen/extension/IrUtils.kt b/compiler-plugin/compiler-plugin-backend/src/main/kotlin/kotlinx/rpc/codegen/extension/IrUtils.kt index 5f3800ee7..7ab1364db 100644 --- a/compiler-plugin/compiler-plugin-backend/src/main/kotlin/kotlinx/rpc/codegen/extension/IrUtils.kt +++ b/compiler-plugin/compiler-plugin-backend/src/main/kotlin/kotlinx/rpc/codegen/extension/IrUtils.kt @@ -11,7 +11,9 @@ import org.jetbrains.kotlin.ir.builders.declarations.buildField import org.jetbrains.kotlin.ir.declarations.* import org.jetbrains.kotlin.ir.expressions.IrExpression import org.jetbrains.kotlin.ir.expressions.IrExpressionBody +import org.jetbrains.kotlin.ir.symbols.IrClassSymbol import org.jetbrains.kotlin.ir.symbols.IrClassifierSymbol +import org.jetbrains.kotlin.ir.symbols.IrPropertySymbol import org.jetbrains.kotlin.ir.types.IrType import org.jetbrains.kotlin.ir.types.IrTypeArgument import org.jetbrains.kotlin.ir.types.IrTypeProjection @@ -19,8 +21,8 @@ import org.jetbrains.kotlin.ir.types.SimpleTypeNullability import org.jetbrains.kotlin.ir.types.impl.IrSimpleTypeImpl import org.jetbrains.kotlin.ir.types.impl.makeTypeProjection import org.jetbrains.kotlin.ir.util.dump +import org.jetbrains.kotlin.ir.util.properties import org.jetbrains.kotlin.types.Variance -import java.util.* fun IrClassifierSymbol.typeWith(type: IrType, variance: Variance): IrType { return IrSimpleTypeImpl( @@ -31,9 +33,10 @@ fun IrClassifierSymbol.typeWith(type: IrType, variance: Variance): IrType { ) } -val IrProperty.getterOrFail: IrSimpleFunction get () { - return getter ?: error("'getter' should be present, but was null: ${dump()}") -} +val IrProperty.getterOrFail: IrSimpleFunction + get() { + return getter ?: error("'getter' should be present, but was null: ${dump()}") + } fun IrProperty.addDefaultGetter( parentClass: IrClass, @@ -95,3 +98,6 @@ fun List.compactIfPossible(): List = @Suppress("EXTENSION_SHADOWED_BY_MEMBER") // TODO(KTIJ-26314): Remove this suppression fun IrFactory.createExpressionBody(expression: IrExpression): IrExpressionBody = createExpressionBody(expression.startOffset, expression.endOffset, expression) + +fun IrClassSymbol.findPropertyByName(name: String): IrPropertySymbol? = + owner.properties.singleOrNull { it.name.asString() == name }?.symbol \ No newline at end of file diff --git a/compiler-plugin/compiler-plugin-backend/src/main/kotlin/kotlinx/rpc/codegen/extension/RpcStubGenerator.kt b/compiler-plugin/compiler-plugin-backend/src/main/kotlin/kotlinx/rpc/codegen/extension/RpcStubGenerator.kt index 11ef4eb13..cd622842e 100644 --- a/compiler-plugin/compiler-plugin-backend/src/main/kotlin/kotlinx/rpc/codegen/extension/RpcStubGenerator.kt +++ b/compiler-plugin/compiler-plugin-backend/src/main/kotlin/kotlinx/rpc/codegen/extension/RpcStubGenerator.kt @@ -4,30 +4,25 @@ package kotlinx.rpc.codegen.extension -import kotlinx.rpc.codegen.VersionSpecificApi -import kotlinx.rpc.codegen.common.RpcClassId -import org.jetbrains.kotlin.backend.common.lower.DeclarationIrBuilder -import org.jetbrains.kotlin.backend.jvm.functionByName -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 kotlinx.rpc.codegen.* +import kotlinx.rpc.codegen.common.* +import org.jetbrains.kotlin.backend.common.lower.* +import org.jetbrains.kotlin.backend.jvm.* +import org.jetbrains.kotlin.cli.common.messages.* +import org.jetbrains.kotlin.descriptors.* +import org.jetbrains.kotlin.ir.* import org.jetbrains.kotlin.ir.builders.* import org.jetbrains.kotlin.ir.builders.declarations.* import org.jetbrains.kotlin.ir.declarations.* import org.jetbrains.kotlin.ir.expressions.* 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.* import org.jetbrains.kotlin.ir.types.* import org.jetbrains.kotlin.ir.util.* -import org.jetbrains.kotlin.name.Name -import org.jetbrains.kotlin.name.SpecialNames -import org.jetbrains.kotlin.types.Variance -import org.jetbrains.kotlin.util.OperatorNameConventions -import kotlin.properties.Delegates +import org.jetbrains.kotlin.name.* +import org.jetbrains.kotlin.types.* +import org.jetbrains.kotlin.util.* +import kotlin.properties.* private object Stub { const val CLIENT = "__rpc_client" @@ -657,7 +652,7 @@ internal class RpcStubGenerator( * A map that holds an RpcCallable that describes it. * * ```kotlin - * private val callableMap: Map> = mapOf( + * override val callableMap: Map> = mapOf( * Pair("", RpcCallable(...)), * ... * Pair("", RpcCallable(...)), @@ -671,11 +666,23 @@ internal class RpcStubGenerator( * - `` - the name of the k-th callable in the service */ private fun IrClass.generateCallableMapProperty() { + + // the RpcServiceDescriptor.callableMap property will not exist for older runtime library versions + val interfaceProperty = ctx.rpcServiceDescriptor.findPropertyByName(Descriptor.CALLABLE_MAP) + callableMap = addProperty { name = Name.identifier(Descriptor.CALLABLE_MAP) - visibility = DescriptorVisibilities.PRIVATE - modality = Modality.FINAL + if (interfaceProperty == null) { + visibility = DescriptorVisibilities.PUBLIC + modality = Modality.OPEN + } else { + visibility = DescriptorVisibilities.PRIVATE + modality = Modality.FINAL + } }.apply { + if (interfaceProperty != null) + overriddenSymbols = listOf(interfaceProperty) + val rpcCallableType = ctx.rpcCallable.typeWith(declaration.serviceType) val mapType = ctx.irBuiltIns.mapClass.typeWith(ctx.irBuiltIns.stringType, rpcCallableType) @@ -699,7 +706,10 @@ internal class RpcStubGenerator( } addDefaultGetter(this@generateCallableMapProperty, ctx.irBuiltIns) { - visibility = DescriptorVisibilities.PRIVATE + visibility = + if (interfaceProperty == null) DescriptorVisibilities.PRIVATE else DescriptorVisibilities.PUBLIC + if (interfaceProperty != null) + overriddenSymbols = listOf(ctx.rpcServiceDescriptor.getPropertyGetter(Descriptor.CALLABLE_MAP)!!) } } } From b22b4472bbec900e27d0cc5a30da00b2b6a9210e Mon Sep 17 00:00:00 2001 From: Ryan Nett Date: Tue, 7 Oct 2025 17:58:06 -0700 Subject: [PATCH 3/4] Make compiler tests not be up-to-date if there have been changes to the test input files --- tests/compiler-plugin-tests/build.gradle.kts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/tests/compiler-plugin-tests/build.gradle.kts b/tests/compiler-plugin-tests/build.gradle.kts index 095972f1b..bd047b357 100644 --- a/tests/compiler-plugin-tests/build.gradle.kts +++ b/tests/compiler-plugin-tests/build.gradle.kts @@ -2,7 +2,7 @@ * Copyright 2023-2025 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.dsl.* import org.jetbrains.kotlin.gradle.tasks.KotlinCompile plugins { @@ -110,6 +110,12 @@ tasks.test { dependsOn(project(":core").tasks.getByName("jvmJar")) dependsOn(project(":utils").tasks.getByName("jvmJar")) dependsOn(project(":krpc:krpc-core").tasks.getByName("jvmJar")) + dependsOn("generateTests") + + inputs.dir("src/testData") + .ignoreEmptyDirectories() + .normalizeLineEndings() + .withPathSensitivity(PathSensitivity.RELATIVE) useJUnitPlatform() From 229c421f103afc394ac2ac7653311f34887ba595 Mon Sep 17 00:00:00 2001 From: Ryan Nett Date: Tue, 7 Oct 2025 17:58:28 -0700 Subject: [PATCH 4/4] Add a compiler test for the generated property --- .../test/runners/BoxTestGenerated.java | 8 +++++- .../testData/box/serviceDescriptor.fir.txt | 27 +++++++++++++++++++ .../src/testData/box/serviceDescriptor.kt | 26 ++++++++++++++++++ 3 files changed, 60 insertions(+), 1 deletion(-) create mode 100644 tests/compiler-plugin-tests/src/testData/box/serviceDescriptor.fir.txt create mode 100644 tests/compiler-plugin-tests/src/testData/box/serviceDescriptor.kt diff --git a/tests/compiler-plugin-tests/src/test-gen/kotlinx/rpc/codegen/test/runners/BoxTestGenerated.java b/tests/compiler-plugin-tests/src/test-gen/kotlinx/rpc/codegen/test/runners/BoxTestGenerated.java index b204c706c..6c39644e3 100644 --- a/tests/compiler-plugin-tests/src/test-gen/kotlinx/rpc/codegen/test/runners/BoxTestGenerated.java +++ b/tests/compiler-plugin-tests/src/test-gen/kotlinx/rpc/codegen/test/runners/BoxTestGenerated.java @@ -3,9 +3,9 @@ package kotlinx.rpc.codegen.test.runners; import com.intellij.testFramework.TestDataPath; -import org.jetbrains.kotlin.test.util.KtTestUtil; import org.jetbrains.kotlin.test.TargetBackend; import org.jetbrains.kotlin.test.TestMetadata; +import org.jetbrains.kotlin.test.util.KtTestUtil; import org.junit.jupiter.api.Test; import java.io.File; @@ -39,6 +39,12 @@ public void testMultiModule() { runTest("src/testData/box/multiModule.kt"); } + @Test + @TestMetadata("serviceDescriptor.kt") + public void testServiceDescriptor() { + runTest("src/testData/box/serviceDescriptor.kt"); + } + @Test @TestMetadata("simple.kt") public void testSimple() { diff --git a/tests/compiler-plugin-tests/src/testData/box/serviceDescriptor.fir.txt b/tests/compiler-plugin-tests/src/testData/box/serviceDescriptor.fir.txt new file mode 100644 index 000000000..bb4582e80 --- /dev/null +++ b/tests/compiler-plugin-tests/src/testData/box/serviceDescriptor.fir.txt @@ -0,0 +1,27 @@ +FILE: serviceDescriptor.kt + @R|kotlinx/rpc/annotations/Rpc|() public abstract interface BoxService : R|kotlin/Any| { + public abstract suspend fun simple(): R|kotlin/String| + + public final class $rpcServiceStub : R|kotlin/Any| { + public final companion object Companion : R|kotlin/Any| { + } + + } + + } + @R|kotlin/OptIn|(markerClass = vararg((Q|kotlinx/rpc/internal/utils/ExperimentalRpcApi|))) public final fun box(): R|kotlin/String| { + ^box R|kotlinx/coroutines/runBlocking|( = runBlocking@fun R|kotlinx/coroutines/CoroutineScope|.(): R|kotlin/String| { + lval descriptor: R|kotlinx/rpc/descriptor/RpcServiceDescriptor| = R|kotlinx/rpc/descriptor/serviceDescriptorOf|() + lval result: R|kotlin/String?| = R|/descriptor|.R|SubstitutionOverride>|>|.R|SubstitutionOverride?|>|(String(simple))?.{ $subj$.R|SubstitutionOverride| } + ^ when () { + ==(R|/result|, String(simple)) -> { + String(OK) + } + else -> { + (String(Fail: ), R|/result|) + } + } + + } + ) + } diff --git a/tests/compiler-plugin-tests/src/testData/box/serviceDescriptor.kt b/tests/compiler-plugin-tests/src/testData/box/serviceDescriptor.kt new file mode 100644 index 000000000..fe8245646 --- /dev/null +++ b/tests/compiler-plugin-tests/src/testData/box/serviceDescriptor.kt @@ -0,0 +1,26 @@ +/* + * Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + */ + +// RUN_PIPELINE_TILL: BACKEND + +import kotlinx.coroutines.runBlocking +import kotlinx.rpc.descriptor.serviceDescriptorOf +import kotlinx.rpc.annotations.Rpc +import kotlinx.rpc.codegen.test.TestRpcClient +import kotlinx.rpc.internal.utils.ExperimentalRpcApi + +@Rpc +interface BoxService { + suspend fun simple(): String +} + +@OptIn(ExperimentalRpcApi::class) +fun box(): String = runBlocking { + val descriptor = serviceDescriptorOf() + val result = descriptor.callableMap["simple"]?.name + + if (result == "simple") "OK" else "Fail: $result" +} + +/* GENERATED_FIR_TAGS: functionDeclaration, interfaceDeclaration, suspend */