Skip to content

Commit 6a172a7

Browse files
authored
A trimmed-down API (#136)
What changed compared to the previous API version: - Removed byte strings and ByteString related APIs - Moved some methods that were previously implemented as member functions to extensions, provided basic implementation - Get rid of hash functions and select API - Get rid of unsafe cursors - Updated documentation - Changed Buffer::toString behavior - Improved test coverage - Changed naming of read/write methods, explicitly documented exception policies - Added extensions for unsigned types - Made code-point-related extensions internal - On JVM, Buffer no longer implements NIO interfaces - Buffer has default equals/hashCode A few new build features were added: - Enable Dokka - Enable Kover for core-io - Enabled explicit API mode
1 parent 0e5ae2b commit 6a172a7

File tree

95 files changed

+5941
-12092
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

95 files changed

+5941
-12092
lines changed

benchmarks/src/commonMain/kotlin/BufferOps.kt

+27-39
Original file line numberDiff line numberDiff line change
@@ -97,18 +97,21 @@ open class DecimalLongBenchmark: BufferRWBenchmarkBase() {
9797
var value = 0L
9898

9999
override fun padding(): ByteArray {
100-
val tmpBuffer = Buffer()
101-
while (tmpBuffer.size < minGap) {
102-
// use space as a delimiter between consecutive decimal values
103-
tmpBuffer.writeDecimalLong(value).writeByte(' '.code)
100+
return with (Buffer()) {
101+
while (size < minGap) {
102+
writeDecimalLong(value)
103+
// use space as a delimiter between consecutive decimal values
104+
writeByte(' '.code.toByte())
105+
}
106+
readByteArray()
104107
}
105-
return tmpBuffer.readByteArray()
106108
}
107109

108110
@Benchmark
109111
fun benchmark(): Long {
110112
// use space as a delimiter between consecutive decimal values
111-
buffer.writeDecimalLong(value).writeByte(' '.code)
113+
buffer.writeDecimalLong(value)
114+
buffer.writeByte(' '.code.toByte())
112115
val l = buffer.readDecimalLong()
113116
buffer.readByte() // consume the delimiter
114117
return l
@@ -120,45 +123,25 @@ open class HexadecimalLongBenchmark: BufferRWBenchmarkBase() {
120123
var value = 0L
121124

122125
override fun padding(): ByteArray {
123-
val tmpBuffer = Buffer()
124-
while (tmpBuffer.size < minGap) {
125-
tmpBuffer.writeHexadecimalUnsignedLong(value).writeByte(' '.code)
126+
return with(Buffer()) {
127+
while (size < minGap) {
128+
writeHexadecimalUnsignedLong(value)
129+
writeByte(' '.code.toByte())
130+
}
131+
readByteArray()
126132
}
127-
return tmpBuffer.readByteArray()
128133
}
129134

130135
@Benchmark
131136
fun benchmark(): Long {
132-
buffer.writeHexadecimalUnsignedLong(value).writeByte(' '.code)
137+
buffer.writeHexadecimalUnsignedLong(value)
138+
buffer.writeByte(' '.code.toByte())
133139
val l = buffer.readHexadecimalUnsignedLong()
134140
buffer.readByte()
135141
return l
136142
}
137143
}
138144

139-
open class Utf8CodePointBenchmark: BufferRWBenchmarkBase() {
140-
@Param("1", "2", "3")
141-
var bytes: Int = 0
142-
143-
private var codePoint: Int = 0
144-
145-
@Setup
146-
fun setupCodePoints() {
147-
codePoint = when (bytes) {
148-
1 -> 'a'.code
149-
2 -> 'ɐ'.code
150-
3 -> ''.code
151-
else -> throw IllegalArgumentException()
152-
}
153-
}
154-
155-
@Benchmark
156-
fun benchmark(): Int {
157-
buffer.writeUtf8CodePoint(codePoint)
158-
return buffer.readUtf8CodePoint()
159-
}
160-
}
161-
162145
// This benchmark is based on Okio benchmark:
163146
// https://raw.githubusercontent.com/square/okio/master/okio/jvm/jmh/src/jmh/java/com/squareup/okio/benchmarks/BufferUtf8Benchmark.java
164147
open class Utf8StringBenchmark: BufferRWBenchmarkBase() {
@@ -211,8 +194,9 @@ open class Utf8StringBenchmark: BufferRWBenchmarkBase() {
211194

212195
override fun padding(): ByteArray {
213196
val baseString = constructString()
214-
if (baseString.utf8Size() >= minGap) {
215-
return baseString.encodeToByteArray()
197+
val baseStringByteArray = baseString.encodeToByteArray()
198+
if (baseStringByteArray.size >= minGap) {
199+
return baseStringByteArray
216200
}
217201
val builder = StringBuilder((minGap * 1.5).toInt())
218202
while (builder.length < minGap) {
@@ -317,7 +301,11 @@ open class IndexOfBenchmark {
317301
if (valueOffset >= 0) array[valueOffset] = VALUE_TO_FIND
318302

319303
val padding = ByteArray(paddingSize)
320-
buffer.write(padding).write(array).skip(paddingSize.toLong())
304+
with(buffer) {
305+
write(padding)
306+
write(array)
307+
skip(paddingSize.toLong())
308+
}
321309
}
322310

323311
@Benchmark
@@ -358,7 +346,7 @@ open class BufferReadWriteByteArray: BufferRWBenchmarkBase() {
358346
@Benchmark
359347
fun benchmark(blackhole: Blackhole) {
360348
buffer.write(inputArray)
361-
buffer.readFully(outputArray)
349+
buffer.readTo(outputArray)
362350
blackhole.consume(outputArray)
363351
}
364352
}
@@ -377,6 +365,6 @@ open class BufferReadNewByteArray: BufferRWBenchmarkBase() {
377365
@Benchmark
378366
fun benchmark(): ByteArray {
379367
buffer.write(inputArray)
380-
return buffer.readByteArray(size.toLong())
368+
return buffer.readByteArray(size)
381369
}
382370
}

benchmarks/src/jvmMain/kotlin/ByteBufferBenchmarks.kt

+6-3
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,13 @@
55

66
package kotlinx.io.benchmarks
77

8+
import kotlinx.benchmark.Benchmark
9+
import kotlinx.benchmark.Param
10+
import kotlinx.benchmark.Setup
11+
import kotlinx.io.readAtMostTo
12+
import kotlinx.io.write
813
import java.nio.ByteBuffer
914

10-
import kotlinx.benchmark.*
11-
1215
open class ByteBufferReadWrite: BufferRWBenchmarkBase() {
1316
private var inputBuffer = ByteBuffer.allocate(0)
1417
private var outputBuffer = ByteBuffer.allocate(0)
@@ -28,7 +31,7 @@ open class ByteBufferReadWrite: BufferRWBenchmarkBase() {
2831
inputBuffer.rewind()
2932
outputBuffer.clear()
3033
buffer.write(inputBuffer)
31-
while (buffer.read(outputBuffer) > 0) {
34+
while (buffer.readAtMostTo(outputBuffer) > 0) {
3235
// do nothing
3336
}
3437
return outputBuffer

benchmarks/src/jvmMain/kotlin/SegmentPoolBenchmarkMT.kt

+3-2
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,16 @@
55

66
package kotlinx.io.benchmarks
77

8-
import kotlinx.io.*
98
import kotlinx.benchmark.*
9+
import kotlinx.io.*
1010
import org.openjdk.jmh.annotations.Group
1111
import org.openjdk.jmh.annotations.GroupThreads
1212

1313
@State(Scope.Benchmark)
1414
open class SegmentPoolBenchmarkMT {
1515
private fun testCycle(): Buffer {
16-
val buffer = Buffer().writeByte(0)
16+
val buffer = Buffer()
17+
buffer.writeByte(0)
1718
buffer.clear()
1819
return buffer
1920
}

benchmarks/src/jvmMain/kotlin/StreamBenchmarks.kt

+12-6
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,16 @@
55

66
package kotlinx.io.benchmarks
77

8-
import kotlinx.benchmark.*
8+
import kotlinx.benchmark.Benchmark
9+
import kotlinx.benchmark.Blackhole
10+
import kotlinx.benchmark.Param
11+
import kotlinx.benchmark.Setup
12+
import kotlinx.io.asInputStream
13+
import kotlinx.io.asOutputStream
14+
import kotlinx.io.readTo
915

1016
open class InputStreamByteRead: BufferRWBenchmarkBase() {
11-
private val stream = buffer.inputStream()
17+
private val stream = buffer.asInputStream()
1218

1319
@Benchmark
1420
fun benchmark(): Int {
@@ -18,7 +24,7 @@ open class InputStreamByteRead: BufferRWBenchmarkBase() {
1824
}
1925

2026
open class OutputStreamByteWrite: BufferRWBenchmarkBase() {
21-
private val stream = buffer.outputStream()
27+
private val stream = buffer.asOutputStream()
2228

2329
@Benchmark
2430
fun benchmark(): Byte {
@@ -42,7 +48,7 @@ abstract class StreamByteArrayBenchmarkBase: BufferRWBenchmarkBase() {
4248
}
4349

4450
open class InputStreamByteArrayRead: StreamByteArrayBenchmarkBase() {
45-
private val stream = buffer.inputStream()
51+
private val stream = buffer.asInputStream()
4652

4753
@Benchmark
4854
fun benchmark(blackhole: Blackhole) {
@@ -56,12 +62,12 @@ open class InputStreamByteArrayRead: StreamByteArrayBenchmarkBase() {
5662
}
5763

5864
open class OutputStreamByteArrayWrite: StreamByteArrayBenchmarkBase() {
59-
private val stream = buffer.outputStream()
65+
private val stream = buffer.asOutputStream()
6066

6167
@Benchmark
6268
fun benchmark(blackhole: Blackhole) {
6369
stream.write(outputArray)
64-
buffer.readFully(inputArray)
70+
buffer.readTo(inputArray)
6571
blackhole.consume(inputArray)
6672
}
6773
}

build.gradle.kts

+15-2
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,12 @@
33
* Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
44
*/
55

6-
import org.jetbrains.kotlin.gradle.tasks.*
6+
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
7+
import org.jetbrains.kotlin.gradle.tasks.KotlinNativeCompile
78

89
plugins {
9-
id("org.jetbrains.kotlinx.binary-compatibility-validator") version "0.11.1"
10+
id("org.jetbrains.kotlinx.binary-compatibility-validator") version "0.13.2"
11+
id("org.jetbrains.dokka") version "1.8.20"
1012
`maven-publish`
1113
signing
1214
}
@@ -33,6 +35,10 @@ apply(plugin = "maven-publish")
3335
apply(plugin = "signing")
3436

3537
subprojects {
38+
if (name.contains("benchmark")) {
39+
return@subprojects
40+
}
41+
3642
repositories {
3743
mavenCentral()
3844
}
@@ -58,6 +64,13 @@ subprojects {
5864
tasks.withType<KotlinCompile>().configureEach {
5965
kotlinOptions {
6066
jvmTarget = JavaVersion.VERSION_1_8.toString()
67+
allWarningsAsErrors = true
68+
freeCompilerArgs += "-Xjvm-default=all"
69+
}
70+
}
71+
tasks.withType<KotlinNativeCompile>().configureEach {
72+
kotlinOptions {
73+
allWarningsAsErrors = true
6174
}
6275
}
6376
}

buildSrc/src/main/kotlin/Platforms.kt

+18-5
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,14 @@ fun KotlinMultiplatformExtension.configureNativePlatforms() {
1717
tvosSimulatorArm64()
1818
watchosArm32()
1919
watchosArm64()
20-
watchosX86()
2120
watchosX64()
2221
watchosSimulatorArm64()
22+
watchosDeviceArm64()
23+
linuxArm64()
24+
androidNativeArm32()
25+
androidNativeArm64()
26+
androidNativeX64()
27+
androidNativeX86()
2328
// Required to generate tests tasks: https://youtrack.jetbrains.com/issue/KT-26547
2429
linuxX64()
2530
macosX64()
@@ -38,20 +43,28 @@ private val appleTargets = listOf(
3843
"tvosSimulatorArm64",
3944
"watchosArm32",
4045
"watchosArm64",
41-
"watchosX86",
4246
"watchosX64",
43-
"watchosSimulatorArm64"
47+
"watchosSimulatorArm64",
48+
"watchosDeviceArm64"
4449
)
4550

4651
private val mingwTargets = listOf(
4752
"mingwX64"
4853
)
4954

5055
private val linuxTargets = listOf(
51-
"linuxX64"
56+
"linuxX64",
57+
"linuxArm64"
5258
)
5359

54-
val nativeTargets = appleTargets + linuxTargets + mingwTargets
60+
private val androidTargets = listOf(
61+
"androidNativeArm32",
62+
"androidNativeArm64",
63+
"androidNativeX64",
64+
"androidNativeX86"
65+
)
66+
67+
val nativeTargets = appleTargets + linuxTargets + mingwTargets + androidTargets
5568

5669
/**
5770
* Creates a source set for a directory that isn't already a built-in platform. Use this to create

core/Module.md

+84
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
# Module kotlinx-io-core
2+
3+
The module provides core multiplatform IO primitives and integrates it with platform-specific APIs.
4+
5+
`kotlinx-io core` aims to provide a concise but powerful API along with efficient implementation.
6+
7+
The main interfaces for the IO interaction are [kotlinx.io.Source] and [kotlinx.io.Sink] providing buffered read and
8+
write operations for integer types, byte arrays, and other sources and sinks. There are also extension functions
9+
bringing support for strings and other types.
10+
Implementations of these interfaces are built on top of [kotlinx.io.Buffer], [kotlinx.io.RawSource],
11+
and [kotlinx.io.RawSink].
12+
13+
A central part of the library, [kotlinx.io.Buffer], is a container optimized to reduce memory allocations and to avoid
14+
data copying when possible.
15+
16+
[kotlinx.io.RawSource] and [kotlinx.io.RawSink] are interfaces aimed for integration with anything that can provide
17+
or receive data: network interfaces, files, etc. The module provides integration with some platform-specific IO APIs,
18+
but if something not yet supported by the library needs to be integrated, then these interfaces are exactly what should
19+
be implemented for that.
20+
21+
Example below shows how to manually serialize an object to [BSON](https://bsonspec.org/spec.html)
22+
and then back to an object using `kotlinx.io`. Please note that the example aimed to show `kotlinx-io` API in action,
23+
rather than to provide a robust BSON-serialization.
24+
```kotlin
25+
data class Message(val timestamp: Long, val text: String) {
26+
companion object
27+
}
28+
29+
fun Message.toBson(sink: Sink) {
30+
val buffer = Buffer()
31+
with (buffer) {
32+
writeByte(0x9) // UTC-timestamp field
33+
writeUtf8("timestamp") // field name
34+
writeByte(0)
35+
writeLongLe(timestamp) // field value
36+
writeByte(0x2) // string field
37+
writeUtf8("text") // field name
38+
writeByte(0)
39+
writeIntLe(text.utf8Size().toInt() + 1) // field value: length followed by the string
40+
writeUtf8(text)
41+
writeByte(0)
42+
writeByte(0) // end of BSON document
43+
}
44+
45+
// Write document length and then its body
46+
sink.writeIntLe(buffer.size.toInt() + 4)
47+
buffer.transferTo(sink)
48+
sink.flush()
49+
}
50+
51+
fun Message.Companion.fromBson(source: Source): Message {
52+
source.require(4) // check if the source contains length
53+
val length = source.readIntLe() - 4L
54+
source.require(length) // check if the source contains the whole message
55+
56+
fun readFieldName(source: Source): String {
57+
val delimiterOffset = source.indexOf(0) // find offset of the 0-byte terminating the name
58+
check(delimiterOffset >= 0) // indexOf return -1 if value not found
59+
val fieldName = source.readUtf8(delimiterOffset) // read the string until terminator
60+
source.skip(1) // skip the terminator
61+
return fieldName
62+
}
63+
64+
// for simplicity, let's assume that the order of fields matches serialization order
65+
var tag = source.readByte().toInt() // read the field type
66+
check(tag == 0x9 && readFieldName(source) == "timestamp")
67+
val timestamp = source.readLongLe() // read long value
68+
tag = source.readByte().toInt()
69+
check(tag == 0x2 && readFieldName(source) == "text")
70+
val textLen = source.readIntLe() - 1L // read string length (it includes the terminator)
71+
val text = source.readUtf8(textLen) // read value
72+
source.skip(1) // skip terminator
73+
source.skip(1) // skip end of the document
74+
return Message(timestamp, text)
75+
}
76+
```
77+
78+
# Package kotlinx.io
79+
80+
Core IO primitives.
81+
82+
# Package kotlinx.io.files
83+
84+
Basic API for working with files.

0 commit comments

Comments
 (0)