Skip to content

Provide extensions to read/write from/to C-arrays #450

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 2 commits into
base: develop
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions core/api/kotlinx-io-core.klib.api
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
// 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]
// Alias: native => [androidNativeArm32, androidNativeArm64, androidNativeX64, androidNativeX86, iosArm64, iosSimulatorArm64, iosX64, linuxArm32Hfp, linuxArm64, linuxX64, macosArm64, macosX64, mingwX64, tvosArm64, tvosSimulatorArm64, tvosX64, watchosArm32, watchosArm64, watchosDeviceArm64, watchosSimulatorArm64, watchosX64]
// Alias: apple => [iosArm64, iosSimulatorArm64, iosX64, macosArm64, macosX64, tvosArm64, tvosSimulatorArm64, tvosX64, watchosArm32, watchosArm64, watchosDeviceArm64, watchosSimulatorArm64, watchosX64]
// Rendering settings:
// - Signature version: 2
Expand Down Expand Up @@ -300,6 +301,15 @@ final inline fun (kotlinx.io.unsafe/SegmentReadContext).kotlinx.io.unsafe/withDa
final inline fun (kotlinx.io/Sink).kotlinx.io/writeToInternalBuffer(kotlin/Function1<kotlinx.io/Buffer, kotlin/Unit>) // kotlinx.io/writeToInternalBuffer|writeToInternalBuffer@kotlinx.io.Sink(kotlin.Function1<kotlinx.io.Buffer,kotlin.Unit>){}[0]
final inline fun <#A: kotlin/Any?> (kotlinx.io/Buffer).kotlinx.io/seek(kotlin/Long, kotlin/Function2<kotlinx.io/Segment?, kotlin/Long, #A>): #A // kotlinx.io/seek|seek@kotlinx.io.Buffer(kotlin.Long;kotlin.Function2<kotlinx.io.Segment?,kotlin.Long,0:0>){0§<kotlin.Any?>}[0]

// Targets: [native]
final fun (kotlinx.io/Sink).kotlinx.io/write(kotlinx.cinterop/CPointer<kotlinx.cinterop/ByteVarOf<kotlin/Byte>>, kotlin/Long) // kotlinx.io/write|write@kotlinx.io.Sink(kotlinx.cinterop.CPointer<kotlinx.cinterop.ByteVarOf<kotlin.Byte>>;kotlin.Long){}[0]

// Targets: [native]
final fun (kotlinx.io/Source).kotlinx.io/readAtMostTo(kotlinx.cinterop/CPointer<kotlinx.cinterop/ByteVarOf<kotlin/Byte>>, kotlin/Long): kotlin/Long // kotlinx.io/readAtMostTo|readAtMostTo@kotlinx.io.Source(kotlinx.cinterop.CPointer<kotlinx.cinterop.ByteVarOf<kotlin.Byte>>;kotlin.Long){}[0]

// Targets: [native]
final fun (kotlinx.io/Source).kotlinx.io/readTo(kotlinx.cinterop/CPointer<kotlinx.cinterop/ByteVarOf<kotlin/Byte>>, kotlin/Long) // kotlinx.io/readTo|readTo@kotlinx.io.Source(kotlinx.cinterop.CPointer<kotlinx.cinterop.ByteVarOf<kotlin.Byte>>;kotlin.Long){}[0]

// Targets: [apple]
final fun (kotlinx.io/Sink).kotlinx.io/asNSOutputStream(): platform.Foundation/NSOutputStream // kotlinx.io/asNSOutputStream|asNSOutputStream@kotlinx.io.Sink(){}[0]

Expand Down
45 changes: 45 additions & 0 deletions core/native/src/SinksNative.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
/*
* Copyright 2010-2025 JetBrains s.r.o. and respective authors and developers.
* Use of this source code is governed by the Apache 2.0 license that can be found in the LICENCE file.
*/

package kotlinx.io

import kotlinx.cinterop.*
import kotlinx.io.unsafe.UnsafeBufferOperations
import platform.posix.memcpy

/**
* Writes exactly [byteCount] bytes from a memory pointed by [ptr] into this [Sink](this).
*
* **Note that this function does not verify whether the [ptr] points to a readable memory region.**
*
* @param ptr The memory region to read data from.
* @param byteCount The number of bytes that should be written into this sink from [ptr].
*
* @throws IllegalArgumentException when [byteCount] is negative.
* @throws IOException when some I/O error happens.
*/
@DelicateIoApi
@OptIn(ExperimentalForeignApi::class, UnsafeIoApi::class, InternalIoApi::class, UnsafeNumber::class)
public fun Sink.write(ptr: CPointer<ByteVar>, byteCount: Long) {
require(byteCount >= 0L) { "byteCount shouldn't be negative: $byteCount" }

var remaining = byteCount
var currentOffset = 0L

while (remaining > 0) {
UnsafeBufferOperations.writeToTail(buffer, 1) { array, startIndex, endIndex ->
val toWrite = minOf(endIndex - startIndex, remaining).toInt()
array.usePinned { pinned ->
memcpy(pinned.addressOf(startIndex), ptr + currentOffset, toWrite.convert())
}
currentOffset += toWrite
remaining -= toWrite

toWrite
}

hintEmit()
}
}
91 changes: 91 additions & 0 deletions core/native/src/SourcesNative.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
/*
* Copyright 2010-2025 JetBrains s.r.o. and respective authors and developers.
* Use of this source code is governed by the Apache 2.0 license that can be found in the LICENCE file.
*/

