Skip to content

Commit 3dc2b8a

Browse files
Jozott00Mr3zee
andcommitted
gRPC/Native: Add Wire Format encoder and decoder (#412)
Co-authored-by: Alexander Sysoev <[email protected]>
1 parent c08cdeb commit 3dc2b8a

File tree

32 files changed

+2861
-59
lines changed

32 files changed

+2861
-59
lines changed

grpc/grpc-core/build.gradle.kts

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ plugins {
1414
alias(libs.plugins.kotlinx.rpc)
1515
}
1616

17+
18+
1719
kotlin {
1820
compilerOptions {
1921
freeCompilerArgs.add("-Xexpect-actual-classes")
@@ -27,6 +29,7 @@ kotlin {
2729
api(libs.coroutines.core)
2830

2931
implementation(libs.atomicfu)
32+
implementation(libs.kotlinx.io.core)
3033
}
3134
}
3235

@@ -58,6 +61,12 @@ kotlin {
5861
}
5962
}
6063

64+
nativeMain {
65+
dependencies {
66+
implementation(libs.kotlinx.collections.immutable)
67+
}
68+
}
69+
6170
nativeTest {
6271
dependencies {
6372
implementation(kotlin("test"))
@@ -83,7 +92,7 @@ kotlin {
8392
val buildGrpcppCLib = tasks.register<Exec>("buildGrpcppCLib") {
8493
group = "build"
8594
workingDir = grpcppCLib
86-
commandLine("bash", "-c", "bazel build :grpcpp_c_static --config=release")
95+
commandLine("bash", "-c", "bazel build :grpcpp_c_static :protowire_static --config=release")
8796
inputs.files(fileTree(grpcppCLib) { exclude("bazel-*/**") })
8897
outputs.dir(grpcppCLib.resolve("bazel-bin"))
8998

@@ -108,6 +117,22 @@ kotlin {
108117
tasks.named(interopTask, CInteropProcess::class) {
109118
dependsOn(buildGrpcppCLib)
110119
}
120+
121+
122+
val libprotowire by creating {
123+
includeDirs(
124+
grpcppCLib.resolve("include")
125+
)
126+
extraOpts(
127+
"-libraryPath", "${grpcppCLib.resolve("bazel-out/darwin_arm64-opt/bin")}",
128+
)
129+
}
130+
131+
val libUpbTask = "cinterop${libprotowire.name.capitalized()}${it.targetName.capitalized()}"
132+
tasks.named(libUpbTask, CInteropProcess::class) {
133+
dependsOn(buildGrpcppCLib)
134+
}
135+
111136
}
112137
}
113138
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
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.internal
6+
7+
import kotlinx.rpc.grpc.internal.KTag.Companion.K_TAG_TYPE_BITS
8+
9+
internal enum class WireType {
10+
VARINT, // 0
11+
FIXED64, // 1
12+
LENGTH_DELIMITED, // 2
13+
START_GROUP, // 3
14+
END_GROUP, // 4
15+
FIXED32, // 5
16+
}
17+
18+
internal data class KTag(val fieldNr: Int, val wireType: WireType) {
19+
20+
init {
21+
check(isValidFieldNr(fieldNr)) { "Invalid field number: $fieldNr" }
22+
}
23+
24+
companion object {
25+
// Number of bits in a tag which identify the wire type.
26+
const val K_TAG_TYPE_BITS: Int = 3;
27+
28+
// Mask for those bits. (just 0b111)
29+
val K_TAG_TYPE_MASK: UInt = (1u shl K_TAG_TYPE_BITS) - 1u
30+
}
31+
}
32+
33+
internal fun KTag.toRawKTag(): UInt {
34+
return (fieldNr.toUInt() shl K_TAG_TYPE_BITS) or wireType.ordinal.toUInt()
35+
}
36+
37+
internal fun KTag.Companion.fromOrNull(rawKTag: UInt): KTag? {
38+
val type = (rawKTag and K_TAG_TYPE_MASK).toInt()
39+
val field = (rawKTag shr K_TAG_TYPE_BITS).toInt()
40+
if (!isValidFieldNr(field)) {
41+
return null
42+
}
43+
if (type >= WireType.entries.size) {
44+
return null
45+
}
46+
return KTag(field, WireType.entries[type])
47+
}
48+
49+
internal fun KTag.Companion.isValidFieldNr(fieldNr: Int): Boolean {
50+
return 1 <= fieldNr && fieldNr <= 536_870_911
51+
}
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
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.internal
6+
7+
import kotlinx.io.Buffer
8+
9+
/**
10+
* A platform-specific decoder for wire format data.
11+
*
12+
* This decoder is used by first calling [readTag], than looking up the field based on the field number in the returned,
13+
* tag and then calling the actual `read*()` method to read the value to the corresponding field.
14+
*
15+
* [hadError] indicates an error during decoding. While calling `read*()` is safe, the returned values
16+
* are meaningless if [hadError] returns `true`.
17+
*
18+
* NOTE: If the [hadError] after a call to `read*()` returns `false`, it doesn't mean that the
19+
* value is correctly decoded. E.g., the following test will pass:
20+
* ```kt
21+
* val fieldNr = 1
22+
* val buffer = Buffer()
23+
*
24+
* val encoder = WireEncoder(buffer)
25+
* assertTrue(encoder.writeInt32(fieldNr, 12312))
26+
* encoder.flush()
27+
*
28+
* WireDecoder(buffer).use { decoder ->
29+
* decoder.readTag()
30+
* decoder.readBool()
31+
* assertFalse(decoder.hasError())
32+
* }
33+
* ```
34+
*/
35+
internal interface WireDecoder : AutoCloseable {
36+
fun hadError(): Boolean
37+
fun readTag(): KTag?
38+
fun readBool(): Boolean
39+
fun readInt32(): Int
40+
fun readInt64(): Long
41+
fun readUInt32(): UInt
42+
fun readUInt64(): ULong
43+
fun readSInt32(): Int
44+
fun readSInt64(): Long
45+
fun readFixed32(): UInt
46+
fun readFixed64(): ULong
47+
fun readSFixed32(): Int
48+
fun readSFixed64(): Long
49+
fun readFloat(): Float
50+
fun readDouble(): Double
51+
52+
fun readEnum(): Int
53+
fun readString(): String
54+
fun readBytes(): ByteArray
55+
fun readPackedBool(): List<Boolean>
56+
fun readPackedInt32(): List<Int>
57+
fun readPackedInt64(): List<Long>
58+
fun readPackedSInt32(): List<Int>
59+
fun readPackedSInt64(): List<Long>
60+
fun readPackedUInt32(): List<UInt>
61+
fun readPackedUInt64(): List<ULong>
62+
fun readPackedFixed32(): List<UInt>
63+
fun readPackedFixed64(): List<ULong>
64+
fun readPackedSFixed32(): List<Int>
65+
fun readPackedSFixed64(): List<Long>
66+
fun readPackedFloat(): List<Float>
67+
fun readPackedDouble(): List<Double>
68+
fun readPackedEnum(): List<Int>
69+
}
70+
71+
/**
72+
* Creates a platform-specific [WireDecoder].
73+
*
74+
* This constructor takes a [Buffer] instead of a [kotlinx.io.Source] because
75+
* the native implementation (`WireDecoderNative`) depends on [Buffer]'s internal structure.
76+
*
77+
* NOTE: Do not use the [source] buffer while the [WireDecoder] is still open.
78+
*
79+
* @param source The buffer containing the encoded wire-format data.
80+
*/
81+
internal expect fun WireDecoder(source: Buffer): WireDecoder
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
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.internal
6+
7+
import kotlinx.io.Sink
8+
9+
/**
10+
* A platform-specific class that encodes values into protobuf's wire format.
11+
*
12+
* If one `write*()` method returns false, the encoding of the value failed
13+
* and no further encodings can be performed on this [WireEncoder].
14+
*
15+
* [flush] must be called to ensure that all data is written to the [Sink].
16+
*/
17+
@OptIn(ExperimentalUnsignedTypes::class)
18+
internal interface WireEncoder {
19+
fun flush()
20+
fun writeBool(field: Int, value: Boolean): Boolean
21+
fun writeInt32(fieldNr: Int, value: Int): Boolean
22+
fun writeInt64(fieldNr: Int, value: Long): Boolean
23+
fun writeUInt32(fieldNr: Int, value: UInt): Boolean
24+
fun writeUInt64(fieldNr: Int, value: ULong): Boolean
25+
fun writeSInt32(fieldNr: Int, value: Int): Boolean
26+
fun writeSInt64(fieldNr: Int, value: Long): Boolean
27+
fun writeFixed32(fieldNr: Int, value: UInt): Boolean
28+
fun writeFixed64(fieldNr: Int, value: ULong): Boolean
29+
fun writeSFixed32(fieldNr: Int, value: Int): Boolean
30+
fun writeSFixed64(fieldNr: Int, value: Long): Boolean
31+
fun writeFloat(fieldNr: Int, value: Float): Boolean
32+
fun writeDouble(fieldNr: Int, value: Double): Boolean
33+
fun writeEnum(fieldNr: Int, value: Int): Boolean
34+
fun writeBytes(fieldNr: Int, value: ByteArray): Boolean
35+
fun writeString(fieldNr: Int, value: String): Boolean
36+
fun writePackedBool(fieldNr: Int, value: List<Boolean>, fieldSize: Int): Boolean
37+
fun writePackedInt32(fieldNr: Int, value: List<Int>, fieldSize: Int): Boolean
38+
fun writePackedInt64(fieldNr: Int, value: List<Long>, fieldSize: Int): Boolean
39+
fun writePackedUInt32(fieldNr: Int, value: List<UInt>, fieldSize: Int): Boolean
40+
fun writePackedUInt64(fieldNr: Int, value: List<ULong>, fieldSize: Int): Boolean
41+
fun writePackedSInt32(fieldNr: Int, value: List<Int>, fieldSize: Int): Boolean
42+
fun writePackedSInt64(fieldNr: Int, value: List<Long>, fieldSize: Int): Boolean
43+
fun writePackedFixed32(fieldNr: Int, value: List<UInt>): Boolean
44+
fun writePackedFixed64(fieldNr: Int, value: List<ULong>): Boolean
45+
fun writePackedSFixed32(fieldNr: Int, value: List<Int>): Boolean
46+
fun writePackedSFixed64(fieldNr: Int, value: List<Long>): Boolean
47+
fun writePackedFloat(fieldNr: Int, value: List<Float>): Boolean
48+
fun writePackedDouble(fieldNr: Int, value: List<Double>): Boolean
49+
fun writePackedEnum(fieldNr: Int, value: List<Int>, fieldSize: Int) =
50+
writePackedInt32(fieldNr, value, fieldSize)
51+
}
52+
53+
54+
internal expect fun WireEncoder(sink: Sink): WireEncoder
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
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.internal
6+
7+
internal object WireSize
8+
9+
internal expect fun WireSize.int32(value: Int): UInt
10+
internal expect fun WireSize.int64(value: Long): UInt
11+
internal expect fun WireSize.uInt32(value: UInt): UInt
12+
internal expect fun WireSize.uInt64(value: ULong): UInt
13+
internal expect fun WireSize.sInt32(value: Int): UInt
14+
internal expect fun WireSize.sInt64(value: Long): UInt
15+
16+
internal fun WireSize.bool(value: Boolean) = int32(if (value) 1 else 0)
17+
internal fun WireSize.enum(value: Int) = int32(value)
18+
internal fun WireSize.packedInt32(value: List<Int>) = value.sumOf { int32(it) }
19+
internal fun WireSize.packedInt64(value: List<Long>) = value.sumOf { int64(it) }
20+
internal fun WireSize.packedUInt32(value: List<UInt>) = value.sumOf { uInt32(it) }
21+
internal fun WireSize.packedUInt64(value: List<ULong>) = value.sumOf { uInt64(it) }
22+
internal fun WireSize.packedSInt32(value: List<Int>) = value.sumOf { sInt32(it) }
23+
internal fun WireSize.packedSInt64(value: List<Long>) = value.sumOf { sInt64(it) }
24+
internal fun WireSize.packedEnum(value: List<Int>) = value.sumOf { enum(it) }
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
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.internal
6+
7+
import kotlinx.io.Buffer
8+
import kotlinx.io.Source
9+
10+
internal actual fun WireDecoder(source: Buffer): WireDecoder {
11+
TODO("Not yet implemented")
12+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
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.internal
6+
7+
import kotlinx.io.Sink
8+
9+
internal actual fun WireEncoder(sink: Sink): WireEncoder {
10+
TODO("Not yet implemented")
11+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
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.internal
6+
7+
internal actual fun WireSize.int32(value: Int): UInt {
8+
TODO("Not yet implemented")
9+
}
10+
11+
internal actual fun WireSize.int64(value: Long): UInt {
12+
TODO("Not yet implemented")
13+
}
14+
15+
internal actual fun WireSize.uInt32(value: UInt): UInt {
16+
TODO("Not yet implemented")
17+
}
18+
19+
internal actual fun WireSize.uInt64(value: ULong): UInt {
20+
TODO("Not yet implemented")
21+
}
22+
23+
internal actual fun WireSize.sInt32(value: Int): UInt {
24+
TODO("Not yet implemented")
25+
}
26+
27+
internal actual fun WireSize.sInt64(value: Long): UInt {
28+
TODO("Not yet implemented")
29+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
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.internal
6+
7+
import kotlinx.io.Buffer
8+
import kotlinx.io.Source
9+
10+
internal actual fun WireDecoder(source: Buffer): WireDecoder {
11+
TODO("Not yet implemented")
12+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
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.internal
6+
7+
import kotlinx.io.Sink
8+
9+
internal actual fun WireEncoder(sink: Sink): WireEncoder {
10+
TODO("Not yet implemented")
11+
}

0 commit comments

Comments
 (0)