Skip to content

Add an adapted version of the EfficientBinaryFormat to the default tests. #2979

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

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
/*
* Copyright 2017-2025 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
*/

package kotlinx.serialization.efficientBinaryFormat

class ByteReadingBuffer(val buffer: ByteArray) {
private var next = 0

private fun nextByte(): Int {
return buffer[next++].toInt() and 0xff
}

private fun nextByteL(): Long {
return buffer[next++].toLong() and 0xffL
}

operator fun get(pos: Int): Byte {
if(pos !in 0..<buffer.size) { throw IndexOutOfBoundsException("Position $pos out of range") }
return buffer[pos]
}

fun readByte(): Byte {
return buffer[next++]
}

fun readShort(): Short {
return (nextByte() or (nextByte() shl 8)).toShort()
}

fun readInt(): Int {
return nextByte() or
(nextByte() shl 8) or
(nextByte() shl 16) or
(nextByte() shl 24)
}

fun readLong(): Long {
return nextByteL() or
(nextByteL() shl 8) or
(nextByteL() shl 16) or
(nextByteL() shl 24) or
(nextByteL() shl 32) or
(nextByteL() shl 40) or
(nextByteL() shl 48) or
(nextByteL() shl 56)
}

fun readFloat(): Float {
return Float.fromBits(readInt())
}

fun readDouble(): Double {
val l = readLong()
return Double.fromBits(l)
}

fun readChar(): Char {
return (nextByte() or (nextByte() shl 8)).toChar()
}

fun readString(): String {
val len = readInt()
val chars = CharArray(len) { readChar() }
return chars.concatToString()
}

fun readString(consumeChunk: (String) -> Unit) {
val len = readInt()
var remaining = len
while (remaining > 1024) {
remaining -= 1024
val chunk = CharArray(1024) { readChar() }
consumeChunk(chunk.concatToString())
}
val chars = CharArray(remaining) { readChar() }
consumeChunk(chars.concatToString())
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
/*
* Copyright 2017-2025 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
*/

package kotlinx.serialization.efficientBinaryFormat

import kotlin.experimental.and

class ByteWritingBuffer() {
private var buffer = ByteArray(8192)
private var next = 0
val size
get() = next

operator fun get(pos: Int): Byte {
if(pos !in 0..<size) { throw IndexOutOfBoundsException("Position $pos out of range") }
return buffer[pos]
}

private fun growIfNeeded(additionalNeeded: Int = 1) {
val minNew = size + additionalNeeded
if (minNew < buffer.size) return

var newSize = buffer.size shl 1
while (newSize < minNew) { newSize = newSize shl 1}

buffer = buffer.copyOf(newSize)
}

fun toByteArray(): ByteArray {
return buffer.copyOf(size)
}

fun writeByte(b: Byte) {
growIfNeeded(1)
buffer[next++] = b
}

fun writeShort(s: Short) {
growIfNeeded(2)
buffer[next++] = (s and 0xff).toByte()
buffer[next++] = ((s.toInt() shr 8) and 0xff).toByte()
}

fun writeInt(i: Int) {
growIfNeeded(4)
buffer[next++] = (i and 0xff).toByte()
buffer[next++] = ((i shr 8) and 0xff).toByte()
buffer[next++] = ((i shr 16) and 0xff).toByte()
buffer[next++] = ((i shr 24) and 0xff).toByte()
}

fun writeLong(l: Long) {
growIfNeeded(4)
buffer[next++] = (l and 0xff).toByte()
buffer[next++] = ((l shr 8) and 0xff).toByte()
buffer[next++] = ((l shr 16) and 0xff).toByte()
buffer[next++] = ((l shr 24) and 0xff).toByte()
buffer[next++] = ((l shr 32) and 0xff).toByte()
buffer[next++] = ((l shr 40) and 0xff).toByte()
buffer[next++] = ((l shr 48) and 0xff).toByte()
buffer[next++] = ((l shr 56) and 0xff).toByte()
}

fun writeFloat(f: Float) {
writeInt(f.toBits())
}

fun writeDouble(d: Double) {
writeLong(d.toBits())
}

fun writeChar(c: Char) {
growIfNeeded(2)
buffer[next++] = (c.code and 0xff).toByte()
buffer[next++] = ((c.code shr 8) and 0xff).toByte()
}

fun writeString(s: String) {
growIfNeeded(s.length * 2+4)
writeInt(s.length)
for (c in s) {
buffer[next++] = (c.code and 0xff).toByte()
buffer[next++] = ((c.code shr 8) and 0xff).toByte()
}
}

}

Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
/*
* Copyright 2017-2025 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
*/

package kotlinx.serialization.efficientBinaryFormat

import kotlinx.serialization.BinaryFormat
import kotlinx.serialization.DeserializationStrategy
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.SerializationStrategy
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.AbstractDecoder
import kotlinx.serialization.encoding.AbstractEncoder
import kotlinx.serialization.encoding.ChunkedDecoder
import kotlinx.serialization.encoding.CompositeDecoder
import kotlinx.serialization.encoding.CompositeEncoder
import kotlinx.serialization.modules.EmptySerializersModule
import kotlinx.serialization.modules.SerializersModule

class EfficientBinaryFormat(
override val serializersModule: SerializersModule = EmptySerializersModule(),
): BinaryFormat {

override fun <T> encodeToByteArray(
serializer: SerializationStrategy<T>,
value: T
): ByteArray {
val encoder = Encoder(serializersModule)
serializer.serialize(encoder, value)
return encoder.byteBuffer.toByteArray()
}

override fun <T> decodeFromByteArray(
deserializer: DeserializationStrategy<T>,
bytes: ByteArray
): T {
val decoder = Decoder(serializersModule, bytes)
return deserializer.deserialize(decoder)
}

class Encoder(
override val serializersModule: SerializersModule,
internal val byteBuffer: ByteWritingBuffer = ByteWritingBuffer(),
elementsCount: Int = -1
): AbstractEncoder() {
var lastWrittenIndex = -1
var currentIndex = -1
val notInStruct = elementsCount < 0

val pending : Array<(() -> Unit)?> = when {
elementsCount <=0 -> emptyArray()
else -> arrayOfNulls(elementsCount)
}

override fun encodeBoolean(value: Boolean) = writeOrSuspend { byteBuffer.writeByte(if (value) 1 else 0) }
override fun encodeByte(value: Byte) = writeOrSuspend { byteBuffer.writeByte(value) }
override fun encodeShort(value: Short) = writeOrSuspend { byteBuffer.writeShort(value) }
override fun encodeInt(value: Int) = writeOrSuspend { byteBuffer.writeInt(value) }
override fun encodeLong(value: Long) = writeOrSuspend { byteBuffer.writeLong(value) }
override fun encodeFloat(value: Float) = writeOrSuspend { byteBuffer.writeFloat(value) }
override fun encodeDouble(value: Double) = writeOrSuspend { byteBuffer.writeDouble(value) }
override fun encodeChar(value: Char) = writeOrSuspend { byteBuffer.writeChar(value) }
override fun encodeString(value: String) = writeOrSuspend { byteBuffer.writeString(value) }
override fun encodeEnum(enumDescriptor: SerialDescriptor, index: Int) = writeOrSuspend {
byteBuffer.writeInt(index)
}

@ExperimentalSerializationApi
override fun <T : Any> encodeNullableSerializableValue(
serializer: SerializationStrategy<T>,
value: T?
) {
writeOrSuspend {
super.encodeNullableSerializableValue(serializer, value)
}
}

override fun <T> encodeSerializableValue(serializer: SerializationStrategy<T>, value: T) {
writeOrSuspend {
super.encodeSerializableValue(serializer, value)
}
}

@Suppress("NOTHING_TO_INLINE")
private inline fun writeOrSuspend(noinline action: () -> Unit) {
val c = currentIndex
currentIndex = -1
when {
notInStruct || c<0 -> action()
lastWrittenIndex < -1 -> pending[c] = action
lastWrittenIndex + 1 == c -> {
++lastWrittenIndex
action()
}
c < pending.size -> pending[c] = action
else -> error("Unexpected index")
}
}

override fun encodeElement(descriptor: SerialDescriptor, index: Int): Boolean {
currentIndex = index
return true
}

@ExperimentalSerializationApi
override fun shouldEncodeElementDefault(descriptor: SerialDescriptor, index: Int): Boolean = true

override fun beginStructure(descriptor: SerialDescriptor): CompositeEncoder {
return Encoder(serializersModule, byteBuffer, descriptor.elementsCount)
}

override fun beginCollection(descriptor: SerialDescriptor, collectionSize: Int): CompositeEncoder {
encodeInt(collectionSize)
return Encoder(serializersModule, byteBuffer, -1)
}

override fun endStructure(descriptor: SerialDescriptor) {
currentIndex = -2 // mark negative to ensure writing
for (i in 0 until pending.size) {
pending[i]?.invoke()
}
}

override fun encodeNull() = encodeBoolean(false)
override fun encodeNotNullMark() = encodeBoolean(true)

}

class Decoder(override val serializersModule: SerializersModule, private val reader: ByteReadingBuffer) : AbstractDecoder(), ChunkedDecoder {

constructor(serializersModule: SerializersModule, bytes: ByteArray) : this(
serializersModule,
ByteReadingBuffer(bytes)
)

private var nextElementIndex = 0
// private var currentDesc: SerialDescriptor? = null

override fun decodeBoolean(): Boolean = reader.readByte().toInt() != 0

override fun decodeByte(): Byte = reader.readByte()

override fun decodeShort(): Short = reader.readShort()

override fun decodeInt(): Int = reader.readInt()

override fun decodeLong(): Long = reader.readLong()

override fun decodeFloat(): Float = reader.readFloat()

override fun decodeDouble(): Double = reader.readDouble()

override fun decodeChar(): Char = reader.readChar()

override fun decodeString(): String = reader.readString()

@ExperimentalSerializationApi
override fun decodeStringChunked(consumeChunk: (String) -> Unit) {
reader.readString(consumeChunk)
}

override fun decodeEnum(enumDescriptor: SerialDescriptor): Int = reader.readInt()

override fun decodeNotNullMark(): Boolean = decodeBoolean()

@ExperimentalSerializationApi
override fun decodeSequentially(): Boolean = true

override fun decodeCollectionSize(descriptor: SerialDescriptor): Int = reader.readInt()

override fun beginStructure(descriptor: SerialDescriptor): CompositeDecoder {
return Decoder(serializersModule, reader)
}

override fun endStructure(descriptor: SerialDescriptor) {
check(nextElementIndex ==0 || descriptor.elementsCount == nextElementIndex) { "Type: ${descriptor.serialName} not fully read: ${descriptor.elementsCount} != $nextElementIndex" }
}

override fun decodeElementIndex(descriptor: SerialDescriptor): Int {
return when (nextElementIndex) {
descriptor.elementsCount -> CompositeDecoder.DECODE_DONE
else -> nextElementIndex++
}
}
}
}
Loading