diff --git a/build.gradle.kts b/build.gradle.kts index 2570a4f9..ee48ba6a 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,17 +1,18 @@ /* - * Copyright 2023-2024 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + * 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.plugin.getKotlinPluginVersion -import util.configureApiValidation -import util.configureNpm -import util.configureProjectReport import util.libs +import util.configureProjectReport +import util.configureNpm +import util.configureApiValidation plugins { alias(libs.plugins.serialization) apply false alias(libs.plugins.kotlinx.rpc) apply false alias(libs.plugins.conventions.kover) + alias(libs.plugins.protobuf) apply false alias(libs.plugins.conventions.gradle.doctor) alias(libs.plugins.atomicfu) id("build-util") diff --git a/compiler-plugin/compiler-plugin-backend/src/main/core/kotlinx/rpc/codegen/extension/RpcIrContext.kt b/compiler-plugin/compiler-plugin-backend/src/main/core/kotlinx/rpc/codegen/extension/RpcIrContext.kt index fa5c429b..00bb9f98 100644 --- a/compiler-plugin/compiler-plugin-backend/src/main/core/kotlinx/rpc/codegen/extension/RpcIrContext.kt +++ b/compiler-plugin/compiler-plugin-backend/src/main/core/kotlinx/rpc/codegen/extension/RpcIrContext.kt @@ -1,5 +1,5 @@ /* - * Copyright 2023-2024 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + * Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. */ package kotlinx.rpc.codegen.extension @@ -105,6 +105,14 @@ internal class RpcIrContext( getRpcIrClassSymbol("RpcServiceDescriptor", "descriptor") } + val grpcServiceDescriptor by lazy { + getIrClassSymbol("kotlinx.rpc.grpc.descriptor", "GrpcServiceDescriptor") + } + + val grpcDelegate by lazy { + getIrClassSymbol("kotlinx.rpc.grpc.descriptor", "GrpcDelegate") + } + val rpcType by lazy { getRpcIrClassSymbol("RpcType", "descriptor") } @@ -262,6 +270,10 @@ internal class RpcIrContext( rpcServiceDescriptor.namedProperty("fqName") } + val grpcServiceDescriptorDelegate by lazy { + grpcServiceDescriptor.namedProperty("delegate") + } + private fun IrClassSymbol.namedProperty(name: String): IrPropertySymbol { return owner.properties.single { it.name.asString() == name }.symbol } @@ -276,7 +288,7 @@ internal class RpcIrContext( return getIrClassSymbol("kotlinx.rpc$suffix", name) } - private fun getIrClassSymbol(packageName: String, name: String): IrClassSymbol { + 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/compiler-plugin-backend/src/main/core/kotlinx/rpc/codegen/extension/RpcIrServiceProcessor.kt b/compiler-plugin/compiler-plugin-backend/src/main/core/kotlinx/rpc/codegen/extension/RpcIrServiceProcessor.kt index b2d8dd78..bb77aff7 100644 --- a/compiler-plugin/compiler-plugin-backend/src/main/core/kotlinx/rpc/codegen/extension/RpcIrServiceProcessor.kt +++ b/compiler-plugin/compiler-plugin-backend/src/main/core/kotlinx/rpc/codegen/extension/RpcIrServiceProcessor.kt @@ -1,5 +1,5 @@ /* - * Copyright 2023-2024 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + * Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. */ package kotlinx.rpc.codegen.extension @@ -9,6 +9,7 @@ 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.util.hasAnnotation +import org.jetbrains.kotlin.ir.util.isInterface import org.jetbrains.kotlin.ir.visitors.IrElementTransformer internal class RpcIrServiceProcessor( @@ -16,7 +17,9 @@ internal class RpcIrServiceProcessor( private val logger: MessageCollector, ) : IrElementTransformer { override fun visitClass(declaration: IrClass, data: RpcIrContext): IrStatement { - if (declaration.hasAnnotation(RpcClassId.rpcAnnotation)) { + if ((declaration.hasAnnotation(RpcClassId.rpcAnnotation) + || declaration.hasAnnotation(RpcClassId.grpcAnnotation)) && declaration.isInterface + ) { processService(declaration, data) } diff --git a/compiler-plugin/compiler-plugin-backend/src/main/core/kotlinx/rpc/codegen/extension/RpcStubGenerator.kt b/compiler-plugin/compiler-plugin-backend/src/main/core/kotlinx/rpc/codegen/extension/RpcStubGenerator.kt index 158788c1..fd8dc28c 100644 --- a/compiler-plugin/compiler-plugin-backend/src/main/core/kotlinx/rpc/codegen/extension/RpcStubGenerator.kt +++ b/compiler-plugin/compiler-plugin-backend/src/main/core/kotlinx/rpc/codegen/extension/RpcStubGenerator.kt @@ -1,5 +1,5 @@ /* - * Copyright 2023-2024 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + * Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. */ package kotlinx.rpc.codegen.extension @@ -44,6 +44,10 @@ private object Descriptor { const val CREATE_INSTANCE = "createInstance" } +private object GrpcDescriptor { + const val DELEGATE = "delegate" +} + @Suppress("detekt.LargeClass", "detekt.TooManyFunctions") internal class RpcStubGenerator( private val declaration: ServiceDeclaration, @@ -123,7 +127,10 @@ internal class RpcStubGenerator( clientProperty() - coroutineContextProperty() + // not for gRPC + if (!declaration.isGrpc) { + coroutineContextProperty() + } declaration.fields.forEach { rpcFlowField(it) @@ -550,7 +557,15 @@ internal class RpcStubGenerator( overriddenSymbols = listOf(method.function.symbol) body = irBuilder(symbol).irBlockBody { - +irReturn( + val call = if (declaration.isGrpc) { + irRpcMethodClientCall( + method = method, + functionThisReceiver = functionThisReceiver, + isMethodObject = isMethodObject, + methodClass = methodClass, + arguments = arguments, + ) + } else { irCall( callee = ctx.functions.scopedClientCall, type = method.function.returnType, @@ -600,7 +615,9 @@ internal class RpcStubGenerator( putValueArgument(1, lambda) } - ) + } + + +irReturn(call) } } } @@ -868,7 +885,10 @@ internal class RpcStubGenerator( stubCompanionObjectThisReceiver = thisReceiver ?: error("Stub companion object expected to have thisReceiver: ${name.asString()}") - superTypes = listOf(ctx.rpcServiceDescriptor.typeWith(declaration.serviceType)) + superTypes = listOfNotNull( + ctx.rpcServiceDescriptor.typeWith(declaration.serviceType), + if (declaration.isGrpc) ctx.grpcServiceDescriptor.typeWith(declaration.serviceType) else null, + ) generateCompanionObjectConstructor() @@ -901,6 +921,10 @@ internal class RpcStubGenerator( generateCreateInstanceFunction() generateGetFieldsFunction() + + if (declaration.isGrpc) { + generateGrpcDelegateProperty() + } } /** @@ -1488,6 +1512,43 @@ internal class RpcStubGenerator( } } + /** + * override val delegate: GrpcDelegate = MyServiceDelegate + */ + private fun IrClass.generateGrpcDelegateProperty() { + addProperty { + name = Name.identifier(GrpcDescriptor.DELEGATE) + visibility = DescriptorVisibilities.PUBLIC + }.apply { + overriddenSymbols = listOf(ctx.properties.grpcServiceDescriptorDelegate) + + addBackingFieldUtil { + visibility = DescriptorVisibilities.PRIVATE + type = ctx.grpcDelegate.defaultType + vsApi { isFinalVS = true } + }.apply { + initializer = factory.createExpressionBody( + IrGetObjectValueImpl( + startOffset = UNDEFINED_OFFSET, + endOffset = UNDEFINED_OFFSET, + type = ctx.grpcDelegate.defaultType, + symbol = ctx.getIrClassSymbol( + declaration.service.packageFqName?.asString() + ?: error("Expected package name fro service ${declaration.service.name}"), + "${declaration.service.name.asString()}Delegate", + ), + ) + ) + } + + addDefaultGetter(this@generateGrpcDelegateProperty, ctx.irBuiltIns) { + visibility = DescriptorVisibilities.PUBLIC + overriddenSymbols = listOf(ctx.properties.grpcServiceDescriptorDelegate.owner.getterOrFail.symbol) + } + } + } + + // Associated object annotation works on JS, WASM, and Native platforms. // See https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.reflect/find-associated-object.html private fun addAssociatedObjectAnnotationIfPossible() { diff --git a/compiler-plugin/compiler-plugin-backend/src/main/core/kotlinx/rpc/codegen/extension/ServiceDeclaration.kt b/compiler-plugin/compiler-plugin-backend/src/main/core/kotlinx/rpc/codegen/extension/ServiceDeclaration.kt index 3c3dce74..1d0c3f61 100644 --- a/compiler-plugin/compiler-plugin-backend/src/main/core/kotlinx/rpc/codegen/extension/ServiceDeclaration.kt +++ b/compiler-plugin/compiler-plugin-backend/src/main/core/kotlinx/rpc/codegen/extension/ServiceDeclaration.kt @@ -1,15 +1,17 @@ /* - * Copyright 2023-2024 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + * Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. */ package kotlinx.rpc.codegen.extension +import kotlinx.rpc.codegen.common.RpcClassId import org.jetbrains.kotlin.ir.declarations.IrClass import org.jetbrains.kotlin.ir.declarations.IrProperty import org.jetbrains.kotlin.ir.declarations.IrSimpleFunction import org.jetbrains.kotlin.ir.declarations.IrValueParameter import org.jetbrains.kotlin.ir.types.IrType import org.jetbrains.kotlin.ir.util.defaultType +import org.jetbrains.kotlin.ir.util.hasAnnotation import org.jetbrains.kotlin.ir.util.kotlinFqName class ServiceDeclaration( @@ -18,6 +20,7 @@ class ServiceDeclaration( val methods: List, val fields: List, ) { + val isGrpc = service.hasAnnotation(RpcClassId.grpcAnnotation) val fqName = service.kotlinFqName.asString() val serviceType = service.defaultType 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 c5da2bca..b534caef 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 @@ -1,5 +1,5 @@ /* - * Copyright 2023-2024 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + * Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. */ package kotlinx.rpc.codegen.common @@ -12,6 +12,7 @@ import org.jetbrains.kotlin.name.Name object RpcClassId { val remoteServiceInterface = ClassId(FqName("kotlinx.rpc"), Name.identifier("RemoteService")) val rpcAnnotation = ClassId(FqName("kotlinx.rpc.annotations"), Name.identifier("Rpc")) + val grpcAnnotation = ClassId(FqName("kotlinx.rpc.grpc.annotations"), Name.identifier("Grpc")) val checkedTypeAnnotation = ClassId(FqName("kotlinx.rpc.annotations"), Name.identifier("CheckedTypeAnnotation")) val serializableAnnotation = ClassId(FqName("kotlinx.serialization"), Name.identifier("Serializable")) 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 index 6dd60983..7f8618c8 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright 2023-2024 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + * Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. */ package kotlinx.rpc.codegen @@ -13,6 +13,7 @@ import org.jetbrains.kotlin.name.Name import org.jetbrains.kotlinx.serialization.compiler.fir.SerializationPluginKey internal class RpcGeneratedStubKey( + val isGrpc: Boolean, private val serviceName: Name, val functions: List>, ) : GeneratedDeclarationKey() { @@ -25,6 +26,7 @@ internal val FirBasedSymbol<*>.generatedRpcServiceStubKey: RpcGeneratedStubKey? (origin as? FirDeclarationOrigin.Plugin)?.key as? RpcGeneratedStubKey internal class RpcGeneratedRpcMethodClassKey( + val isGrpc: Boolean, val rpcMethod: FirFunctionSymbol<*>, ) : GeneratedDeclarationKey() { val isObject = rpcMethod.valueParameterSymbols.isEmpty() diff --git a/compiler-plugin/compiler-plugin-k2/src/main/core/kotlinx/rpc/codegen/FirRpcAdditionalCheckers.kt b/compiler-plugin/compiler-plugin-k2/src/main/core/kotlinx/rpc/codegen/FirRpcAdditionalCheckers.kt index 2a8f3812..7d61073b 100644 --- a/compiler-plugin/compiler-plugin-k2/src/main/core/kotlinx/rpc/codegen/FirRpcAdditionalCheckers.kt +++ b/compiler-plugin/compiler-plugin-k2/src/main/core/kotlinx/rpc/codegen/FirRpcAdditionalCheckers.kt @@ -1,5 +1,5 @@ /* - * Copyright 2023-2024 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + * Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. */ package kotlinx.rpc.codegen @@ -25,6 +25,7 @@ class FirRpcAdditionalCheckers( ) : FirAdditionalCheckersExtension(session) { override fun FirDeclarationPredicateRegistrar.registerPredicates() { register(FirRpcPredicates.rpc) + register(FirRpcPredicates.grpc) register(FirRpcPredicates.checkedAnnotationMeta) } diff --git a/compiler-plugin/compiler-plugin-k2/src/main/core/kotlinx/rpc/codegen/FirRpcPredicates.kt b/compiler-plugin/compiler-plugin-k2/src/main/core/kotlinx/rpc/codegen/FirRpcPredicates.kt index a8bae872..7da4de23 100644 --- a/compiler-plugin/compiler-plugin-k2/src/main/core/kotlinx/rpc/codegen/FirRpcPredicates.kt +++ b/compiler-plugin/compiler-plugin-k2/src/main/core/kotlinx/rpc/codegen/FirRpcPredicates.kt @@ -16,6 +16,10 @@ object FirRpcPredicates { metaAnnotated(RpcClassId.rpcAnnotation.asSingleFqName(), includeItself = true) } + internal val grpc = DeclarationPredicate.create { + annotated(RpcClassId.grpcAnnotation.asSingleFqName()) // @Grpc + } + internal val checkedAnnotationMeta = DeclarationPredicate.create { metaAnnotated(RpcClassId.checkedTypeAnnotation.asSingleFqName(), includeItself = false) } 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 3dcdd998..c2bd0fa6 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 @@ -1,5 +1,5 @@ /* - * Copyright 2023-2024 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + * Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. */ package kotlinx.rpc.codegen @@ -75,6 +75,7 @@ class FirRpcServiceGenerator( override fun FirDeclarationPredicateRegistrar.registerPredicates() { register(FirRpcPredicates.rpc) + register(FirRpcPredicates.grpc) } /** @@ -112,7 +113,7 @@ class FirRpcServiceGenerator( val rpcMethodClassKey = classSymbol.generatedRpcMethodClassKey return when { - rpcMethodClassKey != null -> { + rpcMethodClassKey != null && !rpcMethodClassKey.isGrpc -> { when { !rpcMethodClassKey.isObject -> setOf( SpecialNames.DEFAULT_NAME_FOR_COMPANION_OBJECT, @@ -130,7 +131,10 @@ class FirRpcServiceGenerator( SpecialNames.DEFAULT_NAME_FOR_COMPANION_OBJECT } - classSymbol.isInterface && session.predicateBasedProvider.matches(FirRpcPredicates.rpc, classSymbol) -> { + classSymbol.isInterface && ( + session.predicateBasedProvider.matches(FirRpcPredicates.rpc, classSymbol) || + session.predicateBasedProvider.matches(FirRpcPredicates.grpc, classSymbol) + ) -> { setOf(RpcNames.SERVICE_STUB_NAME) } @@ -159,7 +163,7 @@ class FirRpcServiceGenerator( generateRpcMethodClass(owner, name, rpcServiceStubKey) } - owner.generatedRpcMethodClassKey != null -> { + owner.generatedRpcMethodClassKey != null && owner.generatedRpcMethodClassKey?.isGrpc == false -> { generateNestedClassLikeDeclarationWithSerialization(owner, name) } @@ -191,7 +195,7 @@ class FirRpcServiceGenerator( val methodName = name.rpcMethodName val rpcMethod = rpcServiceStubKey.functions.singleOrNull { it.name == methodName } ?: return null - val rpcMethodClassKey = RpcGeneratedRpcMethodClassKey(rpcMethod) + val rpcMethodClassKey = RpcGeneratedRpcMethodClassKey(rpcServiceStubKey.isGrpc, rpcMethod) val classKind = if (rpcMethodClassKey.isObject) ClassKind.OBJECT else ClassKind.CLASS val rpcMethodClass = createNestedClass( @@ -204,13 +208,15 @@ class FirRpcServiceGenerator( modality = Modality.FINAL } - rpcMethodClass.addAnnotation(RpcClassId.serializableAnnotation, session) + if (!session.predicateBasedProvider.matches(FirRpcPredicates.grpc, owner)) { + rpcMethodClass.addAnnotation(RpcClassId.serializableAnnotation, session) + } /** * Required to pass isSerializableObjectAndNeedsFactory check * from [SerializationFirSupertypesExtension]. */ - if (!isJvmOrMetadata && rpcMethodClassKey.isObject) { + if (!isJvmOrMetadata && rpcMethodClassKey.isObject && !rpcMethodClassKey.isGrpc) { rpcMethodClass.replaceSuperTypeRefs(createSerializationFactorySupertype()) } @@ -265,7 +271,13 @@ class FirRpcServiceGenerator( .filterIsInstance() .map { it.symbol } - return createNestedClass(owner, RpcNames.SERVICE_STUB_NAME, RpcGeneratedStubKey(owner.name, functions)) { + val key = RpcGeneratedStubKey( + isGrpc = session.predicateBasedProvider.matches(FirRpcPredicates.grpc, owner), + serviceName = owner.name, + functions = functions, + ) + + return createNestedClass(owner, RpcNames.SERVICE_STUB_NAME, key) { visibility = Visibilities.Public modality = Modality.FINAL }.symbol @@ -299,7 +311,7 @@ class FirRpcServiceGenerator( context: MemberGenerationContext, rpcMethodClassKey: RpcGeneratedRpcMethodClassKey, ): Set { - return if (rpcMethodClassKey.isObject) { + return if (rpcMethodClassKey.isObject && !rpcMethodClassKey.isGrpc) { // add .serializer() method for a serializable object serializationExtension.getCallableNamesForClass(classSymbol, context) } else { diff --git a/compiler-plugin/compiler-plugin-k2/src/main/core/kotlinx/rpc/codegen/FirRpcSupertypeGeneratorAbstract.kt b/compiler-plugin/compiler-plugin-k2/src/main/core/kotlinx/rpc/codegen/FirRpcSupertypeGeneratorAbstract.kt index b6f17d37..b1205b35 100644 --- a/compiler-plugin/compiler-plugin-k2/src/main/core/kotlinx/rpc/codegen/FirRpcSupertypeGeneratorAbstract.kt +++ b/compiler-plugin/compiler-plugin-k2/src/main/core/kotlinx/rpc/codegen/FirRpcSupertypeGeneratorAbstract.kt @@ -1,5 +1,5 @@ /* - * Copyright 2023-2024 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + * Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. */ package kotlinx.rpc.codegen @@ -7,7 +7,9 @@ package kotlinx.rpc.codegen import kotlinx.rpc.codegen.common.RpcClassId import org.jetbrains.kotlin.cli.common.messages.MessageCollector import org.jetbrains.kotlin.fir.FirSession +import org.jetbrains.kotlin.fir.declarations.FirClass import org.jetbrains.kotlin.fir.declarations.FirClassLikeDeclaration +import org.jetbrains.kotlin.fir.declarations.utils.isInterface import org.jetbrains.kotlin.fir.extensions.FirDeclarationPredicateRegistrar import org.jetbrains.kotlin.fir.extensions.FirSupertypeGenerationExtension import org.jetbrains.kotlin.fir.extensions.predicateBasedProvider @@ -20,10 +22,14 @@ abstract class FirRpcSupertypeGeneratorAbstract( ) : FirSupertypeGenerationExtension(session) { override fun FirDeclarationPredicateRegistrar.registerPredicates() { register(FirRpcPredicates.rpc) + register(FirRpcPredicates.grpc) } override fun needTransformSupertypes(declaration: FirClassLikeDeclaration): Boolean { - return session.predicateBasedProvider.matches(FirRpcPredicates.rpc, declaration) + return session.predicateBasedProvider.matches( + predicate = FirRpcPredicates.rpc, + declaration = declaration, + ) && declaration is FirClass && declaration.isInterface } protected fun computeAdditionalSupertypesAbstract( diff --git a/compiler-plugin/compiler-plugin-k2/src/main/core/kotlinx/rpc/codegen/FirRpcUtils.kt b/compiler-plugin/compiler-plugin-k2/src/main/core/kotlinx/rpc/codegen/FirRpcUtils.kt index 85afba39..b0404f89 100644 --- a/compiler-plugin/compiler-plugin-k2/src/main/core/kotlinx/rpc/codegen/FirRpcUtils.kt +++ b/compiler-plugin/compiler-plugin-k2/src/main/core/kotlinx/rpc/codegen/FirRpcUtils.kt @@ -1,5 +1,5 @@ /* - * Copyright 2023-2024 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + * Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. */ package kotlinx.rpc.codegen @@ -7,9 +7,12 @@ package kotlinx.rpc.codegen import kotlinx.rpc.codegen.common.RpcClassId import org.jetbrains.kotlin.KtSourceElement import org.jetbrains.kotlin.fir.FirSession -import org.jetbrains.kotlin.fir.declarations.getAnnotationByClassId import org.jetbrains.kotlin.fir.expressions.FirAnnotation +import org.jetbrains.kotlin.fir.expressions.UnresolvedExpressionTypeAccess +import org.jetbrains.kotlin.fir.extensions.predicate.DeclarationPredicate +import org.jetbrains.kotlin.fir.extensions.predicateBasedProvider import org.jetbrains.kotlin.fir.resolve.fullyExpandedType +import org.jetbrains.kotlin.fir.resolve.toClassLikeSymbol import org.jetbrains.kotlin.fir.symbols.FirBasedSymbol import org.jetbrains.kotlin.fir.symbols.SymbolInternals import org.jetbrains.kotlin.fir.symbols.impl.FirClassSymbol @@ -23,16 +26,21 @@ fun FirClassSymbol<*>.isRemoteService(session: FirSession): Boolean = resolvedSu it.doesMatchesClassId(session, RpcClassId.remoteServiceInterface) } -fun FirBasedSymbol<*>.rpcAnnotationSource(session: FirSession): KtSourceElement? { - return rpcAnnotation(session)?.source +fun FirBasedSymbol<*>.rpcAnnotationSource(session: FirSession, predicate: DeclarationPredicate): KtSourceElement? { + return rpcAnnotation(session, predicate)?.source } -fun FirBasedSymbol<*>.rpcAnnotation(session: FirSession): FirAnnotation? { - return resolvedCompilerAnnotationsWithClassIds.rpcAnnotation(session) +fun FirBasedSymbol<*>.rpcAnnotation(session: FirSession, predicate: DeclarationPredicate): FirAnnotation? { + return resolvedCompilerAnnotationsWithClassIds.rpcAnnotation(session, predicate) } -fun List.rpcAnnotation(session: FirSession): FirAnnotation? { - return getAnnotationByClassId(RpcClassId.rpcAnnotation, session) +@OptIn(UnresolvedExpressionTypeAccess::class) +fun List.rpcAnnotation(session: FirSession, predicate: DeclarationPredicate): FirAnnotation? { + return find { + it.coneTypeOrNull?.toClassLikeSymbol(session)?.let { declaration -> + session.predicateBasedProvider.matches(predicate, declaration) + } == true + } } fun FirClassSymbol<*>.remoteServiceSupertypeSource(session: FirSession): KtSourceElement? { diff --git a/compiler-plugin/compiler-plugin-k2/src/main/core/kotlinx/rpc/codegen/checkers/FirRpcAnnotationChecker.kt b/compiler-plugin/compiler-plugin-k2/src/main/core/kotlinx/rpc/codegen/checkers/FirRpcAnnotationChecker.kt index 4221420b..c8b095b6 100644 --- a/compiler-plugin/compiler-plugin-k2/src/main/core/kotlinx/rpc/codegen/checkers/FirRpcAnnotationChecker.kt +++ b/compiler-plugin/compiler-plugin-k2/src/main/core/kotlinx/rpc/codegen/checkers/FirRpcAnnotationChecker.kt @@ -30,13 +30,16 @@ class FirRpcAnnotationChecker(private val ctx: FirCheckersContext) : FirRegularC ) { val rpcAnnotated = context.session.predicateBasedProvider.matches(FirRpcPredicates.rpc, declaration) val rpcMetaAnnotated = context.session.predicateBasedProvider.matches(FirRpcPredicates.rpcMeta, declaration) + val grpcAnnotated = context.session.predicateBasedProvider.matches(FirRpcPredicates.grpc, declaration) - if (!declaration.isInterface && declaration.classKind != ClassKind.ANNOTATION_CLASS && rpcMetaAnnotated) { + val isMetaAnnotated = declaration.classKind != ClassKind.ANNOTATION_CLASS + + if (!declaration.isInterface && isMetaAnnotated && rpcMetaAnnotated) { reporter.reportOn( - source = declaration.symbol.rpcAnnotationSource(context.session), + source = declaration.symbol.rpcAnnotationSource(context.session, FirRpcPredicates.rpcMeta), factory = FirRpcDiagnostics.WRONG_RPC_ANNOTATION_TARGET, context = context, - a = declaration.symbol.rpcAnnotation(context.session)?.resolvedType + a = declaration.symbol.rpcAnnotation(context.session, FirRpcPredicates.rpc)?.resolvedType ?: error("Unexpected unresolved annotation type for declaration: ${declaration.symbol.classId.asSingleFqName()}"), ) } @@ -49,9 +52,9 @@ class FirRpcAnnotationChecker(private val ctx: FirCheckersContext) : FirRegularC ) } - if (rpcAnnotated && !ctx.serializationIsPresent) { + if ((rpcAnnotated || grpcAnnotated) && !ctx.serializationIsPresent && isMetaAnnotated) { reporter.reportOn( - source = declaration.symbol.rpcAnnotationSource(context.session), + source = declaration.symbol.rpcAnnotationSource(context.session, FirRpcPredicates.rpcMeta), factory = FirRpcDiagnostics.MISSING_SERIALIZATION_MODULE, context = context, ) diff --git a/compiler-plugin/compiler-plugin-k2/src/main/core/kotlinx/rpc/codegen/checkers/diagnostics/FirRpcDiagnostics.kt b/compiler-plugin/compiler-plugin-k2/src/main/core/kotlinx/rpc/codegen/checkers/diagnostics/FirRpcDiagnostics.kt index ecb6e253..ab8b5e76 100644 --- a/compiler-plugin/compiler-plugin-k2/src/main/core/kotlinx/rpc/codegen/checkers/diagnostics/FirRpcDiagnostics.kt +++ b/compiler-plugin/compiler-plugin-k2/src/main/core/kotlinx/rpc/codegen/checkers/diagnostics/FirRpcDiagnostics.kt @@ -18,7 +18,7 @@ import org.jetbrains.kotlin.psi.KtAnnotationEntry object FirRpcDiagnostics { val MISSING_RPC_ANNOTATION by error0() - val MISSING_SERIALIZATION_MODULE by warning0() + val MISSING_SERIALIZATION_MODULE by error0() val WRONG_RPC_ANNOTATION_TARGET by error1() val CHECKED_ANNOTATION_VIOLATION by error1() val NON_SUSPENDING_REQUEST_WITHOUT_STREAMING_RETURN_TYPE by error0() diff --git a/core/src/jvmMain/kotlin/kotlinx/rpc/internal/internalServiceDescriptorOf.jvm.kt b/core/src/jvmMain/kotlin/kotlinx/rpc/internal/internalServiceDescriptorOf.jvm.kt index 145f6089..3d0d6699 100644 --- a/core/src/jvmMain/kotlin/kotlinx/rpc/internal/internalServiceDescriptorOf.jvm.kt +++ b/core/src/jvmMain/kotlin/kotlinx/rpc/internal/internalServiceDescriptorOf.jvm.kt @@ -1,5 +1,5 @@ /* - * Copyright 2023-2024 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + * Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. */ package kotlinx.rpc.internal @@ -13,8 +13,12 @@ private const val RPC_SERVICE_STUB_SIMPLE_NAME = "\$rpcServiceStub" internal actual fun <@Rpc T : Any> internalServiceDescriptorOf(kClass: KClass): Any? { val className = "${kClass.qualifiedName}\$$RPC_SERVICE_STUB_SIMPLE_NAME" - return kClass.java.classLoader - .loadClass(className) - ?.kotlin - ?.companionObjectInstance + return try { + kClass.java.classLoader + .loadClass(className) + ?.kotlin + ?.companionObjectInstance + } catch (_ : ClassNotFoundException) { + null + } } diff --git a/gradle-conventions/common/src/main/kotlin/util/apiValidation.kt b/gradle-conventions/common/src/main/kotlin/util/apiValidation.kt index bf277e84..6a8109d7 100644 --- a/gradle-conventions/common/src/main/kotlin/util/apiValidation.kt +++ b/gradle-conventions/common/src/main/kotlin/util/apiValidation.kt @@ -1,5 +1,5 @@ /* - * Copyright 2023-2024 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + * Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. */ package util @@ -14,12 +14,14 @@ fun Project.configureApiValidation() { the().apply { ignoredPackages.add("kotlinx.rpc.internal") ignoredPackages.add("kotlinx.rpc.krpc.internal") + ignoredPackages.add("kotlinx.rpc.grpc.internal") ignoredProjects.addAll( listOf( "compiler-plugin-tests", "krpc-test", "utils", + "protobuf-plugin", ) ) diff --git a/gradle-conventions/src/main/kotlin/conventions-publishing.gradle.kts b/gradle-conventions/src/main/kotlin/conventions-publishing.gradle.kts index 5b181a9c..78e88572 100644 --- a/gradle-conventions/src/main/kotlin/conventions-publishing.gradle.kts +++ b/gradle-conventions/src/main/kotlin/conventions-publishing.gradle.kts @@ -1,5 +1,5 @@ /* - * Copyright 2023-2024 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + * Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. */ import util.* @@ -26,7 +26,8 @@ if (isPublicModule) { fun PublishingExtension.configurePublication() { repositories { configureSonatypeRepository() - configureSpaceRepository() + configureSpaceEapRepository() + configureSpaceGrpcRepository() configureForIdeRepository() configureLocalDevRepository() } @@ -110,12 +111,21 @@ fun MavenPom.configureMavenCentralMetadata() { } } -fun RepositoryHandler.configureSpaceRepository() { +fun RepositoryHandler.configureSpaceEapRepository() { configureRepository(project) { username = "SPACE_USERNAME" password = "SPACE_PASSWORD" name = "space" - url = "https://maven.pkg.jetbrains.space/public/p/krpc/maven" + url = "https://maven.pkg.jetbrains.space/public/p/krpc/eap" + } +} + +fun RepositoryHandler.configureSpaceGrpcRepository() { + configureRepository(project) { + username = "SPACE_USERNAME" + password = "SPACE_PASSWORD" + name = "grpc" + url = "https://maven.pkg.jetbrains.space/public/p/krpc/grpc" } } diff --git a/grpc/grpc-core/api/grpc-core.api b/grpc/grpc-core/api/grpc-core.api new file mode 100644 index 00000000..59d28f2b --- /dev/null +++ b/grpc/grpc-core/api/grpc-core.api @@ -0,0 +1,116 @@ +public final class kotlinx/rpc/grpc/GrpcClient : kotlinx/rpc/RpcClient { + public fun call (Lkotlinx/rpc/RpcCall;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public fun callAsync (Lkotlinx/coroutines/CoroutineScope;Lkotlinx/rpc/RpcCall;)Lkotlinx/coroutines/Deferred; + public fun getCoroutineContext ()Lkotlin/coroutines/CoroutineContext; + public fun provideStubContext (J)Lkotlin/coroutines/CoroutineContext; +} + +public final class kotlinx/rpc/grpc/GrpcClientKt { + public static final fun GrpcClient (Ljava/lang/String;ILkotlin/jvm/functions/Function1;)Lkotlinx/rpc/grpc/GrpcClient; + public static final fun GrpcClient (Ljava/lang/String;Lkotlin/jvm/functions/Function1;)Lkotlinx/rpc/grpc/GrpcClient; + public static synthetic fun GrpcClient$default (Ljava/lang/String;ILkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lkotlinx/rpc/grpc/GrpcClient; + public static synthetic fun GrpcClient$default (Ljava/lang/String;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lkotlinx/rpc/grpc/GrpcClient; +} + +public final class kotlinx/rpc/grpc/GrpcServer : kotlinx/rpc/RpcServer, kotlinx/rpc/grpc/Server { + public fun awaitTermination-VtjQ1oo (JLkotlin/coroutines/Continuation;)Ljava/lang/Object; + public fun getCoroutineContext ()Lkotlin/coroutines/CoroutineContext; + public fun getPort ()I + public fun isShutdown ()Z + public fun isTerminated ()Z + public fun registerService (Lkotlin/reflect/KClass;Lkotlin/jvm/functions/Function1;)V + public fun shutdown ()Lkotlinx/rpc/grpc/GrpcServer; + public synthetic fun shutdown ()Lkotlinx/rpc/grpc/Server; + public fun shutdownNow ()Lkotlinx/rpc/grpc/GrpcServer; + public synthetic fun shutdownNow ()Lkotlinx/rpc/grpc/Server; + public fun start ()Lkotlinx/rpc/grpc/GrpcServer; + public synthetic fun start ()Lkotlinx/rpc/grpc/Server; +} + +public final class kotlinx/rpc/grpc/GrpcServerKt { + public static final fun GrpcServer (ILkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;)Lkotlinx/rpc/grpc/GrpcServer; + public static synthetic fun GrpcServer$default (ILkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lkotlinx/rpc/grpc/GrpcServer; +} + +public abstract interface class kotlinx/rpc/grpc/ManagedChannel { + public abstract fun awaitTermination-VtjQ1oo (JLkotlin/coroutines/Continuation;)Ljava/lang/Object; + public abstract fun getPlatformApi ()Lio/grpc/ManagedChannel; + public abstract fun isShutdown ()Z + public abstract fun isTerminated ()Z + public abstract fun shutdown ()Lkotlinx/rpc/grpc/ManagedChannel; + public abstract fun shutdownNow ()Lkotlinx/rpc/grpc/ManagedChannel; +} + +public final class kotlinx/rpc/grpc/ManagedChannel_jvmKt { + public static final fun toKotlin (Lio/grpc/ManagedChannel;)Lkotlinx/rpc/grpc/ManagedChannel; +} + +public abstract interface class kotlinx/rpc/grpc/Server { + public abstract fun awaitTermination-VtjQ1oo (JLkotlin/coroutines/Continuation;)Ljava/lang/Object; + public abstract fun getPort ()I + public abstract fun isShutdown ()Z + public abstract fun isTerminated ()Z + public abstract fun shutdown ()Lkotlinx/rpc/grpc/Server; + public abstract fun shutdownNow ()Lkotlinx/rpc/grpc/Server; + public abstract fun start ()Lkotlinx/rpc/grpc/Server; +} + +public final class kotlinx/rpc/grpc/Server$DefaultImpls { + public static synthetic fun awaitTermination-VtjQ1oo$default (Lkotlinx/rpc/grpc/Server;JLkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; +} + +public abstract interface class kotlinx/rpc/grpc/Status { + public abstract fun getCause ()Ljava/lang/Throwable; + public abstract fun getCode ()Lkotlinx/rpc/grpc/Status$Code; + public abstract fun getDescription ()Ljava/lang/String; +} + +public final class kotlinx/rpc/grpc/Status$Code : java/lang/Enum { + public static final field ABORTED Lkotlinx/rpc/grpc/Status$Code; + public static final field ALREADY_EXISTS Lkotlinx/rpc/grpc/Status$Code; + public static final field CANCELLED Lkotlinx/rpc/grpc/Status$Code; + public static final field DATA_LOSS Lkotlinx/rpc/grpc/Status$Code; + public static final field DEADLINE_EXCEEDED Lkotlinx/rpc/grpc/Status$Code; + public static final field FAILED_PRECONDITION Lkotlinx/rpc/grpc/Status$Code; + public static final field INTERNAL Lkotlinx/rpc/grpc/Status$Code; + public static final field INVALID_ARGUMENT Lkotlinx/rpc/grpc/Status$Code; + public static final field NOT_FOUND Lkotlinx/rpc/grpc/Status$Code; + public static final field OK Lkotlinx/rpc/grpc/Status$Code; + public static final field OUT_OF_RANGE Lkotlinx/rpc/grpc/Status$Code; + public static final field PERMISSION_DENIED Lkotlinx/rpc/grpc/Status$Code; + public static final field RESOURCE_EXHAUSTED Lkotlinx/rpc/grpc/Status$Code; + public static final field UNAUTHENTICATED Lkotlinx/rpc/grpc/Status$Code; + public static final field UNAVAILABLE Lkotlinx/rpc/grpc/Status$Code; + public static final field UNIMPLEMENTED Lkotlinx/rpc/grpc/Status$Code; + public static final field UNKNOWN Lkotlinx/rpc/grpc/Status$Code; + public static fun getEntries ()Lkotlin/enums/EnumEntries; + public final fun getValue ()I + public final fun getValueAscii ()[B + public static fun valueOf (Ljava/lang/String;)Lkotlinx/rpc/grpc/Status$Code; + public static fun values ()[Lkotlinx/rpc/grpc/Status$Code; +} + +public abstract interface class kotlinx/rpc/grpc/StatusRuntimeException { + public abstract fun getStatus ()Lkotlinx/rpc/grpc/Status; +} + +public final class kotlinx/rpc/grpc/StatusRuntimeException_jvmKt { + public static final fun StatusRuntimeException (Lkotlinx/rpc/grpc/Status;)Lkotlinx/rpc/grpc/StatusRuntimeException; + public static final fun toJvm (Lkotlinx/rpc/grpc/StatusRuntimeException;)Lio/grpc/StatusRuntimeException; + public static final fun toKotlin (Lio/grpc/StatusRuntimeException;)Lkotlinx/rpc/grpc/StatusRuntimeException; +} + +public abstract interface class kotlinx/rpc/grpc/descriptor/GrpcClientDelegate { + public abstract fun call (Lkotlinx/rpc/RpcCall;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public abstract fun callAsync (Lkotlinx/rpc/RpcCall;)Lkotlinx/coroutines/Deferred; +} + +public abstract interface class kotlinx/rpc/grpc/descriptor/GrpcDelegate { + public abstract fun clientProvider (Lkotlinx/rpc/grpc/ManagedChannel;)Lkotlinx/rpc/grpc/descriptor/GrpcClientDelegate; + public abstract fun definitionFor (Ljava/lang/Object;)Lio/grpc/ServerServiceDefinition; +} + +public abstract interface class kotlinx/rpc/grpc/descriptor/GrpcServiceDescriptor : kotlinx/rpc/descriptor/RpcServiceDescriptor { + public abstract fun getDelegate ()Lkotlinx/rpc/grpc/descriptor/GrpcDelegate; +} + diff --git a/grpc/grpc-core/build.gradle.kts b/grpc/grpc-core/build.gradle.kts new file mode 100644 index 00000000..68496ae2 --- /dev/null +++ b/grpc/grpc-core/build.gradle.kts @@ -0,0 +1,31 @@ +/* + * Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + */ + +plugins { + alias(libs.plugins.conventions.kmp) + alias(libs.plugins.kotlinx.rpc) +} + +kotlin { + sourceSets { + commonMain { + dependencies { + api(projects.core) + api(projects.utils) + api(libs.coroutines.core) + } + } + + jvmMain { + dependencies { + api(libs.grpc.util) + api(libs.grpc.stub) + api(libs.grpc.protobuf) + api(libs.grpc.kotlin.stub) + api(libs.protobuf.java.util) + api(libs.protobuf.kotlin) + } + } + } +} diff --git a/grpc/grpc-core/gradle.properties b/grpc/grpc-core/gradle.properties new file mode 100644 index 00000000..969e394d --- /dev/null +++ b/grpc/grpc-core/gradle.properties @@ -0,0 +1,5 @@ +# +# Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. +# + +kotlinx.rpc.excludeWasmWasi=true diff --git a/grpc/grpc-core/src/commonMain/kotlin/kotlinx/rpc/grpc/GrpcClient.kt b/grpc/grpc-core/src/commonMain/kotlin/kotlinx/rpc/grpc/GrpcClient.kt new file mode 100644 index 00000000..852da663 --- /dev/null +++ b/grpc/grpc-core/src/commonMain/kotlin/kotlinx/rpc/grpc/GrpcClient.kt @@ -0,0 +1,69 @@ +/* + * Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.rpc.grpc + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Deferred +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.job +import kotlinx.rpc.RpcCall +import kotlinx.rpc.RpcClient +import kotlinx.rpc.grpc.descriptor.GrpcClientDelegate +import kotlinx.rpc.grpc.descriptor.GrpcServiceDescriptor +import kotlinx.rpc.internal.utils.map.ConcurrentHashMap +import kotlin.coroutines.CoroutineContext + +/** + * GrpcClient manages gRPC communication by providing implementation for making asynchronous RPC calls. + * + * @property channel The [ManagedChannel] used to communicate with remote gRPC services. + */ +public class GrpcClient internal constructor(private val channel: ManagedChannel) : RpcClient { + override val coroutineContext: CoroutineContext = SupervisorJob() + + private val stubs = ConcurrentHashMap() + + override suspend fun call(call: RpcCall): T { + return call.delegate().call(call) + } + + override fun callAsync(serviceScope: CoroutineScope, call: RpcCall): Deferred { + return call.delegate().callAsync(call) + } + + private fun RpcCall.delegate(): GrpcClientDelegate { + val grpc = (descriptor as? GrpcServiceDescriptor<*>) + ?: error("Service ${descriptor.fqName} is not a gRPC service") + + return stubs.computeIfAbsent(serviceId) { grpc.delegate.clientProvider(channel) } + } + + override fun provideStubContext(serviceId: Long): CoroutineContext { + return SupervisorJob(coroutineContext.job) + } +} + +/** + * Constructor function for the [GrpcClient] class. + */ +public fun GrpcClient( + name: String, + port: Int, + configure: ManagedChannelBuilder<*>.() -> Unit = {}, +): GrpcClient { + val channel = ManagedChannelBuilder(name, port).apply(configure).buildChannel() + return GrpcClient(channel) +} + +/** + * Constructor function for the [GrpcClient] class. + */ +public fun GrpcClient( + target: String, + configure: ManagedChannelBuilder<*>.() -> Unit = {}, +): GrpcClient { + val channel = ManagedChannelBuilder(target).apply(configure).buildChannel() + return GrpcClient(channel) +} diff --git a/grpc/grpc-core/src/commonMain/kotlin/kotlinx/rpc/grpc/GrpcServer.kt b/grpc/grpc-core/src/commonMain/kotlin/kotlinx/rpc/grpc/GrpcServer.kt new file mode 100644 index 00000000..513b2b7b --- /dev/null +++ b/grpc/grpc-core/src/commonMain/kotlin/kotlinx/rpc/grpc/GrpcServer.kt @@ -0,0 +1,106 @@ +/* + * Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.rpc.grpc + +import kotlinx.coroutines.SupervisorJob +import kotlinx.rpc.RpcServer +import kotlinx.rpc.descriptor.serviceDescriptorOf +import kotlinx.rpc.grpc.annotations.Grpc +import kotlinx.rpc.grpc.descriptor.GrpcServiceDescriptor +import kotlin.coroutines.CoroutineContext +import kotlin.reflect.KClass +import kotlin.time.Duration + +/** + * GrpcServer is an implementation of both [RpcServer] and [Server] interfaces, + * providing the ability to host gRPC services. + * + * @property port Specifies the port used by the server to listen for incoming connections. + * @param builder exposes platform-specific Server builder. + */ +public class GrpcServer internal constructor( + override val port: Int = 8080, + builder: ServerBuilder<*>.() -> Unit, +) : RpcServer, Server { + private var isBuilt = false + private lateinit var internalServer: Server + + private val serverBuilder: ServerBuilder<*> = ServerBuilder(port).apply(builder) + private val registry: MutableHandlerRegistry by lazy { + MutableHandlerRegistry().apply { serverBuilder.fallbackHandlerRegistry(this) } + } + + override val coroutineContext: CoroutineContext + get() = error("coroutineContext is not available for gRPC server") + + override fun <@Grpc Service : Any> registerService( + serviceKClass: KClass, + serviceFactory: (CoroutineContext) -> Service, + ) { + val childJob = SupervisorJob() + val service = serviceFactory(childJob) + + val definition: ServerServiceDefinition = getDefinition(service, serviceKClass) + + if (isBuilt) { + registry.addService(definition) + } else { + serverBuilder.addService(definition) + } + } + + private fun <@Grpc Service : Any> getDefinition( + service: Service, + serviceKClass: KClass, + ): ServerServiceDefinition { + val descriptor = serviceDescriptorOf(serviceKClass) + val grpc = (descriptor as? GrpcServiceDescriptor) + ?: error("Service ${descriptor.fqName} is not a gRPC service") + + return grpc.delegate.definitionFor(service) + } + + internal fun build() { + internalServer = Server(serverBuilder) + isBuilt = true + } + + override val isShutdown: Boolean + get() = internalServer.isShutdown + + override val isTerminated: Boolean + get() = internalServer.isTerminated + + override fun start(): GrpcServer { + internalServer.start() + return this + } + + override fun shutdown(): GrpcServer { + internalServer.shutdown() + return this + } + + override fun shutdownNow(): GrpcServer { + internalServer.shutdownNow() + return this + } + + override suspend fun awaitTermination(duration: Duration): GrpcServer { + internalServer.awaitTermination(duration) + return this + } +} + +/** + * Constructor function for the [GrpcServer] class. + */ +public fun GrpcServer( + port: Int, + configure: ServerBuilder<*>.() -> Unit = {}, + builder: RpcServer.() -> Unit = {}, +): GrpcServer { + return GrpcServer(port, configure).apply(builder).apply { build() } +} diff --git a/grpc/grpc-core/src/commonMain/kotlin/kotlinx/rpc/grpc/ManagedChannel.kt b/grpc/grpc-core/src/commonMain/kotlin/kotlinx/rpc/grpc/ManagedChannel.kt new file mode 100644 index 00000000..5368b683 --- /dev/null +++ b/grpc/grpc-core/src/commonMain/kotlin/kotlinx/rpc/grpc/ManagedChannel.kt @@ -0,0 +1,74 @@ +/* + * Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + */ + +@file:Suppress("EXPECT_ACTUAL_CLASSIFIERS_ARE_IN_BETA_WARNING") + +package kotlinx.rpc.grpc + +import kotlin.time.Duration + +/** + * Same as [ManagedChannel], but is platform-exposed. + */ +public expect abstract class ManagedChannelPlatform + +/** + * A virtual connection to a conceptual endpoint, to perform RPCs. + * A channel is free to have zero or many actual connections to the endpoint based on configuration, + * load, etc. A channel is also free to determine which actual endpoints to use and may change it every RPC, + * permitting client-side load balancing. + * + * Provides lifecycle management. + */ +public interface ManagedChannel { + /** + * Returns whether the channel is shutdown. + * Shutdown channels immediately cancel any new calls but may still have some calls being processed. + */ + public val isShutdown: Boolean + + /** + * Returns whether the channel is terminated. + * Terminated channels have no running calls and relevant resources released (like TCP connections). + */ + public val isTerminated: Boolean + + /** + * Waits for the channel to become terminated, giving up if the timeout is reached. + * + * @return whether the channel is terminated, as would be done by [isTerminated]. + */ + public suspend fun awaitTermination(duration: Duration): Boolean + + /** + * Initiates an orderly shutdown in which preexisting calls continue but new calls are immediately canceled. + * + * @return `this` + */ + public fun shutdown(): ManagedChannel + + /** + * Initiates a forceful shutdown in which preexisting and new calls are canceled. + * Although forceful, the shutdown process is still not instantaneous; [isTerminated] will likely + * return `false` immediately after this method returns. + * + * @return this + */ + public fun shutdownNow(): ManagedChannel + + /** + * Exposes the platform-specific version of this API. + */ + public val platformApi: ManagedChannelPlatform +} + +/** + * Builder class for [ManagedChannel]. + */ +public expect abstract class ManagedChannelBuilder> + +internal expect fun ManagedChannelBuilder(name: String, port: Int): ManagedChannelBuilder<*> +internal expect fun ManagedChannelBuilder(target: String): ManagedChannelBuilder<*> + +internal expect fun ManagedChannelBuilder<*>.buildChannel(): ManagedChannel diff --git a/grpc/grpc-core/src/commonMain/kotlin/kotlinx/rpc/grpc/MutableHandlerRegistry.kt b/grpc/grpc-core/src/commonMain/kotlin/kotlinx/rpc/grpc/MutableHandlerRegistry.kt new file mode 100644 index 00000000..77a4ca78 --- /dev/null +++ b/grpc/grpc-core/src/commonMain/kotlin/kotlinx/rpc/grpc/MutableHandlerRegistry.kt @@ -0,0 +1,17 @@ +/* + * Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + */ + +@file:Suppress("EXPECT_ACTUAL_CLASSIFIERS_ARE_IN_BETA_WARNING") + +package kotlinx.rpc.grpc + +/** + * Registry of services and their methods used by servers to dispatching incoming calls. + */ +public expect abstract class HandlerRegistry + +@Suppress("RedundantConstructorKeyword") +internal expect class MutableHandlerRegistry constructor() : HandlerRegistry { + internal fun addService(@Suppress("unused") service: ServerServiceDefinition): ServerServiceDefinition? +} diff --git a/grpc/grpc-core/src/commonMain/kotlin/kotlinx/rpc/grpc/Server.kt b/grpc/grpc-core/src/commonMain/kotlin/kotlinx/rpc/grpc/Server.kt new file mode 100644 index 00000000..c35b6dfb --- /dev/null +++ b/grpc/grpc-core/src/commonMain/kotlin/kotlinx/rpc/grpc/Server.kt @@ -0,0 +1,113 @@ +/* + * Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + */ + +@file:Suppress("EXPECT_ACTUAL_CLASSIFIERS_ARE_IN_BETA_WARNING") + +package kotlinx.rpc.grpc + +import kotlin.time.Duration + +/** + * Platform-specific gRPC server builder. + */ +public expect abstract class ServerBuilder> { + /** + * Adds a service implementation to the handler registry. + * + * @return `this` + */ + public abstract fun addService(service: ServerServiceDefinition): T + + /** + * Sets a fallback handler registry that will be looked up in if a method is not found in the + * primary registry. + * The primary registry (configured via [addService]) is faster but immutable. + * The fallback registry is more flexible and allows implementations to mutate over + * time and load services on-demand. + * + * @return `this` + */ + public abstract fun fallbackHandlerRegistry(registry: HandlerRegistry?): T +} + +internal expect fun ServerBuilder(port: Int): ServerBuilder<*> + +/** + * Server for listening for and dispatching incoming calls. + * It is not expected to be implemented by application code or interceptors. + */ +public interface Server { + /** + * Returns the port number the server is listening on. + * This can return -1 if there is no actual port or the result otherwise doesn't make sense. + * The result is undefined after the server is terminated. + * If there are multiple possible ports, this will return one arbitrarily. + * Implementations are encouraged to return the same port on each call. + * + * @throws [IllegalStateException] – if the server has not yet been started. + */ + public val port: Int + + /** + * Returns whether the server is shutdown. + * Shutdown servers reject any new calls but may still have some calls being processed. + */ + public val isShutdown: Boolean + + /** + * Returns whether the server is terminated. + * Terminated servers have no running calls and relevant resources released (like TCP connections). + */ + public val isTerminated: Boolean + + /** + * Bind and start the server. + * After this call returns, clients may begin connecting to the listening socket(s). + * @return `this` + * @throws IllegalStateException if already started or shut down + * @throws IOException if unable to bind + */ + // TODO, What is IOException in KMP? KRPC-163 + public fun start(): Server + + /** + * Initiates an orderly shutdown in which preexisting calls continue but new calls are rejected. + * After this call returns, this server has released the listening socket(s) and may be reused by + * another server. + * + * Note that this method will not wait for preexisting calls to finish before returning. + * [awaitTermination] needs to be called to wait for existing calls to finish. + * + * Calling this method before [start] will shut down and terminate the server like + * normal, but prevents starting the server in the future. + * + * @return `this` + */ + public fun shutdown(): Server + + /** + * Initiates a forceful shutdown in which preexisting and new calls are rejected. Although + * forceful, the shutdown process is still not instantaneous; [isTerminated] will likely + * return `false` immediately after this method returns. After this call returns, this + * server has released the listening socket(s) and may be reused by another server. + * + * Calling this method before [start] will shut down and terminate the server like + * normal, but prevents starting the server in the future. + * + * @return `this` + */ + public fun shutdownNow(): Server + + /** + * Waits for the server to become terminated, giving up if the timeout is reached. + * + * Calling this method before [start] or [shutdown] is permitted and doesn't + * change its behavior. + * + * @return `this` + */ + public suspend fun awaitTermination(duration: Duration = Duration.INFINITE): Server +} + +internal expect fun Server(builder: ServerBuilder<*>): Server diff --git a/grpc/grpc-core/src/commonMain/kotlin/kotlinx/rpc/grpc/ServerServiceDefinition.kt b/grpc/grpc-core/src/commonMain/kotlin/kotlinx/rpc/grpc/ServerServiceDefinition.kt new file mode 100644 index 00000000..c69b95c1 --- /dev/null +++ b/grpc/grpc-core/src/commonMain/kotlin/kotlinx/rpc/grpc/ServerServiceDefinition.kt @@ -0,0 +1,12 @@ +/* + * Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + */ + +@file:Suppress("EXPECT_ACTUAL_CLASSIFIERS_ARE_IN_BETA_WARNING") + +package kotlinx.rpc.grpc + +/** + * Definition of a service to be exposed via a Server. + */ +public expect class ServerServiceDefinition diff --git a/grpc/grpc-core/src/commonMain/kotlin/kotlinx/rpc/grpc/Status.kt b/grpc/grpc-core/src/commonMain/kotlin/kotlinx/rpc/grpc/Status.kt new file mode 100644 index 00000000..212d46b6 --- /dev/null +++ b/grpc/grpc-core/src/commonMain/kotlin/kotlinx/rpc/grpc/Status.kt @@ -0,0 +1,54 @@ +/* + * Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + */ + +@file:Suppress("MemberVisibilityCanBePrivate") + +package kotlinx.rpc.grpc + +/** + * Defines the status of an operation by providing a standard [Code] in conjunction with an + * optional descriptive message. + * + * For clients, every remote call will return a status on completion. + * In the case of errors this + * status may be propagated to blocking stubs as a [RuntimeException] or to a listener as an + * explicit parameter. + * + * Similarly, servers can report a status by throwing [StatusRuntimeException] + * or by passing the status to a callback. + * + * Utility functions are provided to convert a status to an exception and to extract them + * back out. + * + * Extended descriptions, including a list of codes that shouldn't be generated by the library, + * can be found at + * [doc/statuscodes.md](https://github.com/grpc/grpc/blob/master/doc/statuscodes.md) + */ +public interface Status { + public val code: Code + public val description: String? + public val cause: Throwable? + + public enum class Code(public val value: Int) { + OK(0), + CANCELLED(1), + UNKNOWN(2), + INVALID_ARGUMENT(3), + DEADLINE_EXCEEDED(4), + NOT_FOUND(5), + ALREADY_EXISTS(6), + PERMISSION_DENIED(7), + RESOURCE_EXHAUSTED(8), + FAILED_PRECONDITION(9), + ABORTED(10), + OUT_OF_RANGE(11), + UNIMPLEMENTED(12), + INTERNAL(13), + UNAVAILABLE(14), + DATA_LOSS(15), + UNAUTHENTICATED(16); + + public val valueAscii: ByteArray = value.toString().encodeToByteArray() + } +} diff --git a/grpc/grpc-core/src/commonMain/kotlin/kotlinx/rpc/grpc/StatusRuntimeException.kt b/grpc/grpc-core/src/commonMain/kotlin/kotlinx/rpc/grpc/StatusRuntimeException.kt new file mode 100644 index 00000000..f816ef45 --- /dev/null +++ b/grpc/grpc-core/src/commonMain/kotlin/kotlinx/rpc/grpc/StatusRuntimeException.kt @@ -0,0 +1,20 @@ +/* + * Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.rpc.grpc + +/** + * [Status] in RuntimeException form, for propagating [Status] information via exceptions. + */ +public interface StatusRuntimeException { + /** + * The status code as a [Status] object. + */ + public val status: Status +} + +/** + * Constructor function for the [StatusRuntimeException] class. + */ +public expect fun StatusRuntimeException(status: Status) : StatusRuntimeException diff --git a/grpc/grpc-core/src/commonMain/kotlin/kotlinx/rpc/grpc/annotations/Grpc.kt b/grpc/grpc-core/src/commonMain/kotlin/kotlinx/rpc/grpc/annotations/Grpc.kt new file mode 100644 index 00000000..08b7b496 --- /dev/null +++ b/grpc/grpc-core/src/commonMain/kotlin/kotlinx/rpc/grpc/annotations/Grpc.kt @@ -0,0 +1,18 @@ +/* + * Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.rpc.grpc.annotations + +import kotlinx.rpc.annotations.Rpc +import kotlinx.rpc.internal.utils.InternalRpcApi + +/** + * Annotation used for marking gRPC services. + * + * Internal use only. + */ +@Target(AnnotationTarget.CLASS, AnnotationTarget.ANNOTATION_CLASS, AnnotationTarget.TYPE_PARAMETER) +@Rpc +@InternalRpcApi +public annotation class Grpc diff --git a/grpc/grpc-core/src/commonMain/kotlin/kotlinx/rpc/grpc/descriptor/GrpcServiceDescriptor.kt b/grpc/grpc-core/src/commonMain/kotlin/kotlinx/rpc/grpc/descriptor/GrpcServiceDescriptor.kt new file mode 100644 index 00000000..12dec2f5 --- /dev/null +++ b/grpc/grpc-core/src/commonMain/kotlin/kotlinx/rpc/grpc/descriptor/GrpcServiceDescriptor.kt @@ -0,0 +1,32 @@ +/* + * Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.rpc.grpc.descriptor + +import kotlinx.coroutines.Deferred +import kotlinx.rpc.RpcCall +import kotlinx.rpc.descriptor.RpcServiceDescriptor +import kotlinx.rpc.grpc.ManagedChannel +import kotlinx.rpc.grpc.ServerServiceDefinition +import kotlinx.rpc.grpc.annotations.Grpc +import kotlinx.rpc.internal.utils.ExperimentalRpcApi + +@ExperimentalRpcApi +public interface GrpcServiceDescriptor<@Grpc T : Any> : RpcServiceDescriptor { + public val delegate: GrpcDelegate +} + +@ExperimentalRpcApi +public interface GrpcDelegate<@Grpc T : Any> { + public fun clientProvider(channel: ManagedChannel): GrpcClientDelegate + + public fun definitionFor(impl: T): ServerServiceDefinition +} + +@ExperimentalRpcApi +public interface GrpcClientDelegate { + public suspend fun call(rpcCall: RpcCall): R + + public fun callAsync(rpcCall: RpcCall): Deferred +} diff --git a/grpc/grpc-core/src/jsMain/kotlin/kotlinx/rpc/grpc/ManagedChannel.js.kt b/grpc/grpc-core/src/jsMain/kotlin/kotlinx/rpc/grpc/ManagedChannel.js.kt new file mode 100644 index 00000000..b7330da4 --- /dev/null +++ b/grpc/grpc-core/src/jsMain/kotlin/kotlinx/rpc/grpc/ManagedChannel.js.kt @@ -0,0 +1,29 @@ +/* + * Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + */ + +@file:Suppress("EXPECT_ACTUAL_CLASSIFIERS_ARE_IN_BETA_WARNING") + +package kotlinx.rpc.grpc + +/** + * Same as [ManagedChannel], but is platform-exposed. + */ +public actual abstract class ManagedChannelPlatform + +/** + * Builder class for [ManagedChannel]. + */ +public actual abstract class ManagedChannelBuilder> + +internal actual fun ManagedChannelBuilder<*>.buildChannel(): ManagedChannel { + error("JS target is not supported in gRPC") +} + +internal actual fun ManagedChannelBuilder(name: String, port: Int): ManagedChannelBuilder<*> { + error("JS target is not supported in gRPC") +} + +internal actual fun ManagedChannelBuilder(target: String): ManagedChannelBuilder<*> { + error("JS target is not supported in gRPC") +} diff --git a/grpc/grpc-core/src/jsMain/kotlin/kotlinx/rpc/grpc/MutableHandlerRegistry.js.kt b/grpc/grpc-core/src/jsMain/kotlin/kotlinx/rpc/grpc/MutableHandlerRegistry.js.kt new file mode 100644 index 00000000..ff545f10 --- /dev/null +++ b/grpc/grpc-core/src/jsMain/kotlin/kotlinx/rpc/grpc/MutableHandlerRegistry.js.kt @@ -0,0 +1,18 @@ +/* + * Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + */ + +@file:Suppress("EXPECT_ACTUAL_CLASSIFIERS_ARE_IN_BETA_WARNING") + +package kotlinx.rpc.grpc + +/** + * Registry of services and their methods used by servers to dispatching incoming calls. + */ +public actual abstract class HandlerRegistry + +internal actual class MutableHandlerRegistry : HandlerRegistry() { + actual fun addService(service: ServerServiceDefinition): ServerServiceDefinition? { + error("JS target is not supported in gRPC") + } +} diff --git a/grpc/grpc-core/src/jsMain/kotlin/kotlinx/rpc/grpc/Server.js.kt b/grpc/grpc-core/src/jsMain/kotlin/kotlinx/rpc/grpc/Server.js.kt new file mode 100644 index 00000000..c0908488 --- /dev/null +++ b/grpc/grpc-core/src/jsMain/kotlin/kotlinx/rpc/grpc/Server.js.kt @@ -0,0 +1,22 @@ +/* + * Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.rpc.grpc + +/** + * Platform-specific gRPC server builder. + */ +public actual abstract class ServerBuilder> { + public actual abstract fun addService(service: ServerServiceDefinition): T + + public actual abstract fun fallbackHandlerRegistry(registry: HandlerRegistry?): T +} + +internal actual fun ServerBuilder(port: Int): ServerBuilder<*> { + error("JS target is not supported in gRPC") +} + +internal actual fun Server(builder: ServerBuilder<*>): Server { + error("JS target is not supported in gRPC") +} diff --git a/grpc/grpc-core/src/jsMain/kotlin/kotlinx/rpc/grpc/ServerServiceDefinition.js.kt b/grpc/grpc-core/src/jsMain/kotlin/kotlinx/rpc/grpc/ServerServiceDefinition.js.kt new file mode 100644 index 00000000..0ac0a6e2 --- /dev/null +++ b/grpc/grpc-core/src/jsMain/kotlin/kotlinx/rpc/grpc/ServerServiceDefinition.js.kt @@ -0,0 +1,12 @@ +/* + * Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + */ + +@file:Suppress("EXPECT_ACTUAL_CLASSIFIERS_ARE_IN_BETA_WARNING") + +package kotlinx.rpc.grpc + +/** + * Definition of a service to be exposed via a Server. + */ +public actual class ServerServiceDefinition diff --git a/grpc/grpc-core/src/jsMain/kotlin/kotlinx/rpc/grpc/StatusRuntimeException.js.kt b/grpc/grpc-core/src/jsMain/kotlin/kotlinx/rpc/grpc/StatusRuntimeException.js.kt new file mode 100644 index 00000000..11f7045d --- /dev/null +++ b/grpc/grpc-core/src/jsMain/kotlin/kotlinx/rpc/grpc/StatusRuntimeException.js.kt @@ -0,0 +1,12 @@ +/* + * Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.rpc.grpc + +/** + * Constructor function for the [StatusRuntimeException] class. + */ +public actual fun StatusRuntimeException(status: Status): StatusRuntimeException { + error("JS target is not supported in gRPC") +} diff --git a/grpc/grpc-core/src/jvmMain/kotlin/kotlinx/rpc/grpc/ManagedChannel.jvm.kt b/grpc/grpc-core/src/jvmMain/kotlin/kotlinx/rpc/grpc/ManagedChannel.jvm.kt new file mode 100644 index 00000000..602f57c8 --- /dev/null +++ b/grpc/grpc-core/src/jvmMain/kotlin/kotlinx/rpc/grpc/ManagedChannel.jvm.kt @@ -0,0 +1,64 @@ +/* + * Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + */ + +@file:Suppress("EXPECT_ACTUAL_CLASSIFIERS_ARE_IN_BETA_WARNING") + +package kotlinx.rpc.grpc + +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import kotlin.time.Duration + +/** + * Same as [ManagedChannel], but is platform-exposed. + */ +public actual typealias ManagedChannelPlatform = io.grpc.ManagedChannel + +/** + * Builder class for [ManagedChannel]. + */ +public actual typealias ManagedChannelBuilder = io.grpc.ManagedChannelBuilder + +internal actual fun ManagedChannelBuilder<*>.buildChannel(): ManagedChannel { + return build().toKotlin() +} + +internal actual fun ManagedChannelBuilder(name: String, port: Int): ManagedChannelBuilder<*> { + return io.grpc.ManagedChannelBuilder.forAddress(name, port) +} + +internal actual fun ManagedChannelBuilder(target: String): ManagedChannelBuilder<*> { + return io.grpc.ManagedChannelBuilder.forTarget(target) +} + +public fun io.grpc.ManagedChannel.toKotlin(): ManagedChannel { + return JvmManagedChannel(this) +} + +private class JvmManagedChannel(private val channel: io.grpc.ManagedChannel) : ManagedChannel { + override val isShutdown: Boolean + get() = channel.isShutdown + + override val isTerminated: Boolean + get() = channel.isTerminated + + override suspend fun awaitTermination(duration: Duration): Boolean { + return withContext(Dispatchers.IO) { + channel.awaitTermination(duration.inWholeNanoseconds, java.util.concurrent.TimeUnit.NANOSECONDS) + } + } + + override fun shutdown(): ManagedChannel { + channel.shutdown() + return this + } + + override fun shutdownNow(): ManagedChannel { + channel.shutdownNow() + return this + } + + override val platformApi: ManagedChannelPlatform + get() = channel +} diff --git a/grpc/grpc-core/src/jvmMain/kotlin/kotlinx/rpc/grpc/MutableHandlerRegistry.jvm.kt b/grpc/grpc-core/src/jvmMain/kotlin/kotlinx/rpc/grpc/MutableHandlerRegistry.jvm.kt new file mode 100644 index 00000000..7f30530a --- /dev/null +++ b/grpc/grpc-core/src/jvmMain/kotlin/kotlinx/rpc/grpc/MutableHandlerRegistry.jvm.kt @@ -0,0 +1,14 @@ +/* + * Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + */ + +@file:Suppress("EXPECT_ACTUAL_CLASSIFIERS_ARE_IN_BETA_WARNING") + +package kotlinx.rpc.grpc + +/** + * Registry of services and their methods used by servers to dispatching incoming calls. + */ +public actual typealias HandlerRegistry = io.grpc.HandlerRegistry + +internal actual typealias MutableHandlerRegistry = io.grpc.util.MutableHandlerRegistry diff --git a/grpc/grpc-core/src/jvmMain/kotlin/kotlinx/rpc/grpc/Server.jvm.kt b/grpc/grpc-core/src/jvmMain/kotlin/kotlinx/rpc/grpc/Server.jvm.kt new file mode 100644 index 00000000..4a01ca51 --- /dev/null +++ b/grpc/grpc-core/src/jvmMain/kotlin/kotlinx/rpc/grpc/Server.jvm.kt @@ -0,0 +1,62 @@ +/* + * Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.rpc.grpc + +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import java.util.concurrent.TimeUnit +import kotlin.time.Duration + +/** + * Platform-specific gRPC server builder. + */ +public actual typealias ServerBuilder = io.grpc.ServerBuilder + +internal actual fun ServerBuilder(port: Int): ServerBuilder<*> { + return io.grpc.ServerBuilder.forPort(port) +} + +internal actual fun Server(builder: ServerBuilder<*>): Server { + return builder.build().toKotlin() +} + +private fun io.grpc.Server.toKotlin(): Server { + return object : Server { + override val port: Int + get() = this@toKotlin.port + + override val isShutdown: Boolean + get() = this@toKotlin.isShutdown + + override val isTerminated: Boolean + get() = this@toKotlin.isTerminated + + override fun start() : Server { + this@toKotlin.start() + return this + } + + override fun shutdown(): Server { + this@toKotlin.shutdown() + return this + } + + override fun shutdownNow(): Server { + this@toKotlin.shutdownNow() + return this + } + + override suspend fun awaitTermination(duration: Duration): Server { + withContext(Dispatchers.IO) { + if (duration == Duration.INFINITE) { + this@toKotlin.awaitTermination() + } else { + this@toKotlin.awaitTermination(duration.inWholeNanoseconds, TimeUnit.NANOSECONDS) + } + } + return this + } + } +} diff --git a/grpc/grpc-core/src/jvmMain/kotlin/kotlinx/rpc/grpc/ServerServiceDefinition.jvm.kt b/grpc/grpc-core/src/jvmMain/kotlin/kotlinx/rpc/grpc/ServerServiceDefinition.jvm.kt new file mode 100644 index 00000000..4f14dce6 --- /dev/null +++ b/grpc/grpc-core/src/jvmMain/kotlin/kotlinx/rpc/grpc/ServerServiceDefinition.jvm.kt @@ -0,0 +1,12 @@ +/* + * Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + */ + +@file:Suppress("EXPECT_ACTUAL_CLASSIFIERS_ARE_IN_BETA_WARNING") + +package kotlinx.rpc.grpc + +/** + * Definition of a service to be exposed via a Server. + */ +public actual typealias ServerServiceDefinition = io.grpc.ServerServiceDefinition diff --git a/grpc/grpc-core/src/jvmMain/kotlin/kotlinx/rpc/grpc/Status.jvm.kt b/grpc/grpc-core/src/jvmMain/kotlin/kotlinx/rpc/grpc/Status.jvm.kt new file mode 100644 index 00000000..b0072d17 --- /dev/null +++ b/grpc/grpc-core/src/jvmMain/kotlin/kotlinx/rpc/grpc/Status.jvm.kt @@ -0,0 +1,64 @@ +/* + * Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + */ + +@file:Suppress("detekt.CyclomaticComplexMethod") + +package kotlinx.rpc.grpc + +internal fun Status.toJvm(): io.grpc.Status { + val code = when (code) { + Status.Code.OK -> io.grpc.Status.Code.OK + Status.Code.CANCELLED -> io.grpc.Status.Code.CANCELLED + Status.Code.UNKNOWN -> io.grpc.Status.Code.UNKNOWN + Status.Code.INVALID_ARGUMENT -> io.grpc.Status.Code.INVALID_ARGUMENT + Status.Code.DEADLINE_EXCEEDED -> io.grpc.Status.Code.DEADLINE_EXCEEDED + Status.Code.NOT_FOUND -> io.grpc.Status.Code.NOT_FOUND + Status.Code.ALREADY_EXISTS -> io.grpc.Status.Code.ALREADY_EXISTS + Status.Code.PERMISSION_DENIED -> io.grpc.Status.Code.PERMISSION_DENIED + Status.Code.RESOURCE_EXHAUSTED -> io.grpc.Status.Code.RESOURCE_EXHAUSTED + Status.Code.FAILED_PRECONDITION -> io.grpc.Status.Code.FAILED_PRECONDITION + Status.Code.ABORTED -> io.grpc.Status.Code.ABORTED + Status.Code.OUT_OF_RANGE -> io.grpc.Status.Code.OUT_OF_RANGE + Status.Code.UNIMPLEMENTED -> io.grpc.Status.Code.UNIMPLEMENTED + Status.Code.INTERNAL -> io.grpc.Status.Code.INTERNAL + Status.Code.UNAVAILABLE -> io.grpc.Status.Code.UNAVAILABLE + Status.Code.DATA_LOSS -> io.grpc.Status.Code.DATA_LOSS + Status.Code.UNAUTHENTICATED -> io.grpc.Status.Code.UNAUTHENTICATED + } + + return io.grpc.Status.fromCode(code) + .withDescription(description) + .withCause(cause) +} + +@Suppress("WHEN_ENUM_CAN_BE_NULL_IN_JAVA") +internal fun io.grpc.Status.toKotlin(): Status { + val code = when (code) { + io.grpc.Status.Code.OK -> Status.Code.OK + io.grpc.Status.Code.CANCELLED -> Status.Code.CANCELLED + io.grpc.Status.Code.UNKNOWN -> Status.Code.UNKNOWN + io.grpc.Status.Code.INVALID_ARGUMENT -> Status.Code.INVALID_ARGUMENT + io.grpc.Status.Code.DEADLINE_EXCEEDED -> Status.Code.DEADLINE_EXCEEDED + io.grpc.Status.Code.NOT_FOUND -> Status.Code.NOT_FOUND + io.grpc.Status.Code.ALREADY_EXISTS -> Status.Code.ALREADY_EXISTS + io.grpc.Status.Code.PERMISSION_DENIED -> Status.Code.PERMISSION_DENIED + io.grpc.Status.Code.RESOURCE_EXHAUSTED -> Status.Code.RESOURCE_EXHAUSTED + io.grpc.Status.Code.FAILED_PRECONDITION -> Status.Code.FAILED_PRECONDITION + io.grpc.Status.Code.ABORTED -> Status.Code.ABORTED + io.grpc.Status.Code.OUT_OF_RANGE -> Status.Code.OUT_OF_RANGE + io.grpc.Status.Code.UNIMPLEMENTED -> Status.Code.UNIMPLEMENTED + io.grpc.Status.Code.INTERNAL -> Status.Code.INTERNAL + io.grpc.Status.Code.UNAVAILABLE -> Status.Code.UNAVAILABLE + io.grpc.Status.Code.DATA_LOSS -> Status.Code.DATA_LOSS + io.grpc.Status.Code.UNAUTHENTICATED -> Status.Code.UNAUTHENTICATED + } + + return JvmStatus(code, description, cause) +} + +internal class JvmStatus( + override val code: Status.Code, + override val description: String? = null, + override val cause: Throwable? = null, +): Status diff --git a/grpc/grpc-core/src/jvmMain/kotlin/kotlinx/rpc/grpc/StatusRuntimeException.jvm.kt b/grpc/grpc-core/src/jvmMain/kotlin/kotlinx/rpc/grpc/StatusRuntimeException.jvm.kt new file mode 100644 index 00000000..4113bafc --- /dev/null +++ b/grpc/grpc-core/src/jvmMain/kotlin/kotlinx/rpc/grpc/StatusRuntimeException.jvm.kt @@ -0,0 +1,22 @@ +/* + * Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.rpc.grpc + +/** + * Constructor function for the [StatusRuntimeException] class. + */ +public actual fun StatusRuntimeException(status: Status): StatusRuntimeException { + return io.grpc.StatusRuntimeException(status.toJvm()).toKotlin() +} + +internal class JvmStatusRuntimeException(override val status: Status) : StatusRuntimeException + +public fun io.grpc.StatusRuntimeException.toKotlin(): StatusRuntimeException { + return JvmStatusRuntimeException(status.toKotlin()) +} + +public fun StatusRuntimeException.toJvm(): io.grpc.StatusRuntimeException { + return io.grpc.StatusRuntimeException(status.toJvm()) +} diff --git a/grpc/grpc-core/src/nativeMain/kotlin/kotlinx/rpc/grpc/ManagedChannel.native.kt b/grpc/grpc-core/src/nativeMain/kotlin/kotlinx/rpc/grpc/ManagedChannel.native.kt new file mode 100644 index 00000000..69d4d7c9 --- /dev/null +++ b/grpc/grpc-core/src/nativeMain/kotlin/kotlinx/rpc/grpc/ManagedChannel.native.kt @@ -0,0 +1,29 @@ +/* + * Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + */ + +@file:Suppress("EXPECT_ACTUAL_CLASSIFIERS_ARE_IN_BETA_WARNING") + +package kotlinx.rpc.grpc + +/** + * Same as [ManagedChannel], but is platform-exposed. + */ +public actual abstract class ManagedChannelPlatform + +/** + * Builder class for [ManagedChannel]. + */ +public actual abstract class ManagedChannelBuilder> + +internal actual fun ManagedChannelBuilder<*>.buildChannel(): ManagedChannel { + error("Native target is not supported in gRPC") +} + +internal actual fun ManagedChannelBuilder(name: String, port: Int): ManagedChannelBuilder<*> { + error("Native target is not supported in gRPC") +} + +internal actual fun ManagedChannelBuilder(target: String): ManagedChannelBuilder<*> { + error("Native target is not supported in gRPC") +} diff --git a/grpc/grpc-core/src/nativeMain/kotlin/kotlinx/rpc/grpc/MutableHandlerRegistry.native.kt b/grpc/grpc-core/src/nativeMain/kotlin/kotlinx/rpc/grpc/MutableHandlerRegistry.native.kt new file mode 100644 index 00000000..19eecc14 --- /dev/null +++ b/grpc/grpc-core/src/nativeMain/kotlin/kotlinx/rpc/grpc/MutableHandlerRegistry.native.kt @@ -0,0 +1,18 @@ +/* + * Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + */ + +@file:Suppress("EXPECT_ACTUAL_CLASSIFIERS_ARE_IN_BETA_WARNING") + +package kotlinx.rpc.grpc + +/** + * Registry of services and their methods used by servers to dispatching incoming calls. + */ +public actual abstract class HandlerRegistry + +internal actual class MutableHandlerRegistry : HandlerRegistry() { + actual fun addService(service: ServerServiceDefinition): ServerServiceDefinition? { + error("Native target is not supported in gRPC") + } +} diff --git a/grpc/grpc-core/src/nativeMain/kotlin/kotlinx/rpc/grpc/Server.native.kt b/grpc/grpc-core/src/nativeMain/kotlin/kotlinx/rpc/grpc/Server.native.kt new file mode 100644 index 00000000..8d7ee951 --- /dev/null +++ b/grpc/grpc-core/src/nativeMain/kotlin/kotlinx/rpc/grpc/Server.native.kt @@ -0,0 +1,22 @@ +/* + * Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.rpc.grpc + +/** + * Platform-specific gRPC server builder. + */ +public actual abstract class ServerBuilder> { + public actual abstract fun addService(service: ServerServiceDefinition): T + + public actual abstract fun fallbackHandlerRegistry(registry: HandlerRegistry?): T +} + +internal actual fun ServerBuilder(port: Int): ServerBuilder<*> { + error("Native target is not supported in gRPC") +} + +internal actual fun Server(builder: ServerBuilder<*>): Server { + error("Native target is not supported in gRPC") +} diff --git a/grpc/grpc-core/src/nativeMain/kotlin/kotlinx/rpc/grpc/ServerServiceDefinition.native.kt b/grpc/grpc-core/src/nativeMain/kotlin/kotlinx/rpc/grpc/ServerServiceDefinition.native.kt new file mode 100644 index 00000000..0ac0a6e2 --- /dev/null +++ b/grpc/grpc-core/src/nativeMain/kotlin/kotlinx/rpc/grpc/ServerServiceDefinition.native.kt @@ -0,0 +1,12 @@ +/* + * Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + */ + +@file:Suppress("EXPECT_ACTUAL_CLASSIFIERS_ARE_IN_BETA_WARNING") + +package kotlinx.rpc.grpc + +/** + * Definition of a service to be exposed via a Server. + */ +public actual class ServerServiceDefinition diff --git a/grpc/grpc-core/src/nativeMain/kotlin/kotlinx/rpc/grpc/StatusRuntimeException.native.kt b/grpc/grpc-core/src/nativeMain/kotlin/kotlinx/rpc/grpc/StatusRuntimeException.native.kt new file mode 100644 index 00000000..38c279a8 --- /dev/null +++ b/grpc/grpc-core/src/nativeMain/kotlin/kotlinx/rpc/grpc/StatusRuntimeException.native.kt @@ -0,0 +1,12 @@ +/* + * Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.rpc.grpc + +/** + * Constructor function for the [StatusRuntimeException] class. + */ +public actual fun StatusRuntimeException(status: Status): StatusRuntimeException { + error("Native target is not supported in gRPC") +} diff --git a/grpc/grpc-core/src/wasmJsMain/kotlin/kotlinx/rpc/grpc/ManagedChannel.wasmJs.kt b/grpc/grpc-core/src/wasmJsMain/kotlin/kotlinx/rpc/grpc/ManagedChannel.wasmJs.kt new file mode 100644 index 00000000..0fbd9845 --- /dev/null +++ b/grpc/grpc-core/src/wasmJsMain/kotlin/kotlinx/rpc/grpc/ManagedChannel.wasmJs.kt @@ -0,0 +1,29 @@ +/* + * Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + */ + +@file:Suppress("EXPECT_ACTUAL_CLASSIFIERS_ARE_IN_BETA_WARNING") + +package kotlinx.rpc.grpc + +/** + * Same as [ManagedChannel], but is platform-exposed. + */ +public actual abstract class ManagedChannelPlatform + +/** + * Builder class for [ManagedChannel]. + */ +public actual abstract class ManagedChannelBuilder> + +internal actual fun ManagedChannelBuilder<*>.buildChannel(): ManagedChannel { + error("WasmJS target is not supported in gRPC") +} + +internal actual fun ManagedChannelBuilder(name: String, port: Int): ManagedChannelBuilder<*> { + error("WasmJS target is not supported in gRPC") +} + +internal actual fun ManagedChannelBuilder(target: String): ManagedChannelBuilder<*> { + error("WasmJS target is not supported in gRPC") +} diff --git a/grpc/grpc-core/src/wasmJsMain/kotlin/kotlinx/rpc/grpc/MutableHandlerRegistry.wasmJs.kt b/grpc/grpc-core/src/wasmJsMain/kotlin/kotlinx/rpc/grpc/MutableHandlerRegistry.wasmJs.kt new file mode 100644 index 00000000..97c8028a --- /dev/null +++ b/grpc/grpc-core/src/wasmJsMain/kotlin/kotlinx/rpc/grpc/MutableHandlerRegistry.wasmJs.kt @@ -0,0 +1,18 @@ +/* + * Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + */ + +@file:Suppress("EXPECT_ACTUAL_CLASSIFIERS_ARE_IN_BETA_WARNING") + +package kotlinx.rpc.grpc + +/** + * Registry of services and their methods used by servers to dispatching incoming calls. + */ +public actual abstract class HandlerRegistry + +internal actual class MutableHandlerRegistry : HandlerRegistry() { + actual fun addService(service: ServerServiceDefinition): ServerServiceDefinition? { + error("WasmJS target is not supported in gRPC") + } +} diff --git a/grpc/grpc-core/src/wasmJsMain/kotlin/kotlinx/rpc/grpc/Server.wasmJs.kt b/grpc/grpc-core/src/wasmJsMain/kotlin/kotlinx/rpc/grpc/Server.wasmJs.kt new file mode 100644 index 00000000..698a08f9 --- /dev/null +++ b/grpc/grpc-core/src/wasmJsMain/kotlin/kotlinx/rpc/grpc/Server.wasmJs.kt @@ -0,0 +1,23 @@ +/* + * Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.rpc.grpc + +/** + * Platform-specific gRPC server builder. + */ + +public actual abstract class ServerBuilder> { + public actual abstract fun addService(service: ServerServiceDefinition): T + + public actual abstract fun fallbackHandlerRegistry(registry: HandlerRegistry?): T +} + +internal actual fun ServerBuilder(port: Int): ServerBuilder<*> { + error("WasmJS target is not supported in gRPC") +} + +internal actual fun Server(builder: ServerBuilder<*>): Server { + error("WasmJS target is not supported in gRPC") +} diff --git a/grpc/grpc-core/src/wasmJsMain/kotlin/kotlinx/rpc/grpc/ServerServiceDefinition.wasmJs.kt b/grpc/grpc-core/src/wasmJsMain/kotlin/kotlinx/rpc/grpc/ServerServiceDefinition.wasmJs.kt new file mode 100644 index 00000000..0ac0a6e2 --- /dev/null +++ b/grpc/grpc-core/src/wasmJsMain/kotlin/kotlinx/rpc/grpc/ServerServiceDefinition.wasmJs.kt @@ -0,0 +1,12 @@ +/* + * Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + */ + +@file:Suppress("EXPECT_ACTUAL_CLASSIFIERS_ARE_IN_BETA_WARNING") + +package kotlinx.rpc.grpc + +/** + * Definition of a service to be exposed via a Server. + */ +public actual class ServerServiceDefinition diff --git a/grpc/grpc-core/src/wasmJsMain/kotlin/kotlinx/rpc/grpc/StatusRuntimeException.wasmJs.kt b/grpc/grpc-core/src/wasmJsMain/kotlin/kotlinx/rpc/grpc/StatusRuntimeException.wasmJs.kt new file mode 100644 index 00000000..95f9e22a --- /dev/null +++ b/grpc/grpc-core/src/wasmJsMain/kotlin/kotlinx/rpc/grpc/StatusRuntimeException.wasmJs.kt @@ -0,0 +1,12 @@ +/* + * Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.rpc.grpc + +/** + * Constructor function for the [StatusRuntimeException] class. + */ +public actual fun StatusRuntimeException(status: Status): StatusRuntimeException { + error("WasmJS target is not supported in gRPC") +} diff --git a/protobuf-plugin/build.gradle.kts b/protobuf-plugin/build.gradle.kts new file mode 100644 index 00000000..a0a75d84 --- /dev/null +++ b/protobuf-plugin/build.gradle.kts @@ -0,0 +1,111 @@ +/* + * 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 + +plugins { + alias(libs.plugins.conventions.jvm) + alias(libs.plugins.kotlinx.rpc) + alias(libs.plugins.serialization) + alias(libs.plugins.protobuf) +} + +dependencies { + implementation(libs.protobuf.java) + + implementation(libs.slf4j.api) + implementation(libs.logback.classic) + + testImplementation(projects.grpc.grpcCore) + testImplementation(libs.coroutines.core) + testImplementation(libs.kotlin.test) + + testImplementation(libs.grpc.stub) + testImplementation(libs.grpc.netty) + testImplementation(libs.grpc.protobuf) + testImplementation(libs.grpc.kotlin.stub) + testImplementation(libs.protobuf.java.util) + testImplementation(libs.protobuf.kotlin) +} + +sourceSets { + test { + proto { + exclude( + "**/empty_deprecated.proto", + "**/enum.proto", + "**/example.proto", + "**/funny_types.proto", + "**/map.proto", + "**/multiple_files.proto", + "**/nested.proto", + "**/one_of.proto", + "**/options.proto", + "**/with_comments.proto", + ) + } + } +} + +tasks.jar { + manifest { + attributes["Main-Class"] = "kotlinx.rpc.protobuf.MainKt" + } + + duplicatesStrategy = DuplicatesStrategy.EXCLUDE + archiveClassifier = "all" + + // Protoc plugins are all fat jars basically (the ones built on jvm) + // be really careful of what you put in the classpath here + from( + configurations.runtimeClasspath.map { prop -> + prop.map { if (it.isDirectory()) it else zipTree(it) } + } + ) +} + +val buildDirPath: String = project.layout.buildDirectory.get().asFile.absolutePath + +protobuf { + protoc { + artifact = libs.protoc.get().toString() + } + + plugins { + create("kotlinx-rpc") { + path = "$buildDirPath/libs/protobuf-plugin-$version-all.jar" + } + + create("grpc") { + artifact = libs.grpc.protoc.gen.java.get().toString() + } + + create("grpckt") { + artifact = libs.grpc.protoc.gen.kotlin.get().toString() + ":jdk8@jar" + } + } + + generateProtoTasks { + all().matching { it.isTest }.all { + plugins { + create("kotlinx-rpc") { + option("debugOutput=$buildDirPath/protobuf-plugin.log") + option("messageMode=interface") + } + create("grpc") + create("grpckt") + } + + dependsOn(tasks.jar) + } + } +} + +kotlin { + explicitApi = ExplicitApiMode.Disabled +} + +tasks.test { + useJUnitPlatform() +} diff --git a/protobuf-plugin/src/main/kotlin/kotlinx/rpc/protobuf/CodeGenerator.kt b/protobuf-plugin/src/main/kotlin/kotlinx/rpc/protobuf/CodeGenerator.kt new file mode 100644 index 00000000..fff6f565 --- /dev/null +++ b/protobuf-plugin/src/main/kotlin/kotlinx/rpc/protobuf/CodeGenerator.kt @@ -0,0 +1,307 @@ +/* + * Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.rpc.protobuf + +import org.slf4j.Logger +import org.slf4j.helpers.NOPLogger + +data class CodeGenerationParameters( + val messageMode: RpcProtobufPlugin.MessageMode, +) + +open class CodeGenerator( + val parameters: CodeGenerationParameters, + private val indent: String, + private val builder: StringBuilder = StringBuilder(), + private val logger: Logger = NOPLogger.NOP_LOGGER, +) { + private var isEmpty: Boolean = true + private var result: String? = null + private var lastIsDeclaration: Boolean = false + + @Suppress("FunctionName") + private fun _append( + value: String? = null, + newLineBefore: Boolean = false, + newLineAfter: Boolean = false, + newLineIfAbsent: Boolean = false, + ) { + var addedNewLineBefore = false + + if (lastIsDeclaration) { + builder.appendLine() + lastIsDeclaration = false + addedNewLineBefore = true + } else if (newLineIfAbsent) { + val last = builder.lastOrNull() + if (last != null && last != '\n') { + builder.appendLine() + addedNewLineBefore = true + } + } + + if (!addedNewLineBefore && newLineBefore) { + builder.appendLine() + } + if (value != null) { + builder.append(value) + } + if (newLineAfter) { + builder.appendLine() + } + + isEmpty = false + } + + private fun append(value: String) { + _append(value) + } + + private fun addLine(value: String? = null) { + _append("$indent${value ?: ""}", newLineIfAbsent = true) + } + + fun newLine() { + _append(newLineBefore = true) + } + + private fun withNextIndent(block: CodeGenerator.() -> Unit) { + CodeGenerator(parameters, "$indent$ONE_INDENT", builder, logger).block() + } + + internal fun scope(prefix: String, suffix: String = "", block: (CodeGenerator.() -> Unit)? = null) { + addLine(prefix) + scopeWithSuffix(suffix, block) + } + + private fun scopeWithSuffix(suffix: String = "", block: (CodeGenerator.() -> Unit)? = null) { + if (block == null) { + newLine() + lastIsDeclaration = true + return + } + + val nested = CodeGenerator(parameters, "$indent$ONE_INDENT", logger = logger).apply(block) + + if (nested.isEmpty) { + newLine() + lastIsDeclaration = true + return + } + + append(" {") + newLine() + append(nested.build().trimEnd()) + addLine("}$suffix") + newLine() + lastIsDeclaration = true + } + + fun code(code: String) { + code.lines().forEach { addLine(it) } + } + + fun property( + name: String, + modifiers: String = "", + contextReceiver: String = "", + type: String = "", + delegate: Boolean = false, + value: String = "", + block: (CodeGenerator.() -> Unit)? = null, + ) { + val modifiersString = if (modifiers.isEmpty()) "" else "$modifiers " + val contextString = if (contextReceiver.isEmpty()) "" else "$contextReceiver." + val typeString = if (type.isEmpty()) "" else ": $type" + val delegateString = if (delegate) " by " else " = " + scope("${modifiersString}val $contextString$name$typeString$delegateString$value", block = block) + } + + fun function( + name: String, + modifiers: String = "", + typeParameters: String = "", + args: String = "", + contextReceiver: String = "", + returnType: String = "", + block: (CodeGenerator.() -> Unit)? = null, + ) { + val modifiersString = if (modifiers.isEmpty()) "" else "$modifiers " + val contextString = if (contextReceiver.isEmpty()) "" else "$contextReceiver." + val returnTypeString = if (returnType.isEmpty()) "" else ": $returnType" + val typeParametersString = if (typeParameters.isEmpty()) "" else " <$typeParameters>" + scope("${modifiersString}fun$typeParametersString $contextString$name($args)$returnTypeString", block = block) + } + + enum class DeclarationType(val strValue: String) { + Class("class"), Interface("interface"), Object("object"); + } + + @JvmName("clazz_no_constructorArgs") + fun clazz( + name: String, + modifiers: String = "", + superTypes: List = emptyList(), + annotations: List = emptyList(), + declarationType: DeclarationType = DeclarationType.Class, + block: (CodeGenerator.() -> Unit)? = null, + ) { + clazz( + name = name, + modifiers = modifiers, + constructorArgs = emptyList(), + superTypes = superTypes, + annotations = annotations, + declarationType = declarationType, + block = block, + ) + } + + @JvmName("clazz_constructorArgs_no_default") + fun clazz( + name: String, + modifiers: String = "", + constructorArgs: List = emptyList(), + superTypes: List = emptyList(), + annotations: List = emptyList(), + declarationType: DeclarationType = DeclarationType.Class, + block: (CodeGenerator.() -> Unit)? = null, + ) { + clazz( + name = name, + modifiers = modifiers, + constructorArgs = constructorArgs.map { it to null }, + superTypes = superTypes, + annotations = annotations, + declarationType = declarationType, + block = block, + ) + } + + fun clazz( + name: String, + modifiers: String = "", + constructorArgs: List> = emptyList(), + superTypes: List = emptyList(), + annotations: List = emptyList(), + declarationType: DeclarationType = DeclarationType.Class, + block: (CodeGenerator.() -> Unit)? = null, + ) { + for (annotation in annotations) { + addLine(annotation) + } + + val modifiersString = if (modifiers.isEmpty()) "" else "$modifiers " + + val firstLine = "$modifiersString${declarationType.strValue}${if (name.isNotEmpty()) " " else ""}$name" + addLine(firstLine) + + val shouldPutArgsOnNewLines = + firstLine.length + constructorArgs.sumOf { + it.first.length + (it.second?.length?.plus(3) ?: 0) + 2 + } + indent.length > 80 + + val constructorArgsTransformed = constructorArgs.map { (arg, default) -> + val defaultString = default?.let { " = $it" } ?: "" + "$arg$defaultString" + } + + when { + shouldPutArgsOnNewLines && constructorArgsTransformed.isNotEmpty() -> { + append("(") + newLine() + withNextIndent { + for (arg in constructorArgsTransformed) { + addLine("$arg,") + } + } + addLine(")") + } + + constructorArgsTransformed.isNotEmpty() -> { + append("(") + append(constructorArgsTransformed.joinToString(", ")) + append(")") + } + } + + val superString = superTypes + .takeIf { it.isNotEmpty() } + ?.joinToString(", ") + ?.let { ": $it" } + ?: "" + + append(superString) + + scopeWithSuffix(block = block) + } + + open fun build(): String { + if (result == null) { + result = builder.toString() + } + + return result!! + } + + companion object { + private const val ONE_INDENT = " " + } +} + +class FileGenerator( + codeGenerationParameters: CodeGenerationParameters, + var filename: String? = null, + var packageName: String? = null, + var fileOptIns: List = emptyList(), + logger: Logger = NOPLogger.NOP_LOGGER, +) : CodeGenerator(codeGenerationParameters, "", logger = logger) { + private val imports = mutableListOf() + + fun importPackage(name: String) { + if (name != packageName) { + imports.add("$name.*") + } + } + + fun import(name: String) { + imports.add(name) + } + + override fun build(): String { + val sortedImports = imports.toSortedSet() + val prefix = buildString { + if (fileOptIns.isNotEmpty()) { + appendLine("@file:OptIn(${fileOptIns.joinToString(", ")})") + newLine() + } + + var packageName = packageName + if (packageName != null && packageName.isNotEmpty()) { + appendLine("package $packageName") + } + + appendLine() + + for (import in sortedImports) { + appendLine("import $import") + } + + if (imports.isNotEmpty()) { + appendLine() + } + } + + return prefix + super.build() + } +} + +fun file( + codeGenerationParameters: CodeGenerationParameters, + name: String? = null, + packageName: String? = null, + logger: Logger = NOPLogger.NOP_LOGGER, + block: FileGenerator.() -> Unit, +): FileGenerator = FileGenerator(codeGenerationParameters, name, packageName, emptyList(), logger).apply(block) diff --git a/protobuf-plugin/src/main/kotlin/kotlinx/rpc/protobuf/Main.kt b/protobuf-plugin/src/main/kotlin/kotlinx/rpc/protobuf/Main.kt new file mode 100644 index 00000000..9671859c --- /dev/null +++ b/protobuf-plugin/src/main/kotlin/kotlinx/rpc/protobuf/Main.kt @@ -0,0 +1,15 @@ +/* + * Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.rpc.protobuf + +import com.google.protobuf.compiler.PluginProtos + +fun main() { + val inputBytes = System.`in`.readBytes() + val request = PluginProtos.CodeGeneratorRequest.parseFrom(inputBytes) + val plugin = RpcProtobufPlugin() + val output: PluginProtos.CodeGeneratorResponse = plugin.run(request) + output.writeTo(System.out) +} diff --git a/protobuf-plugin/src/main/kotlin/kotlinx/rpc/protobuf/ModelToKotlinGenerator.kt b/protobuf-plugin/src/main/kotlin/kotlinx/rpc/protobuf/ModelToKotlinGenerator.kt new file mode 100644 index 00000000..a0cde535 --- /dev/null +++ b/protobuf-plugin/src/main/kotlin/kotlinx/rpc/protobuf/ModelToKotlinGenerator.kt @@ -0,0 +1,375 @@ +/* + * Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.rpc.protobuf + +import kotlinx.rpc.protobuf.CodeGenerator.DeclarationType +import kotlinx.rpc.protobuf.model.* +import org.slf4j.Logger + +class ModelToKotlinGenerator( + private val model: Model, + private val logger: Logger, + private val codeGenerationParameters: CodeGenerationParameters, +) { + fun generateKotlinFiles(): List { + return model.files.map { it.generateKotlinFile() } + } + + private fun FileDeclaration.generateKotlinFile(): FileGenerator { + return file(codeGenerationParameters, logger = logger) { + filename = name.simpleName + packageName = name.packageName + fileOptIns = listOf("ExperimentalRpcApi::class", "InternalRpcApi::class") + + dependencies.forEach { dependency -> + importPackage(dependency.name.packageName) + } + + generateDeclaredEntities(this@generateKotlinFile) + + additionalImports.forEach { + import(it) + } + import("kotlinx.rpc.internal.utils.*") + } + } + + private val additionalImports = mutableSetOf() + + private fun CodeGenerator.generateDeclaredEntities(fileDeclaration: FileDeclaration) { + fileDeclaration.messageDeclarations.forEach { generateMessage(it) } + // KRPC-141 Enum Types +// fileDeclaration.enumDeclarations.forEach { generateEnum(it) } + fileDeclaration.serviceDeclarations.forEach { generateService(it) } + } + + @Suppress("detekt.CyclomaticComplexMethod") + private fun CodeGenerator.generateMessage(declaration: MessageDeclaration) { + val fields = declaration.actualFields.map { it.generateFieldDeclaration() to it.type.defaultValue } + + val isInterfaceMode = parameters.messageMode == RpcProtobufPlugin.MessageMode.Interface + + val (declarationType, modifiers) = when { + isInterfaceMode -> { + DeclarationType.Interface to "" + } + + fields.isEmpty() -> { + DeclarationType.Object to "" + } + + else -> { + DeclarationType.Class to "data" + } + } + + clazz( + name = declaration.name.simpleName, + modifiers = modifiers, + constructorArgs = if (isInterfaceMode) emptyList() else fields.map { "val ${it.first}" to it.second }, + declarationType = declarationType, + ) { + if (isInterfaceMode) { + fields.forEach { + code("val ${it.first}") + newLine() + } + } + + // KRPC-147 OneOf Types +// declaration.oneOfDeclarations.forEach { oneOf -> +// generateOneOf(oneOf) +// } +// + // KRPC-146 Nested Types +// declaration.nestedDeclarations.forEach { nested -> +// generateMessage(nested) +// } +// + // KRPC-141 Enum Types +// declaration.enumDeclarations.forEach { enum -> +// generateEnum(enum) +// } + + if (isInterfaceMode) { + clazz("", modifiers = "companion", declarationType = DeclarationType.Object) + } + } + + if (isInterfaceMode) { + clazz( + name = "${declaration.name.simpleName}Builder", + declarationType = DeclarationType.Class, + superTypes = listOf(declaration.name.simpleName), + ) { + fields.forEach { + code("override var ${it.first} = ${it.second}") + newLine() + } + } + + function( + name = "invoke", + modifiers = "operator", + args = "body: ${declaration.name.simpleName}Builder.() -> Unit", + contextReceiver = "${declaration.name.simpleName}.Companion", + returnType = declaration.name.simpleName, + ) { + code("return ${declaration.name.simpleName}Builder().apply(body)") + } + } + + val platformType = "${declaration.outerClassName.simpleName}.${declaration.name.simpleName}" + + function( + name = "toPlatform", + contextReceiver = declaration.name.simpleName, + returnType = platformType, + ) { + scope("return $platformType.newBuilder().apply", ".build()") { + declaration.actualFields.forEach { field -> + val call = "this@toPlatform.${field.name}${field.toPlatformCast()}" + code("set${field.name.replaceFirstChar { ch -> ch.uppercase() }}($call)") + } + } + } + + function( + name = "toKotlin", + contextReceiver = platformType, + returnType = declaration.name.simpleName, + ) { + scope("return ${declaration.name.simpleName}") { + declaration.actualFields.forEach { field -> + code("${field.name} = this@toKotlin.${field.name}${field.toKotlinCast()}") + } + } + } + } + + private fun FieldDeclaration.toPlatformCast(): String { + val type = type as? FieldType.IntegralType ?: return "" + + return when (type) { + FieldType.IntegralType.FIXED32 -> ".toInt()" + FieldType.IntegralType.FIXED64 -> ".toLong()" + FieldType.IntegralType.UINT32 -> ".toInt()" + FieldType.IntegralType.UINT64 -> ".toLong()" + FieldType.IntegralType.BYTES -> ".let { bytes -> com.google.protobuf.ByteString.copyFrom(bytes) }" + else -> "" + } + } + + private fun FieldDeclaration.toKotlinCast(): String { + val type = type as? FieldType.IntegralType ?: return "" + + return when (type) { + FieldType.IntegralType.FIXED32 -> ".toUInt()" + FieldType.IntegralType.FIXED64 -> ".toULong()" + FieldType.IntegralType.UINT32 -> ".toUInt()" + FieldType.IntegralType.UINT64 -> ".toULong()" + FieldType.IntegralType.BYTES -> ".toByteArray()" + else -> "" + } + } + + private fun FieldDeclaration.generateFieldDeclaration(): String { + return "${name}: ${typeFqName()}" + } + + private fun FieldDeclaration.typeFqName(): String { + return when (type) { + // KRPC-156 Reference Types +// is FieldType.Reference -> { +// type.value.simpleName +// } + + is FieldType.IntegralType -> { + type.fqName.simpleName + } + + // KRPC-143 Repeated Types +// is FieldType.List -> { +// "List<${type.valueName.simpleName}>" +// } +// + // KRPC-145 Map Types +// is FieldType.Map -> { +// "Map<${type.keyName.simpleName}, ${type.valueName.simpleName}>" +// } + else -> { + error("Unsupported type: $type") + } + } + } + + @Suppress("unused") + private fun CodeGenerator.generateOneOf(declaration: OneOfDeclaration) { + val interfaceName = declaration.name.simpleName + + clazz(declaration.name.simpleName, "sealed", declarationType = DeclarationType.Interface) { + declaration.variants.forEach { variant -> + clazz( + name = variant.name, + modifiers = "value", + constructorArgs = listOf("val value: ${variant.typeFqName()}"), + annotations = listOf("@JvmInline"), + superTypes = listOf(interfaceName), + ) + + additionalImports.add("kotlin.jvm.JvmInline") + } + } + } + + @Suppress("unused") + private fun CodeGenerator.generateEnum(declaration: EnumDeclaration) { + clazz(declaration.name.simpleName, "enum") { + code(declaration.originalEntries.joinToString(", ", postfix = ";") { enumEntry -> + enumEntry.name.simpleName + }) + + if (declaration.aliases.isNotEmpty()) { + newLine() + + clazz("", modifiers = "companion", declarationType = DeclarationType.Object) { + declaration.aliases.forEach { alias: EnumDeclaration.Alias -> + code( + "val ${alias.name.simpleName}: ${declaration.name.simpleName} " + + "= ${alias.original.name.simpleName}" + ) + } + } + } + } + } + + @Suppress("detekt.LongMethod") + private fun CodeGenerator.generateService(service: ServiceDeclaration) { + code("@kotlinx.rpc.grpc.annotations.Grpc") + clazz(service.name.simpleName, declarationType = DeclarationType.Interface) { + service.methods.forEach { method -> + // no streaming for now + val inputType by method.inputType + val outputType by method.outputType + function( + name = method.name.simpleName, + modifiers = "suspend", + args = "message: ${inputType.name.simpleName}", + returnType = outputType.name.simpleName, + ) + } + } + + newLine() + + code("@Suppress(\"unused\", \"all\")") + clazz( + modifiers = "private", + name = "${service.name.simpleName}Delegate", + declarationType = DeclarationType.Object, + superTypes = listOf("kotlinx.rpc.grpc.descriptor.GrpcDelegate<${service.name.simpleName}>"), + ) { + function( + name = "clientProvider", + modifiers = "override", + args = "channel: kotlinx.rpc.grpc.ManagedChannel", + returnType = "kotlinx.rpc.grpc.descriptor.GrpcClientDelegate", + ) { + code("return ${service.name.simpleName}ClientDelegate(channel)") + } + + function( + name = "definitionFor", + modifiers = "override", + args = "impl: ${service.name.simpleName}", + returnType = "kotlinx.rpc.grpc.ServerServiceDefinition", + ) { + scope("return ${service.name.simpleName}ServerDelegate(impl).bindService()") + } + } + + code("@Suppress(\"unused\", \"all\")") + clazz( + modifiers = "private", + name = "${service.name.simpleName}ServerDelegate", + declarationType = DeclarationType.Class, + superTypes = listOf("${service.name.simpleName}GrpcKt.${service.name.simpleName}CoroutineImplBase()"), + constructorArgs = listOf("private val impl: ${service.name.simpleName}"), + ) { + service.methods.forEach { method -> + val grpcName = method.name.simpleName.replaceFirstChar { it.lowercase() } + + val inputType by method.inputType + val outputType by method.outputType + + function( + name = grpcName, + modifiers = "override suspend", + args = "request: ${inputType.toPlatformMessageType()}", + returnType = outputType.toPlatformMessageType(), + ) { + code("return impl.${method.name.simpleName}(request.toKotlin()).toPlatform()") + } + } + } + + code("@Suppress(\"unused\", \"all\")") + clazz( + modifiers = "private", + name = "${service.name.simpleName}ClientDelegate", + declarationType = DeclarationType.Class, + superTypes = listOf("kotlinx.rpc.grpc.descriptor.GrpcClientDelegate"), + constructorArgs = listOf("private val channel: kotlinx.rpc.grpc.ManagedChannel"), + ) { + val stubType = "${service.name.simpleName}GrpcKt.${service.name.simpleName}CoroutineStub" + + property( + name = "stub", + modifiers = "private", + type = stubType, + delegate = true, + value = "lazy", + ) { + code("$stubType(channel.platformApi)") + } + + function( + name = "call", + modifiers = "override suspend", + args = "call: kotlinx.rpc.RpcCall", + typeParameters = "R", + returnType = "R", + ) { + code("val message = (call.data as kotlinx.rpc.internal.RpcMethodClass).asArray()[0]") + code("@Suppress(\"UNCHECKED_CAST\")") + scope("return when (call.callableName)") { + service.methods.forEach { method -> + val inputType by method.inputType + val grpcName = method.name.simpleName.replaceFirstChar { it.lowercase() } + val result = "stub.$grpcName((message as ${inputType.name.simpleName}).toPlatform())" + code("\"${method.name.simpleName}\" -> $result.toKotlin() as R") + } + + code("else -> error(\"Illegal call: \${call.callableName}\")") + } + } + + function( + name = "callAsync", + modifiers = "override", + args = "call: kotlinx.rpc.RpcCall", + typeParameters = "R", + returnType = "kotlinx.coroutines.Deferred", + ) { + code("error(\"Async calls are not supported\")") + } + } + } + + private fun MessageDeclaration.toPlatformMessageType(): String { + return "${outerClassName.simpleName}.${name.simpleName.removePrefix(name.parentNameAsPrefix)}" + } +} diff --git a/protobuf-plugin/src/main/kotlin/kotlinx/rpc/protobuf/ProtoToModelInterpreter.kt b/protobuf-plugin/src/main/kotlin/kotlinx/rpc/protobuf/ProtoToModelInterpreter.kt new file mode 100644 index 00000000..8496398a --- /dev/null +++ b/protobuf-plugin/src/main/kotlin/kotlinx/rpc/protobuf/ProtoToModelInterpreter.kt @@ -0,0 +1,305 @@ +/* + * Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.rpc.protobuf + +import com.google.protobuf.DescriptorProtos +import com.google.protobuf.DescriptorProtos.FieldDescriptorProto.Type +import com.google.protobuf.compiler.PluginProtos.CodeGeneratorRequest +import kotlinx.rpc.protobuf.model.* +import kotlinx.rpc.protobuf.model.FieldType.IntegralType +import org.slf4j.Logger +import kotlin.properties.Delegates + +class ProtoToModelInterpreter( + @Suppress("unused") + private val logger: Logger, +) { + private val fileDependencies = mutableMapOf() + private val messages = mutableMapOf() + + fun interpretProtocRequest(message: CodeGeneratorRequest): Model { + return Model(message.protoFileList.map { it.toModel() }) + } + + // package name of a currently parsed file + private var packageName by Delegates.notNull() + + private fun DescriptorProtos.FileDescriptorProto.toModel(): FileDeclaration { + val dependencies = dependencyList.map { depFilename -> + fileDependencies[depFilename] + ?: error("Unknown dependency $depFilename for $name proto file, wrong topological order") + } + + packageName = kotlinPackageName(`package`, options) + + return FileDeclaration( + name = SimpleFqName( + packageName = packageName, + simpleName = kotlinFileName(name) + ), + dependencies = dependencies, + messageDeclarations = messageTypeList.map { it.toModel(fqOuterClass()) }, + enumDeclarations = enumTypeList.map { it.toModel() }, + serviceDeclarations = serviceList.map { it.toModel() }, + deprecated = options.deprecated, + doc = null, + ).also { + fileDependencies[name] = it + } + } + + private fun DescriptorProtos.FileDescriptorProto.fqOuterClass(): FqName { + return "${name.removeSuffix(".proto").fullProtoNameToKotlin(firstLetterUpper = true)}OuterClass".toFqName() + } + + private fun kotlinFileName(originalName: String): String { + return "${originalName.removeSuffix(".proto").fullProtoNameToKotlin(firstLetterUpper = true)}.kt" + } + + private fun kotlinPackageName(originalPackage: String, options: DescriptorProtos.FileOptions): String { + // todo check forbidden package names + return originalPackage + } + + private fun DescriptorProtos.DescriptorProto.toModel(outerClass: FqName): MessageDeclaration { + val fields = fieldList.mapNotNull { + val oneOfName = if (it.hasOneofIndex()) { + oneofDeclList[it.oneofIndex].name + } else { + null + } + + it.toModel(oneOfName) + } + + return MessageDeclaration( + outerClassName = outerClass, + name = name.fullProtoNameToKotlin(firstLetterUpper = true).toFqName(), + actualFields = fields, + oneOfDeclarations = oneofDeclList.mapIndexedNotNull { i, desc -> desc.toModel(i) }, + enumDeclarations = enumTypeList.map { it.toModel() }, + nestedDeclarations = nestedTypeList.map { it.toModel(outerClass) }, + deprecated = options.deprecated, + doc = null, + ).apply { + val name = if (packageName.isEmpty()) { + name + } else { + "$packageName.$name".toFqName() + } + messages[name] = this + } + } + + private val oneOfFieldMembers = mutableMapOf>() + + private fun DescriptorProtos.FieldDescriptorProto.toModel(oneOfName: String?): FieldDeclaration? { + if (oneOfName != null) { + val fieldType = when { + // effectively optional + // https://github.com/protocolbuffers/protobuf/blob/main/docs/implementing_proto3_presence.md#updating-a-code-generator + oneOfName == "_$name" -> { + fieldType() + } + + oneOfFieldMembers[oneofIndex] == null -> { + oneOfFieldMembers[oneofIndex] = mutableListOf() + .also { list -> list.add(this) } + + FieldType.Reference(oneOfName.fullProtoNameToKotlin(firstLetterUpper = true).toFqName()) + } + + else -> { + oneOfFieldMembers[oneofIndex]!!.add(this) + null + } + } ?: return null + + return FieldDeclaration( + name = oneOfName.removePrefix("_").fullProtoNameToKotlin(), + type = fieldType, + nullable = true, + deprecated = options.deprecated, + doc = null, + ) + } + + return FieldDeclaration( + name = name.fullProtoNameToKotlin(), + type = fieldType(), + nullable = false, + deprecated = options.deprecated, + doc = null, + ) + } + + private fun DescriptorProtos.FieldDescriptorProto.fieldType(): FieldType { + return when { + hasTypeName() -> { + typeName + // from https://github.com/protocolbuffers/protobuf/blob/main/src/google/protobuf/descriptor.proto + // if the name starts with a '.', it is fully-qualified. + .substringAfter('.') + .fullProtoNameToKotlin(firstLetterUpper = true) + .toFqName() + .let { wrapWithLabel(it) } + } + + else -> { + primitiveType() + } + } + } + + @Suppress("detekt.CyclomaticComplexMethod") + private fun DescriptorProtos.FieldDescriptorProto.primitiveType(): FieldType { + return when (type) { + Type.TYPE_STRING -> IntegralType.STRING + Type.TYPE_BYTES -> IntegralType.BYTES + Type.TYPE_BOOL -> IntegralType.BOOL + Type.TYPE_FLOAT -> IntegralType.FLOAT + Type.TYPE_DOUBLE -> IntegralType.DOUBLE + Type.TYPE_INT32 -> IntegralType.INT32 + Type.TYPE_INT64 -> IntegralType.INT64 + Type.TYPE_UINT32 -> IntegralType.UINT32 + Type.TYPE_UINT64 -> IntegralType.UINT64 + Type.TYPE_FIXED32 -> IntegralType.FIXED32 + Type.TYPE_FIXED64 -> IntegralType.FIXED64 + Type.TYPE_SINT32 -> IntegralType.SINT32 + Type.TYPE_SINT64 -> IntegralType.SINT64 + Type.TYPE_SFIXED32 -> IntegralType.SFIXED32 + Type.TYPE_SFIXED64 -> IntegralType.SFIXED64 + + Type.TYPE_ENUM, Type.TYPE_MESSAGE, Type.TYPE_GROUP, null -> + error("Expected to find primitive type, instead got $type with name '$typeName'") + } + } + + private fun DescriptorProtos.FieldDescriptorProto.wrapWithLabel(fqName: FqName): FieldType { + return when (label) { + DescriptorProtos.FieldDescriptorProto.Label.LABEL_REPEATED -> { + FieldType.List(fqName) + } + // LABEL_OPTIONAL is not actually optional in proto3. + // Actual optional is oneOf with one option and same name + else -> { + FieldType.Reference(fqName) + } + } + } + + private fun DescriptorProtos.OneofDescriptorProto.toModel(index: Int): OneOfDeclaration? { + val name = name.fullProtoNameToKotlin(firstLetterUpper = true).toFqName() + + val fields = oneOfFieldMembers[index] ?: return null + return OneOfDeclaration( + name = name, + variants = fields.map { field -> + FieldDeclaration( + name = field.name.fullProtoNameToKotlin(firstLetterUpper = true), + type = field.fieldType(), + nullable = false, + deprecated = field.options.deprecated, + doc = null, + ) + } + ) + } + + private fun DescriptorProtos.EnumDescriptorProto.toModel(): EnumDeclaration { + val allowAlias = options.allowAlias + val originalEntries = mutableMapOf() + val aliases = mutableListOf() + + valueList.forEach { enumEntry -> + val original = originalEntries[enumEntry.number] + if (original != null) { + if (!allowAlias) { + error( + "Aliases are not allow for enum type $name: " + + "${enumEntry.number} of ${enumEntry.name} is already used by $original entry. " + + "Allow aliases via `allow_alias = true` option to avoid this error." + ) + } + + aliases.add( + EnumDeclaration.Alias( + name = enumEntry.name.toFqName(), + original = original, + deprecated = enumEntry.options.deprecated, + doc = null, + ) + ) + } else { + originalEntries[enumEntry.number] = EnumDeclaration.Entry( + name = enumEntry.name.toFqName(), + deprecated = enumEntry.options.deprecated, + doc = null, + ) + } + } + + return EnumDeclaration( + name = name.fullProtoNameToKotlin(firstLetterUpper = true).toFqName(), + originalEntries = originalEntries.values.toList(), + aliases = aliases, + deprecated = options.deprecated, + doc = null, + ) + } + + private fun DescriptorProtos.ServiceDescriptorProto.toModel(): ServiceDeclaration { + return ServiceDeclaration( + name = name.fullProtoNameToKotlin(firstLetterUpper = true).toFqName(), + methods = methodList.map { it.toModel() } + ) + } + + private fun DescriptorProtos.MethodDescriptorProto.toModel(): MethodDeclaration { + return MethodDeclaration( + name = name.fullProtoNameToKotlin(firstLetterUpper = false).toFqName(), + inputType = inputType + .substringAfter('.') // see typeName resolution + .fullProtoNameToKotlin(firstLetterUpper = true).toFqName() + .let { lazy { messages[it] ?: error("Unknown message type $it, available: ${messages.keys.joinToString(",")}") } }, + outputType = outputType + .substringAfter('.') // see typeName resolution + .fullProtoNameToKotlin(firstLetterUpper = true).toFqName() + .let { lazy { messages[it] ?: error("Unknown message type $it, available: ${messages.keys.joinToString(",")}") } }, + clientStreaming = clientStreaming, + serverStreaming = serverStreaming, + ) + } + + private fun String.fullProtoNameToKotlin(firstLetterUpper: Boolean = false): String { + val lastDelimiterIndex = indexOfLast { it == '.' || it == '/' } + return if (lastDelimiterIndex != -1) { + val packageName = substring(0, lastDelimiterIndex) + val name = substring(lastDelimiterIndex + 1) + val delimiter = this[lastDelimiterIndex] + return "$packageName$delimiter${name.simpleProtoNameToKotlin(true)}" + } else { + simpleProtoNameToKotlin(firstLetterUpper) + } + } + + private val snakeRegExp = "(_[a-z]|-[a-z])".toRegex() + + private fun String.snakeToCamelCase(): String { + return replace(snakeRegExp) { it.value.last().uppercase() } + } + + private fun String.simpleProtoNameToKotlin(firstLetterUpper: Boolean = false): String { + return snakeToCamelCase().run { + if (firstLetterUpper) { + replaceFirstChar { it.uppercase() } + } else { + this + } + } + } + + private fun String.toFqName(parent: FqName? = null) = SimpleFqName(packageName, this, parent) +} diff --git a/protobuf-plugin/src/main/kotlin/kotlinx/rpc/protobuf/RpcProtobufPlugin.kt b/protobuf-plugin/src/main/kotlin/kotlinx/rpc/protobuf/RpcProtobufPlugin.kt new file mode 100644 index 00000000..bb7567ea --- /dev/null +++ b/protobuf-plugin/src/main/kotlin/kotlinx/rpc/protobuf/RpcProtobufPlugin.kt @@ -0,0 +1,96 @@ +/* + * Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.rpc.protobuf + +import ch.qos.logback.classic.Level +import ch.qos.logback.classic.encoder.PatternLayoutEncoder +import ch.qos.logback.classic.spi.ILoggingEvent +import ch.qos.logback.core.FileAppender +import com.google.protobuf.compiler.PluginProtos.CodeGeneratorRequest +import com.google.protobuf.compiler.PluginProtos.CodeGeneratorResponse +import com.google.protobuf.compiler.PluginProtos.CodeGeneratorResponse.Feature +import org.slf4j.Logger +import org.slf4j.LoggerFactory +import org.slf4j.helpers.NOPLogger + +class RpcProtobufPlugin { + companion object { + private const val DEBUG_OUTPUT_OPTION = "debugOutput" + private const val MESSAGE_MODE_OPTION = "messageMode" + } + + enum class MessageMode { + Interface, Class; + + companion object { + fun of(value: String?): MessageMode { + return when (value) { + "interface" -> Interface + "class" -> Class + null -> error("Message mode is not specified, use --messageMode=interface or --messageMode=class") + else -> error("Unknown message mode: $value") + } + } + } + } + + private var debugOutput: String? = null + private lateinit var messageGenerationMode: MessageMode + private val logger: Logger by lazy { + val debugOutput = debugOutput ?: return@lazy NOPLogger.NOP_LOGGER + + (LoggerFactory.getILoggerFactory().getLogger("RpcProtobufPlugin") as ch.qos.logback.classic.Logger).apply { + val appender = FileAppender().apply { + isAppend = true + file = debugOutput + encoder = PatternLayoutEncoder().apply { + pattern = "%d{YYYY-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n" + } + } + + addAppender(appender) + + level = Level.ALL + } + } + + fun run(input: CodeGeneratorRequest): CodeGeneratorResponse { + val parameters = input.parameter.split(",").associate { + it.split("=").let { (key, value) -> key to value } + } + + debugOutput = parameters[DEBUG_OUTPUT_OPTION] + messageGenerationMode = MessageMode.of(parameters[MESSAGE_MODE_OPTION]) + + val files = input.generateKotlinFiles() + .map { file -> + CodeGeneratorResponse.File.newBuilder() + .apply { + val dir = file.packageName?.replace('.', '/')?.plus("/") ?: "" + + // some filename already contain package (true for Google's default .proto files) + val filename = file.filename?.removePrefix(dir) ?: error("File name can not be null") + name = "$dir$filename" + content = file.build() + } + .build() + } + + return CodeGeneratorResponse.newBuilder() + .apply { + files.forEach(::addFile) + + supportedFeatures = Feature.FEATURE_PROTO3_OPTIONAL_VALUE.toLong() + } + .build() + } + + private fun CodeGeneratorRequest.generateKotlinFiles(): List { + val interpreter = ProtoToModelInterpreter(logger) + val model = interpreter.interpretProtocRequest(this) + val fileGenerator = ModelToKotlinGenerator(model, logger, CodeGenerationParameters(messageGenerationMode)) + return fileGenerator.generateKotlinFiles() + } +} diff --git a/protobuf-plugin/src/main/kotlin/kotlinx/rpc/protobuf/model/EnumDeclaration.kt b/protobuf-plugin/src/main/kotlin/kotlinx/rpc/protobuf/model/EnumDeclaration.kt new file mode 100644 index 00000000..7aa70357 --- /dev/null +++ b/protobuf-plugin/src/main/kotlin/kotlinx/rpc/protobuf/model/EnumDeclaration.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. + */ + +package kotlinx.rpc.protobuf.model + +data class EnumDeclaration( + val name: FqName, + val originalEntries: List, + val aliases: List, + val deprecated: Boolean, + val doc: String?, +) { + data class Entry( + val name: FqName, + val deprecated: Boolean, + val doc: String?, + ) + + data class Alias( + val name: FqName, + val original: Entry, + val deprecated: Boolean, + val doc: String?, + ) +} diff --git a/protobuf-plugin/src/main/kotlin/kotlinx/rpc/protobuf/model/FieldDeclaration.kt b/protobuf-plugin/src/main/kotlin/kotlinx/rpc/protobuf/model/FieldDeclaration.kt new file mode 100644 index 00000000..ed3d8eac --- /dev/null +++ b/protobuf-plugin/src/main/kotlin/kotlinx/rpc/protobuf/model/FieldDeclaration.kt @@ -0,0 +1,49 @@ +/* + * Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.rpc.protobuf.model + +data class FieldDeclaration( + val name: String, + val type: FieldType, + val nullable: Boolean, + val deprecated: Boolean, + val doc: String?, +) + +sealed interface FieldType { + val defaultValue: String + + data class List(val valueName: FqName) : FieldType { + override val defaultValue: String = "emptyList()" + } + + data class Map(val keyName: FqName, val valueName: FqName) : FieldType { + override val defaultValue: String = "emptyMap()" + } + + data class Reference(val value: FqName) : FieldType { + override val defaultValue: String = "null" + } + + enum class IntegralType(simpleName: String, override val defaultValue: String) : FieldType { + STRING("String", "\"\""), + BYTES("ByteArray", "byteArrayOf()"), + BOOL("Boolean", "false"), + FLOAT("Float", "0.0f"), + DOUBLE("Double", "0.0"), + INT32("Int", "0"), + INT64("Long", "0"), + UINT32("UInt", "0u"), + UINT64("ULong", "0u"), + FIXED32("UInt", "0u"), + FIXED64("ULong", "0u"), + SINT32("Int", "0"), + SINT64("Long", "0"), + SFIXED32("Int", "0"), + SFIXED64("Long", "0"); + + val fqName: FqName = SimpleFqName("kotlin", simpleName) + } +} diff --git a/protobuf-plugin/src/main/kotlin/kotlinx/rpc/protobuf/model/FileDeclaration.kt b/protobuf-plugin/src/main/kotlin/kotlinx/rpc/protobuf/model/FileDeclaration.kt new file mode 100644 index 00000000..239a3166 --- /dev/null +++ b/protobuf-plugin/src/main/kotlin/kotlinx/rpc/protobuf/model/FileDeclaration.kt @@ -0,0 +1,15 @@ +/* + * Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.rpc.protobuf.model + +data class FileDeclaration( + val name: FqName, + val dependencies: List, + val messageDeclarations: List, + val enumDeclarations: List, + val serviceDeclarations: List, + val deprecated: Boolean, + val doc: String?, +) diff --git a/protobuf-plugin/src/main/kotlin/kotlinx/rpc/protobuf/model/FqName.kt b/protobuf-plugin/src/main/kotlin/kotlinx/rpc/protobuf/model/FqName.kt new file mode 100644 index 00000000..030459ed --- /dev/null +++ b/protobuf-plugin/src/main/kotlin/kotlinx/rpc/protobuf/model/FqName.kt @@ -0,0 +1,31 @@ +/* + * Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.rpc.protobuf.model + +interface FqName { + val packageName: String + val simpleName: String + val parentName: FqName? + + val parentNameAsPrefix: String get() = parentName?.let { "$it.".removePrefix(".") } ?: "" +} + +data class SimpleFqName( + override val packageName: String, + override val simpleName: String, + override val parentName: FqName? = null, +): FqName { + override fun equals(other: Any?): Boolean { + return other is FqName && simpleName == other.simpleName + } + + override fun hashCode(): Int { + return simpleName.hashCode() + } + + override fun toString(): String { + return simpleName + } +} diff --git a/protobuf-plugin/src/main/kotlin/kotlinx/rpc/protobuf/model/MessageDeclaration.kt b/protobuf-plugin/src/main/kotlin/kotlinx/rpc/protobuf/model/MessageDeclaration.kt new file mode 100644 index 00000000..675c982a --- /dev/null +++ b/protobuf-plugin/src/main/kotlin/kotlinx/rpc/protobuf/model/MessageDeclaration.kt @@ -0,0 +1,16 @@ +/* + * Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.rpc.protobuf.model + +data class MessageDeclaration( + val outerClassName: FqName, + val name: FqName, + val actualFields: List, // excludes oneOf fields, but includes oneOf itself + val oneOfDeclarations: List, + val enumDeclarations: List, + val nestedDeclarations: List, + val deprecated: Boolean, + val doc: String?, +) diff --git a/protobuf-plugin/src/main/kotlin/kotlinx/rpc/protobuf/model/MethodDeclaration.kt b/protobuf-plugin/src/main/kotlin/kotlinx/rpc/protobuf/model/MethodDeclaration.kt new file mode 100644 index 00000000..bf1ca7df --- /dev/null +++ b/protobuf-plugin/src/main/kotlin/kotlinx/rpc/protobuf/model/MethodDeclaration.kt @@ -0,0 +1,13 @@ +/* + * Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.rpc.protobuf.model + +data class MethodDeclaration( + val name: FqName, + val clientStreaming: Boolean, + val serverStreaming: Boolean, + val inputType: Lazy, + val outputType: Lazy, +) diff --git a/protobuf-plugin/src/main/kotlin/kotlinx/rpc/protobuf/model/Model.kt b/protobuf-plugin/src/main/kotlin/kotlinx/rpc/protobuf/model/Model.kt new file mode 100644 index 00000000..5cb43129 --- /dev/null +++ b/protobuf-plugin/src/main/kotlin/kotlinx/rpc/protobuf/model/Model.kt @@ -0,0 +1,9 @@ +/* + * Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.rpc.protobuf.model + +data class Model( + val files: List, +) diff --git a/protobuf-plugin/src/main/kotlin/kotlinx/rpc/protobuf/model/OneOfDeclaration.kt b/protobuf-plugin/src/main/kotlin/kotlinx/rpc/protobuf/model/OneOfDeclaration.kt new file mode 100644 index 00000000..aab8d237 --- /dev/null +++ b/protobuf-plugin/src/main/kotlin/kotlinx/rpc/protobuf/model/OneOfDeclaration.kt @@ -0,0 +1,10 @@ +/* + * Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.rpc.protobuf.model + +data class OneOfDeclaration( + val name: FqName, + val variants: List, +) diff --git a/protobuf-plugin/src/main/kotlin/kotlinx/rpc/protobuf/model/ServiceDeclaration.kt b/protobuf-plugin/src/main/kotlin/kotlinx/rpc/protobuf/model/ServiceDeclaration.kt new file mode 100644 index 00000000..54e635d6 --- /dev/null +++ b/protobuf-plugin/src/main/kotlin/kotlinx/rpc/protobuf/model/ServiceDeclaration.kt @@ -0,0 +1,10 @@ +/* + * Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.rpc.protobuf.model + +data class ServiceDeclaration( + val name: FqName, + val methods: List, +) diff --git a/protobuf-plugin/src/test/kotlin/kotlinx/rpc/protobuf/test/TestPrimitiveService.kt b/protobuf-plugin/src/test/kotlin/kotlinx/rpc/protobuf/test/TestPrimitiveService.kt new file mode 100644 index 00000000..eabf2123 --- /dev/null +++ b/protobuf-plugin/src/test/kotlin/kotlinx/rpc/protobuf/test/TestPrimitiveService.kt @@ -0,0 +1,41 @@ +/* + * Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.rpc.protobuf.test + +import kotlinx.coroutines.runBlocking +import kotlinx.rpc.grpc.GrpcClient +import kotlinx.rpc.grpc.GrpcServer +import kotlinx.rpc.registerService +import kotlinx.rpc.withService +import kotlin.test.Test +import kotlin.test.assertEquals + +class PrimitiveServiceImpl : PrimitiveService { + override suspend fun Echo(message: AllPrimitives): AllPrimitives { + return message + } +} + +class TestPrimitiveService { + @Test + fun testPrimitive(): Unit = runBlocking { + val grpcClient = GrpcClient("localhost", 8080) { + usePlaintext() + } + + val grpcServer = GrpcServer(8080) { + registerService { PrimitiveServiceImpl() } + } + + grpcServer.start() + + val service = grpcClient.withService() + val result = service.Echo(AllPrimitives { + int32 = 42 + }) + + assertEquals(42, result.int32) + } +} diff --git a/protobuf-plugin/src/test/proto/all_primitives.proto b/protobuf-plugin/src/test/proto/all_primitives.proto new file mode 100644 index 00000000..10cadf9e --- /dev/null +++ b/protobuf-plugin/src/test/proto/all_primitives.proto @@ -0,0 +1,21 @@ +syntax = "proto3"; + +package kotlinx.rpc.protobuf.test; + +message AllPrimitives { + double double = 1; + float float = 2; + int32 int32 = 3; + int64 int64 = 4; + uint32 uint32 = 5; + uint64 uint64 = 6; + sint32 sint32 = 7; + sint64 sint64 = 8; + fixed32 fixed32 = 9; + fixed64 fixed64 = 10; + sfixed32 sfixed32 = 11; + sfixed64 sfixed64 = 12; + bool bool = 13; + string string = 14; + bytes bytes = 15; +} diff --git a/protobuf-plugin/src/test/proto/empty_deprecated.proto b/protobuf-plugin/src/test/proto/empty_deprecated.proto new file mode 100644 index 00000000..ed69217b --- /dev/null +++ b/protobuf-plugin/src/test/proto/empty_deprecated.proto @@ -0,0 +1,7 @@ +syntax = "proto3"; + +package kotlinx.rpc.protobuf.test; + +message EmptyDeprecated { + option deprecated = true; +} diff --git a/protobuf-plugin/src/test/proto/enum.proto b/protobuf-plugin/src/test/proto/enum.proto new file mode 100644 index 00000000..2a2e2299 --- /dev/null +++ b/protobuf-plugin/src/test/proto/enum.proto @@ -0,0 +1,22 @@ +syntax = "proto3"; + +package kotlinx.rpc.protobuf.test; + +import "google/protobuf/descriptor.proto"; +import "options.proto"; + +extend google.protobuf.EnumValueOptions { + optional Options options = 50000; +} + +enum Enum { + option allow_alias = true; + ZERO = 0; + ONE = 1; + ONE_SECOND = 1; + TWO = 2 [deprecated = true]; + THREE = 3 [ + (options).string = "three", + (options).inner.string = "inner three" + ]; +} diff --git a/protobuf-plugin/src/test/proto/example.proto b/protobuf-plugin/src/test/proto/example.proto new file mode 100644 index 00000000..284f0cc2 --- /dev/null +++ b/protobuf-plugin/src/test/proto/example.proto @@ -0,0 +1,25 @@ +syntax = "proto3"; + +message Address { + string street = 1; + City city = 2; + + enum City { + ROME = 0; + BERLIN = 1; + LONDON = 2; + } +} + +message User { + int64 id = 1; + string name = 2; + bool married = 3; + repeated User friends = 4; + optional User spouse = 5; + Address address = 6; + oneof contact { + string email = 7; + string phone = 8; + } +} diff --git a/protobuf-plugin/src/test/proto/funny_types.proto b/protobuf-plugin/src/test/proto/funny_types.proto new file mode 100644 index 00000000..9c1ecef9 --- /dev/null +++ b/protobuf-plugin/src/test/proto/funny_types.proto @@ -0,0 +1,17 @@ +syntax = "proto3"; + +package kotlinx.rpc.protobuf.test; + +import "google/protobuf/any.proto"; +import "enum.proto"; +import "all_primitives.proto"; + +message FunnyTypes { + Enum enum = 1; + optional string optString = 2; + repeated string repString = 3; + AllPrimitives reference = 4; + optional AllPrimitives optReference = 5; + google.protobuf.Any any = 6; + string deprecated = 7 [deprecated = true]; +} diff --git a/protobuf-plugin/src/test/proto/image-recognizer.proto b/protobuf-plugin/src/test/proto/image-recognizer.proto new file mode 100644 index 00000000..38dde9d6 --- /dev/null +++ b/protobuf-plugin/src/test/proto/image-recognizer.proto @@ -0,0 +1,13 @@ +syntax = "proto3"; + +message Image { + bytes data = 1; +} + +message RecogniseResult { + int32 category = 1; +} + +service ImageRecognizer { + rpc recognize(Image) returns (RecogniseResult); +} diff --git a/protobuf-plugin/src/test/proto/map.proto b/protobuf-plugin/src/test/proto/map.proto new file mode 100644 index 00000000..e90f4c19 --- /dev/null +++ b/protobuf-plugin/src/test/proto/map.proto @@ -0,0 +1,10 @@ +syntax = "proto3"; + +package kotlinx.rpc.protobuf.test; + +import "one_of.proto"; + +message Map { + map primitives = 1; + map references = 2; +} diff --git a/protobuf-plugin/src/test/proto/multiple_files.proto b/protobuf-plugin/src/test/proto/multiple_files.proto new file mode 100644 index 00000000..49195a10 --- /dev/null +++ b/protobuf-plugin/src/test/proto/multiple_files.proto @@ -0,0 +1,26 @@ +syntax = "proto3"; + +package kotlinx.rpc.protobuf; + +option java_multiple_files = true; +option java_outer_classname = "MultipleFilesNewClassName"; +option java_package = "kotlinx.rpc.protobuf"; + +message Hello { + string hello = 1; +} + +message World { + string world = 1; + + message Nested { + string nested = 2; + } + + Nested nested = 3; +} + +enum Enum { + ZERO = 0; +} + diff --git a/protobuf-plugin/src/test/proto/nested.proto b/protobuf-plugin/src/test/proto/nested.proto new file mode 100644 index 00000000..8233de00 --- /dev/null +++ b/protobuf-plugin/src/test/proto/nested.proto @@ -0,0 +1,45 @@ +syntax = "proto3"; + +package kotlinx.rpc.protobuf.test; + +message Nested { + message Inner1 { + message Inner11 { + Nested.Inner2.Inner21 reference21 = 1; + Nested.Inner1.Inner12 reference12 = 2; + Nested.Inner2.NestedEnum enum = 3; + } + + message Inner12 { + Inner12 recursion = 1; + } + + Inner11 inner11 = 1; + Inner12 inner22 = 2; + string string = 3; + } + + message Inner2 { + message Inner21 { + Nested.Inner1.Inner11 reference11 = 1; + Nested.Inner2.Inner22 reference22 = 2; + } + + message Inner22 { + NestedEnum enum = 1; + } + + enum NestedEnum { + ZERO = 0; + } + + Inner21 inner21 = 1; + Inner22 inner22 = 2; + string string = 3; + } + + Inner1 inner1 = 1; + Inner2 inner2 = 2; + string string = 3; + Inner2.NestedEnum enum = 4; +} diff --git a/protobuf-plugin/src/test/proto/one_of.proto b/protobuf-plugin/src/test/proto/one_of.proto new file mode 100644 index 00000000..971bb89c --- /dev/null +++ b/protobuf-plugin/src/test/proto/one_of.proto @@ -0,0 +1,29 @@ +syntax = "proto3"; + +package kotlinx.rpc.protobuf.test; + +import "with_comments.proto"; +import "funny_types.proto"; +import "all_primitives.proto"; + +message OneOf { + oneof primitives { + string string = 1; + int32 int32 = 2; + bool bool = 3; + } + + oneof references { + WithComments withComments = 4; + FunnyTypes funnyTypes = 5; + } + + oneof mixed { + int64 int64 = 6; + AllPrimitives allPrimitives = 7; + } + + oneof single { + bytes bytes = 8; + } +} diff --git a/protobuf-plugin/src/test/proto/options.proto b/protobuf-plugin/src/test/proto/options.proto new file mode 100644 index 00000000..9cf2d235 --- /dev/null +++ b/protobuf-plugin/src/test/proto/options.proto @@ -0,0 +1,12 @@ +syntax = "proto3"; + +package kotlinx.rpc.protobuf.test; + +message Options { + message Inner { + string string = 1; + } + + string string = 1; + Inner inner = 2; +} diff --git a/protobuf-plugin/src/test/proto/primitive_service.proto b/protobuf-plugin/src/test/proto/primitive_service.proto new file mode 100644 index 00000000..844306c3 --- /dev/null +++ b/protobuf-plugin/src/test/proto/primitive_service.proto @@ -0,0 +1,9 @@ +syntax = "proto3"; + +package kotlinx.rpc.protobuf.test; + +import "all_primitives.proto"; + +service PrimitiveService { + rpc Echo(AllPrimitives) returns (AllPrimitives); +} diff --git a/protobuf-plugin/src/test/proto/with_comments.proto b/protobuf-plugin/src/test/proto/with_comments.proto new file mode 100644 index 00000000..e1a7e132 --- /dev/null +++ b/protobuf-plugin/src/test/proto/with_comments.proto @@ -0,0 +1,9 @@ +syntax = "proto3"; + +package kotlinx.rpc.protobuf.test; + +// This message has comment +message WithComments { + // and this field too. + string string = 1; +} diff --git a/publishLocal.sh b/publishLocal.sh index 3355b090..006497b9 100755 --- a/publishLocal.sh +++ b/publishLocal.sh @@ -1,11 +1,11 @@ #!/bin/bash # -# Copyright 2023-2024 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. +# Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. # set -euxo pipefail ./gradlew publishAllPublicationsToBuildRepoRepository -./gradlew -p compiler-plugin publishAllPublicationsToBuildRepoRepository -./gradlew -p gradle-plugin publishAllPublicationsToBuildRepoRepository +./gradlew -p compiler-plugin publishAllPublicationsToBuildRepoRepository --no-configuration-cache +./gradlew -p gradle-plugin publishAllPublicationsToBuildRepoRepository --no-configuration-cache diff --git a/samples/grpc-app/.gitignore b/samples/grpc-app/.gitignore new file mode 100644 index 00000000..c426c32f --- /dev/null +++ b/samples/grpc-app/.gitignore @@ -0,0 +1,36 @@ +.gradle +build/ +!gradle/wrapper/gradle-wrapper.jar +!**/src/main/**/build/ +!**/src/test/**/build/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache +bin/ +!**/src/main/**/bin/ +!**/src/test/**/bin/ + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr +out/ +!**/src/main/**/out/ +!**/src/test/**/out/ + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ + +### VS Code ### +.vscode/ \ No newline at end of file diff --git a/samples/grpc-app/build.gradle.kts b/samples/grpc-app/build.gradle.kts new file mode 100644 index 00000000..fb738a19 --- /dev/null +++ b/samples/grpc-app/build.gradle.kts @@ -0,0 +1,63 @@ +/* + * Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + */ + +plugins { + kotlin("jvm") version "2.1.0" + kotlin("plugin.serialization") version "2.1.0" + id("org.jetbrains.kotlinx.rpc.plugin") version "0.5.0-eap-grpc-1" + id("com.google.protobuf") version "0.9.4" +} + +group = "kotlinx.rpc.sample" +version = "0.0.1" + +repositories { + mavenCentral() + maven("https://maven.pkg.jetbrains.space/public/p/krpc/grpc") +} + +kotlin { + jvmToolchain(11) +} + +dependencies { + implementation("org.jetbrains.kotlinx:kotlinx-rpc-grpc-core:0.5.0-eap-grpc-1") + implementation("ch.qos.logback:logback-classic:1.5.16") + implementation("io.grpc:grpc-netty:1.69.0") +} + +val buildDirPath: String = project.layout.buildDirectory.get().asFile.absolutePath + +protobuf { + protoc { + artifact = "com.google.protobuf:protoc:4.29.3" + } + + plugins { + create("kotlinx-rpc") { + artifact = "org.jetbrains.kotlinx:kotlinx-rpc-protobuf-plugin:0.5.0-eap-grpc-1:all@jar" + } + + create("grpc") { + artifact = "io.grpc:protoc-gen-grpc-java:1.69.0" + } + + create("grpckt") { + artifact = "io.grpc:protoc-gen-grpc-kotlin:1.4.1:jdk8@jar" + } + } + + generateProtoTasks { + all().all { + plugins { + create("kotlinx-rpc") { + option("debugOutput=$buildDirPath/protobuf-plugin.log") + option("messageMode=interface") + } + create("grpc") + create("grpckt") + } + } + } +} diff --git a/samples/grpc-app/gradle.properties b/samples/grpc-app/gradle.properties new file mode 100644 index 00000000..eac9698f --- /dev/null +++ b/samples/grpc-app/gradle.properties @@ -0,0 +1,5 @@ +# +# Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. +# + +kotlin.code.style=official diff --git a/samples/grpc-app/gradle/wrapper/gradle-wrapper.jar b/samples/grpc-app/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 00000000..a4b76b95 Binary files /dev/null and b/samples/grpc-app/gradle/wrapper/gradle-wrapper.jar differ diff --git a/samples/grpc-app/gradle/wrapper/gradle-wrapper.properties b/samples/grpc-app/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 00000000..b2f467f6 --- /dev/null +++ b/samples/grpc-app/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,11 @@ +# +# Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. +# + +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/samples/grpc-app/gradlew b/samples/grpc-app/gradlew new file mode 100755 index 00000000..89f1e740 --- /dev/null +++ b/samples/grpc-app/gradlew @@ -0,0 +1,237 @@ +#!/bin/sh + +# +# Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/samples/grpc-app/gradlew.bat b/samples/grpc-app/gradlew.bat new file mode 100644 index 00000000..9d21a218 --- /dev/null +++ b/samples/grpc-app/gradlew.bat @@ -0,0 +1,94 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/samples/grpc-app/settings.gradle.kts b/samples/grpc-app/settings.gradle.kts new file mode 100644 index 00000000..4feb7105 --- /dev/null +++ b/samples/grpc-app/settings.gradle.kts @@ -0,0 +1,13 @@ +/* + * Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + */ + +rootProject.name = "grpc-app" + +pluginManagement { + repositories { + gradlePluginPortal() + mavenCentral() + maven("https://maven.pkg.jetbrains.space/public/p/krpc/grpc") + } +} diff --git a/samples/grpc-app/src/main/kotlin/Client.kt b/samples/grpc-app/src/main/kotlin/Client.kt new file mode 100644 index 00000000..4a81eae3 --- /dev/null +++ b/samples/grpc-app/src/main/kotlin/Client.kt @@ -0,0 +1,24 @@ +/* + * Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + */ + +import kotlinx.coroutines.cancel +import kotlinx.coroutines.runBlocking +import kotlinx.rpc.grpc.GrpcClient +import kotlinx.rpc.withService + +fun main(): Unit = runBlocking { + val grpcClient = GrpcClient("localhost", 8080) { + usePlaintext() + } + + val recognizer = grpcClient.withService() + + val image = Image { + data = byteArrayOf(0, 1, 2, 3) + } + val result = recognizer.recognize(image) + println("Recognized category: ${result.category}") + + grpcClient.cancel() +} diff --git a/samples/grpc-app/src/main/kotlin/ImageRecognizer.kt b/samples/grpc-app/src/main/kotlin/ImageRecognizer.kt new file mode 100644 index 00000000..2608539c --- /dev/null +++ b/samples/grpc-app/src/main/kotlin/ImageRecognizer.kt @@ -0,0 +1,16 @@ +/* + * Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + */ + +import kotlinx.coroutines.delay + +class ImageRecognizerImpl : ImageRecognizer { + override suspend fun recognize(image: Image): RecogniseResult { + val byte = image.data[0].toInt() + delay(100) // heavy processing + val result = RecogniseResult { + category = if (byte == 0) 0 else 1 + } + return result + } +} diff --git a/samples/grpc-app/src/main/kotlin/Server.kt b/samples/grpc-app/src/main/kotlin/Server.kt new file mode 100644 index 00000000..681586c0 --- /dev/null +++ b/samples/grpc-app/src/main/kotlin/Server.kt @@ -0,0 +1,16 @@ +/* + * Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + */ + +import kotlinx.coroutines.runBlocking +import kotlinx.rpc.grpc.GrpcServer +import kotlinx.rpc.registerService + +fun main(): Unit = runBlocking { + val grpcServer = GrpcServer(8080) { + registerService { ImageRecognizerImpl() } + } + + grpcServer.start() + grpcServer.awaitTermination() +} diff --git a/samples/grpc-app/src/main/proto/image-recognizer.proto b/samples/grpc-app/src/main/proto/image-recognizer.proto new file mode 100644 index 00000000..38dde9d6 --- /dev/null +++ b/samples/grpc-app/src/main/proto/image-recognizer.proto @@ -0,0 +1,13 @@ +syntax = "proto3"; + +message Image { + bytes data = 1; +} + +message RecogniseResult { + int32 category = 1; +} + +service ImageRecognizer { + rpc recognize(Image) returns (RecogniseResult); +} diff --git a/samples/grpc-app/src/main/resources/logback.xml b/samples/grpc-app/src/main/resources/logback.xml new file mode 100644 index 00000000..efd0ec94 --- /dev/null +++ b/samples/grpc-app/src/main/resources/logback.xml @@ -0,0 +1,16 @@ + + + + + + %d{YYYY-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + + + + diff --git a/settings.gradle.kts b/settings.gradle.kts index 63f37299..8fa636bb 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1,5 +1,5 @@ /* - * Copyright 2023-2024 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + * Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. */ rootProject.name = "kotlinx-rpc" @@ -34,6 +34,11 @@ dependencyResolutionManagement { includeBuild("compiler-plugin") } +includePublic(":protobuf-plugin") + +include(":grpc") +includePublic(":grpc:grpc-core") + includePublic(":bom") includePublic(":utils") diff --git a/utils/src/commonMain/kotlin/kotlinx/rpc/internal/utils/InternalRpcApi.kt b/utils/src/commonMain/kotlin/kotlinx/rpc/internal/utils/InternalRpcApi.kt index c030f237..3a1985d7 100644 --- a/utils/src/commonMain/kotlin/kotlinx/rpc/internal/utils/InternalRpcApi.kt +++ b/utils/src/commonMain/kotlin/kotlinx/rpc/internal/utils/InternalRpcApi.kt @@ -1,5 +1,5 @@ /* - * Copyright 2023-2024 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + * Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. */ package kotlinx.rpc.internal.utils @@ -9,4 +9,5 @@ package kotlinx.rpc.internal.utils level = RequiresOptIn.Level.ERROR, ) @InternalRpcApi +@Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION, AnnotationTarget.TYPEALIAS, AnnotationTarget.PROPERTY) public annotation class InternalRpcApi diff --git a/versions-root/libs.versions.toml b/versions-root/libs.versions.toml index 30ffae6e..185946c0 100644 --- a/versions-root/libs.versions.toml +++ b/versions-root/libs.versions.toml @@ -21,6 +21,10 @@ intellij = "233.13135.128" gradle-doctor = "0.10.0" kotlinx-browser = "0.3" shadow-jar = "9.0.0-beta4" +grpc = "1.69.0" +grpc-kotlin = "1.4.1" +protobuf = "4.29.3" +protobuf-gradle = "0.9.4" # Stub versions – relpaced based on kotlin, mostly for gradle-related (plugins) dependencies # but also for dependencies for compiler-specific modules. @@ -87,6 +91,19 @@ junit5-platform-launcher = { module = "org.junit.platform:junit-platform-launche junit5-platform-runner = { module = "org.junit.platform:junit-platform-runner" } junit5-platform-suite-api = { module = "org.junit.platform:junit-platform-suite-api" } +# grpc and protobuf +protoc = { module = "com.google.protobuf:protoc", version.ref = "protobuf" } +protobuf-java = { module = "com.google.protobuf:protobuf-java", version.ref = "protobuf" } +protobuf-java-util = { module = "com.google.protobuf:protobuf-java-util", version.ref = "protobuf" } +protobuf-kotlin = { module = "com.google.protobuf:protobuf-kotlin", version.ref = "protobuf" } +grpc-stub = { module = "io.grpc:grpc-stub", version.ref = "grpc" } +grpc-util = { module = "io.grpc:grpc-util", version.ref = "grpc" } +grpc-netty = { module = "io.grpc:grpc-netty", version.ref = "grpc" } +grpc-protobuf = { module = "io.grpc:grpc-protobuf", version.ref = "grpc" } +grpc-kotlin-stub = { module = "io.grpc:grpc-kotlin-stub", version.ref = "grpc-kotlin" } +grpc-protoc-gen-java = { module = "io.grpc:protoc-gen-grpc-java", version.ref = "grpc" } +grpc-protoc-gen-kotlin = { module = "io.grpc:protoc-gen-grpc-kotlin", version.ref = "grpc-kotlin" } + # other kotlin-logging = { module = "io.github.oshai:kotlin-logging", version.ref = "kotlin-logging" } kotlin-logging-legacy = { module = "io.github.microutils:kotlin-logging", version.ref = "kotlin-logging" } @@ -114,6 +131,7 @@ gradle-kotlin-dsl = { id = "org.gradle.kotlin.kotlin-dsl", version.ref = "gradle kover = { id = "org.jetbrains.kotlinx.kover", version.ref = "kover" } gradle-plugin-publish = { id = "com.gradle.plugin-publish", version.ref = "gradle-plugin-publish" } shadow-jar = { id = "com.gradleup.shadow", version.ref = "shadow-jar" } +protobuf = { id = "com.google.protobuf", version.ref = "protobuf-gradle" } # gradle-conventions project conventions-common = { id = "conventions-common", version.ref = "kotlinx-rpc" }