Skip to content

Commit 665ec49

Browse files
committed
Provide extensions to read/write from/to C-arrays
Closes #419
1 parent 6bd6a46 commit 665ec49

File tree

4 files changed

+430
-0
lines changed

4 files changed

+430
-0
lines changed

core/native/src/SinksNative.kt

+45
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
/*
2+
* Copyright 2010-2025 JetBrains s.r.o. and respective authors and developers.
3+
* Use of this source code is governed by the Apache 2.0 license that can be found in the LICENCE file.
4+
*/
5+
6+
package kotlinx.io
7+
8+
import kotlinx.cinterop.*
9+
import kotlinx.io.unsafe.UnsafeBufferOperations
10+
import platform.posix.memcpy
11+
12+
/**
13+
* Writes exactly [byteCount] bytes from a memory pointed by [ptr] into this [Sink](this).
14+
*
15+
* **Note that this function does not verify whether the [ptr] points to a readable memory region.**
16+
*
17+
* @param ptr The memory region to read data from.
18+
* @param byteCount The number of bytes that should be written into this sink from [ptr].
19+
*
20+
* @throws IllegalArgumentException when [byteCount] is negative.
21+
* @throws IOException when some I/O error happens.
22+
*/
23+
@DelicateIoApi
24+
@OptIn(ExperimentalForeignApi::class, UnsafeIoApi::class, InternalIoApi::class, UnsafeNumber::class)
25+
public fun Sink.write(ptr: CPointer<ByteVar>, byteCount: Long) {
26+
require(byteCount >= 0L) { "byteCount shouldn't be negative: $byteCount" }
27+
28+
var remaining = byteCount
29+
var currentOffset = 0L
30+
31+
while (remaining > 0) {
32+
UnsafeBufferOperations.writeToTail(buffer, 1) { array, startIndex, endIndex ->
33+
val toWrite = minOf(endIndex - startIndex, remaining).toInt()
34+
array.usePinned { pinned ->
35+
memcpy(pinned.addressOf(startIndex), ptr + currentOffset, toWrite.convert())
36+
}
37+
currentOffset += toWrite
38+
remaining -= toWrite
39+
40+
toWrite
41+
}
42+
43+
hintEmit()
44+
}
45+
}

core/native/src/SourcesNative.kt

+91
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
/*
2+
* Copyright 2010-2025 JetBrains s.r.o. and respective authors and developers.
3+
* Use of this source code is governed by the Apache 2.0 license that can be found in the LICENCE file.
4+
*/
5+
6+
package kotlinx.io
7+
8+
import kotlinx.cinterop.*
9+
import kotlinx.io.unsafe.UnsafeBufferOperations
10+
import platform.posix.memcpy
11+
12+
/**
13+
* Reads at most [byteCount] bytes from this [Source](this), writes them into [ptr] and returns the number of
14+
* bytes read.
15+
*
16+
* **Note that this function does not verify whether the [ptr] points to a writeable memory region.**
17+
*
18+
* @param ptr The memory region to write data into.
19+
* @param byteCount The maximum number of bytes to read from this source.
20+
*
21+
* @throws IllegalArgumentException when [byteCount] is negative.
22+
* @throws IOException when some I/O error happens.
23+
*/
24+
@DelicateIoApi
25+
@OptIn(ExperimentalForeignApi::class, InternalIoApi::class, UnsafeIoApi::class, UnsafeNumber::class)
26+
public fun Source.readAtMostTo(ptr: CPointer<ByteVar>, byteCount: Long): Long {
27+
require(byteCount >= 0L) { "byteCount shouldn't be negative: $byteCount" }
28+
29+
if (byteCount == 0L) return 0L
30+
31+
if (!request(1L)) {
32+
return if (exhausted()) -1L else 0L
33+
}
34+
35+
var consumed = 0L
36+
UnsafeBufferOperations.readFromHead(buffer) { array, startIndex, endIndex ->
37+
val toRead = minOf(endIndex - startIndex, byteCount).toInt()
38+
39+
array.usePinned {
40+
memcpy(ptr, it.addressOf(startIndex), toRead.convert())
41+
}
42+
43+
consumed += toRead
44+
toRead
45+
}
46+
47+
return consumed
48+
}
49+
50+
/**
51+
* Reads exactly [byteCount] bytes from this [Source](this) and writes them into a memory region pointed by [ptr].
52+
*
53+
* **Note that this function does not verify whether the [ptr] points to a writeable memory region.**
54+
*
55+
* This function consumes data from the source even if an error occurs.
56+
*
57+
* @param ptr The memory region to write data into.
58+
* @param byteCount The exact number of bytes to read from this source.
59+
*
60+
* @throws IllegalArgumentException when [byteCount] is negative.
61+
* @throws EOFException when the source exhausts before [byteCount] were read.
62+
* @throws IOException when some I/O error happens.
63+
*/
64+
@DelicateIoApi
65+
@OptIn(ExperimentalForeignApi::class, InternalIoApi::class, UnsafeIoApi::class, UnsafeNumber::class)
66+
public fun Source.readTo(ptr: CPointer<ByteVar>, byteCount: Long) {
67+
require(byteCount >= 0L) { "byteCount shouldn't be negative: $byteCount" }
68+
69+
if (byteCount == 0L) return
70+
71+
var consumed = 0L
72+
73+
while (consumed < byteCount) {
74+
if (!request(1L)) {
75+
if (exhausted()) {
76+
throw EOFException("The source is exhausted before reading $byteCount bytes " +
77+
"(it contained only $consumed bytes)")
78+
}
79+
}
80+
UnsafeBufferOperations.readFromHead(buffer) { array, startIndex, endIndex ->
81+
val toRead = minOf(endIndex - startIndex, byteCount - consumed).toInt()
82+
83+
array.usePinned {
84+
memcpy(ptr + consumed, it.addressOf(startIndex), toRead.convert())
85+
}
86+
87+
consumed += toRead
88+
toRead
89+
}
90+
}
91+
}

