-
Notifications
You must be signed in to change notification settings - Fork 31
grpc-common: Add field presence tracking, required field enforcing and generation of the MessageCodec implementation #421
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
Changes from all commits
Commits
Show all changes
9 commits
Select commit
Hold shift + click to select a range
ffac0c4
grpc-pb: Refactor subtyping hierarchy
Jozott00 ff36ce8
grpc-pb: Add presence tracking and required field check
Jozott00 545cd61
grpc-pb: Add PresenceIndices object that holds the presence indices o…
Jozott00 64aa73f
grpc-pb: Move BitSet to utils
Jozott00 13a3d37
grpc-pb: Add MessageCodec object for each message
Jozott00 adc277f
grpc-pb: Use only fully qualified names for kotlinx.rpc.grpc.pb.* cla…
Jozott00 f380a05
Revert kotlin version increase
Jozott00 524f820
grpc-pb: Remove demo test
Jozott00 a8a10fe
grpc-pb: Address PR comments
Jozott00 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
13 changes: 13 additions & 0 deletions
13
grpc/grpc-core/src/commonMain/kotlin/kotlinx/rpc/grpc/pb/InternalMessage.kt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
/* | ||
* 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.pb | ||
|
||
import kotlinx.rpc.grpc.utils.BitSet | ||
import kotlinx.rpc.internal.utils.InternalRpcApi | ||
|
||
@InternalRpcApi | ||
public abstract class InternalMessage(fieldsWithPresence: Int) { | ||
public val presenceMask: BitSet = BitSet(fieldsWithPresence) | ||
} |
68 changes: 68 additions & 0 deletions
68
grpc/grpc-core/src/commonMain/kotlin/kotlinx/rpc/grpc/utils/BitSet.kt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,68 @@ | ||
/* | ||
* 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.utils | ||
|
||
import kotlinx.rpc.internal.utils.InternalRpcApi | ||
|
||
/** | ||
* A fixed-sized vector of bits, allowing one to set/clear/read bits from it by a bit index. | ||
*/ | ||
@InternalRpcApi | ||
public class BitSet(public val size: Int) { | ||
private val data: LongArray = LongArray((size + 63) ushr 6) | ||
|
||
/** Sets the bit at [index] to 1. */ | ||
public operator fun set(index: Int, value: Boolean) { | ||
if (!value) return clear(index) | ||
require(index in 0 until size) { "Index $index out‑of‑bounds for length $size" } | ||
val word = index ushr 6 | ||
val mask = 1L shl (index and 63) | ||
data[word] = data[word] or mask | ||
} | ||
|
||
/** Clears the bit at [index] (sets to 0). */ | ||
public fun clear(index: Int) { | ||
require(index >= 0 && index < size) { "Index $index out of bounds for length $size" } | ||
val word = index ushr 6 | ||
data[word] = data[word] and (1L shl (index and 63)).inv() | ||
} | ||
|
||
/** Returns true if the bit at [index] is set. */ | ||
public operator fun get(index: Int): Boolean { | ||
require(index >= 0 && index < size) { "Index $index out of bounds for length $size" } | ||
val word = index ushr 6 | ||
return (data[word] ushr (index and 63) and 1L) != 0L | ||
} | ||
|
||
/** Clears all bits. */ | ||
public fun clearAll() { | ||
data.fill(0L) | ||
} | ||
|
||
/** Returns the number of bits set to 1. */ | ||
public fun cardinality(): Int { | ||
var sum = 0 | ||
for (w in data) { | ||
sum += w.countOneBits() | ||
} | ||
return sum | ||
} | ||
|
||
/** Returns true if all bits are set. */ | ||
public fun allSet(): Boolean { | ||
val fullWords = size ushr 6 | ||
// check full 64-bit words | ||
for (i in 0 until fullWords) { | ||
if (data[i] != -1L) return false | ||
} | ||
// check leftover bits | ||
val rem = size and 63 | ||
if (rem != 0) { | ||
val mask = (-1L ushr (64 - rem)) | ||
if (data[fullWords] != mask) return false | ||
} | ||
return true | ||
} | ||
} |
305 changes: 305 additions & 0 deletions
305
grpc/grpc-core/src/commonTest/kotlin/kotlinx/rpc/grpc/internal/BitSetTest.kt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,305 @@ | ||
/* | ||
* 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.rpc.grpc.utils.BitSet | ||
import kotlin.test.* | ||
|
||
class BitSetTest { | ||
|
||
@Test | ||
fun testConstructor() { | ||
// Test with size 0 | ||
val bitSet0 = BitSet(0) | ||
assertEquals(0, bitSet0.size) | ||
assertEquals(0, bitSet0.cardinality()) | ||
|
||
// Test with small size | ||
val bitSet10 = BitSet(10) | ||
assertEquals(10, bitSet10.size) | ||
assertEquals(0, bitSet10.cardinality()) | ||
|
||
// Test with size that spans multiple words | ||
val bitSet100 = BitSet(100) | ||
assertEquals(100, bitSet100.size) | ||
assertEquals(0, bitSet100.cardinality()) | ||
|
||
// Test with size at word boundary | ||
val bitSet64 = BitSet(64) | ||
assertEquals(64, bitSet64.size) | ||
assertEquals(0, bitSet64.cardinality()) | ||
|
||
// Test with size just over word boundary | ||
val bitSet65 = BitSet(65) | ||
assertEquals(65, bitSet65.size) | ||
assertEquals(0, bitSet65.cardinality()) | ||
} | ||
|
||
@Test | ||
fun testSetAndGet() { | ||
val bitSet = BitSet(100) | ||
|
||
// Initially all bits should be unset | ||
for (i in 0 until 100) { | ||
assertFalse(bitSet[i], "Bit $i should be initially unset") | ||
} | ||
|
||
// Set some bits | ||
bitSet[0] = true | ||
bitSet[1] = true | ||
bitSet[63] = true | ||
bitSet[64] = true | ||
bitSet[99] = true | ||
|
||
// Verify the bits are set | ||
assertTrue(bitSet[0], "Bit 0 should be set") | ||
assertTrue(bitSet[1], "Bit 1 should be set") | ||
assertTrue(bitSet[63], "Bit 63 should be set") | ||
assertTrue(bitSet[64], "Bit 64 should be set") | ||
assertTrue(bitSet[99], "Bit 99 should be set") | ||
|
||
// Verify other bits are still unset | ||
assertFalse(bitSet[2], "Bit 2 should be unset") | ||
assertFalse(bitSet[62], "Bit 62 should be unset") | ||
assertFalse(bitSet[65], "Bit 65 should be unset") | ||
assertFalse(bitSet[98], "Bit 98 should be unset") | ||
} | ||
|
||
@Test | ||
fun testClear() { | ||
val bitSet = BitSet(100) | ||
|
||
// Set all bits | ||
for (i in 0 until 100) { | ||
bitSet[i] = true | ||
} | ||
|
||
// Verify all bits are set | ||
for (i in 0 until 100) { | ||
assertTrue(bitSet[i], "Bit $i should be set") | ||
} | ||
|
||
// Clear some bits | ||
bitSet[0] = false | ||
bitSet[1] = false | ||
bitSet[63] = false | ||
bitSet[64] = false | ||
bitSet[99] = false | ||
|
||
// Verify the bits are cleared | ||
assertFalse(bitSet[0], "Bit 0 should be cleared") | ||
assertFalse(bitSet[1], "Bit 1 should be cleared") | ||
assertFalse(bitSet[63], "Bit 63 should be cleared") | ||
assertFalse(bitSet[64], "Bit 64 should be cleared") | ||
assertFalse(bitSet[99], "Bit 99 should be cleared") | ||
|
||
// Verify other bits are still set | ||
assertTrue(bitSet[2], "Bit 2 should still be set") | ||
assertTrue(bitSet[62], "Bit 62 should still be set") | ||
assertTrue(bitSet[65], "Bit 65 should still be set") | ||
assertTrue(bitSet[98], "Bit 98 should still be set") | ||
} | ||
|
||
@Test | ||
fun testClearAll() { | ||
val bitSet = BitSet(100) | ||
|
||
// Set all bits | ||
for (i in 0 until 100) { | ||
bitSet[i] = true | ||
} | ||
|
||
// Verify all bits are set | ||
for (i in 0 until 100) { | ||
assertTrue(bitSet[i], "Bit $i should be set") | ||
} | ||
|
||
// Clear all bits | ||
bitSet.clearAll() | ||
|
||
// Verify all bits are cleared | ||
for (i in 0 until 100) { | ||
assertFalse(bitSet[i], "Bit $i should be cleared after clearAll") | ||
} | ||
} | ||
|
||
@Test | ||
fun testCardinality() { | ||
val bitSet = BitSet(100) | ||
assertEquals(0, bitSet.cardinality(), "Initial cardinality should be 0") | ||
|
||
// Set some bits | ||
bitSet[0] = true | ||
assertEquals(1, bitSet.cardinality(), "Cardinality should be 1 after setting 1 bit") | ||
|
||
bitSet[63] = true | ||
assertEquals(2, bitSet.cardinality(), "Cardinality should be 2 after setting 2 bits") | ||
|
||
bitSet[64] = true | ||
assertEquals(3, bitSet.cardinality(), "Cardinality should be 3 after setting 3 bits") | ||
|
||
bitSet[99] = true | ||
assertEquals(4, bitSet.cardinality(), "Cardinality should be 4 after setting 4 bits") | ||
|
||
// Clear a bit | ||
bitSet.clear(0) | ||
assertEquals(3, bitSet.cardinality(), "Cardinality should be 3 after clearing 1 bit") | ||
|
||
// Set a bit that's already set | ||
bitSet[63] = true | ||
assertEquals(3, bitSet.cardinality(), "Cardinality should still be 3 after setting an already set bit") | ||
|
||
// Clear all bits | ||
bitSet.clearAll() | ||
assertEquals(0, bitSet.cardinality(), "Cardinality should be 0 after clearAll") | ||
} | ||
|
||
@Test | ||
fun testAllSet() { | ||
// Test with empty BitSet | ||
val emptyBitSet = BitSet(0) | ||
assertTrue(emptyBitSet.allSet(), "Empty BitSet should return true for allSet") | ||
|
||
// Test with small BitSet | ||
val smallBitSet = BitSet(5) | ||
assertFalse(smallBitSet.allSet(), "New BitSet should return false for allSet") | ||
|
||
smallBitSet[0] = true | ||
smallBitSet[1] = true | ||
smallBitSet[2] = true | ||
smallBitSet[3] = true | ||
smallBitSet[4] = true | ||
assertTrue(smallBitSet.allSet(), "BitSet with all bits set should return true for allSet") | ||
|
||
smallBitSet.clear(2) | ||
assertFalse(smallBitSet.allSet(), "BitSet with one bit cleared should return false for allSet") | ||
|
||
// Test with BitSet that spans multiple words | ||
val largeBitSet = BitSet(100) | ||
assertFalse(largeBitSet.allSet(), "New large BitSet should return false for allSet") | ||
|
||
for (i in 0 until 100) { | ||
largeBitSet[i] = true | ||
} | ||
assertTrue(largeBitSet.allSet(), "Large BitSet with all bits set should return true for allSet") | ||
|
||
largeBitSet.clear(63) | ||
assertFalse(largeBitSet.allSet(), "Large BitSet with one bit cleared should return false for allSet") | ||
|
||
// Test with BitSet at word boundary | ||
val wordBoundaryBitSet = BitSet(64) | ||
assertFalse(wordBoundaryBitSet.allSet(), "New word boundary BitSet should return false for allSet") | ||
|
||
for (i in 0 until 64) { | ||
wordBoundaryBitSet[i] = true | ||
} | ||
assertTrue(wordBoundaryBitSet.allSet(), "Word boundary BitSet with all bits set should return true for allSet") | ||
} | ||
|
||
@Test | ||
fun testEdgeCases() { | ||
val bitSet = BitSet(100) | ||
|
||
// Test setting and getting at boundaries | ||
bitSet[0] = true | ||
assertTrue(bitSet[0], "Should be able to set and get bit 0") | ||
|
||
bitSet[99] = true | ||
assertTrue(bitSet[99], "Should be able to set and get bit at size-1") | ||
|
||
// Test clearing at boundaries | ||
bitSet.clear(0) | ||
assertFalse(bitSet[0], "Should be able to clear bit 0") | ||
|
||
bitSet.clear(99) | ||
assertFalse(bitSet[99], "Should be able to clear bit at size-1") | ||
|
||
// Test out of bounds access | ||
assertFailsWith<IllegalArgumentException> { | ||
bitSet[100] = true | ||
} | ||
|
||
assertFailsWith<IllegalArgumentException> { | ||
bitSet.clear(100) | ||
} | ||
|
||
assertFailsWith<IllegalArgumentException> { | ||
bitSet[100] | ||
} | ||
|
||
assertFailsWith<IllegalArgumentException> { | ||
bitSet[-1] = true | ||
} | ||
|
||
assertFailsWith<IllegalArgumentException> { | ||
bitSet.clear(-1) | ||
} | ||
|
||
assertFailsWith<IllegalArgumentException> { | ||
bitSet[-1] | ||
} | ||
} | ||
|
||
@Test | ||
fun testWordBoundaries() { | ||
// Test BitSet with size at word boundaries | ||
for (size in listOf(63, 64, 65, 127, 128, 129)) { | ||
val bitSet = BitSet(size) | ||
|
||
// Set all bits | ||
for (i in 0 until size) { | ||
bitSet[i] = true | ||
} | ||
|
||
// Verify all bits are set | ||
for (i in 0 until size) { | ||
assertTrue(bitSet[i], "Bit $i should be set in BitSet of size $size") | ||
} | ||
|
||
// Verify cardinality | ||
assertEquals(size, bitSet.cardinality(), "Cardinality should equal size for fully set BitSet") | ||
|
||
// Verify allSet | ||
assertTrue(bitSet.allSet(), "allSet should return true for fully set BitSet") | ||
|
||
// Clear all bits | ||
bitSet.clearAll() | ||
|
||
// Verify all bits are cleared | ||
for (i in 0 until size) { | ||
assertFalse(bitSet[i], "Bit $i should be cleared in BitSet of size $size after clearAll") | ||
} | ||
|
||
// Verify cardinality | ||
assertEquals(0, bitSet.cardinality(), "Cardinality should be 0 after clearAll") | ||
|
||
// Verify allSet | ||
assertFalse(bitSet.allSet(), "allSet should return false after clearAll") | ||
} | ||
} | ||
|
||
@Test | ||
fun testLargeCardinality() { | ||
// Test with a large BitSet to verify cardinality calculation | ||
val size = 1000 | ||
val bitSet = BitSet(size) | ||
|
||
// Set every other bit | ||
for (i in 0 until size step 2) { | ||
bitSet[i] = true | ||
} | ||
|
||
// Verify cardinality | ||
assertEquals(size / 2, bitSet.cardinality(), "Cardinality should be half the size when every other bit is set") | ||
|
||
// Set all bits | ||
for (i in 0 until size) { | ||
bitSet[i] = true | ||
} | ||
|
||
// Verify cardinality | ||
assertEquals(size, bitSet.cardinality(), "Cardinality should equal size when all bits are set") | ||
} | ||
} |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.