Skip to content

Commit ce5ddbc

Browse files
committed
KRPC-113 Support gRPC: Ktor Integration
1 parent 17ab11c commit ce5ddbc

File tree

16 files changed

+260
-10
lines changed

16 files changed

+260
-10
lines changed

grpc/grpc-core/src/commonMain/kotlin/kotlinx/rpc/grpc/GrpcClient.kt

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import kotlinx.rpc.RpcClient
1010
import kotlinx.rpc.grpc.descriptor.GrpcClientDelegate
1111
import kotlinx.rpc.grpc.descriptor.GrpcServiceDescriptor
1212
import kotlinx.rpc.internal.utils.map.RpcInternalConcurrentHashMap
13+
import kotlin.time.Duration
1314

1415
/**
1516
* GrpcClient manages gRPC communication by providing implementation for making asynchronous RPC calls.
@@ -19,6 +20,20 @@ import kotlinx.rpc.internal.utils.map.RpcInternalConcurrentHashMap
1920
public class GrpcClient internal constructor(private val channel: ManagedChannel) : RpcClient {
2021
private val stubs = RpcInternalConcurrentHashMap<Long, GrpcClientDelegate>()
2122

23+
public fun shutdown() {
24+
stubs.clear()
25+
channel.shutdown()
26+
}
27+
28+
public fun shutdownNow() {
29+
stubs.clear()
30+
channel.shutdownNow()
31+
}
32+
33+
public suspend fun awaitTermination(duration: Duration) {
34+
channel.awaitTermination(duration)
35+
}
36+
2237
override suspend fun <T> call(call: RpcCall): T {
2338
return call.delegate().call(call)
2439
}
@@ -39,11 +54,11 @@ public class GrpcClient internal constructor(private val channel: ManagedChannel
3954
* Constructor function for the [GrpcClient] class.
4055
*/
4156
public fun GrpcClient(
42-
name: String,
57+
hostname: String,
4358
port: Int,
4459
configure: ManagedChannelBuilder<*>.() -> Unit = {},
4560
): GrpcClient {
46-
val channel = ManagedChannelBuilder(name, port).apply(configure).buildChannel()
61+
val channel = ManagedChannelBuilder(hostname, port).apply(configure).buildChannel()
4762
return GrpcClient(channel)
4863
}
4964

grpc/grpc-core/src/commonMain/kotlin/kotlinx/rpc/grpc/GrpcServer.kt

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
package kotlinx.rpc.grpc
66

7+
import kotlinx.atomicfu.atomic
78
import kotlinx.rpc.RpcServer
89
import kotlinx.rpc.descriptor.serviceDescriptorOf
910
import kotlinx.rpc.grpc.annotations.Grpc
@@ -65,9 +66,13 @@ public class GrpcServer internal constructor(
6566
return grpc.delegate.definitionFor(service)
6667
}
6768

69+
private val buildLock = atomic(false)
70+
6871
internal fun build() {
69-
internalServer = Server(serverBuilder)
70-
isBuilt = true
72+
if (buildLock.compareAndSet(expect = false, update = true)) {
73+
internalServer = Server(serverBuilder)
74+
isBuilt = true
75+
}
7176
}
7277

7378
override val isShutdown: Boolean

