Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 58 additions & 0 deletions grpc/grpc-core/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@
* Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license.
*/

import org.gradle.internal.extensions.stdlib.capitalized
import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget
import org.jetbrains.kotlin.gradle.tasks.CInteropProcess

plugins {
alias(libs.plugins.conventions.kmp)
alias(libs.plugins.kotlinx.rpc)
Expand All @@ -27,5 +31,59 @@ kotlin {
api(libs.protobuf.kotlin)
}
}

nativeTest {
dependencies {
implementation(kotlin("test"))
}
}
}

val grpcppCLib = projectDir.resolve("../grpcpp-c")

fun findProgram(name: String) = org.gradle.internal.os.OperatingSystem.current().findInPath(name)
val checkBazel by tasks.registering {
doLast {
val bazelPath = findProgram("bazel")
if (bazelPath != null) {
println("bazel: $bazelPath")
} else {
throw GradleException("'bazel' not found on PATH. Please install Bazel (https://bazel.build/).")
}
}
}

val buildGrpcppCLib = tasks.register<Exec>("buildGrpcppCLib") {
group = "build"
workingDir = grpcppCLib
commandLine("bash", "-c", "bazel build :grpcpp_c_static --config=release")
inputs.files(fileTree(grpcppCLib) { exclude("bazel-*/**") })
outputs.dir(grpcppCLib.resolve("bazel-bin"))

dependsOn(checkBazel)
}


targets.filterIsInstance<KotlinNativeTarget>().forEach {
it.compilations.getByName("main") {
cinterops {
val libgrpcpp_c by creating {
includeDirs(
grpcppCLib.resolve("include"),
grpcppCLib.resolve("bazel-grpcpp-c/external/grpc+/include")
)
extraOpts(
"-libraryPath", "${grpcppCLib.resolve("bazel-out/darwin_arm64-opt/bin")}",
)
}

val interopTask = "cinterop${libgrpcpp_c.name.capitalized()}${it.targetName.capitalized()}"
tasks.named(interopTask, CInteropProcess::class) {
dependsOn(buildGrpcppCLib)
}
}
}
}


}
15 changes: 15 additions & 0 deletions grpc/grpc-core/gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,18 @@
#

kotlinx.rpc.exclude.wasmWasi=true
kotlinx.rpc.exclude.iosArm64=true
kotlinx.rpc.exclude.iosX64=true
kotlinx.rpc.exclude.iosSimulatorArm64=true
kotlinx.rpc.exclude.linuxArm64=true
kotlinx.rpc.exclude.linuxX64=true
kotlinx.rpc.exclude.macosX64=true
kotlinx.rpc.exclude.mingwX64=true
kotlinx.rpc.exclude.tvosArm64=true
kotlinx.rpc.exclude.tvosSimulatorArm64=true
kotlinx.rpc.exclude.tvosX64=true
kotlinx.rpc.exclude.watchosArm32=true
kotlinx.rpc.exclude.watchosArm64=true
kotlinx.rpc.exclude.watchosDeviceArm64=true
kotlinx.rpc.exclude.watchosSimulatorArm64=true
kotlinx.rpc.exclude.watchosX64=true
6 changes: 6 additions & 0 deletions grpc/grpc-core/src/nativeInterop/cinterop/libgrpcpp_c.def
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
headers = grpcpp_c.h
headerFilter= grpcpp_c.h grpc/slice.h grpc/byte_buffer.h

noStringConversion = grpc_slice_from_copied_buffer my_grpc_slice_from_copied_buffer

staticLibraries = libgrpcpp_c_static.a
Original file line number Diff line number Diff line change
@@ -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.grpc.bridge

import kotlinx.cinterop.*
import libgrpcpp_c.*

