Skip to content

Commit eb7a5d2

Browse files
qwwdfsadfzhinkin
andauthored
UnsafeBufferOperations.forEachSegment implementation (#383)
More concise replacement for 'iterate' function * Add a single sample for 'iterate' and 'BufferIterationContext' * Rewrite previous samples using 'forEachSegment' * Remove 'forEachSegment' overload with offset, 'iterate' is recommended for that Co-authored-by: Filipp Zhinkin <[email protected]>
1 parent 2bb458e commit eb7a5d2

File tree

11 files changed

+146
-67
lines changed

11 files changed

+146
-67
lines changed

core/api/kotlinx-io-core.api

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -305,6 +305,7 @@ public abstract interface class kotlinx/io/unsafe/SegmentWriteContext {
305305

306306
public final class kotlinx/io/unsafe/UnsafeBufferOperations {
307307
public static final field INSTANCE Lkotlinx/io/unsafe/UnsafeBufferOperations;
308+
public final fun forEachSegment (Lkotlinx/io/Buffer;Lkotlin/jvm/functions/Function2;)V
308309
public final fun getMaxSafeWriteCapacity ()I
309310
public final fun iterate (Lkotlinx/io/Buffer;JLkotlin/jvm/functions/Function3;)V
310311
public final fun iterate (Lkotlinx/io/Buffer;Lkotlin/jvm/functions/Function2;)V

core/api/kotlinx-io-core.klib.api

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,7 @@ final object kotlinx.io.unsafe/UnsafeBufferOperations { // kotlinx.io.unsafe/Uns
211211
final fun <get-maxSafeWriteCapacity>(): kotlin/Int // kotlinx.io.unsafe/UnsafeBufferOperations.maxSafeWriteCapacity.<get-maxSafeWriteCapacity>|<get-maxSafeWriteCapacity>(){}[0]
212212

213213
final fun moveToTail(kotlinx.io/Buffer, kotlin/ByteArray, kotlin/Int = ..., kotlin/Int = ...) // kotlinx.io.unsafe/UnsafeBufferOperations.moveToTail|moveToTail(kotlinx.io.Buffer;kotlin.ByteArray;kotlin.Int;kotlin.Int){}[0]
214+
final inline fun forEachSegment(kotlinx.io/Buffer, kotlin/Function2<kotlinx.io.unsafe/SegmentReadContext, kotlinx.io/Segment, kotlin/Unit>) // kotlinx.io.unsafe/UnsafeBufferOperations.forEachSegment|forEachSegment(kotlinx.io.Buffer;kotlin.Function2<kotlinx.io.unsafe.SegmentReadContext,kotlinx.io.Segment,kotlin.Unit>){}[0]
214215
final inline fun iterate(kotlinx.io/Buffer, kotlin/Function2<kotlinx.io.unsafe/BufferIterationContext, kotlinx.io/Segment?, kotlin/Unit>) // kotlinx.io.unsafe/UnsafeBufferOperations.iterate|iterate(kotlinx.io.Buffer;kotlin.Function2<kotlinx.io.unsafe.BufferIterationContext,kotlinx.io.Segment?,kotlin.Unit>){}[0]
215216
final inline fun iterate(kotlinx.io/Buffer, kotlin/Long, kotlin/Function3<kotlinx.io.unsafe/BufferIterationContext, kotlinx.io/Segment?, kotlin/Long, kotlin/Unit>) // kotlinx.io.unsafe/UnsafeBufferOperations.iterate|iterate(kotlinx.io.Buffer;kotlin.Long;kotlin.Function3<kotlinx.io.unsafe.BufferIterationContext,kotlinx.io.Segment?,kotlin.Long,kotlin.Unit>){}[0]
216217
final inline fun readFromHead(kotlinx.io/Buffer, kotlin/Function2<kotlinx.io.unsafe/SegmentReadContext, kotlinx.io/Segment, kotlin/Int>): kotlin/Int // kotlinx.io.unsafe/UnsafeBufferOperations.readFromHead|readFromHead(kotlinx.io.Buffer;kotlin.Function2<kotlinx.io.unsafe.SegmentReadContext,kotlinx.io.Segment,kotlin.Int>){}[0]

core/apple/src/BuffersApple.kt

Lines changed: 7 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -58,19 +58,14 @@ internal fun Buffer.snapshotAsNSData(): NSData {
5858
val bytes = malloc(size.convert())?.reinterpret<uint8_tVar>()
5959
?: throw Error("malloc failed: ${strerror(errno)?.toKString()}")
6060

61-
UnsafeBufferOperations.iterate(this) { ctx, head ->
62-
var curr: Segment? = head
63-
var index = 0
64-
while (curr != null) {
65-
val segment: Segment = curr
66-
ctx.withData(segment) { data, pos, limit ->
67-
val length = limit - pos
68-
data.usePinned {
69-
memcpy(bytes + index, it.addressOf(pos), length.convert())
70-
}
71-
index += length
61+
var index = 0
62+
UnsafeBufferOperations.forEachSegment(this) { ctx, segment ->
63+
ctx.withData(segment) { data, pos, limit ->
64+
val length = limit - pos
65+
data.usePinned {
66+
memcpy(bytes + index, it.addressOf(pos), length.convert())
7267
}
73-
curr = ctx.next(segment)
68+
index += length
7469
}
7570
}
7671
return NSData.create(bytesNoCopy = bytes, length = size.convert())

core/common/src/Buffer.kt

Lines changed: 10 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -554,21 +554,16 @@ public class Buffer : Source, Sink {
554554
val len = minOf(maxPrintableBytes, size).toInt()
555555

556556
val builder = StringBuilder(len * 2 + if (size > maxPrintableBytes) 1 else 0)
557-
558-
UnsafeBufferOperations.iterate(this) { ctx, head ->
559-
var bytesWritten = 0
560-
var seg: Segment? = head
561-
do {
562-
seg!!
563-
var idx = 0
564-
while (bytesWritten < len && idx < seg.size) {
565-
val b = ctx.getUnchecked(seg, idx++)
566-
bytesWritten++
567-
builder.append(HEX_DIGIT_CHARS[(b shr 4) and 0xf])
568-
.append(HEX_DIGIT_CHARS[b and 0xf])
569-
}
570-
seg = ctx.next(seg)
571-
} while (seg != null)
557+
var bytesWritten = 0
558+
UnsafeBufferOperations.forEachSegment(this) { ctx, segment ->
559+
var idx = 0
560+
while (bytesWritten < len && idx < segment.size) {
561+
val b = ctx.getUnchecked(segment, idx++)
562+
bytesWritten++
563+
builder
564+
.append(HEX_DIGIT_CHARS[(b shr 4) and 0xf])
565+
.append(HEX_DIGIT_CHARS[b and 0xf])
566+
}
572567
}
573568

574569
if (size > maxPrintableBytes) {

core/common/src/Buffers.kt

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,8 @@ public fun Buffer.snapshot(): ByteString {
2222
check(size <= Int.MAX_VALUE) { "Buffer is too long ($size) to be converted into a byte string." }
2323

2424
return buildByteString(size.toInt()) {
25-
UnsafeBufferOperations.iterate(this@snapshot) { ctx, head ->
26-
var curr = head
27-
while (curr != null) {
28-
ctx.withData(curr, this::append)
29-
curr = ctx.next(curr)
30-
}
25+
UnsafeBufferOperations.forEachSegment(this@snapshot) { ctx, segment ->
26+
ctx.withData(segment, this::append)
3127
}
3228
}
3329
}

core/common/src/Utf8.kt

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -607,17 +607,17 @@ private fun Buffer.commonReadUtf8(byteCount: Long): String {
607607
// Invariant: byteCount was request()'ed into this buffer beforehand
608608
if (byteCount == 0L) return ""
609609

610-
UnsafeBufferOperations.iterate(this) { ctx, head ->
611-
head!!
612-
if (head.size >= byteCount) {
610+
UnsafeBufferOperations.forEachSegment(this) { ctx, segment ->
611+
if (segment.size >= byteCount) {
613612
var result = ""
614-
ctx.withData(head) { data, pos, limit ->
613+
ctx.withData(segment) { data, pos, limit ->
615614
result = data.commonToUtf8String(pos, min(limit, pos + byteCount.toInt()))
616615
skip(byteCount)
617616
return result
618617
}
619618
}
620-
}
621619
// If the string spans multiple segments, delegate to readBytes()
622620
return readByteArray(byteCount.toInt()).commonToUtf8String()
623621
}
622+
error("Unreacheable")
623+
}

core/common/src/unsafe/UnsafeBufferOperations.kt

Lines changed: 33 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -295,13 +295,14 @@ public object UnsafeBufferOperations {
295295
* the [iterationAction].
296296
*
297297
* Both [iterationAction] arguments are valid only within [iterationAction] scope,
298-
* it's an error to store and reuse it later.
298+
* it is an error to store and reuse it later.
299+
*
300+
* For a full iteration over buffer's segments, see [forEachSegment].
299301
*
300302
* @param buffer a buffer to iterate over
301303
* @param iterationAction a callback to invoke with the head reference and an iteration context instance
302304
*
303-
* @sample kotlinx.io.samples.unsafe.UnsafeReadWriteSamplesJvm.messageDigest
304-
* @sample kotlinx.io.samples.unsafe.UnsafeBufferOperationsSamples.crc32Unsafe
305+
* @sample kotlinx.io.samples.unsafe.UnsafeBufferOperationsSamples.crc32GetUnchecked
305306
*/
306307
public inline fun iterate(
307308
buffer: Buffer,
@@ -352,6 +353,34 @@ public object UnsafeBufferOperations {
352353
iterationAction(BufferIterationContextImpl, s, o)
353354
}
354355
}
356+
357+
/**
358+
* Iterates over [buffer] segments starting from the head.
359+
*
360+
* [action] is invoked with an instance of [SegmentReadContext]
361+
* allowing to read and write in an unchecked manner from [buffer]'s segments
362+
*
363+
* It is considered an error to use a [SegmentReadContext] or a [Segment] instances outside the scope of
364+
* the [action].
365+
*
366+
* Both [action] arguments are valid only within [action] scope, it is an error to store and reuse it later.
367+
* The action might never be invoked if the given [buffer] is empty.
368+
*
369+
* @param buffer a buffer to iterate over
370+
* @param action a callback to invoke with the head reference and an iteration context instance
371+
* @sample kotlinx.io.samples.unsafe.UnsafeReadWriteSamplesJvm.messageDigest
372+
* @sample kotlinx.io.samples.unsafe.UnsafeBufferOperationsSamples.crc32Unsafe
373+
*/
374+
public inline fun forEachSegment(
375+
buffer: Buffer,
376+
action: (context: SegmentReadContext, segment: Segment) -> Unit
377+
) {
378+
var curr: Segment? = buffer.head
379+
while (curr != null) {
380+
action(SegmentReadContextImpl, curr)
381+
curr = curr.next
382+
}
383+
}
355384
}
356385

357386
/**
@@ -494,8 +523,7 @@ public interface BufferIterationContext : SegmentReadContext {
494523
*
495524
* @param segment a segment for which a successor needs to be found
496525
*
497-
* @sample kotlinx.io.samples.unsafe.UnsafeReadWriteSamplesJvm.messageDigest
498-
* @sample kotlinx.io.samples.unsafe.UnsafeBufferOperationsSamples.crc32Unsafe
526+
* @sample kotlinx.io.samples.unsafe.UnsafeBufferOperationsSamples.crc32GetUnchecked
499527
*/
500528
public fun next(segment: Segment): Segment?
501529
}

core/common/test/samples/unsafe/unsafeSamples.kt

Lines changed: 8 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -267,20 +267,15 @@ class UnsafeBufferOperationsSamples {
267267
fun Buffer.crc32(): UInt {
268268
var crc32 = 0xffffffffU
269269
// Iterate over segments starting from buffer's head
270-
UnsafeBufferOperations.iterate(this) { ctx, head ->
271-
var currentSegment = head
272-
// If a current segment is null, it means we ran out of segments.
273-
while (currentSegment != null) {
274-
// Get data from a segment
275-
ctx.withData(currentSegment) { data, startIndex, endIndex ->
276-
for (idx in startIndex..<endIndex) {
277-
// Update crc32
278-
val index = data[idx].xor(crc32.toByte()).toUByte()
279-
crc32 = crc32Table[index.toInt()].xor(crc32.shr(8))
280-
}
270+
UnsafeBufferOperations.forEachSegment(this) { ctx, segment ->
271+
var currentSegment = segment
272+
// Get data from a segment
273+
ctx.withData(currentSegment) { data, startIndex, endIndex ->
274+
for (idx in startIndex..<endIndex) {
275+
// Update crc32
276+
val index = data[idx].toUInt().xor(crc32).toUByte()
277+
crc32 = crc32Table[index.toInt()].xor(crc32.shr(8))
281278
}
282-
// Advance to the next segment
283-
currentSegment = ctx.next(currentSegment)
284279
}
285280
}
286281
return crc32.xor(0xffffffffU)
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
/*
2+
* Copyright 2010-2024 JetBrains s.r.o. and Kotlin Programming Language contributors.
3+
* Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file.
4+
*/
5+
@file:OptIn(UnsafeIoApi::class)
6+
7+
package kotlinx.io.unsafe
8+
9+
import kotlinx.io.Buffer
10+
import kotlinx.io.UnsafeIoApi
11+
import kotlinx.io.assertArrayEquals
12+
import kotlinx.io.writeString
13+
import kotlin.test.Test
14+
import kotlin.test.assertEquals
15+
import kotlin.test.assertFailsWith
16+
import kotlin.test.assertNotNull
17+
import kotlin.test.assertNull
18+
import kotlin.test.assertTrue
19+
import kotlin.test.fail
20+
21+
class UnsafeBufferOperationsForEachTest {
22+
23+
@Test
24+
fun emptyBuffer() {
25+
UnsafeBufferOperations.forEachSegment(Buffer()) { _, head ->
26+
fail()
27+
}
28+
}
29+
30+
@Test
31+
fun singleSegment() {
32+
var counter = 0
33+
UnsafeBufferOperations.forEachSegment(Buffer().also { it.writeByte(1) }) { ctx, segment ->
34+
++counter
35+
assertEquals(1, segment.size)
36+
}
37+
assertEquals(1, counter)
38+
}
39+
40+
@Test
41+
fun multipleSegments() {
42+
val buffer = Buffer()
43+
44+
val expectedSegments = 10
45+
for (i in 0 ..< expectedSegments) {
46+
UnsafeBufferOperations.moveToTail(buffer, byteArrayOf(i.toByte()))
47+
}
48+
49+
val storedBytes = ByteArray(expectedSegments)
50+
var idx = 0
51+
UnsafeBufferOperations.forEachSegment(buffer) { ctx, segment ->
52+
assertTrue(idx < expectedSegments)
53+
storedBytes[idx++] = ctx.getUnchecked(segment, 0)
54+
}
55+
56+
assertArrayEquals(ByteArray(expectedSegments) { it.toByte() }, storedBytes)
57+
}
58+
59+
@Test
60+
fun acquireDataDuringIteration() {
61+
val buffer = Buffer().also { it.writeString("hello buffer") }
62+
63+
val expectedSize = buffer.size
64+
65+
UnsafeBufferOperations.forEachSegment(buffer) { ctx, segment ->
66+
ctx.withData(segment) { data, startIndex, endIndex ->
67+
assertEquals("hello buffer", data.decodeToString(startIndex, endIndex))
68+
}
69+
}
70+
71+
assertEquals(expectedSize, buffer.size)
72+
}
73+
}

core/common/test/unsafe/UnsafeBufferOperationsIterationTest.kt

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
* Copyright 2010-2024 JetBrains s.r.o. and Kotlin Programming Language contributors.
33
* Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file.
44
*/
5+
@file:OptIn(UnsafeIoApi::class)
56

67
package kotlinx.io.unsafe
78

@@ -11,8 +12,6 @@ import kotlinx.io.assertArrayEquals
1112
import kotlinx.io.writeString
1213
import kotlin.test.*
1314

14-
@OptIn(UnsafeIoApi::class)
15-
1615
class UnsafeBufferOperationsIterationTest {
1716
@Test
1817
fun callsInPlaceContract() {

core/jvm/test/samples/unsafeAccessSamplesJvm.kt

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -108,17 +108,13 @@ class UnsafeReadWriteSamplesJvm {
108108
fun Buffer.digest(algorithm: String): ByteString {
109109
val md = MessageDigest.getInstance(algorithm)
110110
// iterate over all segment and update data
111-
UnsafeBufferOperations.iterate(this) { ctx, head ->
112-
var segment = head
111+
UnsafeBufferOperations.forEachSegment(this) { ctx, segment ->
113112
// when segment is null, we reached the end of a buffer
114-
while (segment != null) {
115-
// access segment data without copying it
116-
ctx.withData(segment) { data, startIndex, endIndex ->
117-
md.update(data, startIndex, endIndex - startIndex)
118-
}
119-
// advance to the next segment
120-
segment = ctx.next(segment)
113+
// access segment data without copying it
114+
ctx.withData(segment) { data, startIndex, endIndex ->
115+
md.update(data, startIndex, endIndex - startIndex)
121116
}
117+
// advance to the next segment
122118
}
123119
return UnsafeByteStringOperations.wrapUnsafe(md.digest())
124120
}

0 commit comments

Comments
 (0)