Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,18 @@ 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
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(
Expand All @@ -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,
Expand Down Expand Up @@ -95,3 +98,6 @@ fun <T> List<T>.compactIfPossible(): List<T> =
@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
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -657,7 +652,7 @@ internal class RpcStubGenerator(
* A map that holds an RpcCallable that describes it.
*
* ```kotlin
* private val callableMap: Map<String, RpcCallable<MyService>> = mapOf(
* override val callableMap: Map<String, RpcCallable<MyService>> = mapOf(
* Pair("<callable-name-1>", RpcCallable(...)),
* ...
* Pair("<callable-name-n>", RpcCallable(...)),
Expand All @@ -671,11 +666,23 @@ internal class RpcStubGenerator(
* - `<callable-name-k>` - 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)

Expand All @@ -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)!!)
}
}
}
Expand Down
1 change: 1 addition & 0 deletions core/api/core.api
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down
2 changes: 2 additions & 0 deletions core/api/core.klib.api
Original file line number Diff line number Diff line change
Expand Up @@ -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 <get-callableMap>(): kotlin.collections/Map<kotlin/String, kotlinx.rpc.descriptor/RpcCallable<#A>> // kotlinx.rpc.descriptor/RpcServiceDescriptor.callableMap.<get-callableMap>|<get-callableMap>(){}[0]
abstract val fqName // kotlinx.rpc.descriptor/RpcServiceDescriptor.fqName|{}fqName[0]
abstract fun <get-fqName>(): kotlin/String // kotlinx.rpc.descriptor/RpcServiceDescriptor.fqName.<get-fqName>|<get-fqName>(){}[0]

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ public interface RpcServiceDescriptor<@Rpc T : Any> {

public fun getCallable(name: String): RpcCallable<T>?

public val callableMap: Map<String, RpcCallable<T>>

public fun createInstance(serviceId: Long, client: RpcClient): T
}

Expand Down
8 changes: 7 additions & 1 deletion tests/compiler-plugin-tests/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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() {
Expand Down
Original file line number Diff line number Diff line change
@@ -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(<getClass>(Q|kotlinx/rpc/internal/utils/ExperimentalRpcApi|))) public final fun box(): R|kotlin/String| {
^box R|kotlinx/coroutines/runBlocking|<R|kotlin/String|>(<L> = runBlocking@fun R|kotlinx/coroutines/CoroutineScope|.<anonymous>(): R|kotlin/String| <inline=NoInline, kind=EXACTLY_ONCE> {
lval descriptor: R|kotlinx/rpc/descriptor/RpcServiceDescriptor<BoxService>| = R|kotlinx/rpc/descriptor/serviceDescriptorOf|<R|BoxService|>()
lval result: R|kotlin/String?| = R|<local>/descriptor|.R|SubstitutionOverride<kotlinx/rpc/descriptor/RpcServiceDescriptor.callableMap: R|kotlin/collections/Map<kotlin/String, kotlinx/rpc/descriptor/RpcCallable<BoxService>>|>|.R|SubstitutionOverride<kotlin/collections/Map.get: R|kotlinx/rpc/descriptor/RpcCallable<BoxService>?|>|(String(simple))?.{ $subj$.R|SubstitutionOverride<kotlinx/rpc/descriptor/RpcCallable.name: R|kotlin/String|>| }
^ when () {
==(R|<local>/result|, String(simple)) -> {
String(OK)
}
else -> {
<strcat>(String(Fail: ), R|<local>/result|)
}
}

}
)
}
26 changes: 26 additions & 0 deletions tests/compiler-plugin-tests/src/testData/box/serviceDescriptor.kt
Original file line number Diff line number Diff line change
@@ -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<BoxService>()
val result = descriptor.callableMap["simple"]?.name

if (result == "simple") "OK" else "Fail: $result"
}

/* GENERATED_FIR_TAGS: functionDeclaration, interfaceDeclaration, suspend */