Skip to content

Commit ecd60a8

Browse files
authored
Add calls-in-place contracts to unsafe operations (#367)
Fixes #361
1 parent f9ae712 commit ecd60a8

13 files changed

+209
-2
lines changed

bytestring/common/src/unsafe/UnsafeByteStringOperations.kt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@
55

66
package kotlinx.io.bytestring.unsafe
77

8+
import kotlin.contracts.ExperimentalContracts
9+
import kotlin.contracts.InvocationKind.EXACTLY_ONCE
10+
import kotlin.contracts.contract
811
import kotlinx.io.bytestring.ByteString
912

1013
/**
@@ -16,6 +19,7 @@ import kotlinx.io.bytestring.ByteString
1619
* consequences in the code using the byte string and should be avoided at all costs.
1720
*/
1821
@UnsafeByteStringApi
22+
@OptIn(ExperimentalContracts::class)
1923
public object UnsafeByteStringOperations {
2024
/**
2125
* Creates a new byte string by wrapping [array] without copying it.
@@ -32,6 +36,9 @@ public object UnsafeByteStringOperations {
3236
* Consider using [ByteString.toByteArray] if it's impossible to guarantee that the array won't be modified.
3337
*/
3438
public inline fun withByteArrayUnsafe(byteString: ByteString, block: (ByteArray) -> Unit) {
39+
contract {
40+
callsInPlace(block, EXACTLY_ONCE)
41+
}
3542
block(byteString.getBackingArrayReference())
3643
}
3744
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
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+
6+
package kotlinx.io.bytestring.unsafe
7+
8+
import kotlin.test.Test
9+
import kotlin.test.assertTrue
10+
import kotlinx.io.bytestring.encodeToByteString
11+
12+
@OptIn(UnsafeByteStringApi::class)
13+
class UnsafeByteStringOperationsTest {
14+
@Test
15+
fun callsInPlaceContract() {
16+
val byteString = "hello byte string".encodeToByteString()
17+
18+
val called: Boolean
19+
UnsafeByteStringOperations.withByteArrayUnsafe(byteString) {
20+
called = true
21+
}
22+
assertTrue(called)
23+
}
24+
}

core/common/src/Buffer.kt

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@
2020
*/
2121
package kotlinx.io
2222

23+
import kotlin.contracts.ExperimentalContracts
24+
import kotlin.contracts.InvocationKind.EXACTLY_ONCE
25+
import kotlin.contracts.contract
2326
import kotlin.jvm.JvmSynthetic
2427

2528
/**
@@ -706,11 +709,18 @@ public class Buffer : Source, Sink {
706709
*/
707710
@PublishedApi
708711
@JvmSynthetic
712+
@OptIn(ExperimentalContracts::class)
709713
internal inline fun <T> Buffer.seek(
710714
fromIndex: Long,
711715
lambda: (Segment?, Long) -> T
712716
): T {
713-
if (this.head == null) lambda(null, -1L)
717+
contract {
718+
callsInPlace(lambda, EXACTLY_ONCE)
719+
}
720+
721+
if (this.head == null) {
722+
return lambda(null, -1L)
723+
}
714724

715725
if (size - fromIndex < fromIndex) {
716726
var s = tail

core/common/src/Sinks.kt

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@
55

66
package kotlinx.io
77

8+
import kotlin.contracts.ExperimentalContracts
9+
import kotlin.contracts.InvocationKind.EXACTLY_ONCE
10+
import kotlin.contracts.contract
11+
812
private val HEX_DIGIT_BYTES = ByteArray(16) {
913
((if (it < 10) '0'.code else ('a'.code - 10)) + it).toByte()
1014
}
@@ -351,8 +355,11 @@ public fun Sink.writeDoubleLe(double: Double) {
351355
* @throws IllegalStateException when the sink is closed.
352356
*/
353357
@DelicateIoApi
354-
@OptIn(InternalIoApi::class)
358+
@OptIn(InternalIoApi::class, ExperimentalContracts::class)
355359
public inline fun Sink.writeToInternalBuffer(lambda: (Buffer) -> Unit) {
360+
contract {
361+
callsInPlace(lambda, EXACTLY_ONCE)
362+
}
356363
lambda(this.buffer)
357364
this.hintEmit()
358365
}

core/common/src/unsafe/UnsafeBufferOperations.kt

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,14 @@
55

66
package kotlinx.io.unsafe
77

8+
import kotlin.contracts.ExperimentalContracts
9+
import kotlin.contracts.InvocationKind.EXACTLY_ONCE
10+
import kotlin.contracts.contract
811
import kotlinx.io.*
912
import kotlin.jvm.JvmSynthetic
1013

1114
@UnsafeIoApi
15+
@OptIn(ExperimentalContracts::class)
1216
public object UnsafeBufferOperations {
1317
/**
1418
* Maximum value that is safe to pass to [writeToTail].
@@ -88,6 +92,10 @@ public object UnsafeBufferOperations {
8892
buffer: Buffer,
8993
readAction: (bytes: ByteArray, startIndexInclusive: Int, endIndexExclusive: Int) -> Int
9094
): Int {
95+
contract {
96+
callsInPlace(readAction, EXACTLY_ONCE)
97+
}
98+
9199
require(!buffer.exhausted()) { "Buffer is empty" }
92100
val head = buffer.head!!
93101
val bytesRead = readAction(head.dataAsByteArray(true), head.pos, head.limit)
@@ -128,6 +136,10 @@ public object UnsafeBufferOperations {
128136
* @sample kotlinx.io.samples.unsafe.UnsafeBufferOperationsSamples.readUleb128
129137
*/
130138
public inline fun readFromHead(buffer: Buffer, readAction: (SegmentReadContext, Segment) -> Int): Int {
139+
contract {
140+
callsInPlace(readAction, EXACTLY_ONCE)
141+
}
142+
131143
require(!buffer.exhausted()) { "Buffer is empty" }
132144
val head = buffer.head!!
133145
val bytesRead = readAction(SegmentReadContextImpl, head)
@@ -176,6 +188,10 @@ public object UnsafeBufferOperations {
176188
buffer: Buffer, minimumCapacity: Int,
177189
writeAction: (bytes: ByteArray, startIndexInclusive: Int, endIndexExclusive: Int) -> Int
178190
): Int {
191+
contract {
192+
callsInPlace(writeAction, EXACTLY_ONCE)
193+
}
194+
179195
val tail = buffer.writableSegment(minimumCapacity)
180196

181197
val data = tail.dataAsByteArray(false)
@@ -240,6 +256,10 @@ public object UnsafeBufferOperations {
240256
minimumCapacity: Int,
241257
writeAction: (SegmentWriteContext, Segment) -> Int
242258
): Int {
259+
contract {
260+
callsInPlace(writeAction, EXACTLY_ONCE)
261+
}
262+
243263
val tail = buffer.writableSegment(minimumCapacity)
244264
val bytesWritten = writeAction(SegmentWriteContextImpl, tail)
245265

@@ -285,6 +305,9 @@ public object UnsafeBufferOperations {
285305
* @sample kotlinx.io.samples.unsafe.UnsafeBufferOperationsSamples.crc32Unsafe
286306
*/
287307
public inline fun iterate(buffer: Buffer, iterationAction: (BufferIterationContext, Segment?) -> Unit) {
308+
contract {
309+
callsInPlace(iterationAction, EXACTLY_ONCE)
310+
}
288311
iterationAction(BufferIterationContextImpl, buffer.head)
289312
}
290313

@@ -314,6 +337,10 @@ public object UnsafeBufferOperations {
314337
buffer: Buffer, offset: Long,
315338
iterationAction: (BufferIterationContext, Segment?, Long) -> Unit
316339
) {
340+
contract {
341+
callsInPlace(iterationAction, EXACTLY_ONCE)
342+
}
343+
317344
require(offset >= 0) { "Offset must be non-negative: $offset" }
318345
if (offset >= buffer.size) {
319346
throw IndexOutOfBoundsException("Offset should be less than buffer's size (${buffer.size}): $offset")
@@ -365,7 +392,11 @@ public interface SegmentReadContext {
365392
*/
366393
@UnsafeIoApi
367394
@JvmSynthetic
395+
@OptIn(ExperimentalContracts::class)
368396
public inline fun SegmentReadContext.withData(segment: Segment, readAction: (ByteArray, Int, Int) -> Unit) {
397+
contract {
398+
callsInPlace(readAction, EXACTLY_ONCE)
399+
}
369400
readAction(segment.dataAsByteArray(true), segment.pos, segment.limit)
370401
}
371402

core/common/test/DelicateApiTest.kt

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,21 @@ package kotlinx.io
77

88
import kotlin.test.Test
99
import kotlin.test.assertEquals
10+
import kotlin.test.assertTrue
1011

1112
@OptIn(DelicateIoApi::class)
1213
class DelicateApiTest {
14+
@Test
15+
fun callsInPlaceContract() {
16+
val sink: Sink = Buffer()
17+
18+
val called: Boolean
19+
sink.writeToInternalBuffer {
20+
called = true
21+
}
22+
assertTrue(called)
23+
}
24+
1325
@Test
1426
@OptIn(InternalIoApi::class)
1527
fun testWriteIntoBuffer() {

core/common/test/unsafe/UnsafeBufferOperationsIterationTest.kt

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,35 @@ import kotlin.test.*
1414
@OptIn(UnsafeIoApi::class)
1515

1616
class UnsafeBufferOperationsIterationTest {
17+
@Test
18+
fun callsInPlaceContract() {
19+
val buffer = Buffer().also { it.writeString("hello buffer") }
20+
21+
val called: Boolean
22+
UnsafeBufferOperations.iterate(buffer) { ctx, segment ->
23+
called = true
24+
25+
val withDataCalled: Boolean
26+
ctx.withData(segment!!) { _, _, _ ->
27+
withDataCalled = true
28+
}
29+
assertTrue(withDataCalled)
30+
}
31+
assertTrue(called)
32+
33+
val offsetCalled: Boolean
34+
UnsafeBufferOperations.iterate(buffer, 1) { ctx, segment, _ ->
35+
offsetCalled = true
36+
37+
val withDataCalled: Boolean
38+
ctx.withData(segment!!) { _, _, _ ->
39+
withDataCalled = true
40+
}
41+
assertTrue(withDataCalled)
42+
}
43+
assertTrue(offsetCalled)
44+
}
45+
1746
@Test
1847
fun emptyBuffer() {
1948
UnsafeBufferOperations.iterate(Buffer()) { _, head ->

core/common/test/unsafe/UnsafeBufferOperationsReadTest.kt

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,25 @@ import kotlin.test.fail
1919
class UnsafeBufferOperationsReadTest {
2020
private class TestException : RuntimeException()
2121

22+
@Test
23+
fun callsInPlaceContract() {
24+
val buffer = Buffer().apply { writeString("hello world") }
25+
26+
val bytesCalled: Boolean
27+
UnsafeBufferOperations.readFromHead(buffer) { _, _, _ ->
28+
bytesCalled = true
29+
0
30+
}
31+
assertTrue(bytesCalled)
32+
33+
val segmentsCalled: Boolean
34+
UnsafeBufferOperations.readFromHead(buffer) { _, _ ->
35+
segmentsCalled = true
36+
0
37+
}
38+
assertTrue(segmentsCalled)
39+
}
40+
2241
@Test
2342
fun bufferCapacity() {
2443
val buffer = Buffer().apply { writeString("hello world") }

core/common/test/unsafe/UnsafeBufferOperationsWriteTest.kt

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,23 @@ import kotlin.test.*
1212
class UnsafeBufferOperationsWriteTest {
1313
private class TestException : RuntimeException()
1414

15+
@Test
16+
fun callsInPlaceContract() {
17+
val bytesCalled: Boolean
18+
UnsafeBufferOperations.writeToTail(Buffer(), 1) { _, _, _ ->
19+
bytesCalled = true
20+
0
21+
}
22+
assertTrue(bytesCalled)
23+
24+
val segmentsCalled: Boolean
25+
UnsafeBufferOperations.writeToTail(Buffer(), 1) { _, _ ->
26+
segmentsCalled = true
27+
0
28+
}
29+
assertTrue(segmentsCalled)
30+
}
31+
1532
@Test
1633
fun bufferCapacity() {
1734
val buffer = Buffer()

core/jvm/src/unsafe/UnsafeBufferOperationsJvm.kt

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@
55

66
package kotlinx.io.unsafe
77

8+
import kotlin.contracts.ExperimentalContracts
9+
import kotlin.contracts.InvocationKind.EXACTLY_ONCE
10+
import kotlin.contracts.contract
811
import kotlinx.io.Buffer
912
import kotlinx.io.Segment
1013
import kotlinx.io.UnsafeIoApi
@@ -39,7 +42,11 @@ import java.nio.ByteBuffer
3942
* @sample kotlinx.io.samples.unsafe.UnsafeReadWriteSamplesJvm.writeToByteChannel
4043
*/
4144
@UnsafeIoApi
45+
@OptIn(ExperimentalContracts::class)
4246
public inline fun UnsafeBufferOperations.readFromHead(buffer: Buffer, readAction: (ByteBuffer) -> Unit): Int {
47+
contract {
48+
callsInPlace(readAction, EXACTLY_ONCE)
49+
}
4350
return readFromHead(buffer) { rawData, pos, limit ->
4451
val bb = ByteBuffer.wrap(rawData, pos, limit - pos).slice().asReadOnlyBuffer()
4552
readAction(bb)
@@ -81,11 +88,15 @@ public inline fun UnsafeBufferOperations.readFromHead(buffer: Buffer, readAction
8188
* @sample kotlinx.io.samples.unsafe.UnsafeReadWriteSamplesJvm.readFromByteChannel
8289
*/
8390
@UnsafeIoApi
91+
@OptIn(ExperimentalContracts::class)
8492
public inline fun UnsafeBufferOperations.writeToTail(
8593
buffer: Buffer,
8694
minimumCapacity: Int,
8795
writeAction: (ByteBuffer) -> Unit
8896
): Int {
97+
contract {
98+
callsInPlace(writeAction, EXACTLY_ONCE)
99+
}
89100
return writeToTail(buffer, minimumCapacity) { rawData, pos, limit ->
90101
val bb = ByteBuffer.wrap(rawData, pos, limit - pos).slice()
91102
writeAction(bb)
@@ -134,11 +145,16 @@ public inline fun UnsafeBufferOperations.writeToTail(
134145
*
135146
*/
136147
@UnsafeIoApi
148+
@OptIn(ExperimentalContracts::class)
137149
public inline fun UnsafeBufferOperations.readBulk(
138150
buffer: Buffer,
139151
iovec: Array<ByteBuffer?>,
140152
readAction: (iovec: Array<ByteBuffer?>, iovecSize: Int) -> Long
141153
): Long {
154+
contract {
155+
callsInPlace(readAction, EXACTLY_ONCE)
156+
}
157+
142158
val head = buffer.head ?: throw IllegalArgumentException("buffer is empty.")
143159
if (iovec.isEmpty()) throw IllegalArgumentException("iovec is empty.")
144160

core/jvm/test/unsafe/UnsafeBufferOperationsJvmReadBulkTest.kt

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,19 @@ import kotlin.test.*
1313
class UnsafeBufferOperationsJvmReadBulkTest {
1414
private class TestException : RuntimeException()
1515

16+
@Test
17+
fun callsInPlaceContract() {
18+
val buffer = Buffer().apply { writeString("hello world") }
19+
val array = Array<ByteBuffer?>(16) { null }
20+
21+
val called: Boolean
22+
UnsafeBufferOperations.readBulk(buffer, array) { _, _ ->
23+
called = true
24+
0
25+
}
26+
assertTrue(called)
27+
}
28+
1629
@Test
1730
fun readAllFromEmptyBuffer() {
1831
assertFailsWith<IllegalArgumentException> {

core/jvm/test/unsafe/UnsafeBufferOperationsJvmReadFromHeadTest.kt

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,17 @@ import kotlin.test.fail
2020
class UnsafeBufferOperationsJvmReadFromHeadTest {
2121
private class TestException : RuntimeException()
2222

23+
@Test
24+
fun callsInPlaceContract() {
25+
val buffer = Buffer().apply { writeString("hello world") }
26+
27+
val called: Boolean
28+
UnsafeBufferOperations.readFromHead(buffer) { _ ->
29+
called = true
30+
}
31+
assertTrue(called)
32+
}
33+
2334
@Test
2435
fun bufferCapacity() {
2536
val buffer = Buffer().apply { writeString("hello world") }

0 commit comments

Comments
 (0)