Skip to content

Added jvm codecs implementations #414

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jul 30, 2025
Merged
Show file tree
Hide file tree
Changes from all 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
21 changes: 8 additions & 13 deletions grpc/grpc-core/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,13 @@ kotlin {

implementation(libs.atomicfu)
implementation(libs.kotlinx.io.core)
implementation(libs.kotlinx.collections.immutable)
}
}

commonTest {
dependencies {
implementation(libs.kotlin.test)
}
}

Expand All @@ -39,6 +46,7 @@ kotlin {
api(libs.grpc.util)
api(libs.grpc.stub)
api(libs.grpc.protobuf)
api("io.grpc:grpc-protobuf-lite:${libs.versions.grpc.asProvider().get()}")
implementation(libs.grpc.kotlin.stub) // causes problems to jpms if api
api(libs.protobuf.java.util)
implementation(libs.protobuf.kotlin)
Expand All @@ -50,7 +58,6 @@ kotlin {
implementation(projects.grpc.grpcCore)
implementation(libs.coroutines.core)
implementation(libs.coroutines.test)
implementation(libs.kotlin.test)

implementation(libs.grpc.stub)
implementation(libs.grpc.netty)
Expand All @@ -60,18 +67,6 @@ kotlin {
implementation(libs.protobuf.kotlin)
}
}

nativeMain {
dependencies {
implementation(libs.kotlinx.collections.immutable)
}
}

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

val grpcppCLib = projectDir.resolve("../grpcpp-c")
Expand Down
4 changes: 3 additions & 1 deletion grpc/grpc-core/gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
# Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license.
#
kotlinx.rpc.exclude.wasmWasi=true
kotlinx.rpc.exclude.js=true
kotlinx.rpc.exclude.wasmJs=true
kotlinx.rpc.exclude.iosArm64=true
kotlinx.rpc.exclude.iosX64=true
kotlinx.rpc.exclude.iosSimulatorArm64=true
Expand All @@ -16,4 +18,4 @@ 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
kotlinx.rpc.exclude.watchosX64=true
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ package kotlinx.rpc.grpc.internal

import kotlinx.io.Buffer

// TODO: Evaluate if this buffer size is suitable for all targets (KRPC-186)
// maximum buffer size to allocate as contiguous memory in bytes
internal const val MAX_PACKED_BULK_SIZE: Int = 1_000_000

/**
* A platform-specific decoder for wire format data.
*
Expand Down Expand Up @@ -78,4 +82,4 @@ internal interface WireDecoder : AutoCloseable {
*
* @param source The buffer containing the encoded wire-format data.
*/
internal expect fun WireDecoder(source: Buffer): WireDecoder
internal expect fun WireDecoder(source: Buffer): WireDecoder
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import kotlinx.io.Sink
@OptIn(ExperimentalUnsignedTypes::class)
internal interface WireEncoder {
fun flush()
fun writeBool(field: Int, value: Boolean): Boolean
fun writeBool(fieldNr: Int, value: Boolean): Boolean
fun writeInt32(fieldNr: Int, value: Int): Boolean
fun writeInt64(fieldNr: Int, value: Long): Boolean
fun writeUInt32(fieldNr: Int, value: UInt): Boolean
Expand Down Expand Up @@ -51,4 +51,4 @@ internal interface WireEncoder {
}


internal expect fun WireEncoder(sink: Sink): WireEncoder
internal expect fun WireEncoder(sink: Sink): WireEncoder
Original file line number Diff line number Diff line change
@@ -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.grpc.internal

import kotlinx.io.Buffer

internal expect fun WireDecoder.pushLimit(byteLen: Int): Int
internal expect fun WireDecoder.popLimit(limit: Int)
internal expect fun WireDecoder.bytesUntilLimit(): Int

internal inline fun <T : Any> WireDecoder.readPackedVarInternal(
crossinline size: () -> Long,
crossinline readFn: () -> T,
crossinline withError: () -> Unit,
crossinline hadError: () -> Boolean,
): List<T> {
val byteLen = readInt32()
if (hadError()) {
return emptyList()
}
if (byteLen < 0) {
return emptyList<T>().apply { withError() }
}
val size = size()
// no size check on jvm
if (size != -1L && size < byteLen) {
return emptyList<T>().apply { withError() }
}
if (byteLen == 0) {
return emptyList() // actually an empty list (no error)
}

val limit = pushLimit(byteLen)

val result = mutableListOf<T>()

while (bytesUntilLimit() > 0) {
val elem = readFn()
if (hadError()) {
break
}
result.add(elem)
}

popLimit(limit)
return result
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,15 @@

package kotlinx.rpc.grpc.internal

import kotlinx.cinterop.ExperimentalForeignApi
import kotlinx.io.Buffer
import kotlin.experimental.ExperimentalNativeApi
import kotlin.test.*

// TODO: Move this to the commonTest
@OptIn(ExperimentalForeignApi::class, ExperimentalNativeApi::class)
enum class TestPlatform {
Jvm, Native, Js, WasmJs, Wasi;
}

expect val testPlatform: TestPlatform

class WireCodecTest {

@Test
Expand Down Expand Up @@ -343,8 +345,7 @@ class WireCodecTest {
val buffer = Buffer()

val decoder = WireDecoder(buffer)
decoder.readTag()
assertTrue(decoder.hadError())
assertNull(decoder.readTag())
}

@Test
Expand Down Expand Up @@ -418,6 +419,11 @@ class WireCodecTest {

@Test
fun testReadAfterClose() {
// jvm has no close method
if (testPlatform == TestPlatform.Jvm) {
return
}

val fieldNr = 19
val buffer = Buffer()

Expand All @@ -432,7 +438,7 @@ class WireCodecTest {
try {
val tag = decoder.readTag()
assertNull(tag)
} catch (e: Exception) {
} catch (_: Exception) {
// Expected exception in some implementations
}
}
Expand Down Expand Up @@ -497,6 +503,11 @@ class WireCodecTest {

@Test
fun testBufferNotExhausted() {
// jvm reads buffer - buffered (ba dum tss)
if (testPlatform == TestPlatform.Jvm) {
return
}

val fieldNr = 1
val buffer = Buffer()

Expand Down
Original file line number Diff line number Diff line change
@@ -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.
*/

package kotlinx.rpc.grpc.internal

internal actual fun WireDecoder.pushLimit(byteLen: Int): Int {
TODO("Not yet implemented")
}

internal actual fun WireDecoder.popLimit(limit: Int) {
TODO("Not yet implemented")
}

internal actual fun WireDecoder.bytesUntilLimit(): Int {
TODO("Not yet implemented")
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,121 @@

package kotlinx.rpc.grpc.internal

import com.google.protobuf.CodedInputStream
import kotlinx.io.Buffer
import kotlinx.io.Source
import kotlinx.io.asInputStream

internal class WireDecoderJvm(source: Buffer) : WireDecoder {
// there is no way to omit coping here
internal val codedInputStream: CodedInputStream = CodedInputStream.newInstance(source.asInputStream())

// errors in jvm are exceptions
override fun hadError(): Boolean {
return false
}
Comment on lines +15 to +18
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should probably unify the behavior between all implementations. Probably, the K/N implementation should also throw exceptions instead of providing the error indicator? (in a separate PR)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, let's do that in the next PR


override fun readTag(): KTag? {
val tag = codedInputStream.readTag().toUInt()
if (tag == 0u) {
return null
}

return KTag.fromOrNull(tag)
}

override fun readBool(): Boolean {
return codedInputStream.readBool()
}

override fun readInt32(): Int {
return codedInputStream.readInt32()
}

override fun readInt64(): Long {
return codedInputStream.readInt64()
}

override fun readUInt32(): UInt {
// todo check java unsigned types
return codedInputStream.readUInt32().toUInt()
}

override fun readUInt64(): ULong {
// todo check java unsigned types
return codedInputStream.readUInt64().toULong()
}

override fun readSInt32(): Int {
return codedInputStream.readSInt32()
}

override fun readSInt64(): Long {
return codedInputStream.readSInt64()
}

override fun readFixed32(): UInt {
// todo check java unsigned types
return codedInputStream.readFixed32().toUInt()
}

override fun readFixed64(): ULong {
// todo check java unsigned types
return codedInputStream.readFixed64().toULong()
}

override fun readSFixed32(): Int {
return codedInputStream.readSFixed32()
}

override fun readSFixed64(): Long {
return codedInputStream.readSFixed64()
}

override fun readFloat(): Float {
return codedInputStream.readFloat()
}

override fun readDouble(): Double {
return codedInputStream.readDouble()
}

override fun readEnum(): Int {
return codedInputStream.readEnum()
}

override fun readString(): String {
return codedInputStream.readStringRequireUtf8()
}

override fun readBytes(): ByteArray {
return codedInputStream.readByteArray()
}

override fun readPackedBool() = readPackedInternal(this::readBool)
override fun readPackedInt32() = readPackedInternal(this::readInt32)
override fun readPackedInt64() = readPackedInternal(this::readInt64)
override fun readPackedUInt32() = readPackedInternal(this::readUInt32)
override fun readPackedUInt64() = readPackedInternal(this::readUInt64)
override fun readPackedSInt32() = readPackedInternal(this::readSInt32)
override fun readPackedSInt64() = readPackedInternal(this::readSInt64)
override fun readPackedEnum() = readPackedInternal(this::readEnum)
override fun readPackedFixed32(): List<UInt> = readPackedInternal(::readFixed32)
override fun readPackedFixed64(): List<ULong> = readPackedInternal(::readFixed64)
override fun readPackedSFixed32(): List<Int> = readPackedInternal(::readSFixed32)
override fun readPackedSFixed64(): List<Long> = readPackedInternal(::readSFixed64)
override fun readPackedFloat(): List<Float> = readPackedInternal(::readFloat)
override fun readPackedDouble(): List<Double> = readPackedInternal(::readDouble)

override fun close() {}

private fun <T : Any> readPackedInternal(read: () -> T) = readPackedVarInternal(
size = { -1 },
readFn = read,
withError = { },
hadError = { false },
)
}

internal actual fun WireDecoder(source: Buffer): WireDecoder {
TODO("Not yet implemented")
}
return WireDecoderJvm(source)
}
Loading
Loading