Skip to content

Commit c08cdeb

Browse files
Jozott00Mr3zee
authored andcommitted
grpc-native: Intial setup with minimal C++/C/Kotlin interface (#400)
* grpc-native: Add grpcpp-c C/C++ library to grpc/ Signed-off-by: Johannes Zottele <[email protected]> * grpc-native: Add gradle configuration for cInterop and grpcpp_c library Signed-off-by: Johannes Zottele <[email protected]> * grpc-native: Add basic C Kotlin wrapper classes Signed-off-by: Johannes Zottele <[email protected]> * grpc-native: Update platforms.topic Signed-off-by: Johannes Zottele <[email protected]> * grpc-native: Resolve review requests Signed-off-by: Johannes Zottele <[email protected]> --------- Signed-off-by: Johannes Zottele <[email protected]>
1 parent e4f29a4 commit c08cdeb

File tree

15 files changed

+1471
-3
lines changed

15 files changed

+1471
-3
lines changed

docs/pages/kotlinx-rpc/topics/platforms.topic

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@
9090
<td>jvm</td>
9191
<td><list><li>browser</li><li>node</li></list></td>
9292
<td><list><li>wasmJs<list><li>browser</li><li>d8</li><li>node</li></list></li></list></td>
93-
<td><list><li>apple<list><li>ios<list><li>iosArm64</li><li>iosSimulatorArm64</li><li>iosX64</li></list></li><li>macos<list><li>macosArm64</li><li>macosX64</li></list></li><li>watchos<list><li>watchosArm32</li><li>watchosArm64</li><li>watchosDeviceArm64</li><li>watchosSimulatorArm64</li><li>watchosX64</li></list></li><li>tvos<list><li>tvosArm64</li><li>tvosSimulatorArm64</li><li>tvosX64</li></list></li></list></li><li>linux<list><li>linuxArm64</li><li>linuxX64</li></list></li><li>windows<list><li>mingwX64</li></list></li></list></td>
93+
<td><list><li>apple<list><li>ios<list></list></li><li>macos<list><li>macosArm64</li></list></li><li>watchos<list></list></li><li>tvos<list></list></li></list></li><li>linux<list></list></li><li>windows<list></list></li></list></td>
9494
</tr>
9595

9696
<tr>

grpc/grpc-core/build.gradle.kts

Lines changed: 56 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@
55
import kotlinx.rpc.buf.tasks.BufGenerateTask
66
import kotlinx.rpc.proto.kotlinMultiplatform
77
import org.gradle.kotlin.dsl.withType
8+
import org.gradle.internal.extensions.stdlib.capitalized
9+
import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget
10+
import org.jetbrains.kotlin.gradle.tasks.CInteropProcess
811

912
plugins {
1013
alias(libs.plugins.conventions.kmp)
@@ -54,9 +57,61 @@ kotlin {
5457
implementation(libs.protobuf.kotlin)
5558
}
5659
}
60+
61+
nativeTest {
62+
dependencies {
63+
implementation(kotlin("test"))
64+
}
65+
}
66+
}
67+
68+
val grpcppCLib = projectDir.resolve("../grpcpp-c")
69+
70+
// TODO: Replace function implementation, so it does not use an internal API
71+
fun findProgram(name: String) = org.gradle.internal.os.OperatingSystem.current().findInPath(name)
72+
val checkBazel by tasks.registering {
73+
doLast {
74+
val bazelPath = findProgram("bazel")
75+
if (bazelPath != null) {
76+
logger.debug("bazel: {}", bazelPath)
77+
} else {
78+
throw GradleException("'bazel' not found on PATH. Please install Bazel (https://bazel.build/).")
79+
}
80+
}
5781
}
58-
}
5982

83+
val buildGrpcppCLib = tasks.register<Exec>("buildGrpcppCLib") {
84+
group = "build"
85+
workingDir = grpcppCLib
86+
commandLine("bash", "-c", "bazel build :grpcpp_c_static --config=release")
87+
inputs.files(fileTree(grpcppCLib) { exclude("bazel-*/**") })
88+
outputs.dir(grpcppCLib.resolve("bazel-bin"))
89+
90+
dependsOn(checkBazel)
91+
}
92+
93+
94+
targets.filterIsInstance<KotlinNativeTarget>().forEach {
95+
it.compilations.getByName("main") {
96+
cinterops {
97+
val libgrpcpp_c by creating {
98+
includeDirs(
99+
grpcppCLib.resolve("include"),
100+
grpcppCLib.resolve("bazel-grpcpp-c/external/grpc+/include")
101+
)
102+
extraOpts(
103+
"-libraryPath", "${grpcppCLib.resolve("bazel-out/darwin_arm64-opt/bin")}",
104+
)
105+
}
106+
107+
val interopTask = "cinterop${libgrpcpp_c.name.capitalized()}${it.targetName.capitalized()}"
108+
tasks.named(interopTask, CInteropProcess::class) {
109+
dependsOn(buildGrpcppCLib)
110+
}
111+
}
112+
}
113+
}
114+
}
60115

