Skip to content

Commit

Permalink
Add integration with kotlinx-io library (#2707)
Browse files Browse the repository at this point in the history
Integration is similar to Okio's one with functions `encodeToSink`, `decodeFromSource`, etc.
  • Loading branch information
sandwwraith authored Jun 13, 2024
1 parent d2dc7d2 commit 08e604a
Show file tree
Hide file tree
Showing 20 changed files with 430 additions and 110 deletions.
3 changes: 3 additions & 0 deletions benchmark/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ tasks.processJmhResources {
tasks.jmhJar {
archiveBaseName.set("benchmarks")
archiveVersion.set("")
archiveClassifier.set("") // benchmarks.jar, not benchmarks-jmh.jar
destinationDirectory.set(file("$rootDir"))
}

Expand Down Expand Up @@ -57,8 +58,10 @@ dependencies {
implementation(libs.jackson.databind)
implementation(libs.jackson.module.kotlin)
implementation(libs.okio)
implementation(libs.kotlinx.io)
implementation(project(":kotlinx-serialization-core"))
implementation(project(":kotlinx-serialization-json"))
implementation(project(":kotlinx-serialization-json-okio"))
implementation(project(":kotlinx-serialization-json-io"))
implementation(project(":kotlinx-serialization-protobuf"))
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,17 @@ import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import kotlinx.benchmarks.model.MacroTwitterFeed
import kotlinx.benchmarks.model.MicroTwitterFeed
import kotlinx.io.*
import kotlinx.serialization.json.*
import kotlinx.serialization.json.io.*
import kotlinx.serialization.json.okio.*
import okio.*
import org.openjdk.jmh.annotations.*
import java.io.*
import java.nio.file.Files
import java.nio.file.Path
import java.util.concurrent.TimeUnit
import kotlin.io.path.deleteIfExists
import kotlin.io.path.outputStream
import kotlin.io.use
import okio.Buffer as OkioBuffer
import okio.Sink as OkioSink

@Warmup(iterations = 7, time = 1)
@Measurement(iterations = 7, time = 1)
Expand Down Expand Up @@ -67,6 +70,25 @@ open class TwitterFeedStreamBenchmark {
}
}

@Benchmark
fun encodeTwitterOkioStream(): OkioSink {
val b = OkioBuffer()
Json.encodeToBufferedSink(MacroTwitterFeed.serializer(), twitter, b)
return b
}

@Benchmark
fun encodeTwitterKotlinxIoStream(): Sink {
val b = Buffer()
Json.encodeToSink(MacroTwitterFeed.serializer(), twitter, b)
return b
}

/**
* While encode* benchmarks use MacroTwitterFeed model to output as many bytes as possible,
* decode* benchmarks use MicroTwitterFeed model to also factor for skipping over unnecessary data.
*/

// Difference with TwitterFeedBenchmark.decodeMicroTwitter shows how heavy Java's standard UTF-8 decoding actually is.
@Benchmark
fun decodeMicroTwitterReadText(): MicroTwitterFeed {
Expand All @@ -88,4 +110,24 @@ open class TwitterFeedStreamBenchmark {
objectMapper.readValue(it, MicroTwitterFeed::class.java)
}
}

@Benchmark
fun decodeMicroTwitterOkioStream(): MicroTwitterFeed {
// It seems there is no optimal way to reuse `bytes` between benchmark, so we are forced
// to write them to buffer every time.
// Note that it makes comparison with Jackson and InputStream integration much less meaningful.
val b = OkioBuffer()
b.write(bytes)
return jsonIgnoreUnknwn.decodeFromBufferedSource(MicroTwitterFeed.serializer(), b)
}

@Benchmark
fun decodeMicroTwitterKotlinxIoStream(): MicroTwitterFeed {
// It seems there is no way to reuse filled buffer between benchmark iterations, so we are forced
// to write bytes to buffer every time.
// Note that it makes comparison with Jackson and InputStream integration much less meaningful.
val b = Buffer()
b.write(bytes)
return jsonIgnoreUnknwn.decodeFromSource(MicroTwitterFeed.serializer(), b)
}
}
3 changes: 2 additions & 1 deletion build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -184,9 +184,10 @@ val experimentalsInTestEnabled get() = listOf(
val documentedSubprojects get() = setOf("kotlinx-serialization-core",
"kotlinx-serialization-json",
"kotlinx-serialization-json-okio",
"kotlinx-serialization-json-io",
"kotlinx-serialization-cbor",
"kotlinx-serialization-properties",
"kotlinx-serialization-hocon",
"kotlinx-serialization-protobuf")

val uncoveredProjects get() = setOf("kotlinx-serialization-bom", "benchmark", "guide", "kotlinx-serialization-json-okio")
val uncoveredProjects get() = setOf("kotlinx-serialization-bom", "benchmark", "guide", "kotlinx-serialization-json-okio", "kotlinx-serialization-json-io")
15 changes: 11 additions & 4 deletions buildSrc/src/main/kotlin/publishing-conventions.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,16 @@ plugins {
signing
}

val isMultiplatform = name in listOf("kotlinx-serialization-core", "kotlinx-serialization-json", "kotlinx-serialization-json-okio",
"kotlinx-serialization-json-tests", "kotlinx-serialization-protobuf", "kotlinx-serialization-cbor",
"kotlinx-serialization-properties")
val isMultiplatform = name in listOf(
"kotlinx-serialization-core",
"kotlinx-serialization-json",
"kotlinx-serialization-json-okio",
"kotlinx-serialization-json-io",
"kotlinx-serialization-json-tests",
"kotlinx-serialization-protobuf",
"kotlinx-serialization-cbor",
"kotlinx-serialization-properties"
)

val isBom = name == "kotlinx-serialization-bom"

Expand Down Expand Up @@ -221,4 +228,4 @@ fun mavenRepositoryUri(): URI {

fun Project.getSensitiveProperty(name: String): String? {
return project.findProperty(name) as? String ?: System.getenv(name)
}
}
7 changes: 7 additions & 0 deletions dokka/moduledoc.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ Stable and ready to use JSON format implementation, `JsonElement` API to operate
Extensions for kotlinx.serialization.json.Json for integration with the popular [Okio](https://square.github.io/okio/) library.
Currently experimental.

# Module kotlinx-serialization-json-io
Extensions for kotlinx.serialization.json.Json for integration with the [kotlinx-io](https://github.com/Kotlin/kotlinx-io) library.
Currently experimental.

# Module kotlinx-serialization-cbor
Concise Binary Object Representation (CBOR) format implementation, as per [RFC 7049](https://tools.ietf.org/html/rfc7049).

Expand Down Expand Up @@ -49,6 +53,9 @@ and JSON-specific serializers.
# Package kotlinx.serialization.json.okio
Extensions for kotlinx.serialization.json.Json for integration with the popular [Okio](https://square.github.io/okio/) library.

# Package kotlinx.serialization.json.io
Extensions for kotlinx.serialization.json.Json for integration with the [kotlinx-io](https://github.com/Kotlin/kotlinx-io) library.

# Package kotlinx.serialization.protobuf
[Protocol buffers](https://protobuf.dev/) serialization format implementation.

Expand Down
7 changes: 7 additions & 0 deletions formats/json-io/api/kotlinx-serialization-json-io.api
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
public final class kotlinx/serialization/json/io/IoStreamsKt {
public static final fun decodeFromSource (Lkotlinx/serialization/json/Json;Lkotlinx/serialization/DeserializationStrategy;Lkotlinx/io/Source;)Ljava/lang/Object;
public static final fun decodeSourceToSequence (Lkotlinx/serialization/json/Json;Lkotlinx/io/Source;Lkotlinx/serialization/DeserializationStrategy;Lkotlinx/serialization/json/DecodeSequenceMode;)Lkotlin/sequences/Sequence;
public static synthetic fun decodeSourceToSequence$default (Lkotlinx/serialization/json/Json;Lkotlinx/io/Source;Lkotlinx/serialization/DeserializationStrategy;Lkotlinx/serialization/json/DecodeSequenceMode;ILjava/lang/Object;)Lkotlin/sequences/Sequence;
public static final fun encodeToSink (Lkotlinx/serialization/json/Json;Lkotlinx/serialization/SerializationStrategy;Ljava/lang/Object;Lkotlinx/io/Sink;)V
}

14 changes: 14 additions & 0 deletions formats/json-io/api/kotlinx-serialization-json-io.klib.api
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// Klib ABI Dump
// Targets: [androidNativeArm32, androidNativeArm64, androidNativeX64, androidNativeX86, iosArm64, iosSimulatorArm64, iosX64, js, linuxArm32Hfp, linuxArm64, linuxX64, macosArm64, macosX64, mingwX64, tvosArm64, tvosSimulatorArm64, tvosX64, wasmJs, wasmWasi, watchosArm32, watchosArm64, watchosDeviceArm64, watchosSimulatorArm64, watchosX64]
// Rendering settings:
// - Signature version: 2
// - Show manifest properties: true
// - Show declarations: true

// Library unique name: <org.jetbrains.kotlinx:kotlinx-serialization-json-io>
final fun <#A: kotlin/Any?> (kotlinx.serialization.json/Json).kotlinx.serialization.json.io/decodeFromSource(kotlinx.serialization/DeserializationStrategy<#A>, kotlinx.io/Source): #A // kotlinx.serialization.json.io/decodeFromSource|decodeFromSource@kotlinx.serialization.json.Json(kotlinx.serialization.DeserializationStrategy<0:0>;kotlinx.io.Source){0§<kotlin.Any?>}[0]
final fun <#A: kotlin/Any?> (kotlinx.serialization.json/Json).kotlinx.serialization.json.io/decodeSourceToSequence(kotlinx.io/Source, kotlinx.serialization/DeserializationStrategy<#A>, kotlinx.serialization.json/DecodeSequenceMode = ...): kotlin.sequences/Sequence<#A> // kotlinx.serialization.json.io/decodeSourceToSequence|decodeSourceToSequence@kotlinx.serialization.json.Json(kotlinx.io.Source;kotlinx.serialization.DeserializationStrategy<0:0>;kotlinx.serialization.json.DecodeSequenceMode){0§<kotlin.Any?>}[0]
final fun <#A: kotlin/Any?> (kotlinx.serialization.json/Json).kotlinx.serialization.json.io/encodeToSink(kotlinx.serialization/SerializationStrategy<#A>, #A, kotlinx.io/Sink) // kotlinx.serialization.json.io/encodeToSink|encodeToSink@kotlinx.serialization.json.Json(kotlinx.serialization.SerializationStrategy<0:0>;0:0;kotlinx.io.Sink){0§<kotlin.Any?>}[0]
final inline fun <#A: reified kotlin/Any?> (kotlinx.serialization.json/Json).kotlinx.serialization.json.io/decodeFromSource(kotlinx.io/Source): #A // kotlinx.serialization.json.io/decodeFromSource|decodeFromSource@kotlinx.serialization.json.Json(kotlinx.io.Source){0§<kotlin.Any?>}[0]
final inline fun <#A: reified kotlin/Any?> (kotlinx.serialization.json/Json).kotlinx.serialization.json.io/decodeSourceToSequence(kotlinx.io/Source, kotlinx.serialization.json/DecodeSequenceMode = ...): kotlin.sequences/Sequence<#A> // kotlinx.serialization.json.io/decodeSourceToSequence|decodeSourceToSequence@kotlinx.serialization.json.Json(kotlinx.io.Source;kotlinx.serialization.json.DecodeSequenceMode){0§<kotlin.Any?>}[0]
final inline fun <#A: reified kotlin/Any?> (kotlinx.serialization.json/Json).kotlinx.serialization.json.io/encodeToSink(#A, kotlinx.io/Sink) // kotlinx.serialization.json.io/encodeToSink|encodeToSink@kotlinx.serialization.json.Json(0:0;kotlinx.io.Sink){0§<kotlin.Any?>}[0]
44 changes: 44 additions & 0 deletions formats/json-io/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/*
* Copyright 2017-2022 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
*/
import Java9Modularity.configureJava9ModuleInfo
import org.jetbrains.dokka.gradle.*
import java.net.*

plugins {
kotlin("multiplatform")
kotlin("plugin.serialization")

id("native-targets-conventions")
id("source-sets-conventions")
}

kotlin {
sourceSets {
configureEach {
languageSettings {
optIn("kotlinx.serialization.internal.CoreFriendModuleApi")
optIn("kotlinx.serialization.json.internal.JsonFriendModuleApi")
}
}
val commonMain by getting {
dependencies {
api(project(":kotlinx-serialization-core"))
api(project(":kotlinx-serialization-json"))
implementation(libs.kotlinx.io)
}
}
}
}

project.configureJava9ModuleInfo()

tasks.named<DokkaTaskPartial>("dokkaHtmlPartial") {
dokkaSourceSets {
configureEach {
externalDocumentationLink {
url.set(URL("https://kotlin.github.io/kotlinx-io/"))
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
/*
* Copyright 2017-2022 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
*/

package kotlinx.serialization.json.io

import kotlinx.serialization.*
import kotlinx.serialization.json.DecodeSequenceMode
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.internal.*
import kotlinx.serialization.json.io.internal.JsonToIoStreamWriter
import kotlinx.serialization.json.internal.decodeToSequenceByReader
import kotlinx.serialization.json.io.internal.IoSerialReader
import kotlinx.io.*

/**
* Serializes the [value] with [serializer] into a [sink] using JSON format and UTF-8 encoding.
*
* @throws [SerializationException] if the given value cannot be serialized to JSON.
* @throws [kotlinx.io.IOException] If an I/O error occurs and sink can't be written to.
*/
@ExperimentalSerializationApi
public fun <T> Json.encodeToSink(
serializer: SerializationStrategy<T>,
value: T,
sink: Sink
) {
val writer = JsonToIoStreamWriter(sink)
try {
encodeByWriter(this, writer, serializer, value)
} finally {
writer.release()
}
}

/**
* Serializes given [value] to a [sink] using UTF-8 encoding and serializer retrieved from the reified type parameter.
*
* @throws [SerializationException] if the given value cannot be serialized to JSON.
* @throws [kotlinx.io.IOException] If an I/O error occurs and sink can't be written to.
*/
@ExperimentalSerializationApi
public inline fun <reified T> Json.encodeToSink(
value: T,
sink: Sink
): Unit = encodeToSink(serializersModule.serializer(), value, sink)


/**
* Deserializes JSON from [source] using UTF-8 encoding to a value of type [T] using [deserializer].
*
* Note that this functions expects that exactly one object would be present in the source
* and throws an exception if there are any dangling bytes after an object.
*
* @throws [SerializationException] if the given JSON input cannot be deserialized to the value of type [T].
* @throws [kotlinx.io.IOException] If an I/O error occurs and source can't be read from.
*/
@ExperimentalSerializationApi
public fun <T> Json.decodeFromSource(
deserializer: DeserializationStrategy<T>,
source: Source
): T {
return decodeByReader(this, deserializer, IoSerialReader(source))
}

/**
* Deserializes the contents of given [source] to the value of type [T] using UTF-8 encoding and
* deserializer retrieved from the reified type parameter.
*
* Note that this functions expects that exactly one object would be present in the stream
* and throws an exception if there are any dangling bytes after an object.
*
* @throws [SerializationException] if the given JSON input cannot be deserialized to the value of type [T].
* @throws [kotlinx.io.IOException] If an I/O error occurs and source can't be read from.
*/
@ExperimentalSerializationApi
public inline fun <reified T> Json.decodeFromSource(source: Source): T =
decodeFromSource(serializersModule.serializer(), source)


/**
* Transforms the given [source] into lazily deserialized sequence of elements of type [T] using UTF-8 encoding and [deserializer].
* Unlike [decodeFromSource], [source] is allowed to have more than one element, separated as [format] declares.
*
* Elements must all be of type [T].
* Elements are parsed lazily when resulting [Sequence] is evaluated.
* Resulting sequence is tied to the stream and can be evaluated only once.
*
* **Resource caution:** this method neither closes the [source] when the parsing is finished nor provides a method to close it manually.
* It is a caller responsibility to hold a reference to a source and close it. Moreover, because source is parsed lazily,
* closing it before returned sequence is evaluated completely will result in [Exception] from decoder.
*
* @throws [SerializationException] if the given JSON input cannot be deserialized to the value of type [T].
* @throws [kotlinx.io.IOException] If an I/O error occurs and source can't be read from.
*/
@ExperimentalSerializationApi
public fun <T> Json.decodeSourceToSequence(
source: Source,
deserializer: DeserializationStrategy<T>,
format: DecodeSequenceMode = DecodeSequenceMode.AUTO_DETECT
): Sequence<T> {
return decodeToSequenceByReader(this, IoSerialReader(source), deserializer, format)
}

/**
* Transforms the given [source] into lazily deserialized sequence of elements of type [T] using UTF-8 encoding and deserializer retrieved from the reified type parameter.
* Unlike [decodeSourceToSequence], [source] is allowed to have more than one element, separated as [format] declares.
*
* Elements must all be of type [T].
* Elements are parsed lazily when resulting [Sequence] is evaluated.
* Resulting sequence is tied to the stream and constrained to be evaluated only once.
*
* **Resource caution:** this method does not close [source] when the parsing is finished neither provides method to close it manually.
* It is a caller responsibility to hold a reference to a source and close it. Moreover, because source is parsed lazily,
* closing it before returned sequence is evaluated fully would result in [Exception] from decoder.
*
* @throws [SerializationException] if the given JSON input cannot be deserialized to the value of type [T].
* @throws [kotlinx.io.IOException] If an I/O error occurs and source can't be read from.
*/
@ExperimentalSerializationApi
public inline fun <reified T> Json.decodeSourceToSequence(
source: Source,
format: DecodeSequenceMode = DecodeSequenceMode.AUTO_DETECT
): Sequence<T> = decodeSourceToSequence(source, serializersModule.serializer(), format)
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/*
* Copyright 2017-2022 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
*/

package kotlinx.serialization.json.io.internal

import kotlinx.io.*
import kotlinx.serialization.json.internal.*

private const val QUOTE_CODE = '"'.code

internal class JsonToIoStreamWriter(private val sink: Sink) : InternalJsonWriter {

override fun writeLong(value: Long) {
write(value.toString())
}

override fun writeChar(char: Char) {
sink.writeCodePointValue(char.code)
}

override fun write(text: String) {
sink.writeString(text)
}

override fun writeQuoted(text: String) {
sink.writeCodePointValue(QUOTE_CODE)
InternalJsonWriter.doWriteEscaping(text) { s, start, end -> sink.writeString(s, start, end) }
sink.writeCodePointValue(QUOTE_CODE)
}

override fun release() {
// no-op, see https://github.com/Kotlin/kotlinx.serialization/pull/1982#discussion_r915043700
}
}

internal class IoSerialReader(private val source: Source): InternalJsonReaderCodePointImpl() {
override fun exhausted(): Boolean = source.exhausted()
override fun nextCodePoint(): Int = source.readCodePointValue()
}
5 changes: 0 additions & 5 deletions formats/json-okio/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,6 @@ kotlin {
implementation(libs.okio)
}
}
val commonTest by getting {
dependencies {
implementation(libs.okio)
}
}
}
}

Expand Down
Loading

0 comments on commit 08e604a

Please sign in to comment.