grpc/grpc-core/src/commonMain/kotlin/kotlinx/rpc/grpc/ManagedChannel.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ public interface ManagedChannel {
6868
*/
6969
public expect abstract class ManagedChannelBuilder<T : ManagedChannelBuilder<T>>
7070

71-
internal expect fun ManagedChannelBuilder(name: String, port: Int): ManagedChannelBuilder<*>
71+
internal expect fun ManagedChannelBuilder(hostname: String, port: Int): ManagedChannelBuilder<*>
7272
internal expect fun ManagedChannelBuilder(target: String): ManagedChannelBuilder<*>
7373

7474
internal expect fun ManagedChannelBuilder<*>.buildChannel(): ManagedChannel

grpc/grpc-core/src/jsMain/kotlin/kotlinx/rpc/grpc/ManagedChannel.js.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ internal actual fun ManagedChannelBuilder<*>.buildChannel(): ManagedChannel {
2020
error("JS target is not supported in gRPC")
2121
}
2222

23-
internal actual fun ManagedChannelBuilder(name: String, port: Int): ManagedChannelBuilder<*> {
23+
internal actual fun ManagedChannelBuilder(hostname: String, port: Int): ManagedChannelBuilder<*> {
2424
error("JS target is not supported in gRPC")
2525
}
2626

grpc/grpc-core/src/jvmMain/kotlin/kotlinx/rpc/grpc/ManagedChannel.jvm.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,8 @@ internal actual fun ManagedChannelBuilder<*>.buildChannel(): ManagedChannel {
2424
return build().toKotlin()
2525
}
2626

27-
internal actual fun ManagedChannelBuilder(name: String, port: Int): ManagedChannelBuilder<*> {
28-
return io.grpc.ManagedChannelBuilder.forAddress(name, port)
27+
internal actual fun ManagedChannelBuilder(hostname: String, port: Int): ManagedChannelBuilder<*> {
28+
return io.grpc.ManagedChannelBuilder.forAddress(hostname, port)
2929
}
3030

3131
internal actual fun ManagedChannelBuilder(target: String): ManagedChannelBuilder<*> {

grpc/grpc-core/src/nativeMain/kotlin/kotlinx/rpc/grpc/ManagedChannel.native.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ internal actual fun ManagedChannelBuilder<*>.buildChannel(): ManagedChannel {
2020
error("Native target is not supported in gRPC")
2121
}
2222

23-
internal actual fun ManagedChannelBuilder(name: String, port: Int): ManagedChannelBuilder<*> {
23+
internal actual fun ManagedChannelBuilder(hostname: String, port: Int): ManagedChannelBuilder<*> {
2424
error("Native target is not supported in gRPC")
2525
}
2626

grpc/grpc-core/src/wasmJsMain/kotlin/kotlinx/rpc/grpc/ManagedChannel.wasmJs.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ internal actual fun ManagedChannelBuilder<*>.buildChannel(): ManagedChannel {
2020
error("WasmJS target is not supported in gRPC")
2121
}
2222

23-
internal actual fun ManagedChannelBuilder(name: String, port: Int): ManagedChannelBuilder<*> {
23+
internal actual fun ManagedChannelBuilder(hostname: String, port: Int): ManagedChannelBuilder<*> {
2424
error("WasmJS target is not supported in gRPC")
2525
}
2626

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
/*
2+
* Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license.
3+
*/
4+
5+
plugins {
6+
alias(libs.plugins.conventions.jvm)
7+
alias(libs.plugins.kotlinx.rpc)
8+
alias(libs.plugins.protobuf)
9+
}
10+
11+
dependencies {
12+
// for the jar dependency
13+
testImplementation(kotlin("test"))
14+
testImplementation(projects.grpc.grpcCore)
15+
testImplementation(projects.grpc.grpcKtorServer)
16+
17+
testImplementation(libs.grpc.kotlin.stub)
18+
testImplementation(libs.grpc.netty)
19+
20+
testImplementation(libs.ktor.server.core)
21+
testImplementation(libs.ktor.server.test.host)
22+
testRuntimeOnly(libs.logback.classic)
23+
}
24+
25+
rpc {
26+
grpc {
27+
enabled = true
28+
29+
val globalRootDir: String by extra
30+
31+
plugin {
32+
locator {
33+
path = "$globalRootDir/protobuf-plugin/build/libs/protobuf-plugin-$version-all.jar"
34+
}
35+
}
36+
37+
tasksMatching { it.isTest }.all {
38+
dependsOn(project(":protobuf-plugin").tasks.jar)
39+
}
40+
}
41+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
#
2+
# Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license.
3+
#
4+
5+
kotlinx.rpc.exclude.wasmWasi=true
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
/*
2+
* Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license.
3+
*/
4+
5+
package kotlinx.rpc.grpc.ktor.server.test
6+
7+
import io.ktor.server.testing.testApplication
8+
import kotlinx.rpc.grpc.GrpcClient
9+
import kotlin.test.Test
10+
import kotlinx.rpc.grpc.ktor.server.grpc
11+
import kotlinx.rpc.registerService
12+
import kotlinx.rpc.withService
13+
import kotlin.test.assertEquals
14+
import kotlin.time.Duration.Companion.minutes
15+
16+
class KtorTestServiceImpl : KtorTestService {
17+
override suspend fun sayHello(message: Hello): Hello {
18+
return message
19+
}
20+
}
21+
22+
const val PORT = 8085
23+
24+
class TestServer {
25+
@Test
26+
fun testPlainRequests() = testApplication {
27+
application {
28+
grpc(PORT) {
29+
registerService<KtorTestService> { KtorTestServiceImpl() }
30+
}
31+
}
32+
33+
startApplication()
34+
35+
val client = GrpcClient("localhost", PORT) {
36+
usePlaintext()
37+
}
38+
39+
val response = client.withService<KtorTestService>().sayHello(Hello { message = "Hello" })
40+
assertEquals("Hello", response.message, "Wrong response message")
41+
42+
client.shutdown()
43+
client.awaitTermination(1.minutes)
44+
}
45+
}

0 commit comments

Comments
 (0)