Skip to content

Commit 7f76590

Browse files
committed
grpc-native: Add basic C Kotlin wrapper classes
Signed-off-by: Johannes Zottele <[email protected]>
1 parent 16c76c3 commit 7f76590

File tree

9 files changed

+205
-106
lines changed

9 files changed

+205
-106
lines changed

gradle-conventions/src/main/kotlin/util/cInterop.kt

Lines changed: 0 additions & 80 deletions
This file was deleted.

gradle-conventions/src/main/kotlin/util/nativeTargets.kt

Lines changed: 0 additions & 11 deletions
This file was deleted.

grpc/grpc-core/build.gradle.kts

Lines changed: 27 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@
33
*/
44

55
import org.gradle.internal.extensions.stdlib.capitalized
6-
import util.createCInterop
6+
import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget
7+
import org.jetbrains.kotlin.gradle.tasks.CInteropProcess
78

89
plugins {
910
alias(libs.plugins.conventions.kmp)
@@ -30,6 +31,12 @@ kotlin {
3031
api(libs.protobuf.kotlin)
3132
}
3233
}
34+
35+
nativeTest {
36+
dependencies {
37+
implementation(kotlin("test"))
38+
}
39+
}
3340
}
3441

3542
val grpcppCLib = projectDir.resolve("../grpcpp-c")
@@ -56,20 +63,27 @@ kotlin {
5663
dependsOn(checkBazel)
5764
}
5865

59-
createCInterop("libgrpcpp_c", sourceSet = "src/nativeMain") { target ->
60-
includeDirs(grpcppCLib.resolve("include"))
61-
extraOpts(
62-
"-libraryPath", "${grpcppCLib.resolve("bazel-out/darwin_arm64-opt/bin")}"
63-
)
64-
includeDirs(
65-
grpcppCLib.resolve("include"),
66-
grpcppCLib.resolve("bazel-grpcpp-c/external/grpc+/include")
67-
)
6866

69-
tasks.named("cinteropLibgrpcpp_c${target.capitalized()}") {
70-
dependsOn(buildGrpcppCLib)
71-
}
67+
targets.filterIsInstance<KotlinNativeTarget>().forEach {
68+
it.compilations.getByName("main") {
69+
cinterops {
70+
val libgrpcpp_c by creating {
71+
includeDirs(
72+
grpcppCLib.resolve("include"),
73+
grpcppCLib.resolve("bazel-grpcpp-c/external/grpc+/include")
74+
)
75+
extraOpts(
76+
"-libraryPath", "${grpcppCLib.resolve("bazel-out/darwin_arm64-opt/bin")}",
77+
)
78+
}
7279

80+
val interopTask = "cinterop${libgrpcpp_c.name.capitalized()}${it.targetName.capitalized()}"
81+
tasks.named(interopTask, CInteropProcess::class) {
82+
dependsOn(buildGrpcppCLib)
83+
}
84+
}
85+
}
7386
}
7487

88+
7589
}

grpc/grpc-core/gradle.properties

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,18 @@
33
#
44

55
kotlinx.rpc.exclude.wasmWasi=true
6+
kotlinx.rpc.exclude.iosArm64=true
7+
kotlinx.rpc.exclude.iosX64=true
8+
kotlinx.rpc.exclude.iosSimulatorArm64=true
9+
kotlinx.rpc.exclude.linuxArm64=true
10+
kotlinx.rpc.exclude.linuxX64=true
11+
kotlinx.rpc.exclude.macosX64=true
12+
kotlinx.rpc.exclude.mingwX64=true
13+
kotlinx.rpc.exclude.tvosArm64=true
14+
kotlinx.rpc.exclude.tvosSimulatorArm64=true
15+
kotlinx.rpc.exclude.tvosX64=true
16+
kotlinx.rpc.exclude.watchosArm32=true
17+
kotlinx.rpc.exclude.watchosArm64=true
18+
kotlinx.rpc.exclude.watchosDeviceArm64=true
19+
kotlinx.rpc.exclude.watchosSimulatorArm64=true
20+
kotlinx.rpc.exclude.watchosX64=true

grpc/grpc-core/src/nativeMain/interop/libgrpcpp_c.def renamed to grpc/grpc-core/src/nativeInterop/cinterop/libgrpcpp_c.def

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,4 @@ headerFilter= grpcpp_c.h grpc/slice.h grpc/byte_buffer.h
33

44
noStringConversion = grpc_slice_from_copied_buffer my_grpc_slice_from_copied_buffer
55