core/native/test/SinksNativeTest.kt

+87
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
/*
2+
* Copyright 2010-2025 JetBrains s.r.o. and respective authors and developers.
3+
* Use of this source code is governed by the Apache 2.0 license that can be found in the LICENCE file.
4+
*/
5+
6+
package kotlinx.io
7+
8+
import kotlinx.cinterop.*
9+
import kotlin.test.*
10+
11+
class BufferSinksNativeTest : SinksNativeTest(SinkFactory.BUFFER)
12+
class BufferedSinkSinksNativeTest : SinksNativeTest(SinkFactory.REAL_BUFFERED_SINK)
13+
14+
private const val SEGMENT_SIZE = Segment.SIZE
15+
16+
@OptIn(ExperimentalForeignApi::class, DelicateIoApi::class)
17+
abstract class SinksNativeTest internal constructor(factory: SinkFactory) {
18+
private val buffer = Buffer()
19+
private val sink = factory.create(buffer)
20+
21+
@Test
22+
fun writePointer() {
23+
val data = "hello world".encodeToByteArray()
24+
25+
data.usePinned { pinned ->
26+
sink.write(pinned.addressOf(0), data.size.toLong())
27+
}
28+
sink.flush()
29+
assertEquals("hello world", buffer.readString())
30+
31+
data.usePinned { pinned ->
32+
sink.write(pinned.addressOf(0), 5)
33+
}
34+
sink.flush()
35+
assertEquals("hello", buffer.readString())
36+
37+
data.usePinned { pinned ->
38+
sink.write(pinned.addressOf(6), 5)
39+
}
40+
sink.flush()
41+
assertEquals("world", buffer.readString())
42+
43+
data.usePinned { pinned ->
44+
sink.write(pinned.addressOf(0), 0)
45+
}
46+
sink.flush()
47+
assertTrue(buffer.exhausted())
48+
}
49+
50+
@Test
51+
fun writeOnSegmentsBorder() {
52+
val data = "hello world".encodeToByteArray()
53+
val padding = ByteArray(SEGMENT_SIZE - 3) { 0xaa.toByte() }
54+
55+
sink.write(padding)
56+
data.usePinned { pinned ->
57+
sink.write(pinned.addressOf(0), data.size.toLong())
58+
}
59+
sink.flush()
60+
61+
buffer.skip(padding.size.toLong())
62+
assertEquals("hello world", buffer.readString())
63+
}
64+
65+
@Test
66+
fun writeOverMultipleSegments() {
67+
val data = ByteArray((2.5 * SEGMENT_SIZE).toInt()) { 0xaa.toByte() }
68+
69+
data.usePinned { pinned ->
70+
sink.write(pinned.addressOf(0), data.size.toLong())
71+
}
72+
sink.flush()
73+
74+
assertContentEquals(data, buffer.readByteArray())
75+
}
76+
77+
@Test
78+
fun writeUsingIllegalLength() {
79+
byteArrayOf(0).usePinned { pinned ->
80+
val ptr = pinned.addressOf(0)
81+
82+
assertFailsWith<IllegalArgumentException> {
83+
sink.write(ptr, byteCount = -1L)
84+
}
85+
}
86+
}
87+
}

0 commit comments

Comments
 (0)