Skip to content

Commit 870f23a

Browse files
committed
Add a slightly more comprehensive (and common) implementation of the
default EfficientBinaryFormat, and add it to the parametrizedTest. It contains some workarounds to handle json format specifics. It implements stringformat by recording the binary serialization as shadow value.
1 parent afd811f commit 870f23a

File tree

4 files changed

+332
-1
lines changed

4 files changed

+332
-1
lines changed
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
/*
2+
* Copyright 2017-2025 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
3+
*/
4+
5+
package kotlinx.serialization.efficientBinaryFormat
6+
7+
class ByteReadingBuffer(val buffer: ByteArray) {
8+
private var next = 0
9+
10+
private fun nextByte(): Int {
11+
return buffer[next++].toInt() and 0xff
12+
}
13+
14+
private fun nextByteL(): Long {
15+
return buffer[next++].toLong() and 0xffL
16+
}
17+
18+
operator fun get(pos: Int): Byte {
19+
if(pos !in 0..<buffer.size) { throw IndexOutOfBoundsException("Position $pos out of range") }
20+
return buffer[pos]
21+
}
22+
23+
fun readByte(): Byte {
24+
return buffer[next++]
25+
}
26+
27+
fun readShort(): Short {
28+
return (nextByte() or (nextByte() shl 8)).toShort()
29+
}
30+
31+
fun readInt(): Int {
32+
return nextByte() or
33+
(nextByte() shl 8) or
34+
(nextByte() shl 16) or
35+
(nextByte() shl 24)
36+
}
37+
38+
fun readLong(): Long {
39+
return nextByteL() or
40+
(nextByteL() shl 8) or
41+
(nextByteL() shl 16) or
42+
(nextByteL() shl 24) or
43+
(nextByteL() shl 32) or
44+
(nextByteL() shl 40) or
45+
(nextByteL() shl 48) or
46+
(nextByteL() shl 56)
47+
}
48+
49+
fun readFloat(): Float {
50+
return Float.fromBits(readInt())
51+
}
52+
53+
fun readDouble(): Double {
54+
val l = readLong()
55+
return Double.fromBits(l)
56+
}
57+
58+
fun readChar(): Char {
59+
return (nextByte() or (nextByte() shl 8)).toChar()
60+
}
61+
62+
fun readString(): String {
63+
val len = readInt()
64+
val chars = CharArray(len) { readChar() }
65+
return chars.concatToString()
66+
}
67+
68+
}
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
/*
2+
* Copyright 2017-2025 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
3+
*/
4+
5+
package kotlinx.serialization.efficientBinaryFormat
6+
7+
import kotlin.experimental.and
8+
9+
class ByteWritingBuffer() {
10+
private var buffer = ByteArray(8192)
11+
private var next = 0
12+
val size
13+
get() = next
14+
15+
operator fun get(pos: Int): Byte {
16+
if(pos !in 0..<size) { throw IndexOutOfBoundsException("Position $pos out of range") }
17+
return buffer[pos]
18+
}
19+
20+
private fun growIfNeeded(additionalNeeded: Int = 1) {
21+
val minNew = size + additionalNeeded
22+
if (minNew < buffer.size) return
23+
24+
var newSize = buffer.size shl 1
25+
while (newSize < minNew) { newSize = newSize shl 1}
26+
27+
buffer = buffer.copyOf(newSize)
28+
}
29+
30+
fun toByteArray(): ByteArray {
31+
return buffer.copyOf(size)
32+
}
33+
34+
fun writeByte(b: Byte) {
35+
growIfNeeded(1)
36+
buffer[next++] = b
37+
}
38+
39+
fun writeShort(s: Short) {
40+
growIfNeeded(2)
41+
buffer[next++] = (s and 0xff).toByte()
42+
buffer[next++] = ((s.toInt() shr 8) and 0xff).toByte()
43+
}
44+
45+
fun writeInt(i: Int) {
46+
growIfNeeded(4)
47+
buffer[next++] = (i and 0xff).toByte()
48+
buffer[next++] = ((i shr 8) and 0xff).toByte()
49+
buffer[next++] = ((i shr 16) and 0xff).toByte()
50+
buffer[next++] = ((i shr 24) and 0xff).toByte()
51+
}
52+
53+
fun writeLong(l: Long) {
54+
growIfNeeded(4)
55+
buffer[next++] = (l and 0xff).toByte()
56+
buffer[next++] = ((l shr 8) and 0xff).toByte()
57+
buffer[next++] = ((l shr 16) and 0xff).toByte()
58+
buffer[next++] = ((l shr 24) and 0xff).toByte()
59+
buffer[next++] = ((l shr 32) and 0xff).toByte()
60+
buffer[next++] = ((l shr 40) and 0xff).toByte()
61+
buffer[next++] = ((l shr 48) and 0xff).toByte()
62+
buffer[next++] = ((l shr 56) and 0xff).toByte()
63+
}
64+
65+
fun writeFloat(f: Float) {
66+
writeInt(f.toBits())
67+
}
68+
69+
fun writeDouble(d: Double) {
70+
writeLong(d.toBits())
71+
}
72+
73+
fun writeChar(c: Char) {
74+
growIfNeeded(2)
75+
buffer[next++] = (c.code and 0xff).toByte()
76+
buffer[next++] = ((c.code shr 8) and 0xff).toByte()
77+
}
78+
79+
fun writeString(s: String) {
80+
growIfNeeded(s.length * 2+4)
81+
writeInt(s.length)
82+
for (c in s) {
83+
buffer[next++] = (c.code and 0xff).toByte()
84+
buffer[next++] = ((c.code shr 8) and 0xff).toByte()
85+
}
86+
}
87+
88+
}
89+
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
/*
2+
* Copyright 2017-2025 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
3+
*/
4+
5+
package kotlinx.serialization.efficientBinaryFormat
6+
7+
import kotlinx.serialization.BinaryFormat
8+
import kotlinx.serialization.DeserializationStrategy
9+
import kotlinx.serialization.ExperimentalSerializationApi
10+
import kotlinx.serialization.SerializationStrategy
11+
import kotlinx.serialization.descriptors.SerialDescriptor
12+
import kotlinx.serialization.encoding.AbstractDecoder
13+
import kotlinx.serialization.encoding.AbstractEncoder
14+
import kotlinx.serialization.encoding.CompositeDecoder
15+
import kotlinx.serialization.encoding.CompositeEncoder
16+
import kotlinx.serialization.modules.EmptySerializersModule
17+
import kotlinx.serialization.modules.SerializersModule
18+
19+
class EfficientBinaryFormat(
20+
override val serializersModule: SerializersModule = EmptySerializersModule(),
21+
): BinaryFormat {
22+
23+
override fun <T> encodeToByteArray(
24+
serializer: SerializationStrategy<T>,
25+
value: T
26+
): ByteArray {
27+
val encoder = Encoder(serializersModule)
28+
serializer.serialize(encoder, value)
29+
return encoder.byteBuffer.toByteArray()
30+
}
31+
32+
override fun <T> decodeFromByteArray(
33+
deserializer: DeserializationStrategy<T>,
34+
bytes: ByteArray
35+
): T {
36+
val decoder = Decoder(serializersModule, bytes)
37+
return deserializer.deserialize(decoder)
38+
}
39+
40+
class Encoder(override val serializersModule: SerializersModule): AbstractEncoder() {
41+
val byteBuffer = ByteWritingBuffer()
42+
override fun encodeBoolean(value: Boolean) = byteBuffer.writeByte(if (value) 1 else 0)
43+
override fun encodeByte(value: Byte) = byteBuffer.writeByte(value)
44+
override fun encodeShort(value: Short) = byteBuffer.writeShort(value)
45+
override fun encodeInt(value: Int) = byteBuffer.writeInt(value)
46+
override fun encodeLong(value: Long) = byteBuffer.writeLong(value)
47+
override fun encodeFloat(value: Float) = byteBuffer.writeFloat(value)
48+
override fun encodeDouble(value: Double) = byteBuffer.writeDouble(value)
49+
override fun encodeChar(value: Char) = byteBuffer.writeChar(value)
50+
override fun encodeString(value: String) = byteBuffer.writeString(value)
51+
override fun encodeEnum(enumDescriptor: SerialDescriptor, index: Int) = byteBuffer.writeInt(index)
52+
53+
@ExperimentalSerializationApi
54+
override fun shouldEncodeElementDefault(descriptor: SerialDescriptor, index: Int): Boolean = true
55+
56+
override fun beginCollection(descriptor: SerialDescriptor, collectionSize: Int): CompositeEncoder {
57+
encodeInt(collectionSize)
58+
return this
59+
}
60+
61+
override fun encodeNull() = encodeBoolean(false)
62+
override fun encodeNotNullMark() = encodeBoolean(true)
63+
64+
}
65+
66+
class Decoder(override val serializersModule: SerializersModule, private val reader: ByteReadingBuffer) : AbstractDecoder() {
67+
68+
constructor(serializersModule: SerializersModule, bytes: ByteArray) : this(
69+
serializersModule,
70+
ByteReadingBuffer(bytes)
71+
)
72+
73+
private var nextElementIndex = 0
74+
// private var currentDesc: SerialDescriptor? = null
75+
76+
override fun decodeBoolean(): Boolean = reader.readByte().toInt() != 0
77+
78+
override fun decodeByte(): Byte = reader.readByte()
79+
80+
override fun decodeShort(): Short = reader.readShort()
81+
82+
override fun decodeInt(): Int = reader.readInt()
83+
84+
override fun decodeLong(): Long = reader.readLong()
85+
86+
override fun decodeFloat(): Float = reader.readFloat()
87+
88+
override fun decodeDouble(): Double = reader.readDouble()
89+
90+
override fun decodeChar(): Char = reader.readChar()
91+
92+
override fun decodeString(): String = reader.readString()
93+
94+
override fun decodeEnum(enumDescriptor: SerialDescriptor): Int = reader.readInt()
95+
96+
override fun decodeNotNullMark(): Boolean = decodeBoolean()
97+
98+
@ExperimentalSerializationApi
99+
override fun decodeSequentially(): Boolean = true
100+
101+
override fun decodeCollectionSize(descriptor: SerialDescriptor): Int = reader.readInt()
102+
103+
override fun beginStructure(descriptor: SerialDescriptor): CompositeDecoder {
104+
return Decoder(serializersModule, reader)
105+
}
106+
107+
override fun endStructure(descriptor: SerialDescriptor) {
108+
check(nextElementIndex ==0 || descriptor.elementsCount == nextElementIndex) { "Type: ${descriptor.serialName} not fully read: ${descriptor.elementsCount} != $nextElementIndex" }
109+
}
110+
111+
override fun decodeElementIndex(descriptor: SerialDescriptor): Int {
112+
return when (nextElementIndex) {
113+
descriptor.elementsCount -> CompositeDecoder.DECODE_DONE
114+
else -> nextElementIndex++
115+
}
116+
}
117+
}
118+
}