@OptIn(ExperimentalForeignApi::class)
internal class GrpcByteBuffer internal constructor(
internal val cByteBuffer: CPointer<grpc_byte_buffer>
): AutoCloseable {

constructor(slice: GrpcSlice): this(memScoped {
grpc_raw_byte_buffer_create(slice.cSlice, 1u) ?: error("Failed to create byte buffer")
})

fun intoSlice(): GrpcSlice {
memScoped {
val resp_slice = alloc<grpc_slice>()
grpc_byte_buffer_dump_to_single_slice(cByteBuffer, resp_slice.ptr)
return GrpcSlice(resp_slice.readValue())
}
}

override fun close() {
grpc_byte_buffer_destroy(cByteBuffer)
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
/*
* 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.bridge

import kotlinx.cinterop.*
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException
import libgrpcpp_c.*


@OptIn(ExperimentalForeignApi::class)
internal class GrpcClient(target: String): AutoCloseable {
private var clientPtr: CPointer<grpc_client_t> = grpc_client_create_insecure(target) ?: error("Failed to create client")

fun callUnaryBlocking(method: String, req: GrpcSlice): GrpcSlice {
memScoped {
val result = alloc<grpc_slice>()
grpc_client_call_unary_blocking(clientPtr, method, req.cSlice, result.ptr)
return GrpcSlice(result.readValue())
}
}

suspend fun callUnary(method: String, req: GrpcByteBuffer): GrpcByteBuffer = suspendCancellableCoroutine { continuation ->
val context = grpc_context_create()
val method = grpc_method_create(method)

val req_raw_buf = nativeHeap.alloc<CPointerVar<grpc_byte_buffer>>()
req_raw_buf.value = req.cByteBuffer

val resp_raw_buf: CPointerVar<grpc_byte_buffer> = nativeHeap.alloc()

val continueCb = { st: grpc_status_code_t ->
// cleanup allocations owned by this method (this runs always)
grpc_method_delete(method)
grpc_context_delete(context)
nativeHeap.free(req_raw_buf)

if (st != GRPC_C_STATUS_OK) {
continuation.resumeWithException(RuntimeException("Call failed with code: $st"))
} else {
val result = resp_raw_buf.value
if (result == null) {
continuation.resumeWithException(RuntimeException("No response received"))
} else {
continuation.resume(GrpcByteBuffer(result))
}
}

nativeHeap.free(resp_raw_buf)
}
val cbCtxStable = StableRef.create(continueCb)

grpc_client_call_unary_callback(clientPtr, method, context, req_raw_buf.ptr, resp_raw_buf.ptr, cbCtxStable.asCPointer(), staticCFunction { st, ctx ->
val cbCtxStable = ctx!!.asStableRef<(grpc_status_code_t) -> Unit>()
cbCtxStable.get()(st)
cbCtxStable.dispose()
})
}

override fun close() {
grpc_client_delete(clientPtr)
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/*
* 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.bridge

import kotlinx.cinterop.CValue
import kotlinx.cinterop.ExperimentalForeignApi
import kotlinx.cinterop.addressOf
import kotlinx.cinterop.usePinned
import libgrpcpp_c.grpc_slice
import libgrpcpp_c.grpc_slice_from_copied_buffer
import libgrpcpp_c.grpc_slice_unref


@OptIn(ExperimentalForeignApi::class)
internal class GrpcSlice internal constructor(internal val cSlice: CValue<grpc_slice>) : AutoCloseable {

constructor(buffer: ByteArray) : this(
buffer.usePinned { pinned ->
grpc_slice_from_copied_buffer(pinned.addressOf(0), buffer.size.toULong())
}
)

override fun close() {
grpc_slice_unref(cSlice)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/*
* 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.runBlocking
import kotlinx.rpc.grpc.bridge.GrpcByteBuffer
import kotlinx.rpc.grpc.bridge.GrpcClient
import kotlinx.rpc.grpc.bridge.GrpcSlice
import kotlin.test.Test
import libgrpcpp_c.*

@OptIn(kotlinx.cinterop.ExperimentalForeignApi::class)
class BridgeTest {

@Test
fun `test basic unary async call`() {
runBlocking {
GrpcClient("localhost:50051").use { client ->
GrpcSlice(byteArrayOf(8, 4)).use { request ->
GrpcByteBuffer(request).use { req_buf ->
client.callUnary("/Greeter/SayHello", req_buf)
.use { result ->
result.intoSlice().use { response ->
val value = pb_decode_greeter_sayhello_response(response.cSlice)
println("Response received: $value")
}

}
}
}
}
}
}
}
4 changes: 4 additions & 0 deletions grpc/grpcpp-c/.bazelrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# we build the cc_static library bundled with all dependencies
build --experimental_cc_static_library

build:release --compilation_mode=opt --strip=always
14 changes: 14 additions & 0 deletions grpc/grpcpp-c/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# gitignore template for Bazel build system
# website: https://bazel.build/

# Ignore all bazel-* symlinks. There is no full list since this can change
# based on the name of the directory bazel is cloned into.
/bazel-*

# Directories for the Bazel IntelliJ plugin containing the generated
# IntelliJ project files and plugin configuration. Separate directories are
# for the IntelliJ, Android Studio and CLion versions of the plugin.
/.ijwb/
/.aswb/
/.clwb/
.idea/
21 changes: 21 additions & 0 deletions grpc/grpcpp-c/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
load("@rules_cc//cc:defs.bzl", "cc_library")

cc_library(
name = "grpcpp_c",
srcs = ["src/grpcpp_c.cpp"],
hdrs = glob(["include/**/*.h"]),
copts = ["-std=c++20"],
includes = ["include"],
visibility = ["//visibility:public"],
deps = [
"@com_github_grpc_grpc//:grpc++",
"@com_google_protobuf//:protobuf",
],
)

cc_static_library(
name = "grpcpp_c_static",
deps = [
"grpcpp_c",
],
)
24 changes: 24 additions & 0 deletions grpc/grpcpp-c/MODULE.bazel
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
module(
name = "grpcpp_c",
version = "0.1",
)

# rules_cc for cc_library support
bazel_dep(
name = "rules_cc",
version = "0.1.1",
)

# Protobuf
bazel_dep(
name = "protobuf",
version = "31.1",
repo_name = "com_google_protobuf",
)

# gRPC C++ library
bazel_dep(
name = "grpc",
version = "1.73.1",
repo_name = "com_github_grpc_grpc",
)
Loading
Loading