package kotlinx.io

import kotlinx.cinterop.*
import kotlinx.io.unsafe.UnsafeBufferOperations
import platform.posix.memcpy

/**
* Reads at most [byteCount] bytes from this [Source](this), writes them into [ptr] and returns the number of
* bytes read.
*
* **Note that this function does not verify whether the [ptr] points to a writeable memory region.**
*
* @param ptr The memory region to write data into.
* @param byteCount The maximum number of bytes to read from this source.
*
* @throws IllegalArgumentException when [byteCount] is negative.
* @throws IOException when some I/O error happens.
*/
@DelicateIoApi
@OptIn(ExperimentalForeignApi::class, InternalIoApi::class, UnsafeIoApi::class, UnsafeNumber::class)
public fun Source.readAtMostTo(ptr: CPointer<ByteVar>, byteCount: Long): Long {
require(byteCount >= 0L) { "byteCount shouldn't be negative: $byteCount" }

if (byteCount == 0L) return 0L

if (!request(1L)) {
return if (exhausted()) -1L else 0L
}

var consumed = 0L
UnsafeBufferOperations.readFromHead(buffer) { array, startIndex, endIndex ->
val toRead = minOf(endIndex - startIndex, byteCount).toInt()

array.usePinned {
memcpy(ptr, it.addressOf(startIndex), toRead.convert())
}

consumed += toRead
toRead
}

return consumed
}

/**
* Reads exactly [byteCount] bytes from this [Source](this) and writes them into a memory region pointed by [ptr].
*
* **Note that this function does not verify whether the [ptr] points to a writeable memory region.**
*
* This function consumes data from the source even if an error occurs.
*
* @param ptr The memory region to write data into.
* @param byteCount The exact number of bytes to read from this source.
*
* @throws IllegalArgumentException when [byteCount] is negative.
* @throws EOFException when the source exhausts before [byteCount] were read.
* @throws IOException when some I/O error happens.
*/
@DelicateIoApi
@OptIn(ExperimentalForeignApi::class, InternalIoApi::class, UnsafeIoApi::class, UnsafeNumber::class)
public fun Source.readTo(ptr: CPointer<ByteVar>, byteCount: Long) {
require(byteCount >= 0L) { "byteCount shouldn't be negative: $byteCount" }

if (byteCount == 0L) return

var consumed = 0L

while (consumed < byteCount) {
if (!request(1L)) {
if (exhausted()) {
throw EOFException("The source is exhausted before reading $byteCount bytes " +
"(it contained only $consumed bytes)")
}
}
UnsafeBufferOperations.readFromHead(buffer) { array, startIndex, endIndex ->
val toRead = minOf(endIndex - startIndex, byteCount - consumed).toInt()

array.usePinned {
memcpy(ptr + consumed, it.addressOf(startIndex), toRead.convert())
}

consumed += toRead
toRead
}
}
}
87 changes: 87 additions & 0 deletions core/native/test/SinksNativeTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
/*
* Copyright 2010-2025 JetBrains s.r.o. and respective authors and developers.
* Use of this source code is governed by the Apache 2.0 license that can be found in the LICENCE file.
*/

package kotlinx.io

import kotlinx.cinterop.*
import kotlin.test.*

class BufferSinksNativeTest : SinksNativeTest(SinkFactory.BUFFER)
class BufferedSinkSinksNativeTest : SinksNativeTest(SinkFactory.REAL_BUFFERED_SINK)

private const val SEGMENT_SIZE = Segment.SIZE

@OptIn(ExperimentalForeignApi::class, DelicateIoApi::class)
abstract class SinksNativeTest internal constructor(factory: SinkFactory) {
private val buffer = Buffer()
private val sink = factory.create(buffer)

@Test
fun writePointer() {
val data = "hello world".encodeToByteArray()

data.usePinned { pinned ->
sink.write(pinned.addressOf(0), data.size.toLong())
}
sink.flush()
assertEquals("hello world", buffer.readString())

data.usePinned { pinned ->
sink.write(pinned.addressOf(0), 5)
}
sink.flush()
assertEquals("hello", buffer.readString())

data.usePinned { pinned ->
sink.write(pinned.addressOf(6), 5)
}
sink.flush()
assertEquals("world", buffer.readString())

data.usePinned { pinned ->
sink.write(pinned.addressOf(0), 0)
}
sink.flush()
assertTrue(buffer.exhausted())
}

@Test
fun writeOnSegmentsBorder() {
val data = "hello world".encodeToByteArray()
val padding = ByteArray(SEGMENT_SIZE - 3) { 0xaa.toByte() }

sink.write(padding)
data.usePinned { pinned ->
sink.write(pinned.addressOf(0), data.size.toLong())
}
sink.flush()

buffer.skip(padding.size.toLong())
assertEquals("hello world", buffer.readString())
}

@Test
fun writeOverMultipleSegments() {
val data = ByteArray((2.5 * SEGMENT_SIZE).toInt()) { 0xaa.toByte() }

data.usePinned { pinned ->
sink.write(pinned.addressOf(0), data.size.toLong())
}
sink.flush()

assertContentEquals(data, buffer.readByteArray())
}

@Test
fun writeUsingIllegalLength() {
byteArrayOf(0).usePinned { pinned ->
val ptr = pinned.addressOf(0)

assertFailsWith<IllegalArgumentException> {
sink.write(ptr, byteCount = -1L)
}
}
}
}
Loading