61116
protoSourceSets {
62117
jvmTest {

grpc/grpc-core/gradle.properties

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,19 @@
11
#
22
# Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license.
33
#
4-
54
kotlinx.rpc.exclude.wasmWasi=true
5+
kotlinx.rpc.exclude.iosArm64=true
6+
kotlinx.rpc.exclude.iosX64=true
7+
kotlinx.rpc.exclude.iosSimulatorArm64=true
8+
kotlinx.rpc.exclude.linuxArm64=true
9+
kotlinx.rpc.exclude.linuxX64=true
10+
kotlinx.rpc.exclude.macosX64=true
11+
kotlinx.rpc.exclude.mingwX64=true
12+
kotlinx.rpc.exclude.tvosArm64=true
13+
kotlinx.rpc.exclude.tvosSimulatorArm64=true
14+
kotlinx.rpc.exclude.tvosX64=true
15+
kotlinx.rpc.exclude.watchosArm32=true
16+
kotlinx.rpc.exclude.watchosArm64=true
17+
kotlinx.rpc.exclude.watchosDeviceArm64=true
18+
kotlinx.rpc.exclude.watchosSimulatorArm64=true
19+
kotlinx.rpc.exclude.watchosX64=true
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
headers = grpcpp_c.h
2+
headerFilter= grpcpp_c.h grpc/slice.h grpc/byte_buffer.h
3+
4+
noStringConversion = grpc_slice_from_copied_buffer my_grpc_slice_from_copied_buffer
5+
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 respSlice = alloc<grpc_slice>()
22+
grpc_byte_buffer_dump_to_single_slice(cByteBuffer, respSlice.ptr)
23+
return GrpcSlice(respSlice.readValue())
24+
}
25+
}
26+
27+
override fun close() {
28+
grpc_byte_buffer_destroy(cByteBuffer)
29+
}
30+
31+
}
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
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 libgrpcpp_c.*
10+
import kotlin.coroutines.resume
11+
import kotlin.coroutines.resumeWithException
12+
13+
@OptIn(ExperimentalForeignApi::class)
14+
internal class GrpcClient(target: String) : AutoCloseable {
15+
private var clientPtr: CPointer<grpc_client_t> =
16+
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 =
27+
suspendCancellableCoroutine { continuation ->
28+
val context = grpc_context_create()
29+
val method = grpc_method_create(method)
30+
31+
val reqRawBuf = nativeHeap.alloc<CPointerVar<grpc_byte_buffer>>()
32+
reqRawBuf.value = req.cByteBuffer
33+
34+
val respRawBuf: CPointerVar<grpc_byte_buffer> = nativeHeap.alloc()
35+
36+
val continueCb = { st: grpc_status_code_t ->
37+
// cleanup allocations owned by this method (this runs always)
38+
grpc_method_delete(method)
39+
grpc_context_delete(context)
40+
nativeHeap.free(reqRawBuf)
41+
42+
if (st != GRPC_C_STATUS_OK) {
43+
continuation.resumeWithException(RuntimeException("Call failed with code: $st"))
44+
} else {
45+
val result = respRawBuf.value
46+
if (result == null) {
47+
continuation.resumeWithException(RuntimeException("No response received"))
48+
} else {
49+
continuation.resume(GrpcByteBuffer(result))
50+
}
51+
}
52+
53+
nativeHeap.free(respRawBuf)
54+
}
55+
val cbCtxStable = StableRef.create(continueCb)
56+
57+
grpc_client_call_unary_callback(
58+
clientPtr, method, context, reqRawBuf.ptr, respRawBuf.ptr,
59+
cbCtxStable.asCPointer(), staticCFunction { st, ctx ->
60+
val cbCtxStable = ctx!!.asStableRef<(grpc_status_code_t) -> Unit>()
61+
cbCtxStable.get()(st)
62+
cbCtxStable.dispose()
63+
})
64+
}
65+
66+
override fun close() {
67+
grpc_client_delete(clientPtr)
68+
}
69+
70+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
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+
@OptIn(ExperimentalForeignApi::class)
16+
internal class GrpcSlice internal constructor(internal val cSlice: CValue<grpc_slice>) : AutoCloseable {
17+
18+
constructor(buffer: ByteArray) : this(
19+
buffer.usePinned { pinned ->
20+
grpc_slice_from_copied_buffer(pinned.addressOf(0), buffer.size.toULong())
21+
}
22+
)
23+
24+
override fun close() {
25+
grpc_slice_unref(cSlice)
26+
}
27+
}
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 libgrpcpp_c.pb_decode_greeter_sayhello_response
12+
import kotlin.test.Test
13+
14+
@OptIn(kotlinx.cinterop.ExperimentalForeignApi::class)
15+
class BridgeTest {
16+
17+
@Test
18+
fun testBasicUnaryAsyncCall() {
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+
}

grpc/grpcpp-c/.bazelrc

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
# we build the cc_static library bundled with all dependencies
2+
build --experimental_cc_static_library
3+
4+
build:release --compilation_mode=opt --strip=always

grpc/grpcpp-c/.gitignore

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
# gitignore template for Bazel build system
2+
# website: https://bazel.build/
3+
4+
# Ignore all bazel-* symlinks. There is no full list since this can change
5+
# based on the name of the directory bazel is cloned into.
6+
/bazel-*
7+
8+
# Directories for the Bazel IntelliJ plugin containing the generated
9+
# IntelliJ project files and plugin configuration. Separate directories are
10+
# for the IntelliJ, Android Studio and CLion versions of the plugin.
11+
/.ijwb/
12+
/.aswb/
13+
/.clwb/
14+
.idea/

0 commit comments

Comments
 (0)