6-
linkerOpts.osx = -lgrpcpp_c_static
7-
#staticLibraries = libgrpcpp_c_static.a
6+
staticLibraries = libgrpcpp_c_static.a
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
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.bridge
6+
7+
import kotlinx.cinterop.*
8+
import libgrpcpp_c.*
9+
10+
@OptIn(ExperimentalForeignApi::class)
11+
internal class GrpcByteBuffer internal constructor(
12+
internal val cByteBuffer: CPointer<grpc_byte_buffer>
13+
): AutoCloseable {
14+
15+
constructor(slice: GrpcSlice): this(memScoped {
16+
grpc_raw_byte_buffer_create(slice.cSlice, 1u) ?: error("Failed to create byte buffer")
17+
})
18+
19+
fun intoSlice(): GrpcSlice {
20+
memScoped {
21+
val resp_slice = alloc<grpc_slice>()
22+
grpc_byte_buffer_dump_to_single_slice(cByteBuffer, resp_slice.ptr)
23+
return GrpcSlice(resp_slice.readValue())
24+
}
25+
}
26+
27+
override fun close() {
28+
grpc_byte_buffer_destroy(cByteBuffer)
29+
}
30+
31+
}
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
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.bridge
6+
7+
import kotlinx.cinterop.*
8+
import kotlinx.coroutines.suspendCancellableCoroutine
9+
import kotlin.coroutines.resume
10+
import kotlin.coroutines.resumeWithException
11+
import libgrpcpp_c.*
12+
13+
14+
@OptIn(ExperimentalForeignApi::class)
15+
internal class GrpcClient(target: String): AutoCloseable {
16+
private var clientPtr: CPointer<grpc_client_t> = grpc_client_create_insecure(target) ?: error("Failed to create client")
17+
18+
fun callUnaryBlocking(method: String, req: GrpcSlice): GrpcSlice {
19+
memScoped {
20+
val result = alloc<grpc_slice>()
21+
grpc_client_call_unary_blocking(clientPtr, method, req.cSlice, result.ptr)
22+
return GrpcSlice(result.readValue())
23+
}
24+
}
25+
26+
suspend fun callUnary(method: String, req: GrpcByteBuffer): GrpcByteBuffer = suspendCancellableCoroutine { continuation ->
27+
val context = grpc_context_create()
28+
val method = grpc_method_create(method)
29+
30+
val req_raw_buf = nativeHeap.alloc<CPointerVar<grpc_byte_buffer>>()
31+
req_raw_buf.value = req.cByteBuffer
32+
33+
val resp_raw_buf: CPointerVar<grpc_byte_buffer> = nativeHeap.alloc()
34+
35+
val continueCb = { st: grpc_status_code_t ->
36+
// cleanup allocations owned by this method (this runs always)
37+
grpc_method_delete(method)
38+
grpc_context_delete(context)
39+
nativeHeap.free(req_raw_buf)
40+
41+
if (st != GRPC_C_STATUS_OK) {
42+
continuation.resumeWithException(RuntimeException("Call failed with code: $st"))
43+
} else {
44+
val result = resp_raw_buf.value
45+
if (result == null) {
46+
continuation.resumeWithException(RuntimeException("No response received"))
47+
} else {
48+
continuation.resume(GrpcByteBuffer(result))
49+
}
50+
}
51+
52+
nativeHeap.free(resp_raw_buf)
53+
}
54+
val cbCtxStable = StableRef.create(continueCb)
55+
56+
grpc_client_call_unary_callback(clientPtr, method, context, req_raw_buf.ptr, resp_raw_buf.ptr, cbCtxStable.asCPointer(), staticCFunction { st, ctx ->
57+
val cbCtxStable = ctx!!.asStableRef<(grpc_status_code_t) -> Unit>()
58+
cbCtxStable.get()(st)
59+
cbCtxStable.dispose()
60+
})
61+
}
62+
63+
override fun close() {
64+
grpc_client_delete(clientPtr)
65+
}
66+
67+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
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.bridge
6+
7+
import kotlinx.cinterop.CValue
8+
import kotlinx.cinterop.ExperimentalForeignApi
9+
import kotlinx.cinterop.addressOf
10+
import kotlinx.cinterop.usePinned
11+
import libgrpcpp_c.grpc_slice
12+
import libgrpcpp_c.grpc_slice_from_copied_buffer
13+
import libgrpcpp_c.grpc_slice_unref
14+
15+
16+
@OptIn(ExperimentalForeignApi::class)
17+
internal class GrpcSlice internal constructor(internal val cSlice: CValue<grpc_slice>) : AutoCloseable {
18+
19+
constructor(buffer: ByteArray) : this(
20+
buffer.usePinned { pinned ->
21+
grpc_slice_from_copied_buffer(pinned.addressOf(0), buffer.size.toULong())
22+
}
23+
)
24+
25+
override fun close() {
26+
grpc_slice_unref(cSlice)
27+
}
28+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
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
6+
7+
import kotlinx.coroutines.runBlocking
8+
import kotlinx.rpc.grpc.bridge.GrpcByteBuffer
9+
import kotlinx.rpc.grpc.bridge.GrpcClient
10+
import kotlinx.rpc.grpc.bridge.GrpcSlice
11+
import kotlin.test.Test
12+
import libgrpcpp_c.*
13+
14+
@OptIn(kotlinx.cinterop.ExperimentalForeignApi::class)
15+
class BridgeTest {
16+
17+
@Test
18+
fun `test basic unary async call`() {
19+
runBlocking {
20+
GrpcClient("localhost:50051").use { client ->
21+
GrpcSlice(byteArrayOf(8, 4)).use { request ->
22+
GrpcByteBuffer(request).use { req_buf ->
23+
client.callUnary("/Greeter/SayHello", req_buf)
24+
.use { result ->
25+
result.intoSlice().use { response ->
26+
val value = pb_decode_greeter_sayhello_response(response.cSlice)
27+
println("Response received: $value")
28+
}
29+
30+
}
31+
}
32+
}
33+
}
34+
}
35+
}
36+
}

0 commit comments

Comments
 (0)