formats/json-tests/commonTest/src/kotlinx/serialization/json/JsonTestBase.kt

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ package kotlinx.serialization.json
66

77
import kotlinx.io.*
88
import kotlinx.serialization.*
9+
import kotlinx.serialization.efficientBinaryFormat.EfficientBinaryFormat
910
import kotlinx.serialization.json.internal.*
1011
import kotlinx.serialization.json.io.*
1112
import kotlinx.serialization.json.okio.decodeFromBufferedSource
@@ -129,12 +130,67 @@ abstract class JsonTestBase {
129130
}
130131
}
131132

133+
/** A test runner that effectively handles the json tests to also test serialization to
134+
* "efficient" binary. This mainly checks serializer implementations.
135+
*/
136+
private inner class EfficientBinary(
137+
val json: Json,
138+
val ebf: EfficientBinaryFormat = EfficientBinaryFormat(),
139+
) : StringFormat {
140+
override val serializersModule: SerializersModule = ebf.serializersModule
141+
142+
private var bytes: ByteArray? = null
143+
private var jsonStr: String? = null
144+
145+
@OptIn(ExperimentalStdlibApi::class)
146+
override fun <T> encodeToString(serializer: SerializationStrategy<T>, value: T): String {
147+
bytes = runCatching { ebf.encodeToByteArray(serializer, value) }
148+
.onFailure { if ("Json format" !in it.message!!) throw it }
149+
.getOrNull()
150+
return json.encodeToString(serializer, value).also {
151+
if (bytes != null) jsonStr = it
152+
}
153+
}
154+
155+
@OptIn(ExperimentalStdlibApi::class)
156+
override fun <T> decodeFromString(deserializer: DeserializationStrategy<T>, string: String): T {
157+
/*
158+
* to retain compatibility with json we support different cases. If
159+
* the string has been encoded already use that. Instead, if the
160+
* deserializer is also a serializer (the default) then use that to
161+
* get the value from json and encode that to bytes which are then
162+
* decoded. In this case capture and ignore cases that require a
163+
* json encoder.
164+
*
165+
* Finally fall back to json decoding (nothing can be done)
166+
*/
167+
168+
var bytes = this@EfficientBinary.bytes
169+
if (string == jsonStr && bytes != null) {
170+
return ebf.decodeFromByteArray(deserializer, bytes)
171+
} else if (deserializer is SerializationStrategy<*>) {
172+
val value = json.decodeFromString(deserializer, string)
173+
//
174+
@Suppress("UNCHECKED_CAST")
175+
runCatching { ebf.encodeToByteArray(deserializer as SerializationStrategy<T>, value) }.onSuccess { r ->
176+
bytes = r
177+
jsonStr = string
178+
return ebf.decodeFromByteArray(deserializer, bytes)
179+
}.onFailure { e ->
180+
if ("Json format" !in e.message!!) throw e
181+
}
182+
}
183+
return json.decodeFromString(deserializer, string)
184+
}
185+
}
186+
132187
protected fun parametrizedTest(json: Json, test: StringFormat.() -> Unit) {
133188
val streamingResult = runCatching { SwitchableJson(json, JsonTestingMode.STREAMING).test() }
134189
val treeResult = runCatching { SwitchableJson(json, JsonTestingMode.TREE).test() }
135190
val okioResult = runCatching { SwitchableJson(json, JsonTestingMode.OKIO_STREAMS).test() }
136191
val kxioResult = runCatching { SwitchableJson(json, JsonTestingMode.KXIO_STREAMS).test() }
137-
processResults(listOf(streamingResult, treeResult, okioResult, kxioResult))
192+
val efficientBinaryResult = runCatching { EfficientBinary(json).test() }
193+
processResults(listOf(streamingResult, treeResult, okioResult, kxioResult, efficientBinaryResult))
138194
}
139195

140196
protected fun processResults(results: List<Result<*>>) {

0 commit comments

Comments
 (0)