From c0f042fe55552a33f8da2f882bd96bf6c0ad5ac0 Mon Sep 17 00:00:00 2001 From: Jeff Lockhart Date: Wed, 12 Jul 2023 01:55:07 -0600 Subject: [PATCH 01/51] NSInputStream.asSource() and Source.asNSInputStream() Modeled after JVM's InputStream.asSource() and Source.asInputStream(), add extension functions to similarly map to and from Apple's NSInputStream --- buildSrc/src/main/kotlin/Platforms.kt | 4 +- core/apple/src/AppleCore.kt | 56 +++++++++++ core/apple/src/SourcesApple.kt | 109 +++++++++++++++++++++ core/apple/test/NSInputStreamSourceTest.kt | 73 ++++++++++++++ core/apple/test/SourceNSInputStreamTest.kt | 95 ++++++++++++++++++ core/apple/test/samples/samplesApple.kt | 17 ++++ core/apple/test/utilApple.kt | 18 ++++ core/build.gradle.kts | 6 +- core/common/test/util.kt | 5 + core/jvm/test/utilJVM.kt | 5 - 10 files changed, 379 insertions(+), 9 deletions(-) create mode 100644 core/apple/src/AppleCore.kt create mode 100644 core/apple/src/SourcesApple.kt create mode 100644 core/apple/test/NSInputStreamSourceTest.kt create mode 100644 core/apple/test/SourceNSInputStreamTest.kt create mode 100644 core/apple/test/samples/samplesApple.kt create mode 100644 core/apple/test/utilApple.kt diff --git a/buildSrc/src/main/kotlin/Platforms.kt b/buildSrc/src/main/kotlin/Platforms.kt index 157edd255..fbe30c1bf 100644 --- a/buildSrc/src/main/kotlin/Platforms.kt +++ b/buildSrc/src/main/kotlin/Platforms.kt @@ -32,7 +32,7 @@ fun KotlinMultiplatformExtension.configureNativePlatforms() { mingwX64() } -private val appleTargets = listOf( +val appleTargets = listOf( "iosArm64", "iosX64", "iosSimulatorArm64", @@ -64,7 +64,7 @@ private val androidTargets = listOf( "androidNativeX86" ) -val nativeTargets = appleTargets + linuxTargets + mingwTargets + androidTargets +val nativeTargets = linuxTargets + mingwTargets + androidTargets /** * Creates a source set for a directory that isn't already a built-in platform. Use this to create diff --git a/core/apple/src/AppleCore.kt b/core/apple/src/AppleCore.kt new file mode 100644 index 000000000..f0903bb38 --- /dev/null +++ b/core/apple/src/AppleCore.kt @@ -0,0 +1,56 @@ +/* + * Copyright 2017-2023 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 platform.Foundation.NSInputStream +import platform.darwin.UInt8Var + +/** + * Returns [RawSource] that reads from an input stream. + * + * Use [RawSource.buffered] to create a buffered source from it. + * + * @sample kotlinx.io.samples.KotlinxIoSamplesApple.inputStreamAsSource + */ +public fun NSInputStream.asSource(): RawSource = NSInputStreamSource(this) + +private open class NSInputStreamSource( + private val input: NSInputStream, +) : RawSource { + + init { + input.open() + } + + @OptIn(UnsafeNumber::class) + override fun readAtMostTo(sink: Buffer, byteCount: Long): Long { + if (byteCount == 0L) return 0L + checkByteCount(byteCount) + val tail = sink.writableSegment(1) + val maxToCopy = minOf(byteCount, Segment.SIZE - tail.limit) + val bytesRead = tail.data.usePinned { + val bytes = it.addressOf(tail.limit).reinterpret() + input.read(bytes, maxToCopy.convert()).toLong() + } + if (bytesRead < 0) throw IOException(input.streamError?.localizedDescription) + if (bytesRead == 0L) { + if (tail.pos == tail.limit) { + // We allocated a tail segment, but didn't end up needing it. Recycle! + sink.head = tail.pop() + SegmentPool.recycle(tail) + } + return -1 + } + tail.limit += bytesRead.toInt() + sink.size += bytesRead + return bytesRead + } + + override fun close() = input.close() + + override fun toString() = "source($input)" +} diff --git a/core/apple/src/SourcesApple.kt b/core/apple/src/SourcesApple.kt new file mode 100644 index 000000000..79228d80b --- /dev/null +++ b/core/apple/src/SourcesApple.kt @@ -0,0 +1,109 @@ +/* + * Copyright 2017-2023 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 platform.Foundation.* +import platform.darwin.NSInteger +import platform.darwin.NSUInteger +import platform.darwin.NSUIntegerVar +import platform.posix.memcpy +import platform.posix.uint8_tVar + +/** + * Returns an input stream that reads from this source. Closing the stream will also close this source. + */ +public fun Source.asNSInputStream(): NSInputStream = SourceNSInputStream(this) + +@OptIn(InternalIoApi::class, UnsafeNumber::class) +private class SourceNSInputStream( + private val source: Source, +) : NSInputStream(NSData()) { + + val isClosed: () -> Boolean = when (source) { + is RealSource -> source::closed + is Buffer -> { + { false } + } + } + + private var error: NSError? = null + private var pinnedBuffer: Pinned? = null + + override fun streamError(): NSError? = error + + override fun open() { + // no-op + } + + override fun read(buffer: CPointer?, maxLength: NSUInteger): NSInteger { + try { + if (isClosed()) throw IOException("Underlying source is closed.") + if (source.exhausted()) { + return 0 + } + val toRead = minOf(maxLength.toInt(), source.buffer.size).toInt() + return source.buffer.readNative(buffer, toRead).convert() + } catch (e: Exception) { + error = e.toNSError() + return -1 + } + } + + override fun getBuffer(buffer: CPointer>?, length: CPointer?): Boolean { + if (source.buffer.size > 0) { + source.buffer.head?.let { s -> + pinnedBuffer?.unpin() + s.data.pin().let { + pinnedBuffer = it + buffer?.pointed?.value = it.addressOf(s.pos).reinterpret() + length?.pointed?.value = (s.limit - s.pos).convert() + return true + } + } + } + return false + } + + override fun hasBytesAvailable(): Boolean = source.buffer.size > 0 + + override fun close() { + pinnedBuffer?.unpin() + pinnedBuffer = null + source.close() + } + + override fun description(): String = "$source.inputStream()" + + private fun Exception.toNSError(): NSError { + return NSError( + "Kotlin", + 0, + mapOf( + NSLocalizedDescriptionKey to message, + NSUnderlyingErrorKey to this, + ), + ) + } + + private fun Buffer.readNative(sink: CPointer?, maxLength: Int): Int { + val s = head ?: return 0 + val toCopy = minOf(maxLength, s.limit - s.pos) + s.data.usePinned { + memcpy(sink, it.addressOf(s.pos), toCopy.convert()) + } + + s.pos += toCopy + size -= toCopy.toLong() + + if (s.pos == s.limit) { + head = s.pop() + SegmentPool.recycle(s) + } + + return toCopy + } +} diff --git a/core/apple/test/NSInputStreamSourceTest.kt b/core/apple/test/NSInputStreamSourceTest.kt new file mode 100644 index 000000000..99a6f75c1 --- /dev/null +++ b/core/apple/test/NSInputStreamSourceTest.kt @@ -0,0 +1,73 @@ +/* + * Copyright 2017-2023 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 platform.Foundation.NSInputStream +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.fail + +private const val SEGMENT_SIZE = Segment.SIZE + +class NSInputStreamSourceTest { + @Test + fun nsInputStreamSource() { + val nsis = NSInputStream(byteArrayOf(0x61).toNSData()) + val source = nsis.asSource() + val buffer = Buffer() + source.readAtMostTo(buffer, 1) + assertEquals("a", buffer.readString()) + } + + @Test + fun sourceFromInputStream() { + val nsis = NSInputStream( + ("a" + "b".repeat(SEGMENT_SIZE * 2) + "c").encodeToByteArray().toNSData(), + ) + + // Source: ab...bc + val source: RawSource = nsis.asSource() + val sink = Buffer() + + // Source: b...bc. Sink: abb. + assertEquals(3, source.readAtMostTo(sink, 3)) + assertEquals("abb", sink.readString(3)) + + // Source: b...bc. Sink: b...b. + assertEquals(SEGMENT_SIZE.toLong(), source.readAtMostTo(sink, 20000)) + assertEquals("b".repeat(SEGMENT_SIZE), sink.readString()) + + // Source: b...bc. Sink: b...bc. + assertEquals((SEGMENT_SIZE - 1).toLong(), source.readAtMostTo(sink, 20000)) + assertEquals("b".repeat(SEGMENT_SIZE - 2) + "c", sink.readString()) + + // Source and sink are empty. + assertEquals(-1, source.readAtMostTo(sink, 1)) + } + + @Test + fun sourceFromInputStreamWithSegmentSize() { + val nsis = NSInputStream(ByteArray(SEGMENT_SIZE).toNSData()) + val source = nsis.asSource() + val sink = Buffer() + + assertEquals(SEGMENT_SIZE.toLong(), source.readAtMostTo(sink, SEGMENT_SIZE.toLong())) + assertEquals(-1, source.readAtMostTo(sink, SEGMENT_SIZE.toLong())) + + assertNoEmptySegments(sink) + } + + @Test + fun sourceFromInputStreamBounds() { + val source = NSInputStream(ByteArray(100).toNSData()).asSource() + try { + source.readAtMostTo(Buffer(), -1) + fail() + } catch (expected: IllegalArgumentException) { + // expected + } + } +} diff --git a/core/apple/test/SourceNSInputStreamTest.kt b/core/apple/test/SourceNSInputStreamTest.kt new file mode 100644 index 000000000..263d0a5ba --- /dev/null +++ b/core/apple/test/SourceNSInputStreamTest.kt @@ -0,0 +1,95 @@ +/* + * Copyright 2017-2023 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 platform.Foundation.NSInputStream +import platform.darwin.NSUIntegerVar +import platform.darwin.UInt8Var +import kotlin.test.* + +@OptIn(UnsafeNumber::class) +class SourceNSInputStreamTest { + @Test + fun bufferInputStream() { + val source = Buffer() + source.writeString("abc") + testInputStream(source.asNSInputStream()) + } + + @Test + fun realBufferedSourceInputStream() { + val source = Buffer() + source.writeString("abc") + testInputStream(RealSource(source).asNSInputStream()) + } + + private fun testInputStream(nsis: NSInputStream) { + nsis.open() + val byteArray = ByteArray(4) + byteArray.usePinned { + val cPtr = it.addressOf(0).reinterpret() + + byteArray.fill(-5) + assertEquals(3, nsis.read(cPtr, 4U)) + assertEquals("[97, 98, 99, -5]", byteArray.contentToString()) + + byteArray.fill(-7) + assertEquals(0, nsis.read(cPtr, 4U)) + assertEquals("[-7, -7, -7, -7]", byteArray.contentToString()) + } + } + + @Test + fun nsInputStreamGetBuffer() { + val source = Buffer() + source.writeString("abc") + + val nsis = source.asNSInputStream() + nsis.open() + assertTrue(nsis.hasBytesAvailable) + + memScoped { + val bufferPtr = alloc>() + val lengthPtr = alloc() + assertTrue(nsis.getBuffer(bufferPtr.ptr, lengthPtr.ptr)) + + val length = lengthPtr.value + assertNotNull(length) + assertEquals(3.convert(), length) + + val buffer = bufferPtr.value + assertNotNull(buffer) + assertEquals('a'.code.convert(), buffer[0]) + assertEquals('b'.code.convert(), buffer[1]) + assertEquals('c'.code.convert(), buffer[2]) + } + } + + @Test + fun nsInputStreamClose() { + val buffer = Buffer() + buffer.writeString("abc") + val source = RealSource(buffer) + assertFalse(source.closed) + + val nsis = source.asNSInputStream() + nsis.open() + nsis.close() + assertTrue(source.closed) + + val byteArray = ByteArray(4) + byteArray.usePinned { + val cPtr = it.addressOf(0).reinterpret() + + byteArray.fill(-5) + assertEquals(-1, nsis.read(cPtr, 4U)) + assertNotNull(nsis.streamError) + assertEquals("Underlying source is closed.", nsis.streamError?.localizedDescription) + assertEquals("[-5, -5, -5, -5]", byteArray.contentToString()) + } + } +} diff --git a/core/apple/test/samples/samplesApple.kt b/core/apple/test/samples/samplesApple.kt new file mode 100644 index 000000000..a45ed7f7a --- /dev/null +++ b/core/apple/test/samples/samplesApple.kt @@ -0,0 +1,17 @@ +package kotlinx.io.samples + +import kotlinx.io.* +import platform.Foundation.NSInputStream +import kotlin.test.Test +import kotlin.test.assertContentEquals + +class KotlinxIoSamplesApple { + @Test + fun inputStreamAsSource() { + val data = ByteArray(100) { it.toByte() } + val inputStream = NSInputStream(data.toNSData()) + + val receivedData = inputStream.asSource().buffered().readByteArray() + assertContentEquals(data, receivedData) + } +} diff --git a/core/apple/test/utilApple.kt b/core/apple/test/utilApple.kt new file mode 100644 index 000000000..bd424d0d5 --- /dev/null +++ b/core/apple/test/utilApple.kt @@ -0,0 +1,18 @@ +package kotlinx.io + +import kotlinx.cinterop.UnsafeNumber +import kotlinx.cinterop.addressOf +import kotlinx.cinterop.convert +import kotlinx.cinterop.usePinned +import platform.Foundation.NSData +import platform.Foundation.create +import platform.Foundation.data + +fun ByteArray.toNSData() = if (isNotEmpty()) { + usePinned { + @OptIn(UnsafeNumber::class) + NSData.create(bytes = it.addressOf(0), length = size.convert()) + } +} else { + NSData.data() +} diff --git a/core/build.gradle.kts b/core/build.gradle.kts index 9dac96609..69df80dea 100644 --- a/core/build.gradle.kts +++ b/core/build.gradle.kts @@ -53,8 +53,10 @@ kotlin { val jvmMain by getting val jvmTest by getting - createSourceSet("nativeMain", parent = commonMain, children = nativeTargets) - createSourceSet("nativeTest", parent = commonTest, children = nativeTargets) + val nativeMain = createSourceSet("nativeMain", parent = commonMain, children = nativeTargets) + val nativeTest = createSourceSet("nativeTest", parent = commonTest, children = nativeTargets) + createSourceSet("appleMain", parent = nativeMain, children = appleTargets) + createSourceSet("appleTest", parent = nativeTest, children = appleTargets) } explicitApi() diff --git a/core/common/test/util.kt b/core/common/test/util.kt index c621b3c17..9a98e2ed0 100644 --- a/core/common/test/util.kt +++ b/core/common/test/util.kt @@ -21,6 +21,7 @@ package kotlinx.io import kotlin.test.assertEquals +import kotlin.test.assertTrue fun segmentSizes(buffer: Buffer): List { var segment = buffer.head ?: return emptyList() @@ -34,6 +35,10 @@ fun segmentSizes(buffer: Buffer): List { return sizes } +fun assertNoEmptySegments(buffer: Buffer) { + assertTrue(segmentSizes(buffer).all { it != 0 }, "Expected all segments to be non-empty") +} + expect fun createTempFile(): String expect fun deleteFile(path: String) diff --git a/core/jvm/test/utilJVM.kt b/core/jvm/test/utilJVM.kt index 6aae75a50..369d1aad8 100644 --- a/core/jvm/test/utilJVM.kt +++ b/core/jvm/test/utilJVM.kt @@ -7,7 +7,6 @@ package kotlinx.io import java.nio.file.Files import java.nio.file.Paths import kotlin.test.assertEquals -import kotlin.test.assertTrue actual fun createTempFile(): String = Files.createTempFile(null, null).toString() @@ -18,10 +17,6 @@ actual fun deleteFile(path: String) { Files.delete(Paths.get(path)) } -fun assertNoEmptySegments(buffer: Buffer) { - assertTrue(segmentSizes(buffer).all { it != 0 }, "Expected all segments to be non-empty") -} - fun assertByteArrayEquals(expectedUtf8: String, b: ByteArray) { assertEquals(expectedUtf8, b.toString(Charsets.UTF_8)) } From 13bf3b118d0ad09dff08424c976d991be8080bb8 Mon Sep 17 00:00:00 2001 From: Jeff Lockhart Date: Wed, 12 Jul 2023 03:01:20 -0600 Subject: [PATCH 02/51] Move Exception.toNSError() to -Util file --- core/apple/src/-Util.kt | 16 ++++++++++++++++ core/apple/src/SourcesApple.kt | 15 ++------------- core/common/src/Sink.kt | 2 +- 3 files changed, 19 insertions(+), 14 deletions(-) create mode 100644 core/apple/src/-Util.kt diff --git a/core/apple/src/-Util.kt b/core/apple/src/-Util.kt new file mode 100644 index 000000000..a979dc29c --- /dev/null +++ b/core/apple/src/-Util.kt @@ -0,0 +1,16 @@ +package kotlinx.io + +import kotlinx.cinterop.UnsafeNumber +import platform.Foundation.NSError +import platform.Foundation.NSLocalizedDescriptionKey +import platform.Foundation.NSUnderlyingErrorKey + +@OptIn(UnsafeNumber::class) +internal fun Exception.toNSError() = NSError( + domain = "Kotlin", + code = 0, + userInfo = mapOf( + NSLocalizedDescriptionKey to message, + NSUnderlyingErrorKey to this + ) +) diff --git a/core/apple/src/SourcesApple.kt b/core/apple/src/SourcesApple.kt index 79228d80b..2a8a530ef 100644 --- a/core/apple/src/SourcesApple.kt +++ b/core/apple/src/SourcesApple.kt @@ -68,7 +68,7 @@ private class SourceNSInputStream( return false } - override fun hasBytesAvailable(): Boolean = source.buffer.size > 0 + override fun hasBytesAvailable() = source.buffer.size > 0 override fun close() { pinnedBuffer?.unpin() @@ -76,18 +76,7 @@ private class SourceNSInputStream( source.close() } - override fun description(): String = "$source.inputStream()" - - private fun Exception.toNSError(): NSError { - return NSError( - "Kotlin", - 0, - mapOf( - NSLocalizedDescriptionKey to message, - NSUnderlyingErrorKey to this, - ), - ) - } + override fun description() = "$source.inputStream()" private fun Buffer.readNative(sink: CPointer?, maxLength: Int): Int { val s = head ?: return 0 diff --git a/core/common/src/Sink.kt b/core/common/src/Sink.kt index 4d887de1d..677b587fe 100644 --- a/core/common/src/Sink.kt +++ b/core/common/src/Sink.kt @@ -41,7 +41,7 @@ package kotlinx.io * * ### Write methods' behavior and naming conventions * - * Methods writing a value of some type are usually name `write`, like [writeByte] or [writeInt], except methods + * Methods writing a value of some type are usually named `write`, like [writeByte] or [writeInt], except methods * writing data from a some collection of bytes, like `write(ByteArray, Int, Int)` or * `write(source: RawSource, byteCount: Long)`. * In the latter case, if a collection is consumable (i.e., once data was read from it will no longer be available for From 3386ab3478a9496f4a4e2dca39edaac581e90b1f Mon Sep 17 00:00:00 2001 From: Jeff Lockhart Date: Wed, 12 Jul 2023 03:05:55 -0600 Subject: [PATCH 03/51] Make isClosed explicitly private --- core/apple/src/SourcesApple.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/apple/src/SourcesApple.kt b/core/apple/src/SourcesApple.kt index 2a8a530ef..de0f62018 100644 --- a/core/apple/src/SourcesApple.kt +++ b/core/apple/src/SourcesApple.kt @@ -23,7 +23,7 @@ private class SourceNSInputStream( private val source: Source, ) : NSInputStream(NSData()) { - val isClosed: () -> Boolean = when (source) { + private val isClosed: () -> Boolean = when (source) { is RealSource -> source::closed is Buffer -> { { false } From 6b4bb8050cbfe92d701a3f6c1608763f2bd83ef1 Mon Sep 17 00:00:00 2001 From: Jeff Lockhart Date: Wed, 12 Jul 2023 12:39:31 -0600 Subject: [PATCH 04/51] Code review feedback --- core/apple/src/AppleCore.kt | 2 +- core/apple/src/SourcesApple.kt | 4 ++-- core/jvm/src/JvmCore.kt | 2 +- core/jvm/src/SourcesJvm.kt | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/core/apple/src/AppleCore.kt b/core/apple/src/AppleCore.kt index f0903bb38..335968955 100644 --- a/core/apple/src/AppleCore.kt +++ b/core/apple/src/AppleCore.kt @@ -52,5 +52,5 @@ private open class NSInputStreamSource( override fun close() = input.close() - override fun toString() = "source($input)" + override fun toString() = "RawSource($input)" } diff --git a/core/apple/src/SourcesApple.kt b/core/apple/src/SourcesApple.kt index de0f62018..9ebcee5ff 100644 --- a/core/apple/src/SourcesApple.kt +++ b/core/apple/src/SourcesApple.kt @@ -68,7 +68,7 @@ private class SourceNSInputStream( return false } - override fun hasBytesAvailable() = source.buffer.size > 0 + override fun hasBytesAvailable() = !source.exhausted() override fun close() { pinnedBuffer?.unpin() @@ -76,7 +76,7 @@ private class SourceNSInputStream( source.close() } - override fun description() = "$source.inputStream()" + override fun description() = "$source.asNSInputStream()" private fun Buffer.readNative(sink: CPointer?, maxLength: Int): Int { val s = head ?: return 0 diff --git a/core/jvm/src/JvmCore.kt b/core/jvm/src/JvmCore.kt index 1506ab1ba..d92b88249 100644 --- a/core/jvm/src/JvmCore.kt +++ b/core/jvm/src/JvmCore.kt @@ -104,7 +104,7 @@ private open class InputStreamSource( override fun close() = input.close() - override fun toString() = "source($input)" + override fun toString() = "RawSource($input)" } /** diff --git a/core/jvm/src/SourcesJvm.kt b/core/jvm/src/SourcesJvm.kt index 7c940cdb1..ed7f20fc1 100644 --- a/core/jvm/src/SourcesJvm.kt +++ b/core/jvm/src/SourcesJvm.kt @@ -126,7 +126,7 @@ public fun Source.asInputStream(): InputStream { override fun close() = this@asInputStream.close() - override fun toString() = "${this@asInputStream}.inputStream()" + override fun toString() = "${this@asInputStream}.asInputStream()" } } From 8ca65a2053baf09453d734cd40482f6fb934db6c Mon Sep 17 00:00:00 2001 From: Jeff Lockhart Date: Wed, 12 Jul 2023 14:17:35 -0600 Subject: [PATCH 05/51] Implement NSStreamStatus Enforce opening SourceNSInputStream before read --- core/apple/src/SourcesApple.kt | 22 +++++++++++++++++++--- core/apple/test/SourceNSInputStreamTest.kt | 10 +++++++++- 2 files changed, 28 insertions(+), 4 deletions(-) diff --git a/core/apple/src/SourcesApple.kt b/core/apple/src/SourcesApple.kt index 9ebcee5ff..a02e9e8e4 100644 --- a/core/apple/src/SourcesApple.kt +++ b/core/apple/src/SourcesApple.kt @@ -30,23 +30,38 @@ private class SourceNSInputStream( } } + private var status = NSStreamStatusNotOpen private var error: NSError? = null + set(value) { + status = NSStreamStatusError + field = value + } + private var pinnedBuffer: Pinned? = null - override fun streamError(): NSError? = error + override fun streamStatus() = if (isClosed()) NSStreamStatusClosed else status + + override fun streamError() = error override fun open() { - // no-op + if (status == NSStreamStatusNotOpen) { + status = NSStreamStatusOpen + } } override fun read(buffer: CPointer?, maxLength: NSUInteger): NSInteger { try { if (isClosed()) throw IOException("Underlying source is closed.") + if (status != NSStreamStatusOpen) return -1 + status = NSStreamStatusReading if (source.exhausted()) { + status = NSStreamStatusAtEnd return 0 } val toRead = minOf(maxLength.toInt(), source.buffer.size).toInt() - return source.buffer.readNative(buffer, toRead).convert() + val read = source.buffer.readNative(buffer, toRead).convert() + status = NSStreamStatusOpen + return read } catch (e: Exception) { error = e.toNSError() return -1 @@ -71,6 +86,7 @@ private class SourceNSInputStream( override fun hasBytesAvailable() = !source.exhausted() override fun close() { + status = NSStreamStatusClosed pinnedBuffer?.unpin() pinnedBuffer = null source.close() diff --git a/core/apple/test/SourceNSInputStreamTest.kt b/core/apple/test/SourceNSInputStreamTest.kt index 263d0a5ba..72233fcfc 100644 --- a/core/apple/test/SourceNSInputStreamTest.kt +++ b/core/apple/test/SourceNSInputStreamTest.kt @@ -7,6 +7,9 @@ package kotlinx.io import kotlinx.cinterop.* import platform.Foundation.NSInputStream +import platform.Foundation.NSStreamStatusClosed +import platform.Foundation.NSStreamStatusNotOpen +import platform.Foundation.NSStreamStatusOpen import platform.darwin.NSUIntegerVar import platform.darwin.UInt8Var import kotlin.test.* @@ -28,11 +31,15 @@ class SourceNSInputStreamTest { } private fun testInputStream(nsis: NSInputStream) { - nsis.open() val byteArray = ByteArray(4) byteArray.usePinned { val cPtr = it.addressOf(0).reinterpret() + assertEquals(NSStreamStatusNotOpen, nsis.streamStatus) + assertEquals(-1, nsis.read(cPtr, 4U)) + nsis.open() + assertEquals(NSStreamStatusOpen, nsis.streamStatus) + byteArray.fill(-5) assertEquals(3, nsis.read(cPtr, 4U)) assertEquals("[97, 98, 99, -5]", byteArray.contentToString()) @@ -80,6 +87,7 @@ class SourceNSInputStreamTest { nsis.open() nsis.close() assertTrue(source.closed) + assertEquals(NSStreamStatusClosed, nsis.streamStatus) val byteArray = ByteArray(4) byteArray.usePinned { From 2be70a6fea4a957c3f29e887954c9183cc07f428 Mon Sep 17 00:00:00 2001 From: Jeff Lockhart Date: Wed, 12 Jul 2023 14:18:46 -0600 Subject: [PATCH 06/51] Open NSInputStream on first read --- core/apple/src/AppleCore.kt | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/core/apple/src/AppleCore.kt b/core/apple/src/AppleCore.kt index 335968955..75cefed2e 100644 --- a/core/apple/src/AppleCore.kt +++ b/core/apple/src/AppleCore.kt @@ -7,6 +7,7 @@ package kotlinx.io import kotlinx.cinterop.* import platform.Foundation.NSInputStream +import platform.Foundation.NSStreamStatusNotOpen import platform.darwin.UInt8Var /** @@ -22,20 +23,20 @@ private open class NSInputStreamSource( private val input: NSInputStream, ) : RawSource { - init { - input.open() - } - @OptIn(UnsafeNumber::class) override fun readAtMostTo(sink: Buffer, byteCount: Long): Long { + if (input.streamStatus == NSStreamStatusNotOpen) input.open() + if (byteCount == 0L) return 0L checkByteCount(byteCount) + val tail = sink.writableSegment(1) val maxToCopy = minOf(byteCount, Segment.SIZE - tail.limit) val bytesRead = tail.data.usePinned { val bytes = it.addressOf(tail.limit).reinterpret() input.read(bytes, maxToCopy.convert()).toLong() } + if (bytesRead < 0) throw IOException(input.streamError?.localizedDescription) if (bytesRead == 0L) { if (tail.pos == tail.limit) { From 54e247c6f4fe5689aeb269be38bfa22b0f1c920f Mon Sep 17 00:00:00 2001 From: Jeff Lockhart Date: Wed, 12 Jul 2023 17:28:07 -0600 Subject: [PATCH 07/51] Unknown error when no streamError description Rename variable --- core/apple/src/AppleCore.kt | 2 +- core/apple/test/NSInputStreamSourceTest.kt | 12 ++++---- core/apple/test/SourceNSInputStreamTest.kt | 36 +++++++++++----------- 3 files changed, 25 insertions(+), 25 deletions(-) diff --git a/core/apple/src/AppleCore.kt b/core/apple/src/AppleCore.kt index 75cefed2e..2f7a1af62 100644 --- a/core/apple/src/AppleCore.kt +++ b/core/apple/src/AppleCore.kt @@ -37,7 +37,7 @@ private open class NSInputStreamSource( input.read(bytes, maxToCopy.convert()).toLong() } - if (bytesRead < 0) throw IOException(input.streamError?.localizedDescription) + if (bytesRead < 0L) throw IOException(input.streamError?.localizedDescription ?: "Unknown error") if (bytesRead == 0L) { if (tail.pos == tail.limit) { // We allocated a tail segment, but didn't end up needing it. Recycle! diff --git a/core/apple/test/NSInputStreamSourceTest.kt b/core/apple/test/NSInputStreamSourceTest.kt index 99a6f75c1..d946269ec 100644 --- a/core/apple/test/NSInputStreamSourceTest.kt +++ b/core/apple/test/NSInputStreamSourceTest.kt @@ -15,8 +15,8 @@ private const val SEGMENT_SIZE = Segment.SIZE class NSInputStreamSourceTest { @Test fun nsInputStreamSource() { - val nsis = NSInputStream(byteArrayOf(0x61).toNSData()) - val source = nsis.asSource() + val input = NSInputStream(byteArrayOf(0x61).toNSData()) + val source = input.asSource() val buffer = Buffer() source.readAtMostTo(buffer, 1) assertEquals("a", buffer.readString()) @@ -24,12 +24,12 @@ class NSInputStreamSourceTest { @Test fun sourceFromInputStream() { - val nsis = NSInputStream( + val input = NSInputStream( ("a" + "b".repeat(SEGMENT_SIZE * 2) + "c").encodeToByteArray().toNSData(), ) // Source: ab...bc - val source: RawSource = nsis.asSource() + val source: RawSource = input.asSource() val sink = Buffer() // Source: b...bc. Sink: abb. @@ -50,8 +50,8 @@ class NSInputStreamSourceTest { @Test fun sourceFromInputStreamWithSegmentSize() { - val nsis = NSInputStream(ByteArray(SEGMENT_SIZE).toNSData()) - val source = nsis.asSource() + val input = NSInputStream(ByteArray(SEGMENT_SIZE).toNSData()) + val source = input.asSource() val sink = Buffer() assertEquals(SEGMENT_SIZE.toLong(), source.readAtMostTo(sink, SEGMENT_SIZE.toLong())) diff --git a/core/apple/test/SourceNSInputStreamTest.kt b/core/apple/test/SourceNSInputStreamTest.kt index 72233fcfc..b193770ed 100644 --- a/core/apple/test/SourceNSInputStreamTest.kt +++ b/core/apple/test/SourceNSInputStreamTest.kt @@ -30,22 +30,22 @@ class SourceNSInputStreamTest { testInputStream(RealSource(source).asNSInputStream()) } - private fun testInputStream(nsis: NSInputStream) { + private fun testInputStream(input: NSInputStream) { val byteArray = ByteArray(4) byteArray.usePinned { val cPtr = it.addressOf(0).reinterpret() - assertEquals(NSStreamStatusNotOpen, nsis.streamStatus) - assertEquals(-1, nsis.read(cPtr, 4U)) - nsis.open() - assertEquals(NSStreamStatusOpen, nsis.streamStatus) + assertEquals(NSStreamStatusNotOpen, input.streamStatus) + assertEquals(-1, input.read(cPtr, 4U)) + input.open() + assertEquals(NSStreamStatusOpen, input.streamStatus) byteArray.fill(-5) - assertEquals(3, nsis.read(cPtr, 4U)) + assertEquals(3, input.read(cPtr, 4U)) assertEquals("[97, 98, 99, -5]", byteArray.contentToString()) byteArray.fill(-7) - assertEquals(0, nsis.read(cPtr, 4U)) + assertEquals(0, input.read(cPtr, 4U)) assertEquals("[-7, -7, -7, -7]", byteArray.contentToString()) } } @@ -55,14 +55,14 @@ class SourceNSInputStreamTest { val source = Buffer() source.writeString("abc") - val nsis = source.asNSInputStream() - nsis.open() - assertTrue(nsis.hasBytesAvailable) + val input = source.asNSInputStream() + input.open() + assertTrue(input.hasBytesAvailable) memScoped { val bufferPtr = alloc>() val lengthPtr = alloc() - assertTrue(nsis.getBuffer(bufferPtr.ptr, lengthPtr.ptr)) + assertTrue(input.getBuffer(bufferPtr.ptr, lengthPtr.ptr)) val length = lengthPtr.value assertNotNull(length) @@ -83,20 +83,20 @@ class SourceNSInputStreamTest { val source = RealSource(buffer) assertFalse(source.closed) - val nsis = source.asNSInputStream() - nsis.open() - nsis.close() + val input = source.asNSInputStream() + input.open() + input.close() assertTrue(source.closed) - assertEquals(NSStreamStatusClosed, nsis.streamStatus) + assertEquals(NSStreamStatusClosed, input.streamStatus) val byteArray = ByteArray(4) byteArray.usePinned { val cPtr = it.addressOf(0).reinterpret() byteArray.fill(-5) - assertEquals(-1, nsis.read(cPtr, 4U)) - assertNotNull(nsis.streamError) - assertEquals("Underlying source is closed.", nsis.streamError?.localizedDescription) + assertEquals(-1, input.read(cPtr, 4U)) + assertNotNull(input.streamError) + assertEquals("Underlying source is closed.", input.streamError?.localizedDescription) assertEquals("[-5, -5, -5, -5]", byteArray.contentToString()) } } From dccd22af7e9a11e1228d8868e76a15b3d2414c62 Mon Sep 17 00:00:00 2001 From: Jeff Lockhart Date: Wed, 12 Jul 2023 21:12:22 -0600 Subject: [PATCH 08/51] NSOutputStream.asSink() and Sink.asNSOutputStream() --- core/apple/src/AppleCore.kt | 51 ++++++++++++ core/apple/src/SinksApple.kt | 92 ++++++++++++++++++++++ core/apple/src/SourcesApple.kt | 2 + core/apple/test/NSInputStreamSourceTest.kt | 2 +- core/apple/test/NSOutputStreamSinkTest.kt | 45 +++++++++++ core/apple/test/SinkNSOutputStreamTest.kt | 62 +++++++++++++++ core/apple/test/SourceNSInputStreamTest.kt | 2 +- core/apple/test/samples/samplesApple.kt | 38 ++++++++- core/apple/test/utilApple.kt | 13 ++- core/jvm/src/JvmCore.kt | 2 +- core/jvm/src/SinksJvm.kt | 6 +- 11 files changed, 304 insertions(+), 11 deletions(-) create mode 100644 core/apple/src/SinksApple.kt create mode 100644 core/apple/test/NSOutputStreamSinkTest.kt create mode 100644 core/apple/test/SinkNSOutputStreamTest.kt diff --git a/core/apple/src/AppleCore.kt b/core/apple/src/AppleCore.kt index 2f7a1af62..032220132 100644 --- a/core/apple/src/AppleCore.kt +++ b/core/apple/src/AppleCore.kt @@ -7,9 +7,60 @@ package kotlinx.io import kotlinx.cinterop.* import platform.Foundation.NSInputStream +import platform.Foundation.NSOutputStream import platform.Foundation.NSStreamStatusNotOpen import platform.darwin.UInt8Var +/** + * Returns [RawSink] that writes to an output stream. + * + * Use [RawSink.buffered] to create a buffered sink from it. + * + * @sample kotlinx.io.samples.KotlinxIoSamplesApple.outputStreamAsSink + */ +public fun NSOutputStream.asSink(): RawSink = OutputStreamSink(this) + +private open class OutputStreamSink( + private val out: NSOutputStream, +) : RawSink { + + @OptIn(UnsafeNumber::class) + override fun write(source: Buffer, byteCount: Long) { + if (out.streamStatus == NSStreamStatusNotOpen) out.open() + + checkOffsetAndCount(source.size, 0, byteCount) + var remaining = byteCount + while (remaining > 0) { + val head = source.head!! + val toCopy = minOf(remaining, head.limit - head.pos).toInt() + val bytesWritten = head.data.usePinned { + val bytes = it.addressOf(head.pos).reinterpret() + out.write(bytes, toCopy.convert()).toLong() + } + + if (bytesWritten < 0L) throw IOException(out.streamError?.localizedDescription ?: "Unknown error") + if (bytesWritten == 0L) throw IOException("NSOutputStream reached capacity") + + head.pos += bytesWritten.toInt() + remaining -= bytesWritten + source.size -= bytesWritten + + if (head.pos == head.limit) { + source.head = head.pop() + SegmentPool.recycle(head) + } + } + } + + override fun flush() { + // no-op + } + + override fun close() = out.close() + + override fun toString() = "RawSink($out)" +} + /** * Returns [RawSource] that reads from an input stream. * diff --git a/core/apple/src/SinksApple.kt b/core/apple/src/SinksApple.kt new file mode 100644 index 000000000..b8dbeb3cf --- /dev/null +++ b/core/apple/src/SinksApple.kt @@ -0,0 +1,92 @@ +/* + * Copyright 2017-2023 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 platform.Foundation.* +import platform.darwin.NSInteger +import platform.darwin.NSUInteger +import platform.posix.memcpy +import platform.posix.uint8_tVar + +/** + * Returns an output stream that writes to this sink. Closing the stream will also close this sink. + * + * @sample kotlinx.io.samples.KotlinxIoSamplesApple.asStream + */ +public fun Sink.asNSOutputStream(): NSOutputStream = SinkNSOutputStream(this) + +@OptIn(UnsafeNumber::class) +private class SinkNSOutputStream( + private val sink: Sink, +) : NSOutputStream(toMemory = Unit) { + + private val isClosed: () -> Boolean = when (sink) { + is RealSink -> sink::closed + is Buffer -> { + { false } + } + } + + private var status = NSStreamStatusNotOpen + private var error: NSError? = null + set(value) { + status = NSStreamStatusError + field = value + } + + override fun streamStatus() = status + + override fun streamError() = error + + override fun open() { + if (status == NSStreamStatusNotOpen) { + status = NSStreamStatusOpen + } + } + + @OptIn(DelicateIoApi::class) + override fun write(buffer: CPointer?, maxLength: NSUInteger): NSInteger { + return try { + if (isClosed()) throw IOException("Underlying sink is closed.") + if (status != NSStreamStatusOpen) return -1 + status = NSStreamStatusWriting + sink.writeToInternalBuffer { + it.writeNative(buffer, maxLength.toInt()) + } + status = NSStreamStatusOpen + maxLength.convert() + } catch (e: Exception) { + error = e.toNSError() + -1 + } + } + + override fun hasSpaceAvailable() = true + + override fun close() { + status = NSStreamStatusClosed + sink.close() + } + + override fun description() = "$sink.asNSOutputStream()" + + private fun Buffer.writeNative(source: CPointer?, maxLength: Int) { + var currentOffset = 0 + while (currentOffset < maxLength) { + val tail = writableSegment(1) + + val toCopy = minOf(maxLength - currentOffset, Segment.SIZE - tail.limit) + tail.data.usePinned { + memcpy(it.addressOf(tail.pos), source + currentOffset, toCopy.convert()) + } + + currentOffset += toCopy + tail.limit += toCopy + } + size += maxLength + } +} diff --git a/core/apple/src/SourcesApple.kt b/core/apple/src/SourcesApple.kt index a02e9e8e4..f3bd95742 100644 --- a/core/apple/src/SourcesApple.kt +++ b/core/apple/src/SourcesApple.kt @@ -15,6 +15,8 @@ import platform.posix.uint8_tVar /** * Returns an input stream that reads from this source. Closing the stream will also close this source. + * + * @sample kotlinx.io.samples.KotlinxIoSamplesApple.asStream */ public fun Source.asNSInputStream(): NSInputStream = SourceNSInputStream(this) diff --git a/core/apple/test/NSInputStreamSourceTest.kt b/core/apple/test/NSInputStreamSourceTest.kt index d946269ec..31877c11f 100644 --- a/core/apple/test/NSInputStreamSourceTest.kt +++ b/core/apple/test/NSInputStreamSourceTest.kt @@ -18,7 +18,7 @@ class NSInputStreamSourceTest { val input = NSInputStream(byteArrayOf(0x61).toNSData()) val source = input.asSource() val buffer = Buffer() - source.readAtMostTo(buffer, 1) + source.readAtMostTo(buffer, 1L) assertEquals("a", buffer.readString()) } diff --git a/core/apple/test/NSOutputStreamSinkTest.kt b/core/apple/test/NSOutputStreamSinkTest.kt new file mode 100644 index 000000000..f06d08b81 --- /dev/null +++ b/core/apple/test/NSOutputStreamSinkTest.kt @@ -0,0 +1,45 @@ +package kotlinx.io + +import kotlinx.cinterop.IntVar +import kotlinx.cinterop.pointed +import kotlinx.cinterop.reinterpret +import kotlinx.cinterop.value +import platform.Foundation.* +import kotlin.test.Test +import kotlin.test.assertEquals + +class NSOutputStreamSinkTest { + @Test + fun nsOutputStreamSink() { + val out = NSOutputStream.outputStreamToMemory() + val sink = out.asSink() + val buffer = Buffer().apply { + writeString("a") + } + sink.write(buffer, 1L) + val data = out.propertyForKey(NSStreamDataWrittenToMemoryStreamKey) as NSData + val byte = data.bytes?.reinterpret()?.pointed?.value + assertEquals(0x61, byte) + } + + @Test + fun sinkFromOutputStream() { + val data = Buffer().apply { + writeString("a") + writeString("b".repeat(9998)) + writeString("c") + } + val out = NSOutputStream.outputStreamToMemory() + val sink = out.asSink() + + sink.write(data, 3) + val outData = out.propertyForKey(NSStreamDataWrittenToMemoryStreamKey) as NSData + val outString = outData.toByteArray().decodeToString() + assertEquals("abb", outString) + + sink.write(data, data.size) + val outData2 = out.propertyForKey(NSStreamDataWrittenToMemoryStreamKey) as NSData + val outString2 = outData2.toByteArray().decodeToString() + assertEquals("a" + "b".repeat(9998) + "c", outString2) + } +} diff --git a/core/apple/test/SinkNSOutputStreamTest.kt b/core/apple/test/SinkNSOutputStreamTest.kt new file mode 100644 index 000000000..f0ee764b2 --- /dev/null +++ b/core/apple/test/SinkNSOutputStreamTest.kt @@ -0,0 +1,62 @@ +package kotlinx.io + +import kotlinx.cinterop.* +import platform.Foundation.* +import platform.darwin.UInt8Var +import kotlin.test.* + +@OptIn(UnsafeNumber::class) +class SinkNSOutputStreamTest { + @Test + fun bufferOutputStream() { + val sink = Buffer() + testOutputStream(sink) + } + + @Test + fun realSinkOutputStream() { + val sink = RealSink(Buffer()) + testOutputStream(sink) + } + + @OptIn(InternalIoApi::class) + private fun testOutputStream(sink: Sink) { + val out = sink.asNSOutputStream() + val byteArray = "abc".encodeToByteArray() + byteArray.usePinned { + val cPtr = it.addressOf(0).reinterpret() + + assertEquals(NSStreamStatusNotOpen, out.streamStatus) + assertEquals(-1, out.write(cPtr, 3U)) + out.open() + assertEquals(NSStreamStatusOpen, out.streamStatus) + + assertEquals(3, out.write(cPtr, 3U)) + assertEquals("[97, 98, 99]", sink.buffer.readByteArray().contentToString()) + } + } + + @Test + @OptIn(DelicateIoApi::class) + fun nsOutputStreamClose() { + val buffer = Buffer() + val sink = RealSink(buffer) + assertFalse(sink.closed) + + val out = sink.asNSOutputStream() + out.open() + out.close() + assertTrue(sink.closed) + assertEquals(NSStreamStatusClosed, out.streamStatus) + + val byteArray = ByteArray(4) + byteArray.usePinned { + val cPtr = it.addressOf(0).reinterpret() + + assertEquals(-1, out.write(cPtr, 4U)) + assertNotNull(out.streamError) + assertEquals("Underlying sink is closed.", out.streamError?.localizedDescription) + assertTrue(sink.buffer.readByteArray().isEmpty()) + } + } +} diff --git a/core/apple/test/SourceNSInputStreamTest.kt b/core/apple/test/SourceNSInputStreamTest.kt index b193770ed..f20bd82ae 100644 --- a/core/apple/test/SourceNSInputStreamTest.kt +++ b/core/apple/test/SourceNSInputStreamTest.kt @@ -24,7 +24,7 @@ class SourceNSInputStreamTest { } @Test - fun realBufferedSourceInputStream() { + fun realSourceInputStream() { val source = Buffer() source.writeString("abc") testInputStream(RealSource(source).asNSInputStream()) diff --git a/core/apple/test/samples/samplesApple.kt b/core/apple/test/samples/samplesApple.kt index a45ed7f7a..c4b07df54 100644 --- a/core/apple/test/samples/samplesApple.kt +++ b/core/apple/test/samples/samplesApple.kt @@ -1,9 +1,14 @@ package kotlinx.io.samples +import kotlinx.cinterop.UnsafeNumber +import kotlinx.cinterop.convert +import kotlinx.cinterop.objcPtr +import kotlinx.cinterop.reinterpret import kotlinx.io.* -import platform.Foundation.NSInputStream +import platform.Foundation.* import kotlin.test.Test import kotlin.test.assertContentEquals +import kotlin.test.assertEquals class KotlinxIoSamplesApple { @Test @@ -14,4 +19,35 @@ class KotlinxIoSamplesApple { val receivedData = inputStream.asSource().buffered().readByteArray() assertContentEquals(data, receivedData) } + + @Test + fun outputStreamAsSink() { + val data = ByteArray(100) { it.toByte() } + val outputStream = NSOutputStream.outputStreamToMemory() + + val sink = outputStream.asSink().buffered() + sink.write(data) + sink.flush() + + val writtenData = outputStream.propertyForKey(NSStreamDataWrittenToMemoryStreamKey) as NSData + assertContentEquals(data, writtenData.toByteArray()) + } + + @Test + @OptIn(UnsafeNumber::class) + fun asStream() { + val buffer = Buffer() + val data = ByteArray(100) { it.toByte() }.toNSData() + + val outputStream = buffer.asNSOutputStream() + outputStream.open() + outputStream.write(data.bytes?.reinterpret(), data.length) + + val inputStream = buffer.asNSInputStream() + inputStream.open() + val readData = NSMutableData.create(length = 100.convert())!! + inputStream.read(readData.bytes?.reinterpret(), 100.convert()) + + assertContentEquals(data.toByteArray(), readData.toByteArray()) + } } diff --git a/core/apple/test/utilApple.kt b/core/apple/test/utilApple.kt index bd424d0d5..144095b4b 100644 --- a/core/apple/test/utilApple.kt +++ b/core/apple/test/utilApple.kt @@ -1,12 +1,10 @@ package kotlinx.io -import kotlinx.cinterop.UnsafeNumber -import kotlinx.cinterop.addressOf -import kotlinx.cinterop.convert -import kotlinx.cinterop.usePinned +import kotlinx.cinterop.* import platform.Foundation.NSData import platform.Foundation.create import platform.Foundation.data +import platform.posix.memcpy fun ByteArray.toNSData() = if (isNotEmpty()) { usePinned { @@ -16,3 +14,10 @@ fun ByteArray.toNSData() = if (isNotEmpty()) { } else { NSData.data() } + +@OptIn(UnsafeNumber::class) +fun NSData.toByteArray() = ByteArray(length.toInt()).apply { + if (isNotEmpty()) { + memcpy(refTo(0), bytes, length) + } +} diff --git a/core/jvm/src/JvmCore.kt b/core/jvm/src/JvmCore.kt index d92b88249..fd2a108bf 100644 --- a/core/jvm/src/JvmCore.kt +++ b/core/jvm/src/JvmCore.kt @@ -62,7 +62,7 @@ private open class OutputStreamSink( override fun close() = out.close() - override fun toString() = "sink($out)" + override fun toString() = "RawSink($out)" } /** diff --git a/core/jvm/src/SinksJvm.kt b/core/jvm/src/SinksJvm.kt index 8c5500349..3b70cd100 100644 --- a/core/jvm/src/SinksJvm.kt +++ b/core/jvm/src/SinksJvm.kt @@ -64,12 +64,12 @@ public fun Sink.asOutputStream(): OutputStream { return object : OutputStream() { override fun write(byte: Int) { - if (isClosed()) throw IOException("Underlying sink is closed") + if (isClosed()) throw IOException("Underlying sink is closed.") writeToInternalBuffer { it.writeByte(byte.toByte()) } } override fun write(data: ByteArray, offset: Int, byteCount: Int) { - if (isClosed()) throw IOException("Underlying sink is closed") + if (isClosed()) throw IOException("Underlying sink is closed.") writeToInternalBuffer { it.write(data, offset, offset + byteCount) } } @@ -82,7 +82,7 @@ public fun Sink.asOutputStream(): OutputStream { override fun close() = this@asOutputStream.close() - override fun toString() = "${this@asOutputStream}.outputStream()" + override fun toString() = "${this@asOutputStream}.asOutputStream()" } } From 2ba8beda24ba47aced7e2ceb6a25145a41782bb9 Mon Sep 17 00:00:00 2001 From: Jeff Lockhart Date: Wed, 12 Jul 2023 21:47:29 -0600 Subject: [PATCH 09/51] Support SinkNSOutputStream NSStreamDataWrittenToMemoryStreamKey --- core/apple/src/-Util.kt | 16 ++++++++++++--- core/apple/src/SinksApple.kt | 24 ++++++++++++++--------- core/apple/src/SourcesApple.kt | 22 ++++++++++----------- core/apple/test/SinkNSOutputStreamTest.kt | 4 ++++ core/apple/test/utilApple.kt | 11 ----------- 5 files changed, 43 insertions(+), 34 deletions(-) diff --git a/core/apple/src/-Util.kt b/core/apple/src/-Util.kt index a979dc29c..2c935602d 100644 --- a/core/apple/src/-Util.kt +++ b/core/apple/src/-Util.kt @@ -1,9 +1,10 @@ package kotlinx.io import kotlinx.cinterop.UnsafeNumber -import platform.Foundation.NSError -import platform.Foundation.NSLocalizedDescriptionKey -import platform.Foundation.NSUnderlyingErrorKey +import kotlinx.cinterop.addressOf +import kotlinx.cinterop.convert +import kotlinx.cinterop.usePinned +import platform.Foundation.* @OptIn(UnsafeNumber::class) internal fun Exception.toNSError() = NSError( @@ -14,3 +15,12 @@ internal fun Exception.toNSError() = NSError( NSUnderlyingErrorKey to this ) ) + +internal fun ByteArray.toNSData() = if (isNotEmpty()) { + usePinned { + @OptIn(UnsafeNumber::class) + NSData.create(bytes = it.addressOf(0), length = size.convert()) + } +} else { + NSData.data() +} diff --git a/core/apple/src/SinksApple.kt b/core/apple/src/SinksApple.kt index b8dbeb3cf..ce270f9a2 100644 --- a/core/apple/src/SinksApple.kt +++ b/core/apple/src/SinksApple.kt @@ -48,6 +48,11 @@ private class SinkNSOutputStream( } } + override fun close() { + status = NSStreamStatusClosed + sink.close() + } + @OptIn(DelicateIoApi::class) override fun write(buffer: CPointer?, maxLength: NSUInteger): NSInteger { return try { @@ -65,15 +70,6 @@ private class SinkNSOutputStream( } } - override fun hasSpaceAvailable() = true - - override fun close() { - status = NSStreamStatusClosed - sink.close() - } - - override fun description() = "$sink.asNSOutputStream()" - private fun Buffer.writeNative(source: CPointer?, maxLength: Int) { var currentOffset = 0 while (currentOffset < maxLength) { @@ -89,4 +85,14 @@ private class SinkNSOutputStream( } size += maxLength } + + override fun hasSpaceAvailable() = true + + @OptIn(InternalIoApi::class) + override fun propertyForKey(key: NSStreamPropertyKey): Any? = when (key) { + NSStreamDataWrittenToMemoryStreamKey -> sink.buffer.readByteArray().toNSData() + else -> null + } + + override fun description() = "$sink.asNSOutputStream()" } diff --git a/core/apple/src/SourcesApple.kt b/core/apple/src/SourcesApple.kt index f3bd95742..db7169da7 100644 --- a/core/apple/src/SourcesApple.kt +++ b/core/apple/src/SourcesApple.kt @@ -51,6 +51,13 @@ private class SourceNSInputStream( } } + override fun close() { + status = NSStreamStatusClosed + pinnedBuffer?.unpin() + pinnedBuffer = null + source.close() + } + override fun read(buffer: CPointer?, maxLength: NSUInteger): NSInteger { try { if (isClosed()) throw IOException("Underlying source is closed.") @@ -85,17 +92,6 @@ private class SourceNSInputStream( return false } - override fun hasBytesAvailable() = !source.exhausted() - - override fun close() { - status = NSStreamStatusClosed - pinnedBuffer?.unpin() - pinnedBuffer = null - source.close() - } - - override fun description() = "$source.asNSInputStream()" - private fun Buffer.readNative(sink: CPointer?, maxLength: Int): Int { val s = head ?: return 0 val toCopy = minOf(maxLength, s.limit - s.pos) @@ -113,4 +109,8 @@ private class SourceNSInputStream( return toCopy } + + override fun hasBytesAvailable() = !source.exhausted() + + override fun description() = "$source.asNSInputStream()" } diff --git a/core/apple/test/SinkNSOutputStreamTest.kt b/core/apple/test/SinkNSOutputStreamTest.kt index f0ee764b2..3de1d9896 100644 --- a/core/apple/test/SinkNSOutputStreamTest.kt +++ b/core/apple/test/SinkNSOutputStreamTest.kt @@ -33,6 +33,10 @@ class SinkNSOutputStreamTest { assertEquals(3, out.write(cPtr, 3U)) assertEquals("[97, 98, 99]", sink.buffer.readByteArray().contentToString()) + + assertEquals(3, out.write(cPtr, 3U)) + val data = out.propertyForKey(NSStreamDataWrittenToMemoryStreamKey) as NSData + assertEquals("abc", data.toByteArray().decodeToString()) } } diff --git a/core/apple/test/utilApple.kt b/core/apple/test/utilApple.kt index 144095b4b..3a0d56b63 100644 --- a/core/apple/test/utilApple.kt +++ b/core/apple/test/utilApple.kt @@ -2,19 +2,8 @@ package kotlinx.io import kotlinx.cinterop.* import platform.Foundation.NSData -import platform.Foundation.create -import platform.Foundation.data import platform.posix.memcpy -fun ByteArray.toNSData() = if (isNotEmpty()) { - usePinned { - @OptIn(UnsafeNumber::class) - NSData.create(bytes = it.addressOf(0), length = size.convert()) - } -} else { - NSData.data() -} - @OptIn(UnsafeNumber::class) fun NSData.toByteArray() = ByteArray(length.toInt()).apply { if (isNotEmpty()) { From d7b8e1d7be45f9b6891aafa6813b440f39fab40f Mon Sep 17 00:00:00 2001 From: Jeff Lockhart Date: Wed, 12 Jul 2023 22:22:32 -0600 Subject: [PATCH 10/51] Override SourceNSInputStream.propertyForKey as no-op --- core/apple/src/SourcesApple.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/core/apple/src/SourcesApple.kt b/core/apple/src/SourcesApple.kt index db7169da7..5c4ff3940 100644 --- a/core/apple/src/SourcesApple.kt +++ b/core/apple/src/SourcesApple.kt @@ -112,5 +112,7 @@ private class SourceNSInputStream( override fun hasBytesAvailable() = !source.exhausted() + override fun propertyForKey(key: NSStreamPropertyKey): Any? = null + override fun description() = "$source.asNSInputStream()" } From 62757c57c1afca485bdc1f757bea1437d7fdb5ad Mon Sep 17 00:00:00 2001 From: Jeff Lockhart Date: Wed, 12 Jul 2023 23:12:59 -0600 Subject: [PATCH 11/51] Mark status property @Volatile --- core/apple/src/SinksApple.kt | 3 +++ core/apple/src/SourcesApple.kt | 3 +++ 2 files changed, 6 insertions(+) diff --git a/core/apple/src/SinksApple.kt b/core/apple/src/SinksApple.kt index ce270f9a2..9845f4322 100644 --- a/core/apple/src/SinksApple.kt +++ b/core/apple/src/SinksApple.kt @@ -11,6 +11,7 @@ import platform.darwin.NSInteger import platform.darwin.NSUInteger import platform.posix.memcpy import platform.posix.uint8_tVar +import kotlin.concurrent.Volatile /** * Returns an output stream that writes to this sink. Closing the stream will also close this sink. @@ -31,6 +32,8 @@ private class SinkNSOutputStream( } } + @OptIn(ExperimentalStdlibApi::class) + @Volatile private var status = NSStreamStatusNotOpen private var error: NSError? = null set(value) { diff --git a/core/apple/src/SourcesApple.kt b/core/apple/src/SourcesApple.kt index 5c4ff3940..fdce97d41 100644 --- a/core/apple/src/SourcesApple.kt +++ b/core/apple/src/SourcesApple.kt @@ -12,6 +12,7 @@ import platform.darwin.NSUInteger import platform.darwin.NSUIntegerVar import platform.posix.memcpy import platform.posix.uint8_tVar +import kotlin.concurrent.Volatile /** * Returns an input stream that reads from this source. Closing the stream will also close this source. @@ -32,6 +33,8 @@ private class SourceNSInputStream( } } + @OptIn(ExperimentalStdlibApi::class) + @Volatile private var status = NSStreamStatusNotOpen private var error: NSError? = null set(value) { From 613e2beb1f4e47ce5c761cdb69f0fed0d8305752 Mon Sep 17 00:00:00 2001 From: Jeff Lockhart Date: Thu, 13 Jul 2023 16:06:51 -0600 Subject: [PATCH 12/51] Code review feedback and fixes --- core/apple/src/-Util.kt | 4 +-- core/apple/src/AppleCore.kt | 3 ++ core/apple/src/BuffersApple.kt | 41 +++++++++++++++++++++++ core/apple/src/SinksApple.kt | 20 ++--------- core/apple/src/SourcesApple.kt | 31 +++++------------ core/apple/test/NSOutputStreamSinkTest.kt | 10 +++--- 6 files changed, 63 insertions(+), 46 deletions(-) create mode 100644 core/apple/src/BuffersApple.kt diff --git a/core/apple/src/-Util.kt b/core/apple/src/-Util.kt index 2c935602d..2df53b8f0 100644 --- a/core/apple/src/-Util.kt +++ b/core/apple/src/-Util.kt @@ -1,3 +1,5 @@ +@file:OptIn(UnsafeNumber::class) + package kotlinx.io import kotlinx.cinterop.UnsafeNumber @@ -6,7 +8,6 @@ import kotlinx.cinterop.convert import kotlinx.cinterop.usePinned import platform.Foundation.* -@OptIn(UnsafeNumber::class) internal fun Exception.toNSError() = NSError( domain = "Kotlin", code = 0, @@ -18,7 +19,6 @@ internal fun Exception.toNSError() = NSError( internal fun ByteArray.toNSData() = if (isNotEmpty()) { usePinned { - @OptIn(UnsafeNumber::class) NSData.create(bytes = it.addressOf(0), length = size.convert()) } } else { diff --git a/core/apple/src/AppleCore.kt b/core/apple/src/AppleCore.kt index 032220132..c727a65c6 100644 --- a/core/apple/src/AppleCore.kt +++ b/core/apple/src/AppleCore.kt @@ -8,6 +8,7 @@ package kotlinx.io import kotlinx.cinterop.* import platform.Foundation.NSInputStream import platform.Foundation.NSOutputStream +import platform.Foundation.NSStreamStatusClosed import platform.Foundation.NSStreamStatusNotOpen import platform.darwin.UInt8Var @@ -26,6 +27,7 @@ private open class OutputStreamSink( @OptIn(UnsafeNumber::class) override fun write(source: Buffer, byteCount: Long) { + if (out.streamStatus == NSStreamStatusClosed) throw IOException("Stream Closed") if (out.streamStatus == NSStreamStatusNotOpen) out.open() checkOffsetAndCount(source.size, 0, byteCount) @@ -76,6 +78,7 @@ private open class NSInputStreamSource( @OptIn(UnsafeNumber::class) override fun readAtMostTo(sink: Buffer, byteCount: Long): Long { + if (input.streamStatus == NSStreamStatusClosed) throw IOException("Stream Closed") if (input.streamStatus == NSStreamStatusNotOpen) input.open() if (byteCount == 0L) return 0L diff --git a/core/apple/src/BuffersApple.kt b/core/apple/src/BuffersApple.kt new file mode 100644 index 000000000..788fda71f --- /dev/null +++ b/core/apple/src/BuffersApple.kt @@ -0,0 +1,41 @@ +@file:OptIn(UnsafeNumber::class) + +package kotlinx.io + +import kotlinx.cinterop.* +import platform.posix.memcpy +import platform.posix.uint8_tVar + +internal fun Buffer.write(source: CPointer?, maxLength: Int) { + var currentOffset = 0 + while (currentOffset < maxLength) { + val tail = writableSegment(1) + + val toCopy = minOf(maxLength - currentOffset, Segment.SIZE - tail.limit) + tail.data.usePinned { + memcpy(it.addressOf(tail.pos), source + currentOffset, toCopy.convert()) + } + + currentOffset += toCopy + tail.limit += toCopy + } + size += maxLength +} + +internal fun Buffer.readAtMostTo(sink: CPointer?, maxLength: Int): Int { + val s = head ?: return 0 + val toCopy = minOf(maxLength, s.limit - s.pos) + s.data.usePinned { + memcpy(sink, it.addressOf(s.pos), toCopy.convert()) + } + + s.pos += toCopy + size -= toCopy.toLong() + + if (s.pos == s.limit) { + head = s.pop() + SegmentPool.recycle(s) + } + + return toCopy +} diff --git a/core/apple/src/SinksApple.kt b/core/apple/src/SinksApple.kt index 9845f4322..fab670b97 100644 --- a/core/apple/src/SinksApple.kt +++ b/core/apple/src/SinksApple.kt @@ -63,7 +63,7 @@ private class SinkNSOutputStream( if (status != NSStreamStatusOpen) return -1 status = NSStreamStatusWriting sink.writeToInternalBuffer { - it.writeNative(buffer, maxLength.toInt()) + it.write(buffer, maxLength.toInt()) } status = NSStreamStatusOpen maxLength.convert() @@ -73,23 +73,7 @@ private class SinkNSOutputStream( } } - private fun Buffer.writeNative(source: CPointer?, maxLength: Int) { - var currentOffset = 0 - while (currentOffset < maxLength) { - val tail = writableSegment(1) - - val toCopy = minOf(maxLength - currentOffset, Segment.SIZE - tail.limit) - tail.data.usePinned { - memcpy(it.addressOf(tail.pos), source + currentOffset, toCopy.convert()) - } - - currentOffset += toCopy - tail.limit += toCopy - } - size += maxLength - } - - override fun hasSpaceAvailable() = true + override fun hasSpaceAvailable() = !isClosed() @OptIn(InternalIoApi::class) override fun propertyForKey(key: NSStreamPropertyKey): Any? = when (key) { diff --git a/core/apple/src/SourcesApple.kt b/core/apple/src/SourcesApple.kt index fdce97d41..3fe3fb63d 100644 --- a/core/apple/src/SourcesApple.kt +++ b/core/apple/src/SourcesApple.kt @@ -10,7 +10,6 @@ import platform.Foundation.* import platform.darwin.NSInteger import platform.darwin.NSUInteger import platform.darwin.NSUIntegerVar -import platform.posix.memcpy import platform.posix.uint8_tVar import kotlin.concurrent.Volatile @@ -62,16 +61,22 @@ private class SourceNSInputStream( } override fun read(buffer: CPointer?, maxLength: NSUInteger): NSInteger { + pinnedBuffer?.unpin() + pinnedBuffer = null + try { if (isClosed()) throw IOException("Underlying source is closed.") - if (status != NSStreamStatusOpen) return -1 - status = NSStreamStatusReading + if (status != NSStreamStatusOpen && status != NSStreamStatusAtEnd) { + return -1 + } if (source.exhausted()) { status = NSStreamStatusAtEnd return 0 } + + status = NSStreamStatusReading val toRead = minOf(maxLength.toInt(), source.buffer.size).toInt() - val read = source.buffer.readNative(buffer, toRead).convert() + val read = source.buffer.readAtMostTo(buffer, toRead).convert() status = NSStreamStatusOpen return read } catch (e: Exception) { @@ -95,24 +100,6 @@ private class SourceNSInputStream( return false } - private fun Buffer.readNative(sink: CPointer?, maxLength: Int): Int { - val s = head ?: return 0 - val toCopy = minOf(maxLength, s.limit - s.pos) - s.data.usePinned { - memcpy(sink, it.addressOf(s.pos), toCopy.convert()) - } - - s.pos += toCopy - size -= toCopy.toLong() - - if (s.pos == s.limit) { - head = s.pop() - SegmentPool.recycle(s) - } - - return toCopy - } - override fun hasBytesAvailable() = !source.exhausted() override fun propertyForKey(key: NSStreamPropertyKey): Any? = null diff --git a/core/apple/test/NSOutputStreamSinkTest.kt b/core/apple/test/NSOutputStreamSinkTest.kt index f06d08b81..bd615063e 100644 --- a/core/apple/test/NSOutputStreamSinkTest.kt +++ b/core/apple/test/NSOutputStreamSinkTest.kt @@ -1,15 +1,16 @@ package kotlinx.io import kotlinx.cinterop.IntVar -import kotlinx.cinterop.pointed +import kotlinx.cinterop.UnsafeNumber +import kotlinx.cinterop.get import kotlinx.cinterop.reinterpret -import kotlinx.cinterop.value import platform.Foundation.* import kotlin.test.Test import kotlin.test.assertEquals class NSOutputStreamSinkTest { @Test + @OptIn(UnsafeNumber::class) fun nsOutputStreamSink() { val out = NSOutputStream.outputStreamToMemory() val sink = out.asSink() @@ -18,8 +19,9 @@ class NSOutputStreamSinkTest { } sink.write(buffer, 1L) val data = out.propertyForKey(NSStreamDataWrittenToMemoryStreamKey) as NSData - val byte = data.bytes?.reinterpret()?.pointed?.value - assertEquals(0x61, byte) + assertEquals(1U, data.length) + val bytes = data.bytes!!.reinterpret() + assertEquals(0x61, bytes[0]) } @Test From e5f5c27bde31a28162106f4e63a16fab9433fa7f Mon Sep 17 00:00:00 2001 From: Jeff Lockhart Date: Sun, 16 Jul 2023 20:23:48 -0600 Subject: [PATCH 13/51] Fix reading byte as int --- core/apple/test/NSOutputStreamSinkTest.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/apple/test/NSOutputStreamSinkTest.kt b/core/apple/test/NSOutputStreamSinkTest.kt index bd615063e..97530639a 100644 --- a/core/apple/test/NSOutputStreamSinkTest.kt +++ b/core/apple/test/NSOutputStreamSinkTest.kt @@ -1,6 +1,6 @@ package kotlinx.io -import kotlinx.cinterop.IntVar +import kotlinx.cinterop.ByteVar import kotlinx.cinterop.UnsafeNumber import kotlinx.cinterop.get import kotlinx.cinterop.reinterpret @@ -20,7 +20,7 @@ class NSOutputStreamSinkTest { sink.write(buffer, 1L) val data = out.propertyForKey(NSStreamDataWrittenToMemoryStreamKey) as NSData assertEquals(1U, data.length) - val bytes = data.bytes!!.reinterpret() + val bytes = data.bytes!!.reinterpret() assertEquals(0x61, bytes[0]) } From f5dfc1a1514ea61cdf36fe91e57768091a67c335 Mon Sep 17 00:00:00 2001 From: Jeff Lockhart Date: Sun, 16 Jul 2023 23:26:34 -0600 Subject: [PATCH 14/51] Buffer.snapshotAsNSData() for NSStreamDataWrittenToMemoryStreamKey --- core/apple/src/BuffersApple.kt | 26 ++++++++++++++++++++++++++ core/apple/src/SinksApple.kt | 2 +- 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/core/apple/src/BuffersApple.kt b/core/apple/src/BuffersApple.kt index 788fda71f..7a789f849 100644 --- a/core/apple/src/BuffersApple.kt +++ b/core/apple/src/BuffersApple.kt @@ -3,6 +3,11 @@ package kotlinx.io import kotlinx.cinterop.* +import kotlinx.io.bytestring.ByteString +import kotlinx.io.bytestring.buildByteString +import platform.Foundation.* +import platform.darwin.NSUInteger +import platform.darwin.NSUIntegerMax import platform.posix.memcpy import platform.posix.uint8_tVar @@ -39,3 +44,24 @@ internal fun Buffer.readAtMostTo(sink: CPointer?, maxLength: Int): I return toCopy } + +internal fun Buffer.snapshotAsNSData(): NSData { + if (size == 0L) return NSData.data() + + check(size.toULong() <= NSUIntegerMax) { "Buffer is too long ($size) to be converted into NSData." } + + val data = NSMutableData.create(length = size.convert())!! + var curr = head + var index: NSUInteger = 0U + do { + check(curr != null) { "Current segment is null" } + val pos = curr.pos + val length: NSUInteger = (curr.limit - pos).convert() + curr.data.usePinned { + data.replaceBytesInRange(NSMakeRange(index, length), it.addressOf(pos)) + } + curr = curr.next + index += length + } while (curr !== head) + return data +} diff --git a/core/apple/src/SinksApple.kt b/core/apple/src/SinksApple.kt index fab670b97..942e51be1 100644 --- a/core/apple/src/SinksApple.kt +++ b/core/apple/src/SinksApple.kt @@ -77,7 +77,7 @@ private class SinkNSOutputStream( @OptIn(InternalIoApi::class) override fun propertyForKey(key: NSStreamPropertyKey): Any? = when (key) { - NSStreamDataWrittenToMemoryStreamKey -> sink.buffer.readByteArray().toNSData() + NSStreamDataWrittenToMemoryStreamKey -> sink.buffer.snapshotAsNSData() else -> null } From c9d1f442b07bc03016bd2b6d1bfc3a1dfc2257ce Mon Sep 17 00:00:00 2001 From: Jeff Lockhart Date: Sun, 16 Jul 2023 23:33:17 -0600 Subject: [PATCH 15/51] Test SinkNSOutputStream with data longer than Segment --- core/apple/test/NSInputStreamSourceTest.kt | 18 ++++++------ core/apple/test/SinkNSOutputStreamTest.kt | 32 +++++++++++++--------- 2 files changed, 27 insertions(+), 23 deletions(-) diff --git a/core/apple/test/NSInputStreamSourceTest.kt b/core/apple/test/NSInputStreamSourceTest.kt index 31877c11f..e99891d4c 100644 --- a/core/apple/test/NSInputStreamSourceTest.kt +++ b/core/apple/test/NSInputStreamSourceTest.kt @@ -10,8 +10,6 @@ import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.fail -private const val SEGMENT_SIZE = Segment.SIZE - class NSInputStreamSourceTest { @Test fun nsInputStreamSource() { @@ -25,7 +23,7 @@ class NSInputStreamSourceTest { @Test fun sourceFromInputStream() { val input = NSInputStream( - ("a" + "b".repeat(SEGMENT_SIZE * 2) + "c").encodeToByteArray().toNSData(), + ("a" + "b".repeat(Segment.SIZE * 2) + "c").encodeToByteArray().toNSData(), ) // Source: ab...bc @@ -37,12 +35,12 @@ class NSInputStreamSourceTest { assertEquals("abb", sink.readString(3)) // Source: b...bc. Sink: b...b. - assertEquals(SEGMENT_SIZE.toLong(), source.readAtMostTo(sink, 20000)) - assertEquals("b".repeat(SEGMENT_SIZE), sink.readString()) + assertEquals(Segment.SIZE.toLong(), source.readAtMostTo(sink, 20000)) + assertEquals("b".repeat(Segment.SIZE), sink.readString()) // Source: b...bc. Sink: b...bc. - assertEquals((SEGMENT_SIZE - 1).toLong(), source.readAtMostTo(sink, 20000)) - assertEquals("b".repeat(SEGMENT_SIZE - 2) + "c", sink.readString()) + assertEquals((Segment.SIZE - 1).toLong(), source.readAtMostTo(sink, 20000)) + assertEquals("b".repeat(Segment.SIZE - 2) + "c", sink.readString()) // Source and sink are empty. assertEquals(-1, source.readAtMostTo(sink, 1)) @@ -50,12 +48,12 @@ class NSInputStreamSourceTest { @Test fun sourceFromInputStreamWithSegmentSize() { - val input = NSInputStream(ByteArray(SEGMENT_SIZE).toNSData()) + val input = NSInputStream(ByteArray(Segment.SIZE).toNSData()) val source = input.asSource() val sink = Buffer() - assertEquals(SEGMENT_SIZE.toLong(), source.readAtMostTo(sink, SEGMENT_SIZE.toLong())) - assertEquals(-1, source.readAtMostTo(sink, SEGMENT_SIZE.toLong())) + assertEquals(Segment.SIZE.toLong(), source.readAtMostTo(sink, Segment.SIZE.toLong())) + assertEquals(-1, source.readAtMostTo(sink, Segment.SIZE.toLong())) assertNoEmptySegments(sink) } diff --git a/core/apple/test/SinkNSOutputStreamTest.kt b/core/apple/test/SinkNSOutputStreamTest.kt index 3de1d9896..04ca45343 100644 --- a/core/apple/test/SinkNSOutputStreamTest.kt +++ b/core/apple/test/SinkNSOutputStreamTest.kt @@ -2,6 +2,7 @@ package kotlinx.io import kotlinx.cinterop.* import platform.Foundation.* +import platform.darwin.NSUInteger import platform.darwin.UInt8Var import kotlin.test.* @@ -9,34 +10,39 @@ import kotlin.test.* class SinkNSOutputStreamTest { @Test fun bufferOutputStream() { - val sink = Buffer() - testOutputStream(sink) + testOutputStream(Buffer(), "abc") + testOutputStream(Buffer(), "a" + "b".repeat(Segment.SIZE * 2) + "c") } @Test fun realSinkOutputStream() { - val sink = RealSink(Buffer()) - testOutputStream(sink) + testOutputStream(RealSink(Buffer()), "abc") + testOutputStream(RealSink(Buffer()), "a" + "b".repeat(Segment.SIZE * 2) + "c") } @OptIn(InternalIoApi::class) - private fun testOutputStream(sink: Sink) { + private fun testOutputStream(sink: Sink, input: String) { val out = sink.asNSOutputStream() - val byteArray = "abc".encodeToByteArray() + val byteArray = input.encodeToByteArray() + val size: NSUInteger = input.length.convert() byteArray.usePinned { val cPtr = it.addressOf(0).reinterpret() assertEquals(NSStreamStatusNotOpen, out.streamStatus) - assertEquals(-1, out.write(cPtr, 3U)) + assertEquals(-1, out.write(cPtr, size)) out.open() assertEquals(NSStreamStatusOpen, out.streamStatus) - assertEquals(3, out.write(cPtr, 3U)) - assertEquals("[97, 98, 99]", sink.buffer.readByteArray().contentToString()) - - assertEquals(3, out.write(cPtr, 3U)) - val data = out.propertyForKey(NSStreamDataWrittenToMemoryStreamKey) as NSData - assertEquals("abc", data.toByteArray().decodeToString()) + assertEquals(size.convert(), out.write(cPtr, size)) + sink.flush() + when (sink) { + is Buffer -> { + val data = out.propertyForKey(NSStreamDataWrittenToMemoryStreamKey) as NSData + assertContentEquals(byteArray, data.toByteArray()) + assertContentEquals(byteArray, sink.buffer.readByteArray()) + } + is RealSink -> assertContentEquals(byteArray, (sink.sink as Buffer).readByteArray()) + } } } From 242fda00d5282113c20636e169bf348cef5d0ff2 Mon Sep 17 00:00:00 2001 From: Jeff Lockhart Date: Tue, 18 Jul 2023 09:31:43 -0600 Subject: [PATCH 16/51] Open streams on init --- core/apple/src/AppleCore.kt | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/core/apple/src/AppleCore.kt b/core/apple/src/AppleCore.kt index c727a65c6..c57f2649b 100644 --- a/core/apple/src/AppleCore.kt +++ b/core/apple/src/AppleCore.kt @@ -3,6 +3,8 @@ * Use of this source code is governed by the Apache 2.0 license that can be found in the LICENCE file. */ +@file:OptIn(UnsafeNumber::class) + package kotlinx.io import kotlinx.cinterop.* @@ -25,10 +27,12 @@ private open class OutputStreamSink( private val out: NSOutputStream, ) : RawSink { - @OptIn(UnsafeNumber::class) + init { + if (out.streamStatus == NSStreamStatusNotOpen) out.open() + } + override fun write(source: Buffer, byteCount: Long) { if (out.streamStatus == NSStreamStatusClosed) throw IOException("Stream Closed") - if (out.streamStatus == NSStreamStatusNotOpen) out.open() checkOffsetAndCount(source.size, 0, byteCount) var remaining = byteCount @@ -76,10 +80,12 @@ private open class NSInputStreamSource( private val input: NSInputStream, ) : RawSource { - @OptIn(UnsafeNumber::class) + init { + if (input.streamStatus == NSStreamStatusNotOpen) input.open() + } + override fun readAtMostTo(sink: Buffer, byteCount: Long): Long { if (input.streamStatus == NSStreamStatusClosed) throw IOException("Stream Closed") - if (input.streamStatus == NSStreamStatusNotOpen) input.open() if (byteCount == 0L) return 0L checkByteCount(byteCount) From ead4f7883e7511ecea4565eda70b52bf92adff94 Mon Sep 17 00:00:00 2001 From: Jeff Lockhart Date: Tue, 18 Jul 2023 10:27:19 -0600 Subject: [PATCH 17/51] Update core/apple/src/-Util.kt Co-authored-by: Filipp Zhinkin --- core/apple/src/-Util.kt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/core/apple/src/-Util.kt b/core/apple/src/-Util.kt index 2df53b8f0..d03a4de9f 100644 --- a/core/apple/src/-Util.kt +++ b/core/apple/src/-Util.kt @@ -1,3 +1,7 @@ +/* + * Copyright 2017-2023 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. + */ @file:OptIn(UnsafeNumber::class) package kotlinx.io From f9c93058ed4b4be817656e3d8d77d522dbcc2e3e Mon Sep 17 00:00:00 2001 From: Jeff Lockhart Date: Tue, 18 Jul 2023 10:27:37 -0600 Subject: [PATCH 18/51] Update core/apple/src/BuffersApple.kt Co-authored-by: Filipp Zhinkin --- core/apple/src/BuffersApple.kt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/core/apple/src/BuffersApple.kt b/core/apple/src/BuffersApple.kt index 7a789f849..302ff0dc7 100644 --- a/core/apple/src/BuffersApple.kt +++ b/core/apple/src/BuffersApple.kt @@ -1,3 +1,7 @@ +/* + * Copyright 2017-2023 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. + */ @file:OptIn(UnsafeNumber::class) package kotlinx.io From 18ff1d05ef1b876dbe1566833870d1acf4264f0d Mon Sep 17 00:00:00 2001 From: Jeff Lockhart Date: Tue, 18 Jul 2023 10:27:54 -0600 Subject: [PATCH 19/51] Update core/apple/test/NSOutputStreamSinkTest.kt Co-authored-by: Filipp Zhinkin --- core/apple/test/NSOutputStreamSinkTest.kt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/core/apple/test/NSOutputStreamSinkTest.kt b/core/apple/test/NSOutputStreamSinkTest.kt index 97530639a..00b73b8d7 100644 --- a/core/apple/test/NSOutputStreamSinkTest.kt +++ b/core/apple/test/NSOutputStreamSinkTest.kt @@ -1,3 +1,7 @@ +/* + * Copyright 2017-2023 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.ByteVar From c4eb1b9b9dd554a2c93aaee55a1d45a75fa7395d Mon Sep 17 00:00:00 2001 From: Jeff Lockhart Date: Tue, 18 Jul 2023 10:28:14 -0600 Subject: [PATCH 20/51] Update core/apple/test/SinkNSOutputStreamTest.kt Co-authored-by: Filipp Zhinkin --- core/apple/test/SinkNSOutputStreamTest.kt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/core/apple/test/SinkNSOutputStreamTest.kt b/core/apple/test/SinkNSOutputStreamTest.kt index 04ca45343..4c89c3f11 100644 --- a/core/apple/test/SinkNSOutputStreamTest.kt +++ b/core/apple/test/SinkNSOutputStreamTest.kt @@ -1,3 +1,7 @@ +/* + * Copyright 2017-2023 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.* From 56bb0e42e2e11321d52f6f97cfdbbeb5b62b2ced Mon Sep 17 00:00:00 2001 From: Jeff Lockhart Date: Tue, 18 Jul 2023 10:28:50 -0600 Subject: [PATCH 21/51] Update core/apple/test/samples/samplesApple.kt Co-authored-by: Filipp Zhinkin --- core/apple/test/samples/samplesApple.kt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/core/apple/test/samples/samplesApple.kt b/core/apple/test/samples/samplesApple.kt index c4b07df54..9802719b4 100644 --- a/core/apple/test/samples/samplesApple.kt +++ b/core/apple/test/samples/samplesApple.kt @@ -1,3 +1,7 @@ +/* + * Copyright 2017-2023 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.samples import kotlinx.cinterop.UnsafeNumber From 9d53c8eeca5240baf22c33a26efda55a7ad1ca25 Mon Sep 17 00:00:00 2001 From: Jeff Lockhart Date: Tue, 18 Jul 2023 10:29:21 -0600 Subject: [PATCH 22/51] Update core/apple/test/utilApple.kt Co-authored-by: Filipp Zhinkin --- core/apple/test/utilApple.kt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/core/apple/test/utilApple.kt b/core/apple/test/utilApple.kt index 3a0d56b63..7e7502e9b 100644 --- a/core/apple/test/utilApple.kt +++ b/core/apple/test/utilApple.kt @@ -1,3 +1,7 @@ +/* + * Copyright 2017-2023 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.* From 4e3ff8786afe1a42c8b88c32001ed3e6fc591cfc Mon Sep 17 00:00:00 2001 From: Jeff Lockhart Date: Tue, 18 Jul 2023 10:39:09 -0600 Subject: [PATCH 23/51] Add samplesApple.kt to Dokka samples --- core/apple/src/SourcesApple.kt | 4 +++- core/apple/test/NSOutputStreamSinkTest.kt | 1 + core/apple/test/SinkNSOutputStreamTest.kt | 1 + core/apple/test/samples/samplesApple.kt | 3 +-- core/apple/test/utilApple.kt | 1 + core/build.gradle.kts | 3 ++- 6 files changed, 9 insertions(+), 4 deletions(-) diff --git a/core/apple/src/SourcesApple.kt b/core/apple/src/SourcesApple.kt index 3fe3fb63d..79e8e750a 100644 --- a/core/apple/src/SourcesApple.kt +++ b/core/apple/src/SourcesApple.kt @@ -86,9 +86,11 @@ private class SourceNSInputStream( } override fun getBuffer(buffer: CPointer>?, length: CPointer?): Boolean { + pinnedBuffer?.unpin() + pinnedBuffer = null + if (source.buffer.size > 0) { source.buffer.head?.let { s -> - pinnedBuffer?.unpin() s.data.pin().let { pinnedBuffer = it buffer?.pointed?.value = it.addressOf(s.pos).reinterpret() diff --git a/core/apple/test/NSOutputStreamSinkTest.kt b/core/apple/test/NSOutputStreamSinkTest.kt index 00b73b8d7..14e24c6d1 100644 --- a/core/apple/test/NSOutputStreamSinkTest.kt +++ b/core/apple/test/NSOutputStreamSinkTest.kt @@ -2,6 +2,7 @@ * Copyright 2017-2023 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.ByteVar diff --git a/core/apple/test/SinkNSOutputStreamTest.kt b/core/apple/test/SinkNSOutputStreamTest.kt index 4c89c3f11..c92a9dba8 100644 --- a/core/apple/test/SinkNSOutputStreamTest.kt +++ b/core/apple/test/SinkNSOutputStreamTest.kt @@ -2,6 +2,7 @@ * Copyright 2017-2023 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.* diff --git a/core/apple/test/samples/samplesApple.kt b/core/apple/test/samples/samplesApple.kt index 9802719b4..871838d99 100644 --- a/core/apple/test/samples/samplesApple.kt +++ b/core/apple/test/samples/samplesApple.kt @@ -2,17 +2,16 @@ * Copyright 2017-2023 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.samples import kotlinx.cinterop.UnsafeNumber import kotlinx.cinterop.convert -import kotlinx.cinterop.objcPtr import kotlinx.cinterop.reinterpret import kotlinx.io.* import platform.Foundation.* import kotlin.test.Test import kotlin.test.assertContentEquals -import kotlin.test.assertEquals class KotlinxIoSamplesApple { @Test diff --git a/core/apple/test/utilApple.kt b/core/apple/test/utilApple.kt index 7e7502e9b..49c9fd168 100644 --- a/core/apple/test/utilApple.kt +++ b/core/apple/test/utilApple.kt @@ -2,6 +2,7 @@ * Copyright 2017-2023 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.* diff --git a/core/build.gradle.kts b/core/build.gradle.kts index 69df80dea..036992b92 100644 --- a/core/build.gradle.kts +++ b/core/build.gradle.kts @@ -89,7 +89,8 @@ tasks.withType().configureEach { "common/test/samples/moduleDescriptionSample.kt", "common/test/samples/samples.kt", "common/test/samples/byteStringSample.kt", - "jvm/test/samples/samplesJvm.kt" + "jvm/test/samples/samplesJvm.kt", + "apple/test/samples/samplesApple.kt" ) } } From aa5830cf021009bd97913cd63aad5217ab6071fa Mon Sep 17 00:00:00 2001 From: Jeff Lockhart Date: Tue, 18 Jul 2023 11:06:02 -0600 Subject: [PATCH 24/51] Verify buffer != null and maxLength >= 0 --- core/apple/src/BuffersApple.kt | 8 ++++++-- core/apple/src/SinksApple.kt | 8 +++++--- core/apple/src/SourcesApple.kt | 1 + 3 files changed, 12 insertions(+), 5 deletions(-) diff --git a/core/apple/src/BuffersApple.kt b/core/apple/src/BuffersApple.kt index 302ff0dc7..be44ccf2b 100644 --- a/core/apple/src/BuffersApple.kt +++ b/core/apple/src/BuffersApple.kt @@ -15,7 +15,9 @@ import platform.darwin.NSUIntegerMax import platform.posix.memcpy import platform.posix.uint8_tVar -internal fun Buffer.write(source: CPointer?, maxLength: Int) { +internal fun Buffer.write(source: CPointer, maxLength: Int) { + require(maxLength >= 0) { "maxLength ($maxLength) must not be negative" } + var currentOffset = 0 while (currentOffset < maxLength) { val tail = writableSegment(1) @@ -31,7 +33,9 @@ internal fun Buffer.write(source: CPointer?, maxLength: Int) { size += maxLength } -internal fun Buffer.readAtMostTo(sink: CPointer?, maxLength: Int): Int { +internal fun Buffer.readAtMostTo(sink: CPointer, maxLength: Int): Int { + require(maxLength >= 0) { "maxLength ($maxLength) must not be negative" } + val s = head ?: return 0 val toCopy = minOf(maxLength, s.limit - s.pos) s.data.usePinned { diff --git a/core/apple/src/SinksApple.kt b/core/apple/src/SinksApple.kt index 942e51be1..00f65de78 100644 --- a/core/apple/src/SinksApple.kt +++ b/core/apple/src/SinksApple.kt @@ -58,18 +58,20 @@ private class SinkNSOutputStream( @OptIn(DelicateIoApi::class) override fun write(buffer: CPointer?, maxLength: NSUInteger): NSInteger { - return try { + try { if (isClosed()) throw IOException("Underlying sink is closed.") if (status != NSStreamStatusOpen) return -1 + if (buffer == null) return -1 + status = NSStreamStatusWriting sink.writeToInternalBuffer { it.write(buffer, maxLength.toInt()) } status = NSStreamStatusOpen - maxLength.convert() + return maxLength.convert() } catch (e: Exception) { error = e.toNSError() - -1 + return -1 } } diff --git a/core/apple/src/SourcesApple.kt b/core/apple/src/SourcesApple.kt index 79e8e750a..3a53146ec 100644 --- a/core/apple/src/SourcesApple.kt +++ b/core/apple/src/SourcesApple.kt @@ -73,6 +73,7 @@ private class SourceNSInputStream( status = NSStreamStatusAtEnd return 0 } + if (buffer == null) return -1 status = NSStreamStatusReading val toRead = minOf(maxLength.toInt(), source.buffer.size).toInt() From 6f076d94b2e92e18bec387061348573236395365 Mon Sep 17 00:00:00 2001 From: Jeff Lockhart Date: Tue, 18 Jul 2023 11:14:35 -0600 Subject: [PATCH 25/51] Check isClosed() in SinkNSOutputStream.streamStatus --- core/apple/src/SinksApple.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/apple/src/SinksApple.kt b/core/apple/src/SinksApple.kt index 00f65de78..e3140ec0b 100644 --- a/core/apple/src/SinksApple.kt +++ b/core/apple/src/SinksApple.kt @@ -41,7 +41,7 @@ private class SinkNSOutputStream( field = value } - override fun streamStatus() = status + override fun streamStatus() = if (isClosed()) NSStreamStatusClosed else status override fun streamError() = error From f78951819f2dc5a6bd8822531af0026ff6fcdddd Mon Sep 17 00:00:00 2001 From: Jeff Lockhart Date: Tue, 18 Jul 2023 11:17:12 -0600 Subject: [PATCH 26/51] Use assertFailsWith --- core/apple/test/NSInputStreamSourceTest.kt | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/core/apple/test/NSInputStreamSourceTest.kt b/core/apple/test/NSInputStreamSourceTest.kt index e99891d4c..f2dea1b6b 100644 --- a/core/apple/test/NSInputStreamSourceTest.kt +++ b/core/apple/test/NSInputStreamSourceTest.kt @@ -8,7 +8,7 @@ package kotlinx.io import platform.Foundation.NSInputStream import kotlin.test.Test import kotlin.test.assertEquals -import kotlin.test.fail +import kotlin.test.assertFailsWith class NSInputStreamSourceTest { @Test @@ -61,11 +61,6 @@ class NSInputStreamSourceTest { @Test fun sourceFromInputStreamBounds() { val source = NSInputStream(ByteArray(100).toNSData()).asSource() - try { - source.readAtMostTo(Buffer(), -1) - fail() - } catch (expected: IllegalArgumentException) { - // expected - } + assertFailsWith { source.readAtMostTo(Buffer(), -1) } } } From feb31452ac0ee28365d9a63ea69c7031ad68af39 Mon Sep 17 00:00:00 2001 From: Jeff Lockhart Date: Tue, 18 Jul 2023 11:19:10 -0600 Subject: [PATCH 27/51] Update core/apple/src/BuffersApple.kt Co-authored-by: Filipp Zhinkin --- core/apple/src/BuffersApple.kt | 2 -- 1 file changed, 2 deletions(-) diff --git a/core/apple/src/BuffersApple.kt b/core/apple/src/BuffersApple.kt index be44ccf2b..41de62d8e 100644 --- a/core/apple/src/BuffersApple.kt +++ b/core/apple/src/BuffersApple.kt @@ -7,8 +7,6 @@ package kotlinx.io import kotlinx.cinterop.* -import kotlinx.io.bytestring.ByteString -import kotlinx.io.bytestring.buildByteString import platform.Foundation.* import platform.darwin.NSUInteger import platform.darwin.NSUIntegerMax From 3508104350231ef0e271fc7a3de88bb950b08ece Mon Sep 17 00:00:00 2001 From: Jeff Lockhart Date: Tue, 18 Jul 2023 11:22:11 -0600 Subject: [PATCH 28/51] Update core/apple/src/SinksApple.kt Co-authored-by: Filipp Zhinkin --- core/apple/src/SinksApple.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/core/apple/src/SinksApple.kt b/core/apple/src/SinksApple.kt index e3140ec0b..6efecac4d 100644 --- a/core/apple/src/SinksApple.kt +++ b/core/apple/src/SinksApple.kt @@ -9,7 +9,6 @@ import kotlinx.cinterop.* import platform.Foundation.* import platform.darwin.NSInteger import platform.darwin.NSUInteger -import platform.posix.memcpy import platform.posix.uint8_tVar import kotlin.concurrent.Volatile From 9e71d4b7cc4f07cda29ff18c1406497c3a7f12cd Mon Sep 17 00:00:00 2001 From: Jeff Lockhart Date: Tue, 18 Jul 2023 11:37:42 -0600 Subject: [PATCH 29/51] Don't close a stream with the error status Also, close the underlying source and sink when the stream is terminated with an error --- core/apple/src/SinksApple.kt | 2 ++ core/apple/src/SourcesApple.kt | 5 ++++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/core/apple/src/SinksApple.kt b/core/apple/src/SinksApple.kt index 6efecac4d..3fd62c602 100644 --- a/core/apple/src/SinksApple.kt +++ b/core/apple/src/SinksApple.kt @@ -38,6 +38,7 @@ private class SinkNSOutputStream( set(value) { status = NSStreamStatusError field = value + sink.close() } override fun streamStatus() = if (isClosed()) NSStreamStatusClosed else status @@ -51,6 +52,7 @@ private class SinkNSOutputStream( } override fun close() { + if (status == NSStreamStatusError) return status = NSStreamStatusClosed sink.close() } diff --git a/core/apple/src/SourcesApple.kt b/core/apple/src/SourcesApple.kt index 3a53146ec..71fb9c0eb 100644 --- a/core/apple/src/SourcesApple.kt +++ b/core/apple/src/SourcesApple.kt @@ -39,6 +39,7 @@ private class SourceNSInputStream( set(value) { status = NSStreamStatusError field = value + source.close() } private var pinnedBuffer: Pinned? = null @@ -54,9 +55,11 @@ private class SourceNSInputStream( } override fun close() { - status = NSStreamStatusClosed pinnedBuffer?.unpin() pinnedBuffer = null + + if (status == NSStreamStatusError) return + status = NSStreamStatusClosed source.close() } From f40d47240694367611dae223af488a7d06fa35fa Mon Sep 17 00:00:00 2001 From: Jeff Lockhart Date: Tue, 18 Jul 2023 13:44:53 -0600 Subject: [PATCH 30/51] Use malloc and NSData.dataWithBytesNoCopy:length: Avoids zeroing out memory before copy and returning an NSData that can be cast to NSMutableData --- core/apple/src/-Util.kt | 1 + core/apple/src/BuffersApple.kt | 14 ++++++++------ 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/core/apple/src/-Util.kt b/core/apple/src/-Util.kt index d03a4de9f..f89fe2469 100644 --- a/core/apple/src/-Util.kt +++ b/core/apple/src/-Util.kt @@ -2,6 +2,7 @@ * Copyright 2017-2023 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. */ + @file:OptIn(UnsafeNumber::class) package kotlinx.io diff --git a/core/apple/src/BuffersApple.kt b/core/apple/src/BuffersApple.kt index 41de62d8e..db55b8c67 100644 --- a/core/apple/src/BuffersApple.kt +++ b/core/apple/src/BuffersApple.kt @@ -2,14 +2,16 @@ * Copyright 2017-2023 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. */ + @file:OptIn(UnsafeNumber::class) package kotlinx.io import kotlinx.cinterop.* import platform.Foundation.* -import platform.darwin.NSUInteger +import platform.darwin.ByteVar import platform.darwin.NSUIntegerMax +import platform.posix.malloc import platform.posix.memcpy import platform.posix.uint8_tVar @@ -56,18 +58,18 @@ internal fun Buffer.snapshotAsNSData(): NSData { check(size.toULong() <= NSUIntegerMax) { "Buffer is too long ($size) to be converted into NSData." } - val data = NSMutableData.create(length = size.convert())!! + val bytes = malloc(size.convert())!!.reinterpret() var curr = head - var index: NSUInteger = 0U + var index = 0 do { check(curr != null) { "Current segment is null" } val pos = curr.pos - val length: NSUInteger = (curr.limit - pos).convert() + val length = curr.limit - pos curr.data.usePinned { - data.replaceBytesInRange(NSMakeRange(index, length), it.addressOf(pos)) + memcpy(bytes + index, it.addressOf(pos), length.convert()) } curr = curr.next index += length } while (curr !== head) - return data + return NSData.create(bytesNoCopy = bytes, length = size.convert()) } From caba9b444ff059050893c056beaafcc065068c10 Mon Sep 17 00:00:00 2001 From: Jeff Lockhart Date: Tue, 18 Jul 2023 14:41:21 -0600 Subject: [PATCH 31/51] Better variable names --- core/apple/test/SourceNSInputStreamTest.kt | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/core/apple/test/SourceNSInputStreamTest.kt b/core/apple/test/SourceNSInputStreamTest.kt index f20bd82ae..ba2680907 100644 --- a/core/apple/test/SourceNSInputStreamTest.kt +++ b/core/apple/test/SourceNSInputStreamTest.kt @@ -60,15 +60,15 @@ class SourceNSInputStreamTest { assertTrue(input.hasBytesAvailable) memScoped { - val bufferPtr = alloc>() - val lengthPtr = alloc() - assertTrue(input.getBuffer(bufferPtr.ptr, lengthPtr.ptr)) + val bufferVar = alloc>() + val lengthVar = alloc() + assertTrue(input.getBuffer(bufferVar.ptr, lengthVar.ptr)) - val length = lengthPtr.value + val length = lengthVar.value assertNotNull(length) assertEquals(3.convert(), length) - val buffer = bufferPtr.value + val buffer = bufferVar.value assertNotNull(buffer) assertEquals('a'.code.convert(), buffer[0]) assertEquals('b'.code.convert(), buffer[1]) From afab5b71adedbd4c12e5741c3ef3ad1e57dea594 Mon Sep 17 00:00:00 2001 From: Jeff Lockhart Date: Tue, 18 Jul 2023 14:55:42 -0600 Subject: [PATCH 32/51] Use uint8_tVar (same typealias, but matches function signature) --- core/apple/src/AppleCore.kt | 6 +++--- core/apple/test/SinkNSOutputStreamTest.kt | 6 +++--- core/apple/test/SourceNSInputStreamTest.kt | 8 ++++---- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/core/apple/src/AppleCore.kt b/core/apple/src/AppleCore.kt index c57f2649b..3c12a18fa 100644 --- a/core/apple/src/AppleCore.kt +++ b/core/apple/src/AppleCore.kt @@ -12,7 +12,7 @@ import platform.Foundation.NSInputStream import platform.Foundation.NSOutputStream import platform.Foundation.NSStreamStatusClosed import platform.Foundation.NSStreamStatusNotOpen -import platform.darwin.UInt8Var +import platform.posix.uint8_tVar /** * Returns [RawSink] that writes to an output stream. @@ -40,7 +40,7 @@ private open class OutputStreamSink( val head = source.head!! val toCopy = minOf(remaining, head.limit - head.pos).toInt() val bytesWritten = head.data.usePinned { - val bytes = it.addressOf(head.pos).reinterpret() + val bytes = it.addressOf(head.pos).reinterpret() out.write(bytes, toCopy.convert()).toLong() } @@ -93,7 +93,7 @@ private open class NSInputStreamSource( val tail = sink.writableSegment(1) val maxToCopy = minOf(byteCount, Segment.SIZE - tail.limit) val bytesRead = tail.data.usePinned { - val bytes = it.addressOf(tail.limit).reinterpret() + val bytes = it.addressOf(tail.limit).reinterpret() input.read(bytes, maxToCopy.convert()).toLong() } diff --git a/core/apple/test/SinkNSOutputStreamTest.kt b/core/apple/test/SinkNSOutputStreamTest.kt index c92a9dba8..ea3ad6cba 100644 --- a/core/apple/test/SinkNSOutputStreamTest.kt +++ b/core/apple/test/SinkNSOutputStreamTest.kt @@ -8,7 +8,7 @@ package kotlinx.io import kotlinx.cinterop.* import platform.Foundation.* import platform.darwin.NSUInteger -import platform.darwin.UInt8Var +import platform.posix.uint8_tVar import kotlin.test.* @OptIn(UnsafeNumber::class) @@ -31,7 +31,7 @@ class SinkNSOutputStreamTest { val byteArray = input.encodeToByteArray() val size: NSUInteger = input.length.convert() byteArray.usePinned { - val cPtr = it.addressOf(0).reinterpret() + val cPtr = it.addressOf(0).reinterpret() assertEquals(NSStreamStatusNotOpen, out.streamStatus) assertEquals(-1, out.write(cPtr, size)) @@ -66,7 +66,7 @@ class SinkNSOutputStreamTest { val byteArray = ByteArray(4) byteArray.usePinned { - val cPtr = it.addressOf(0).reinterpret() + val cPtr = it.addressOf(0).reinterpret() assertEquals(-1, out.write(cPtr, 4U)) assertNotNull(out.streamError) diff --git a/core/apple/test/SourceNSInputStreamTest.kt b/core/apple/test/SourceNSInputStreamTest.kt index ba2680907..4d5000a68 100644 --- a/core/apple/test/SourceNSInputStreamTest.kt +++ b/core/apple/test/SourceNSInputStreamTest.kt @@ -11,7 +11,7 @@ import platform.Foundation.NSStreamStatusClosed import platform.Foundation.NSStreamStatusNotOpen import platform.Foundation.NSStreamStatusOpen import platform.darwin.NSUIntegerVar -import platform.darwin.UInt8Var +import platform.posix.uint8_tVar import kotlin.test.* @OptIn(UnsafeNumber::class) @@ -33,7 +33,7 @@ class SourceNSInputStreamTest { private fun testInputStream(input: NSInputStream) { val byteArray = ByteArray(4) byteArray.usePinned { - val cPtr = it.addressOf(0).reinterpret() + val cPtr = it.addressOf(0).reinterpret() assertEquals(NSStreamStatusNotOpen, input.streamStatus) assertEquals(-1, input.read(cPtr, 4U)) @@ -60,7 +60,7 @@ class SourceNSInputStreamTest { assertTrue(input.hasBytesAvailable) memScoped { - val bufferVar = alloc>() + val bufferVar = alloc>() val lengthVar = alloc() assertTrue(input.getBuffer(bufferVar.ptr, lengthVar.ptr)) @@ -91,7 +91,7 @@ class SourceNSInputStreamTest { val byteArray = ByteArray(4) byteArray.usePinned { - val cPtr = it.addressOf(0).reinterpret() + val cPtr = it.addressOf(0).reinterpret() byteArray.fill(-5) assertEquals(-1, input.read(cPtr, 4U)) From 365c35421ce4402d5127abc3fca6478edb5b54cd Mon Sep 17 00:00:00 2001 From: Jeff Lockhart Date: Tue, 18 Jul 2023 16:27:38 -0600 Subject: [PATCH 33/51] Test SourceNSInputStream with long input data --- core/apple/test/SourceNSInputStreamTest.kt | 43 ++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/core/apple/test/SourceNSInputStreamTest.kt b/core/apple/test/SourceNSInputStreamTest.kt index 4d5000a68..2f9449c0c 100644 --- a/core/apple/test/SourceNSInputStreamTest.kt +++ b/core/apple/test/SourceNSInputStreamTest.kt @@ -50,6 +50,49 @@ class SourceNSInputStreamTest { } } + @Test + fun bufferInputStreamLongData() { + val source = Buffer() + source.writeString("a" + "b".repeat(Segment.SIZE * 2) + "c") + testInputStreamLongData(source.asNSInputStream()) + } + + @Test + fun realSourceInputStreamLongData() { + val source = Buffer() + source.writeString("a" + "b".repeat(Segment.SIZE * 2) + "c") + testInputStreamLongData(RealSource(source).asNSInputStream()) + } + + private fun testInputStreamLongData(input: NSInputStream) { + val lengthPlusOne = Segment.SIZE * 2 + 3 + val byteArray = ByteArray(lengthPlusOne) + byteArray.usePinned { + val cPtr = it.addressOf(0).reinterpret() + + assertEquals(NSStreamStatusNotOpen, input.streamStatus) + assertEquals(-1, input.read(cPtr, lengthPlusOne.convert())) + input.open() + assertEquals(NSStreamStatusOpen, input.streamStatus) + + byteArray.fill(-5) + assertEquals(Segment.SIZE.convert(), input.read(cPtr, lengthPlusOne.convert())) + assertEquals("[97${", 98".repeat(Segment.SIZE - 1)}${", -5".repeat(Segment.SIZE + 3)}]", byteArray.contentToString()) + + byteArray.fill(-6) + assertEquals(Segment.SIZE.convert(), input.read(cPtr, lengthPlusOne.convert())) + assertEquals("[98${", 98".repeat(Segment.SIZE - 1)}${", -6".repeat(Segment.SIZE + 3)}]", byteArray.contentToString()) + + byteArray.fill(-7) + assertEquals(2, input.read(cPtr, lengthPlusOne.convert())) + assertEquals("[98, 99${", -7".repeat(Segment.SIZE * 2 + 1)}]", byteArray.contentToString()) + + byteArray.fill(-8) + assertEquals(0, input.read(cPtr, lengthPlusOne.convert())) + assertEquals("[-8${", -8".repeat(lengthPlusOne - 1)}]", byteArray.contentToString()) + } + } + @Test fun nsInputStreamGetBuffer() { val source = Buffer() From a19c463fa3c1c7ac2766ea54bda2557e60b1afd9 Mon Sep 17 00:00:00 2001 From: Jeff Lockhart Date: Tue, 18 Jul 2023 16:34:39 -0600 Subject: [PATCH 34/51] Add apple source set to bytestring module --- bytestring/build.gradle.kts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/bytestring/build.gradle.kts b/bytestring/build.gradle.kts index c0be1e677..acaaecd9c 100644 --- a/bytestring/build.gradle.kts +++ b/bytestring/build.gradle.kts @@ -44,8 +44,10 @@ kotlin { val jvmMain by getting val jvmTest by getting - createSourceSet("nativeMain", parent = commonMain, children = nativeTargets) - createSourceSet("nativeTest", parent = commonTest, children = nativeTargets) + val nativeMain = createSourceSet("nativeMain", parent = commonMain, children = nativeTargets) + val nativeTest = createSourceSet("nativeTest", parent = commonTest, children = nativeTargets) + createSourceSet("appleMain", parent = nativeMain, children = appleTargets) + createSourceSet("appleTest", parent = nativeTest, children = appleTargets) } explicitApi() From 002141238f5a3ac19f309bb47436160f6912cea9 Mon Sep 17 00:00:00 2001 From: Jeff Lockhart Date: Tue, 18 Jul 2023 18:24:26 -0600 Subject: [PATCH 35/51] Add NSInputStream from file test --- core/apple/test/NSInputStreamSourceTest.kt | 24 +++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/core/apple/test/NSInputStreamSourceTest.kt b/core/apple/test/NSInputStreamSourceTest.kt index f2dea1b6b..a114a3597 100644 --- a/core/apple/test/NSInputStreamSourceTest.kt +++ b/core/apple/test/NSInputStreamSourceTest.kt @@ -5,7 +5,10 @@ package kotlinx.io +import kotlinx.io.files.Path +import kotlinx.io.files.sink import platform.Foundation.NSInputStream +import platform.Foundation.NSURL import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertFailsWith @@ -16,10 +19,29 @@ class NSInputStreamSourceTest { val input = NSInputStream(byteArrayOf(0x61).toNSData()) val source = input.asSource() val buffer = Buffer() - source.readAtMostTo(buffer, 1L) + assertEquals(1, source.readAtMostTo(buffer, 1L)) assertEquals("a", buffer.readString()) } + @OptIn(ExperimentalStdlibApi::class) + @Test + fun nsInputStreamSourceFromFile() { + val file = createTempFile() + try { + Path(file).sink().use { + it.writeString("example") + } + + val input = NSInputStream(uRL = NSURL.fileURLWithPath(file)) + val source = input.asSource() + val buffer = Buffer() + assertEquals(7, source.readAtMostTo(buffer, 10)) + assertEquals("example", buffer.readString()) + } finally { + deleteFile(file) + } + } + @Test fun sourceFromInputStream() { val input = NSInputStream( From 211c5f5c8f67af0d002a24e4a7b6371be9b6de25 Mon Sep 17 00:00:00 2001 From: Jeff Lockhart Date: Wed, 19 Jul 2023 10:26:59 -0600 Subject: [PATCH 36/51] Remove @Volatile annotations --- core/apple/src/SinksApple.kt | 2 -- core/apple/src/SourcesApple.kt | 2 -- 2 files changed, 4 deletions(-) diff --git a/core/apple/src/SinksApple.kt b/core/apple/src/SinksApple.kt index 3fd62c602..5b824d809 100644 --- a/core/apple/src/SinksApple.kt +++ b/core/apple/src/SinksApple.kt @@ -31,8 +31,6 @@ private class SinkNSOutputStream( } } - @OptIn(ExperimentalStdlibApi::class) - @Volatile private var status = NSStreamStatusNotOpen private var error: NSError? = null set(value) { diff --git a/core/apple/src/SourcesApple.kt b/core/apple/src/SourcesApple.kt index 71fb9c0eb..dc0386431 100644 --- a/core/apple/src/SourcesApple.kt +++ b/core/apple/src/SourcesApple.kt @@ -32,8 +32,6 @@ private class SourceNSInputStream( } } - @OptIn(ExperimentalStdlibApi::class) - @Volatile private var status = NSStreamStatusNotOpen private var error: NSError? = null set(value) { From ae33893eabcde62b2962bf4ddb508ed88ce77492 Mon Sep 17 00:00:00 2001 From: Jeff Lockhart Date: Wed, 19 Jul 2023 10:35:23 -0600 Subject: [PATCH 37/51] Remove getBuffer() implementation --- core/apple/src/SinksApple.kt | 1 - core/apple/src/SourcesApple.kt | 26 +--------------------- core/apple/test/SourceNSInputStreamTest.kt | 26 ---------------------- 3 files changed, 1 insertion(+), 52 deletions(-) diff --git a/core/apple/src/SinksApple.kt b/core/apple/src/SinksApple.kt index 5b824d809..6b86f5671 100644 --- a/core/apple/src/SinksApple.kt +++ b/core/apple/src/SinksApple.kt @@ -10,7 +10,6 @@ import platform.Foundation.* import platform.darwin.NSInteger import platform.darwin.NSUInteger import platform.posix.uint8_tVar -import kotlin.concurrent.Volatile /** * Returns an output stream that writes to this sink. Closing the stream will also close this sink. diff --git a/core/apple/src/SourcesApple.kt b/core/apple/src/SourcesApple.kt index dc0386431..b178886f7 100644 --- a/core/apple/src/SourcesApple.kt +++ b/core/apple/src/SourcesApple.kt @@ -11,7 +11,6 @@ import platform.darwin.NSInteger import platform.darwin.NSUInteger import platform.darwin.NSUIntegerVar import platform.posix.uint8_tVar -import kotlin.concurrent.Volatile /** * Returns an input stream that reads from this source. Closing the stream will also close this source. @@ -40,8 +39,6 @@ private class SourceNSInputStream( source.close() } - private var pinnedBuffer: Pinned? = null - override fun streamStatus() = if (isClosed()) NSStreamStatusClosed else status override fun streamError() = error @@ -53,18 +50,12 @@ private class SourceNSInputStream( } override fun close() { - pinnedBuffer?.unpin() - pinnedBuffer = null - if (status == NSStreamStatusError) return status = NSStreamStatusClosed source.close() } override fun read(buffer: CPointer?, maxLength: NSUInteger): NSInteger { - pinnedBuffer?.unpin() - pinnedBuffer = null - try { if (isClosed()) throw IOException("Underlying source is closed.") if (status != NSStreamStatusOpen && status != NSStreamStatusAtEnd) { @@ -87,22 +78,7 @@ private class SourceNSInputStream( } } - override fun getBuffer(buffer: CPointer>?, length: CPointer?): Boolean { - pinnedBuffer?.unpin() - pinnedBuffer = null - - if (source.buffer.size > 0) { - source.buffer.head?.let { s -> - s.data.pin().let { - pinnedBuffer = it - buffer?.pointed?.value = it.addressOf(s.pos).reinterpret() - length?.pointed?.value = (s.limit - s.pos).convert() - return true - } - } - } - return false - } + override fun getBuffer(buffer: CPointer>?, length: CPointer?) = false override fun hasBytesAvailable() = !source.exhausted() diff --git a/core/apple/test/SourceNSInputStreamTest.kt b/core/apple/test/SourceNSInputStreamTest.kt index 2f9449c0c..2c78322c2 100644 --- a/core/apple/test/SourceNSInputStreamTest.kt +++ b/core/apple/test/SourceNSInputStreamTest.kt @@ -93,32 +93,6 @@ class SourceNSInputStreamTest { } } - @Test - fun nsInputStreamGetBuffer() { - val source = Buffer() - source.writeString("abc") - - val input = source.asNSInputStream() - input.open() - assertTrue(input.hasBytesAvailable) - - memScoped { - val bufferVar = alloc>() - val lengthVar = alloc() - assertTrue(input.getBuffer(bufferVar.ptr, lengthVar.ptr)) - - val length = lengthVar.value - assertNotNull(length) - assertEquals(3.convert(), length) - - val buffer = bufferVar.value - assertNotNull(buffer) - assertEquals('a'.code.convert(), buffer[0]) - assertEquals('b'.code.convert(), buffer[1]) - assertEquals('c'.code.convert(), buffer[2]) - } - } - @Test fun nsInputStreamClose() { val buffer = Buffer() From 5d14977c5ad6e5a0251c4eacfa0c85566027df69 Mon Sep 17 00:00:00 2001 From: Jeff Lockhart Date: Wed, 19 Jul 2023 10:45:22 -0600 Subject: [PATCH 38/51] createTempFile() not working on Apple platforms --- core/apple/test/NSInputStreamSourceTest.kt | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/core/apple/test/NSInputStreamSourceTest.kt b/core/apple/test/NSInputStreamSourceTest.kt index a114a3597..6a12c61eb 100644 --- a/core/apple/test/NSInputStreamSourceTest.kt +++ b/core/apple/test/NSInputStreamSourceTest.kt @@ -8,7 +8,9 @@ package kotlinx.io import kotlinx.io.files.Path import kotlinx.io.files.sink import platform.Foundation.NSInputStream +import platform.Foundation.NSTemporaryDirectory import platform.Foundation.NSURL +import platform.Foundation.NSUUID import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertFailsWith @@ -26,7 +28,9 @@ class NSInputStreamSourceTest { @OptIn(ExperimentalStdlibApi::class) @Test fun nsInputStreamSourceFromFile() { - val file = createTempFile() + // can be replaced with createTempFile() when #183 is fixed + // https://github.com/Kotlin/kotlinx-io/issues/183 + val file = "${NSTemporaryDirectory()}${NSUUID().UUIDString()}" try { Path(file).sink().use { it.writeString("example") From e9fcaebc22baa2106e7ce38dc6f17e90529fe587 Mon Sep 17 00:00:00 2001 From: Jeff Lockhart Date: Wed, 19 Jul 2023 10:56:20 -0600 Subject: [PATCH 39/51] Add apple source set --- .../io/conventions/kotlinx-io-multiplatform.gradle.kts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/build-logic/src/main/kotlin/kotlinx/io/conventions/kotlinx-io-multiplatform.gradle.kts b/build-logic/src/main/kotlin/kotlinx/io/conventions/kotlinx-io-multiplatform.gradle.kts index dd76066e2..b50e1f769 100644 --- a/build-logic/src/main/kotlin/kotlinx/io/conventions/kotlinx-io-multiplatform.gradle.kts +++ b/build-logic/src/main/kotlin/kotlinx/io/conventions/kotlinx-io-multiplatform.gradle.kts @@ -52,9 +52,12 @@ kotlin { configureNativePlatforms() val nativeTargets = nativeTargets() + val appleTargets = appleTargets() sourceSets { - createSourceSet("nativeMain", parent = commonMain.get(), children = nativeTargets) - createSourceSet("nativeTest", parent = commonTest.get(), children = nativeTargets) + val nativeMain = createSourceSet("nativeMain", parent = commonMain.get(), children = nativeTargets) + val nativeTest = createSourceSet("nativeTest", parent = commonTest.get(), children = nativeTargets) + createSourceSet("appleMain", parent = nativeMain, children = appleTargets) + createSourceSet("appleTest", parent = nativeTest, children = appleTargets) } } @@ -126,7 +129,7 @@ fun KotlinMultiplatformExtension.configureNativePlatforms() { } fun nativeTargets(): List { - return appleTargets() + linuxTargets() + mingwTargets() + androidTargets() + return linuxTargets() + mingwTargets() + androidTargets() } fun appleTargets() = listOf( From 50fe63fa76093decaa93bf10486099ddb07ed7d8 Mon Sep 17 00:00:00 2001 From: Jeff Lockhart Date: Wed, 19 Jul 2023 19:18:13 -0600 Subject: [PATCH 40/51] SourceNSInputStream run loop delegate support --- core/apple/src/-Util.kt | 18 +- core/apple/src/SourcesApple.kt | 72 +++++- core/apple/test/NSInputStreamSourceTest.kt | 10 +- core/apple/test/SourceNSInputStreamTest.kt | 244 ++++++++++++++++++++- core/apple/test/samples/samplesApple.kt | 2 +- core/apple/test/utilApple.kt | 13 +- core/build.gradle.kts | 6 + 7 files changed, 336 insertions(+), 29 deletions(-) diff --git a/core/apple/src/-Util.kt b/core/apple/src/-Util.kt index f89fe2469..6871df21e 100644 --- a/core/apple/src/-Util.kt +++ b/core/apple/src/-Util.kt @@ -3,16 +3,14 @@ * Use of this source code is governed by the Apache 2.0 license that can be found in the LICENCE file. */ -@file:OptIn(UnsafeNumber::class) - package kotlinx.io import kotlinx.cinterop.UnsafeNumber -import kotlinx.cinterop.addressOf -import kotlinx.cinterop.convert -import kotlinx.cinterop.usePinned -import platform.Foundation.* +import platform.Foundation.NSError +import platform.Foundation.NSLocalizedDescriptionKey +import platform.Foundation.NSUnderlyingErrorKey +@OptIn(UnsafeNumber::class) internal fun Exception.toNSError() = NSError( domain = "Kotlin", code = 0, @@ -21,11 +19,3 @@ internal fun Exception.toNSError() = NSError( NSUnderlyingErrorKey to this ) ) - -internal fun ByteArray.toNSData() = if (isNotEmpty()) { - usePinned { - NSData.create(bytes = it.addressOf(0), length = size.convert()) - } -} else { - NSData.data() -} diff --git a/core/apple/src/SourcesApple.kt b/core/apple/src/SourcesApple.kt index b178886f7..8711dbf5c 100644 --- a/core/apple/src/SourcesApple.kt +++ b/core/apple/src/SourcesApple.kt @@ -11,6 +11,7 @@ import platform.darwin.NSInteger import platform.darwin.NSUInteger import platform.darwin.NSUIntegerVar import platform.posix.uint8_tVar +import kotlin.native.ref.WeakReference /** * Returns an input stream that reads from this source. Closing the stream will also close this source. @@ -21,8 +22,8 @@ public fun Source.asNSInputStream(): NSInputStream = SourceNSInputStream(this) @OptIn(InternalIoApi::class, UnsafeNumber::class) private class SourceNSInputStream( - private val source: Source, -) : NSInputStream(NSData()) { + private val source: Source +) : NSInputStream(NSData()), NSStreamDelegateProtocol { private val isClosed: () -> Boolean = when (source) { is RealSource -> source::closed @@ -36,6 +37,7 @@ private class SourceNSInputStream( set(value) { status = NSStreamStatusError field = value + postEvent(NSStreamEventErrorOccurred) source.close() } @@ -46,6 +48,8 @@ private class SourceNSInputStream( override fun open() { if (status == NSStreamStatusNotOpen) { status = NSStreamStatusOpen + postEvent(NSStreamEventOpenCompleted) + checkBytes() } } @@ -53,6 +57,8 @@ private class SourceNSInputStream( if (status == NSStreamStatusError) return status = NSStreamStatusClosed source.close() + runLoop = null + runLoopModes.clear() } override fun read(buffer: CPointer?, maxLength: NSUInteger): NSInteger { @@ -71,6 +77,7 @@ private class SourceNSInputStream( val toRead = minOf(maxLength.toInt(), source.buffer.size).toInt() val read = source.buffer.readAtMostTo(buffer, toRead).convert() status = NSStreamStatusOpen + checkBytes() return read } catch (e: Exception) { error = e.toNSError() @@ -84,5 +91,66 @@ private class SourceNSInputStream( override fun propertyForKey(key: NSStreamPropertyKey): Any? = null + override fun setProperty(property: Any?, forKey: NSStreamPropertyKey) = false + + private var _delegate = WeakReference(this) + private var runLoop: NSRunLoop? = null + private var runLoopModes = mutableListOf() + + private fun postEvent(event: NSStreamEvent) { + val delegate = delegate ?: return + runLoop?.performInModes(runLoopModes) { + delegate.stream(this, event) + } + } + + private fun checkBytes(sendEndEvent: Boolean = true) { + runLoop?.performInModes(runLoopModes) { + try { + if (source.exhausted()) { + if (sendEndEvent) delegate?.stream(this, NSStreamEventEndEncountered) + val timer = NSTimer.timerWithTimeInterval(0.1, false) { + checkBytes(sendEndEvent = false) + } + runLoopModes.forEach { mode -> + runLoop?.addTimer(timer, mode) + } + } else { + delegate?.stream(this, NSStreamEventHasBytesAvailable) + } + } catch (e: IllegalStateException) { + // ignore closed + } + } + } + + override fun delegate() = _delegate.value + + override fun setDelegate(delegate: NSStreamDelegateProtocol?) { + _delegate = WeakReference(delegate ?: this) + } + + override fun stream(aStream: NSStream, handleEvent: NSStreamEvent) { + // no-op + } + + override fun scheduleInRunLoop(aRunLoop: NSRunLoop, forMode: NSRunLoopMode) { + if (runLoop == null) { + runLoop = aRunLoop + } + if (runLoop == aRunLoop) { + runLoopModes += forMode + } + } + + override fun removeFromRunLoop(aRunLoop: NSRunLoop, forMode: NSRunLoopMode) { + if (aRunLoop == runLoop) { + runLoopModes -= forMode + if (runLoopModes.isEmpty()) { + runLoop = null + } + } + } + override fun description() = "$source.asNSInputStream()" } diff --git a/core/apple/test/NSInputStreamSourceTest.kt b/core/apple/test/NSInputStreamSourceTest.kt index 6a12c61eb..2a65584e4 100644 --- a/core/apple/test/NSInputStreamSourceTest.kt +++ b/core/apple/test/NSInputStreamSourceTest.kt @@ -18,7 +18,7 @@ import kotlin.test.assertFailsWith class NSInputStreamSourceTest { @Test fun nsInputStreamSource() { - val input = NSInputStream(byteArrayOf(0x61).toNSData()) + val input = NSInputStream(data = byteArrayOf(0x61).toNSData()) val source = input.asSource() val buffer = Buffer() assertEquals(1, source.readAtMostTo(buffer, 1L)) @@ -48,9 +48,7 @@ class NSInputStreamSourceTest { @Test fun sourceFromInputStream() { - val input = NSInputStream( - ("a" + "b".repeat(Segment.SIZE * 2) + "c").encodeToByteArray().toNSData(), - ) + val input = NSInputStream(data = ("a" + "b".repeat(Segment.SIZE * 2) + "c").encodeToByteArray().toNSData()) // Source: ab...bc val source: RawSource = input.asSource() @@ -74,7 +72,7 @@ class NSInputStreamSourceTest { @Test fun sourceFromInputStreamWithSegmentSize() { - val input = NSInputStream(ByteArray(Segment.SIZE).toNSData()) + val input = NSInputStream(data = ByteArray(Segment.SIZE).toNSData()) val source = input.asSource() val sink = Buffer() @@ -86,7 +84,7 @@ class NSInputStreamSourceTest { @Test fun sourceFromInputStreamBounds() { - val source = NSInputStream(ByteArray(100).toNSData()).asSource() + val source = NSInputStream(data = ByteArray(100).toNSData()).asSource() assertFailsWith { source.readAtMostTo(Buffer(), -1) } } } diff --git a/core/apple/test/SourceNSInputStreamTest.kt b/core/apple/test/SourceNSInputStreamTest.kt index 2c78322c2..63967edc2 100644 --- a/core/apple/test/SourceNSInputStreamTest.kt +++ b/core/apple/test/SourceNSInputStreamTest.kt @@ -5,14 +5,25 @@ package kotlinx.io +import io.ktor.server.cio.* +import io.ktor.server.engine.* +import io.ktor.server.routing.* +import io.ktor.utils.io.core.* +import kotlinx.atomicfu.atomic import kotlinx.cinterop.* -import platform.Foundation.NSInputStream -import platform.Foundation.NSStreamStatusClosed -import platform.Foundation.NSStreamStatusNotOpen -import platform.Foundation.NSStreamStatusOpen -import platform.darwin.NSUIntegerVar +import kotlinx.coroutines.delay +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.withTimeout +import platform.CoreFoundation.CFRunLoopStop +import platform.Foundation.* +import platform.darwin.NSObject +import platform.posix.sleep import platform.posix.uint8_tVar +import platform.posix.usleep +import kotlin.random.Random import kotlin.test.* +import kotlin.time.Duration.Companion.seconds @OptIn(UnsafeNumber::class) class SourceNSInputStreamTest { @@ -117,4 +128,227 @@ class SourceNSInputStreamTest { assertEquals("[-5, -5, -5, -5]", byteArray.contentToString()) } } + + private fun startRunLoop(name: String = "run-loop"): NSRunLoop { + val created = Mutex(true) + lateinit var runLoop: NSRunLoop + val thread = NSThread { + runLoop = NSRunLoop.currentRunLoop + runLoop.addPort(NSMachPort.port(), NSDefaultRunLoopMode) + created.unlock() + runLoop.run() + } + thread.name = name + thread.start() + runBlocking { + withTimeout(5.seconds) { + created.lock() + } + } + return runLoop + } + + private fun NSStreamEvent.asString(): String { + return when (this) { + NSStreamEventNone -> "NSStreamEventNone" + NSStreamEventOpenCompleted -> "NSStreamEventOpenCompleted" + NSStreamEventHasBytesAvailable -> "NSStreamEventHasBytesAvailable" + NSStreamEventHasSpaceAvailable -> "NSStreamEventHasSpaceAvailable" + NSStreamEventErrorOccurred -> "NSStreamEventErrorOccurred" + NSStreamEventEndEncountered -> "NSStreamEventEndEncountered" + else -> "Unknown event $this" + } + } + + @Test + fun delegateTest() { + val runLoop = startRunLoop() + + fun consumeWithDelegate(input: NSInputStream, data: String) { + val opened = Mutex(true) + val read = atomic(0) + val completed = Mutex(true) + + input.delegate = object : NSObject(), NSStreamDelegateProtocol { + val sink = ByteArray(data.length) + override fun stream(aStream: NSStream, handleEvent: NSStreamEvent) { + println("$data event = ${handleEvent.asString()} (${NSThread.currentThread.name})") + assertEquals("run-loop", NSThread.currentThread.name) + when (handleEvent) { + NSStreamEventOpenCompleted -> opened.unlock() + NSStreamEventHasBytesAvailable -> { + sink.usePinned { + assertEquals(1, input.read(it.addressOf(read.value).reinterpret(), 1U)) + read.value++ + } + } + NSStreamEventEndEncountered -> { + assertEquals(data, sink.decodeToString()) + input.close() + completed.unlock() + } + } + } + } + input.scheduleInRunLoop(runLoop, NSDefaultRunLoopMode) + input.open() + runBlocking { + withTimeout(5.seconds) { + opened.lock() + completed.lock() + } + } + assertEquals(data.length, read.value) + } + + consumeWithDelegate(NSInputStream(data = "default".encodeToByteArray().toNSData()), "default") + consumeWithDelegate(Buffer().apply { writeString("custom") }.asNSInputStream(), "custom") + consumeWithDelegate(NSInputStream(data = NSData.data()), "") + consumeWithDelegate(Buffer().asNSInputStream(), "") + CFRunLoopStop(runLoop.getCFRunLoop()) + } + + @Test + fun uploadTest() { + val port = Random.nextInt(4000, 8000) + fun tryUpload(stream: NSInputStream) { + val request = NSMutableURLRequest.requestWithURL( + NSURL(string = "http://127.0.0.1:$port") + ) + request.HTTPMethod = "POST" + request.HTTPBodyStream = stream + request.setValue("application/octet-stream", "Content-Type") + + val session = NSURLSession.sharedSession + val task = session.uploadTaskWithStreamedRequest(request) + task.resume() + while (stream.streamStatus != NSStreamStatusAtEnd && + stream.streamStatus != NSStreamStatusClosed && + stream.streamStatus != NSStreamStatusError + ) { + usleep(100_000U) + } + } + @Suppress("ExtractKtorModule") + val server = embeddedServer(CIO, port = port, host = "localhost") { + routing { + post { + this.context.request.receiveChannel().readAvailable(1) { + println(it.readText()) + } + } + } + } + server.start(false) + tryUpload(NSInputStream(data = "default".encodeToByteArray().toNSData())) + tryUpload(Buffer().apply { writeString("custom") }.asNSInputStream()) + server.stop() + } + + @Test + fun testRunLoopPolling() { + val runLoop = startRunLoop() + + val opened = Mutex(true) + val read = atomic(0) + val end = Mutex(true) + + val buffer = Buffer() + val input = buffer.asNSInputStream() + val sink = ByteArray(6) + input.delegate = object : NSObject(), NSStreamDelegateProtocol { + override fun stream(aStream: NSStream, handleEvent: NSStreamEvent) { + println("event = ${handleEvent.asString()} (${NSThread.currentThread.name})") + when (handleEvent) { + NSStreamEventOpenCompleted -> opened.unlock() + NSStreamEventHasBytesAvailable -> { + sink.usePinned { + assertEquals(3, input.read(it.addressOf(read.value).reinterpret(), 3U)) + read.value += 3 + } + } + NSStreamEventEndEncountered -> end.unlock() + } + } + } + input.scheduleInRunLoop(runLoop, NSDefaultRunLoopMode) + input.open() + runBlocking { + withTimeout(5.seconds) { + opened.lock() + end.lock() + buffer.writeString("abc") + end.lock() + assertEquals(3, read.value) + buffer.writeString("123") + end.lock() + } + } + assertEquals(6, read.value) + assertEquals("abc123", sink.decodeToString()) + input.close() + CFRunLoopStop(runLoop.getCFRunLoop()) + } + + @Test + fun testRunLoopSwitch() { + val runLoop1 = startRunLoop("run-loop-1") + val runLoop2 = startRunLoop("run-loop-2") + + fun consumeSwitching(input: NSInputStream, data: String) { + val opened = Mutex(true) + val read = atomic(0) + val completed = Mutex(true) + + input.delegate = object : NSObject(), NSStreamDelegateProtocol { + val sink = ByteArray(data.length) + override fun stream(aStream: NSStream, handleEvent: NSStreamEvent) { + println("$data event = ${handleEvent.asString()} (${NSThread.currentThread.name})") + if (read.value == 0) { + assertEquals("run-loop-1", NSThread.currentThread.name) + } else { + assertEquals("run-loop-2", NSThread.currentThread.name) + } + when (handleEvent) { + NSStreamEventOpenCompleted -> opened.unlock() + NSStreamEventHasBytesAvailable -> { + if (read.value == 0) { + // switch to other run loop + input.removeFromRunLoop(runLoop1, NSDefaultRunLoopMode) + input.scheduleInRunLoop(runLoop2, NSDefaultRunLoopMode) + } else if (read.value >= data.length - 3) { + // unsubscribe + input.removeFromRunLoop(runLoop2, NSDefaultRunLoopMode) + } + sink.usePinned { + val readBytes = input.read(it.addressOf(read.value).reinterpret(), 3U) + read.value += readBytes.toInt() + } + if (read.value == data.length) { + assertEquals(data, sink.decodeToString()) + completed.unlock() + } + } + NSStreamEventEndEncountered -> fail("shouldn't be subscribed") + } + } + } + input.scheduleInRunLoop(runLoop1, NSDefaultRunLoopMode) + input.open() + runBlocking { + withTimeout(5.seconds) { + opened.lock() + completed.lock() + // wait a bit to be sure delegate is no longer called + delay(100) + } + } + input.close() + } + + consumeSwitching(NSInputStream(data = "default".encodeToByteArray().toNSData()), "default") + consumeSwitching(Buffer().apply { writeString("custom") }.asNSInputStream(), "custom") + CFRunLoopStop(runLoop1.getCFRunLoop()) + CFRunLoopStop(runLoop2.getCFRunLoop()) + } } diff --git a/core/apple/test/samples/samplesApple.kt b/core/apple/test/samples/samplesApple.kt index 871838d99..7c8d5e142 100644 --- a/core/apple/test/samples/samplesApple.kt +++ b/core/apple/test/samples/samplesApple.kt @@ -17,7 +17,7 @@ class KotlinxIoSamplesApple { @Test fun inputStreamAsSource() { val data = ByteArray(100) { it.toByte() } - val inputStream = NSInputStream(data.toNSData()) + val inputStream = NSInputStream(data = data.toNSData()) val receivedData = inputStream.asSource().buffered().readByteArray() assertContentEquals(data, receivedData) diff --git a/core/apple/test/utilApple.kt b/core/apple/test/utilApple.kt index 49c9fd168..222fb0b8a 100644 --- a/core/apple/test/utilApple.kt +++ b/core/apple/test/utilApple.kt @@ -3,13 +3,24 @@ * Use of this source code is governed by the Apache 2.0 license that can be found in the LICENCE file. */ +@file:OptIn(UnsafeNumber::class) + package kotlinx.io import kotlinx.cinterop.* import platform.Foundation.NSData +import platform.Foundation.create +import platform.Foundation.data import platform.posix.memcpy -@OptIn(UnsafeNumber::class) +internal fun ByteArray.toNSData() = if (isNotEmpty()) { + usePinned { + NSData.create(bytes = it.addressOf(0), length = size.convert()) + } +} else { + NSData.data() +} + fun NSData.toByteArray() = ByteArray(length.toInt()).apply { if (isNotEmpty()) { memcpy(refTo(0), bytes, length) diff --git a/core/build.gradle.kts b/core/build.gradle.kts index d628190e2..ebd5599e7 100644 --- a/core/build.gradle.kts +++ b/core/build.gradle.kts @@ -36,6 +36,12 @@ kotlin { api(project(":kotlinx-io-bytestring")) } } + appleTest { + dependencies { + implementation("io.ktor:ktor-server-core:2.3.2") + implementation("io.ktor:ktor-server-cio:2.3.2") + } + } } } From 89a4f80c566a7db5cdaca688988aa4c9ab40f7c6 Mon Sep 17 00:00:00 2001 From: Jeff Lockhart Date: Wed, 19 Jul 2023 22:23:10 -0600 Subject: [PATCH 41/51] Test subscribe after open --- core/apple/src/SourcesApple.kt | 14 ++++-- core/apple/test/SourceNSInputStreamTest.kt | 51 ++++++++++++++-------- 2 files changed, 44 insertions(+), 21 deletions(-) diff --git a/core/apple/src/SourcesApple.kt b/core/apple/src/SourcesApple.kt index 8711dbf5c..fbca712f0 100644 --- a/core/apple/src/SourcesApple.kt +++ b/core/apple/src/SourcesApple.kt @@ -58,7 +58,7 @@ private class SourceNSInputStream( status = NSStreamStatusClosed source.close() runLoop = null - runLoopModes.clear() + runLoopModes = listOf() } override fun read(buffer: CPointer?, maxLength: NSUInteger): NSInteger { @@ -95,7 +95,7 @@ private class SourceNSInputStream( private var _delegate = WeakReference(this) private var runLoop: NSRunLoop? = null - private var runLoopModes = mutableListOf() + private var runLoopModes = listOf() private fun postEvent(event: NSStreamEvent) { val delegate = delegate ?: return @@ -105,7 +105,9 @@ private class SourceNSInputStream( } private fun checkBytes(sendEndEvent: Boolean = true) { - runLoop?.performInModes(runLoopModes) { + val runLoop = runLoop ?: return + runLoop.performInModes(runLoopModes) { + if (runLoop != this.runLoop) return@performInModes try { if (source.exhausted()) { if (sendEndEvent) delegate?.stream(this, NSStreamEventEndEncountered) @@ -113,9 +115,10 @@ private class SourceNSInputStream( checkBytes(sendEndEvent = false) } runLoopModes.forEach { mode -> - runLoop?.addTimer(timer, mode) + runLoop.addTimer(timer, mode) } } else { + status = NSStreamStatusOpen delegate?.stream(this, NSStreamEventHasBytesAvailable) } } catch (e: IllegalStateException) { @@ -141,6 +144,9 @@ private class SourceNSInputStream( if (runLoop == aRunLoop) { runLoopModes += forMode } + if (status == NSStreamStatusOpen) { + checkBytes() + } } override fun removeFromRunLoop(aRunLoop: NSRunLoop, forMode: NSRunLoopMode) { diff --git a/core/apple/test/SourceNSInputStreamTest.kt b/core/apple/test/SourceNSInputStreamTest.kt index 63967edc2..cce803e9a 100644 --- a/core/apple/test/SourceNSInputStreamTest.kt +++ b/core/apple/test/SourceNSInputStreamTest.kt @@ -18,7 +18,6 @@ import kotlinx.coroutines.withTimeout import platform.CoreFoundation.CFRunLoopStop import platform.Foundation.* import platform.darwin.NSObject -import platform.posix.sleep import platform.posix.uint8_tVar import platform.posix.usleep import kotlin.random.Random @@ -148,18 +147,6 @@ class SourceNSInputStreamTest { return runLoop } - private fun NSStreamEvent.asString(): String { - return when (this) { - NSStreamEventNone -> "NSStreamEventNone" - NSStreamEventOpenCompleted -> "NSStreamEventOpenCompleted" - NSStreamEventHasBytesAvailable -> "NSStreamEventHasBytesAvailable" - NSStreamEventHasSpaceAvailable -> "NSStreamEventHasSpaceAvailable" - NSStreamEventErrorOccurred -> "NSStreamEventErrorOccurred" - NSStreamEventEndEncountered -> "NSStreamEventEndEncountered" - else -> "Unknown event $this" - } - } - @Test fun delegateTest() { val runLoop = startRunLoop() @@ -172,7 +159,6 @@ class SourceNSInputStreamTest { input.delegate = object : NSObject(), NSStreamDelegateProtocol { val sink = ByteArray(data.length) override fun stream(aStream: NSStream, handleEvent: NSStreamEvent) { - println("$data event = ${handleEvent.asString()} (${NSThread.currentThread.name})") assertEquals("run-loop", NSThread.currentThread.name) when (handleEvent) { NSStreamEventOpenCompleted -> opened.unlock() @@ -258,7 +244,6 @@ class SourceNSInputStreamTest { val sink = ByteArray(6) input.delegate = object : NSObject(), NSStreamDelegateProtocol { override fun stream(aStream: NSStream, handleEvent: NSStreamEvent) { - println("event = ${handleEvent.asString()} (${NSThread.currentThread.name})") when (handleEvent) { NSStreamEventOpenCompleted -> opened.unlock() NSStreamEventHasBytesAvailable -> { @@ -299,11 +284,11 @@ class SourceNSInputStreamTest { val opened = Mutex(true) val read = atomic(0) val completed = Mutex(true) + val endEvent = atomic(false) input.delegate = object : NSObject(), NSStreamDelegateProtocol { val sink = ByteArray(data.length) override fun stream(aStream: NSStream, handleEvent: NSStreamEvent) { - println("$data event = ${handleEvent.asString()} (${NSThread.currentThread.name})") if (read.value == 0) { assertEquals("run-loop-1", NSThread.currentThread.name) } else { @@ -329,7 +314,7 @@ class SourceNSInputStreamTest { completed.unlock() } } - NSStreamEventEndEncountered -> fail("shouldn't be subscribed") + NSStreamEventEndEncountered -> endEvent.value = true } } } @@ -341,6 +326,7 @@ class SourceNSInputStreamTest { completed.lock() // wait a bit to be sure delegate is no longer called delay(100) + assertFalse(endEvent.value, "$data shouldn't be subscribed for end") } } input.close() @@ -351,4 +337,35 @@ class SourceNSInputStreamTest { CFRunLoopStop(runLoop1.getCFRunLoop()) CFRunLoopStop(runLoop2.getCFRunLoop()) } + + @Test + fun testSubscribeAfterOpen() { + val runLoop = startRunLoop() + + fun subscribeAfterOpen(input: NSInputStream) { + val available = Mutex(true) + + input.delegate = object : NSObject(), NSStreamDelegateProtocol { + override fun stream(aStream: NSStream, handleEvent: NSStreamEvent) { + assertEquals("run-loop", NSThread.currentThread.name) + when (handleEvent) { + NSStreamEventOpenCompleted -> fail("opened before subscribe") + NSStreamEventHasBytesAvailable -> available.unlock() + } + } + } + input.open() + input.scheduleInRunLoop(runLoop, NSDefaultRunLoopMode) + runBlocking { + withTimeout(5.seconds) { + available.lock() + } + } + input.close() + } + + subscribeAfterOpen(NSInputStream(data = "default".encodeToByteArray().toNSData())) + subscribeAfterOpen(Buffer().apply { writeString("custom") }.asNSInputStream()) + CFRunLoopStop(runLoop.getCFRunLoop()) + } } From e296c01e93f618f7ed8e19830e333c267b7e0ba8 Mon Sep 17 00:00:00 2001 From: Jeff Lockhart Date: Wed, 19 Jul 2023 22:40:02 -0600 Subject: [PATCH 42/51] Check run loop on postEvent() --- core/apple/src/SourcesApple.kt | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/core/apple/src/SourcesApple.kt b/core/apple/src/SourcesApple.kt index fbca712f0..42ead56ab 100644 --- a/core/apple/src/SourcesApple.kt +++ b/core/apple/src/SourcesApple.kt @@ -98,9 +98,11 @@ private class SourceNSInputStream( private var runLoopModes = listOf() private fun postEvent(event: NSStreamEvent) { - val delegate = delegate ?: return - runLoop?.performInModes(runLoopModes) { - delegate.stream(this, event) + val runLoop = runLoop ?: return + runLoop.performInModes(runLoopModes) { + if (runLoop == this.runLoop) { + delegate?.stream(this, event) + } } } From 7b3ab06c851ec80989fc07d17cebe865e53acfa4 Mon Sep 17 00:00:00 2001 From: Jeff Lockhart Date: Thu, 20 Jul 2023 00:37:09 -0600 Subject: [PATCH 43/51] SinkNSOutputStream run loop delegate support --- core/apple/src/SinksApple.kt | 56 +++++++++++++- core/apple/test/SinkNSOutputStreamTest.kt | 89 ++++++++++++++++++++++ core/apple/test/SourceNSInputStreamTest.kt | 25 +----- core/apple/test/utilApple.kt | 27 ++++++- 4 files changed, 169 insertions(+), 28 deletions(-) diff --git a/core/apple/src/SinksApple.kt b/core/apple/src/SinksApple.kt index 6b86f5671..1043a28d9 100644 --- a/core/apple/src/SinksApple.kt +++ b/core/apple/src/SinksApple.kt @@ -10,6 +10,7 @@ import platform.Foundation.* import platform.darwin.NSInteger import platform.darwin.NSUInteger import platform.posix.uint8_tVar +import kotlin.native.ref.WeakReference /** * Returns an output stream that writes to this sink. Closing the stream will also close this sink. @@ -20,8 +21,8 @@ public fun Sink.asNSOutputStream(): NSOutputStream = SinkNSOutputStream(this) @OptIn(UnsafeNumber::class) private class SinkNSOutputStream( - private val sink: Sink, -) : NSOutputStream(toMemory = Unit) { + private val sink: Sink +) : NSOutputStream(toMemory = Unit), NSStreamDelegateProtocol { private val isClosed: () -> Boolean = when (sink) { is RealSink -> sink::closed @@ -35,6 +36,7 @@ private class SinkNSOutputStream( set(value) { status = NSStreamStatusError field = value + postEvent(NSStreamEventErrorOccurred) sink.close() } @@ -45,6 +47,8 @@ private class SinkNSOutputStream( override fun open() { if (status == NSStreamStatusNotOpen) { status = NSStreamStatusOpen + postEvent(NSStreamEventOpenCompleted) + postEvent(NSStreamEventHasSpaceAvailable) } } @@ -52,6 +56,8 @@ private class SinkNSOutputStream( if (status == NSStreamStatusError) return status = NSStreamStatusClosed sink.close() + runLoop = null + runLoopModes = listOf() } @OptIn(DelicateIoApi::class) @@ -81,5 +87,51 @@ private class SinkNSOutputStream( else -> null } + override fun setProperty(property: Any?, forKey: NSStreamPropertyKey) = false + + private var _delegate = WeakReference(this) + private var runLoop: NSRunLoop? = null + private var runLoopModes = listOf() + + private fun postEvent(event: NSStreamEvent) { + val runLoop = runLoop ?: return + runLoop.performInModes(runLoopModes) { + if (runLoop == this.runLoop) { + delegate?.stream(this, event) + } + } + } + + override fun delegate() = _delegate.value + + override fun setDelegate(delegate: NSStreamDelegateProtocol?) { + _delegate = WeakReference(delegate ?: this) + } + + override fun stream(aStream: NSStream, handleEvent: NSStreamEvent) { + // no-op + } + + override fun scheduleInRunLoop(aRunLoop: NSRunLoop, forMode: NSRunLoopMode) { + if (runLoop == null) { + runLoop = aRunLoop + } + if (runLoop == aRunLoop) { + runLoopModes += forMode + } + if (status == NSStreamStatusOpen) { + postEvent(NSStreamEventHasSpaceAvailable) + } + } + + override fun removeFromRunLoop(aRunLoop: NSRunLoop, forMode: NSRunLoopMode) { + if (aRunLoop == runLoop) { + runLoopModes -= forMode + if (runLoopModes.isEmpty()) { + runLoop = null + } + } + } + override fun description() = "$sink.asNSOutputStream()" } diff --git a/core/apple/test/SinkNSOutputStreamTest.kt b/core/apple/test/SinkNSOutputStreamTest.kt index ea3ad6cba..fac68960f 100644 --- a/core/apple/test/SinkNSOutputStreamTest.kt +++ b/core/apple/test/SinkNSOutputStreamTest.kt @@ -5,11 +5,18 @@ package kotlinx.io +import kotlinx.atomicfu.atomic import kotlinx.cinterop.* +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.withTimeout +import platform.CoreFoundation.CFRunLoopStop import platform.Foundation.* +import platform.darwin.NSObject import platform.darwin.NSUInteger import platform.posix.uint8_tVar import kotlin.test.* +import kotlin.time.Duration.Companion.seconds @OptIn(UnsafeNumber::class) class SinkNSOutputStreamTest { @@ -74,4 +81,86 @@ class SinkNSOutputStreamTest { assertTrue(sink.buffer.readByteArray().isEmpty()) } } + + @Test + fun delegateTest() { + val runLoop = startRunLoop() + + fun produceWithDelegate(out: NSOutputStream, data: String) { + val opened = Mutex(true) + val written = atomic(0) + val completed = Mutex(true) + + out.delegate = object : NSObject(), NSStreamDelegateProtocol { + val source = data.encodeToByteArray() + override fun stream(aStream: NSStream, handleEvent: NSStreamEvent) { + assertEquals("run-loop", NSThread.currentThread.name) + when (handleEvent) { + NSStreamEventOpenCompleted -> opened.unlock() + NSStreamEventHasSpaceAvailable -> { + if (source.isNotEmpty()) { + source.usePinned { + assertEquals( + data.length.convert(), + out.write(it.addressOf(written.value).reinterpret(), data.length.convert()) + ) + written.value += data.length + } + } + val writtenData = out.propertyForKey(NSStreamDataWrittenToMemoryStreamKey) as NSData + assertEquals(data, writtenData.toByteArray().decodeToString()) + out.close() + completed.unlock() + } + } + } + } + out.scheduleInRunLoop(runLoop, NSDefaultRunLoopMode) + out.open() + runBlocking { + withTimeout(5.seconds) { + opened.lock() + completed.lock() + } + } + assertEquals(data.length, written.value) + } + + produceWithDelegate(NSOutputStream.outputStreamToMemory(), "default") + produceWithDelegate(Buffer().asNSOutputStream(), "custom") + produceWithDelegate(NSOutputStream.outputStreamToMemory(), "") + produceWithDelegate(Buffer().asNSOutputStream(), "") + CFRunLoopStop(runLoop.getCFRunLoop()) + } + + @Test + fun testSubscribeAfterOpen() { + val runLoop = startRunLoop() + + fun subscribeAfterOpen(out: NSOutputStream) { + val available = Mutex(true) + + out.delegate = object : NSObject(), NSStreamDelegateProtocol { + override fun stream(aStream: NSStream, handleEvent: NSStreamEvent) { + assertEquals("run-loop", NSThread.currentThread.name) + when (handleEvent) { + NSStreamEventOpenCompleted -> fail("opened before subscribe") + NSStreamEventHasSpaceAvailable -> available.unlock() + } + } + } + out.open() + out.scheduleInRunLoop(runLoop, NSDefaultRunLoopMode) + runBlocking { + withTimeout(5.seconds) { + available.lock() + } + } + out.close() + } + + subscribeAfterOpen(NSOutputStream.outputStreamToMemory()) + subscribeAfterOpen(Buffer().asNSOutputStream()) + CFRunLoopStop(runLoop.getCFRunLoop()) + } } diff --git a/core/apple/test/SourceNSInputStreamTest.kt b/core/apple/test/SourceNSInputStreamTest.kt index cce803e9a..f7020eb2f 100644 --- a/core/apple/test/SourceNSInputStreamTest.kt +++ b/core/apple/test/SourceNSInputStreamTest.kt @@ -128,25 +128,6 @@ class SourceNSInputStreamTest { } } - private fun startRunLoop(name: String = "run-loop"): NSRunLoop { - val created = Mutex(true) - lateinit var runLoop: NSRunLoop - val thread = NSThread { - runLoop = NSRunLoop.currentRunLoop - runLoop.addPort(NSMachPort.port(), NSDefaultRunLoopMode) - created.unlock() - runLoop.run() - } - thread.name = name - thread.start() - runBlocking { - withTimeout(5.seconds) { - created.lock() - } - } - return runLoop - } - @Test fun delegateTest() { val runLoop = startRunLoop() @@ -284,7 +265,6 @@ class SourceNSInputStreamTest { val opened = Mutex(true) val read = atomic(0) val completed = Mutex(true) - val endEvent = atomic(false) input.delegate = object : NSObject(), NSStreamDelegateProtocol { val sink = ByteArray(data.length) @@ -314,7 +294,7 @@ class SourceNSInputStreamTest { completed.unlock() } } - NSStreamEventEndEncountered -> endEvent.value = true + NSStreamEventEndEncountered -> fail("$data shouldn't be subscribed") } } } @@ -325,8 +305,7 @@ class SourceNSInputStreamTest { opened.lock() completed.lock() // wait a bit to be sure delegate is no longer called - delay(100) - assertFalse(endEvent.value, "$data shouldn't be subscribed for end") + delay(200) } } input.close() diff --git a/core/apple/test/utilApple.kt b/core/apple/test/utilApple.kt index 222fb0b8a..91b7bb866 100644 --- a/core/apple/test/utilApple.kt +++ b/core/apple/test/utilApple.kt @@ -8,10 +8,12 @@ package kotlinx.io import kotlinx.cinterop.* -import platform.Foundation.NSData -import platform.Foundation.create -import platform.Foundation.data +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.withTimeout +import platform.Foundation.* import platform.posix.memcpy +import kotlin.time.Duration.Companion.seconds internal fun ByteArray.toNSData() = if (isNotEmpty()) { usePinned { @@ -26,3 +28,22 @@ fun NSData.toByteArray() = ByteArray(length.toInt()).apply { memcpy(refTo(0), bytes, length) } } + +fun startRunLoop(name: String = "run-loop"): NSRunLoop { + val created = Mutex(true) + lateinit var runLoop: NSRunLoop + val thread = NSThread { + runLoop = NSRunLoop.currentRunLoop + runLoop.addPort(NSMachPort.port(), NSDefaultRunLoopMode) + created.unlock() + runLoop.run() + } + thread.name = name + thread.start() + runBlocking { + withTimeout(5.seconds) { + created.lock() + } + } + return runLoop +} From c74845ed20f463a389546ab279aaef2b13c133e6 Mon Sep 17 00:00:00 2001 From: Jeff Lockhart Date: Fri, 21 Jul 2023 21:20:52 -0600 Subject: [PATCH 44/51] lockWithTimeout() with better failure logging --- core/apple/test/SinkNSOutputStreamTest.kt | 14 +++----- core/apple/test/SourceNSInputStreamTest.kt | 42 ++++++++++------------ core/apple/test/utilApple.kt | 14 ++++++-- core/build.gradle.kts | 1 + 4 files changed, 35 insertions(+), 36 deletions(-) diff --git a/core/apple/test/SinkNSOutputStreamTest.kt b/core/apple/test/SinkNSOutputStreamTest.kt index fac68960f..4d3b7a916 100644 --- a/core/apple/test/SinkNSOutputStreamTest.kt +++ b/core/apple/test/SinkNSOutputStreamTest.kt @@ -9,14 +9,12 @@ import kotlinx.atomicfu.atomic import kotlinx.cinterop.* import kotlinx.coroutines.runBlocking import kotlinx.coroutines.sync.Mutex -import kotlinx.coroutines.withTimeout import platform.CoreFoundation.CFRunLoopStop import platform.Foundation.* import platform.darwin.NSObject import platform.darwin.NSUInteger import platform.posix.uint8_tVar import kotlin.test.* -import kotlin.time.Duration.Companion.seconds @OptIn(UnsafeNumber::class) class SinkNSOutputStreamTest { @@ -87,6 +85,7 @@ class SinkNSOutputStreamTest { val runLoop = startRunLoop() fun produceWithDelegate(out: NSOutputStream, data: String) { + println("produceWithDelegate(${out::class}, $data)") val opened = Mutex(true) val written = atomic(0) val completed = Mutex(true) @@ -118,10 +117,8 @@ class SinkNSOutputStreamTest { out.scheduleInRunLoop(runLoop, NSDefaultRunLoopMode) out.open() runBlocking { - withTimeout(5.seconds) { - opened.lock() - completed.lock() - } + opened.lockWithTimeout() + completed.lockWithTimeout() } assertEquals(data.length, written.value) } @@ -138,6 +135,7 @@ class SinkNSOutputStreamTest { val runLoop = startRunLoop() fun subscribeAfterOpen(out: NSOutputStream) { + println("subscribeAfterOpen(${out::class})") val available = Mutex(true) out.delegate = object : NSObject(), NSStreamDelegateProtocol { @@ -152,9 +150,7 @@ class SinkNSOutputStreamTest { out.open() out.scheduleInRunLoop(runLoop, NSDefaultRunLoopMode) runBlocking { - withTimeout(5.seconds) { - available.lock() - } + available.lockWithTimeout() } out.close() } diff --git a/core/apple/test/SourceNSInputStreamTest.kt b/core/apple/test/SourceNSInputStreamTest.kt index f7020eb2f..2410f4653 100644 --- a/core/apple/test/SourceNSInputStreamTest.kt +++ b/core/apple/test/SourceNSInputStreamTest.kt @@ -14,7 +14,6 @@ import kotlinx.cinterop.* import kotlinx.coroutines.delay import kotlinx.coroutines.runBlocking import kotlinx.coroutines.sync.Mutex -import kotlinx.coroutines.withTimeout import platform.CoreFoundation.CFRunLoopStop import platform.Foundation.* import platform.darwin.NSObject @@ -22,7 +21,6 @@ import platform.posix.uint8_tVar import platform.posix.usleep import kotlin.random.Random import kotlin.test.* -import kotlin.time.Duration.Companion.seconds @OptIn(UnsafeNumber::class) class SourceNSInputStreamTest { @@ -133,6 +131,7 @@ class SourceNSInputStreamTest { val runLoop = startRunLoop() fun consumeWithDelegate(input: NSInputStream, data: String) { + println("consumeWithDelegate(${input::class}, $data)") val opened = Mutex(true) val read = atomic(0) val completed = Mutex(true) @@ -160,10 +159,8 @@ class SourceNSInputStreamTest { input.scheduleInRunLoop(runLoop, NSDefaultRunLoopMode) input.open() runBlocking { - withTimeout(5.seconds) { - opened.lock() - completed.lock() - } + opened.lockWithTimeout() + completed.lockWithTimeout() } assertEquals(data.length, read.value) } @@ -179,6 +176,7 @@ class SourceNSInputStreamTest { fun uploadTest() { val port = Random.nextInt(4000, 8000) fun tryUpload(stream: NSInputStream) { + println("tryUpload(${stream::class})") val request = NSMutableURLRequest.requestWithURL( NSURL(string = "http://127.0.0.1:$port") ) @@ -240,15 +238,13 @@ class SourceNSInputStreamTest { input.scheduleInRunLoop(runLoop, NSDefaultRunLoopMode) input.open() runBlocking { - withTimeout(5.seconds) { - opened.lock() - end.lock() - buffer.writeString("abc") - end.lock() - assertEquals(3, read.value) - buffer.writeString("123") - end.lock() - } + opened.lockWithTimeout() + end.lockWithTimeout() + buffer.writeString("abc") + end.lockWithTimeout() + assertEquals(3, read.value) + buffer.writeString("123") + end.lockWithTimeout() } assertEquals(6, read.value) assertEquals("abc123", sink.decodeToString()) @@ -262,6 +258,7 @@ class SourceNSInputStreamTest { val runLoop2 = startRunLoop("run-loop-2") fun consumeSwitching(input: NSInputStream, data: String) { + println("consumeSwitching(${input::class}, $data)") val opened = Mutex(true) val read = atomic(0) val completed = Mutex(true) @@ -301,12 +298,10 @@ class SourceNSInputStreamTest { input.scheduleInRunLoop(runLoop1, NSDefaultRunLoopMode) input.open() runBlocking { - withTimeout(5.seconds) { - opened.lock() - completed.lock() - // wait a bit to be sure delegate is no longer called - delay(200) - } + opened.lockWithTimeout() + completed.lockWithTimeout() + // wait a bit to be sure delegate is no longer called + delay(200) } input.close() } @@ -322,6 +317,7 @@ class SourceNSInputStreamTest { val runLoop = startRunLoop() fun subscribeAfterOpen(input: NSInputStream) { + println("subscribeAfterOpen(${input::class})") val available = Mutex(true) input.delegate = object : NSObject(), NSStreamDelegateProtocol { @@ -336,9 +332,7 @@ class SourceNSInputStreamTest { input.open() input.scheduleInRunLoop(runLoop, NSDefaultRunLoopMode) runBlocking { - withTimeout(5.seconds) { - available.lock() - } + available.lockWithTimeout() } input.close() } diff --git a/core/apple/test/utilApple.kt b/core/apple/test/utilApple.kt index 91b7bb866..efcfd99f4 100644 --- a/core/apple/test/utilApple.kt +++ b/core/apple/test/utilApple.kt @@ -13,6 +13,8 @@ import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.withTimeout import platform.Foundation.* import platform.posix.memcpy +import kotlin.test.fail +import kotlin.time.Duration import kotlin.time.Duration.Companion.seconds internal fun ByteArray.toNSData() = if (isNotEmpty()) { @@ -41,9 +43,15 @@ fun startRunLoop(name: String = "run-loop"): NSRunLoop { thread.name = name thread.start() runBlocking { - withTimeout(5.seconds) { - created.lock() - } + created.lockWithTimeout() } return runLoop } + +suspend fun Mutex.lockWithTimeout(timeout: Duration = 5.seconds) { + class MutexSource : Throwable() + val source = MutexSource() + runCatching { + withTimeout(timeout) { lock() } + }.onFailure { fail("Mutex never unlocked", source) } +} diff --git a/core/build.gradle.kts b/core/build.gradle.kts index ebd5599e7..8aec29692 100644 --- a/core/build.gradle.kts +++ b/core/build.gradle.kts @@ -40,6 +40,7 @@ kotlin { dependencies { implementation("io.ktor:ktor-server-core:2.3.2") implementation("io.ktor:ktor-server-cio:2.3.2") + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.2") } } } From d2d040d576a53bb65326dcc1fcf99d5701939357 Mon Sep 17 00:00:00 2001 From: Jeff Lockhart Date: Sat, 22 Jul 2023 00:48:01 -0600 Subject: [PATCH 45/51] Synchronize access to read variable for entire event handler Log events --- core/apple/test/SinkNSOutputStreamTest.kt | 2 + core/apple/test/SourceNSInputStreamTest.kt | 60 +++++++++++++--------- core/apple/test/utilApple.kt | 12 +++++ 3 files changed, 49 insertions(+), 25 deletions(-) diff --git a/core/apple/test/SinkNSOutputStreamTest.kt b/core/apple/test/SinkNSOutputStreamTest.kt index 4d3b7a916..c42c01477 100644 --- a/core/apple/test/SinkNSOutputStreamTest.kt +++ b/core/apple/test/SinkNSOutputStreamTest.kt @@ -93,6 +93,7 @@ class SinkNSOutputStreamTest { out.delegate = object : NSObject(), NSStreamDelegateProtocol { val source = data.encodeToByteArray() override fun stream(aStream: NSStream, handleEvent: NSStreamEvent) { + println("${out::class} $data event = ${handleEvent.asString()} (${NSThread.currentThread.name})") assertEquals("run-loop", NSThread.currentThread.name) when (handleEvent) { NSStreamEventOpenCompleted -> opened.unlock() @@ -140,6 +141,7 @@ class SinkNSOutputStreamTest { out.delegate = object : NSObject(), NSStreamDelegateProtocol { override fun stream(aStream: NSStream, handleEvent: NSStreamEvent) { + println("${out::class} event = ${handleEvent.asString()} (${NSThread.currentThread.name})") assertEquals("run-loop", NSThread.currentThread.name) when (handleEvent) { NSStreamEventOpenCompleted -> fail("opened before subscribe") diff --git a/core/apple/test/SourceNSInputStreamTest.kt b/core/apple/test/SourceNSInputStreamTest.kt index 2410f4653..9a75bafea 100644 --- a/core/apple/test/SourceNSInputStreamTest.kt +++ b/core/apple/test/SourceNSInputStreamTest.kt @@ -10,6 +10,8 @@ import io.ktor.server.engine.* import io.ktor.server.routing.* import io.ktor.utils.io.core.* import kotlinx.atomicfu.atomic +import kotlinx.atomicfu.locks.reentrantLock +import kotlinx.atomicfu.locks.withLock import kotlinx.cinterop.* import kotlinx.coroutines.delay import kotlinx.coroutines.runBlocking @@ -139,6 +141,7 @@ class SourceNSInputStreamTest { input.delegate = object : NSObject(), NSStreamDelegateProtocol { val sink = ByteArray(data.length) override fun stream(aStream: NSStream, handleEvent: NSStreamEvent) { + println("${input::class} $data event = ${handleEvent.asString()} (${NSThread.currentThread.name})") assertEquals("run-loop", NSThread.currentThread.name) when (handleEvent) { NSStreamEventOpenCompleted -> opened.unlock() @@ -223,6 +226,7 @@ class SourceNSInputStreamTest { val sink = ByteArray(6) input.delegate = object : NSObject(), NSStreamDelegateProtocol { override fun stream(aStream: NSStream, handleEvent: NSStreamEvent) { + println("event = ${handleEvent.asString()} (${NSThread.currentThread.name})") when (handleEvent) { NSStreamEventOpenCompleted -> opened.unlock() NSStreamEventHasBytesAvailable -> { @@ -260,38 +264,43 @@ class SourceNSInputStreamTest { fun consumeSwitching(input: NSInputStream, data: String) { println("consumeSwitching(${input::class}, $data)") val opened = Mutex(true) - val read = atomic(0) + val readLock = reentrantLock() + var read = 0 val completed = Mutex(true) input.delegate = object : NSObject(), NSStreamDelegateProtocol { val sink = ByteArray(data.length) override fun stream(aStream: NSStream, handleEvent: NSStreamEvent) { - if (read.value == 0) { - assertEquals("run-loop-1", NSThread.currentThread.name) - } else { - assertEquals("run-loop-2", NSThread.currentThread.name) - } - when (handleEvent) { - NSStreamEventOpenCompleted -> opened.unlock() - NSStreamEventHasBytesAvailable -> { - if (read.value == 0) { - // switch to other run loop - input.removeFromRunLoop(runLoop1, NSDefaultRunLoopMode) - input.scheduleInRunLoop(runLoop2, NSDefaultRunLoopMode) - } else if (read.value >= data.length - 3) { - // unsubscribe - input.removeFromRunLoop(runLoop2, NSDefaultRunLoopMode) - } - sink.usePinned { - val readBytes = input.read(it.addressOf(read.value).reinterpret(), 3U) - read.value += readBytes.toInt() - } - if (read.value == data.length) { - assertEquals(data, sink.decodeToString()) - completed.unlock() + readLock.withLock { + println("${input::class} $data event = ${handleEvent.asString()} (${NSThread.currentThread.name})") + if (read == 0) { + assertEquals("run-loop-1", NSThread.currentThread.name) + } else { + assertEquals("run-loop-2", NSThread.currentThread.name) + } + when (handleEvent) { + NSStreamEventOpenCompleted -> opened.unlock() + NSStreamEventHasBytesAvailable -> { + if (read == 0) { + // switch to other run loop + input.removeFromRunLoop(runLoop1, NSDefaultRunLoopMode) + input.scheduleInRunLoop(runLoop2, NSDefaultRunLoopMode) + } else if (read >= data.length - 3) { + // unsubscribe + input.removeFromRunLoop(runLoop2, NSDefaultRunLoopMode) + } + sink.usePinned { + val readBytes = input.read(it.addressOf(read).reinterpret(), 3U) + assertNotEquals(0, readBytes) + read += readBytes.toInt() + } + if (read == data.length) { + assertEquals(data, sink.decodeToString()) + completed.unlock() + } } + NSStreamEventEndEncountered -> fail("$data shouldn't be subscribed") } - NSStreamEventEndEncountered -> fail("$data shouldn't be subscribed") } } } @@ -322,6 +331,7 @@ class SourceNSInputStreamTest { input.delegate = object : NSObject(), NSStreamDelegateProtocol { override fun stream(aStream: NSStream, handleEvent: NSStreamEvent) { + println("${input::class} event = ${handleEvent.asString()} (${NSThread.currentThread.name})") assertEquals("run-loop", NSThread.currentThread.name) when (handleEvent) { NSStreamEventOpenCompleted -> fail("opened before subscribe") diff --git a/core/apple/test/utilApple.kt b/core/apple/test/utilApple.kt index efcfd99f4..fa801cf30 100644 --- a/core/apple/test/utilApple.kt +++ b/core/apple/test/utilApple.kt @@ -55,3 +55,15 @@ suspend fun Mutex.lockWithTimeout(timeout: Duration = 5.seconds) { withTimeout(timeout) { lock() } }.onFailure { fail("Mutex never unlocked", source) } } + +fun NSStreamEvent.asString(): String { + return when (this) { + NSStreamEventNone -> "NSStreamEventNone" + NSStreamEventOpenCompleted -> "NSStreamEventOpenCompleted" + NSStreamEventHasBytesAvailable -> "NSStreamEventHasBytesAvailable" + NSStreamEventHasSpaceAvailable -> "NSStreamEventHasSpaceAvailable" + NSStreamEventErrorOccurred -> "NSStreamEventErrorOccurred" + NSStreamEventEndEncountered -> "NSStreamEventEndEncountered" + else -> "Unknown event $this" + } +} From 630423ca0b504985b57d15e00d8a176b2b878ed8 Mon Sep 17 00:00:00 2001 From: Jeff Lockhart Date: Sat, 22 Jul 2023 01:13:40 -0600 Subject: [PATCH 46/51] Only catch TimeoutCancellationException --- core/apple/test/utilApple.kt | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/core/apple/test/utilApple.kt b/core/apple/test/utilApple.kt index fa801cf30..d67be91cd 100644 --- a/core/apple/test/utilApple.kt +++ b/core/apple/test/utilApple.kt @@ -8,6 +8,7 @@ package kotlinx.io import kotlinx.cinterop.* +import kotlinx.coroutines.TimeoutCancellationException import kotlinx.coroutines.runBlocking import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.withTimeout @@ -51,9 +52,11 @@ fun startRunLoop(name: String = "run-loop"): NSRunLoop { suspend fun Mutex.lockWithTimeout(timeout: Duration = 5.seconds) { class MutexSource : Throwable() val source = MutexSource() - runCatching { + try { withTimeout(timeout) { lock() } - }.onFailure { fail("Mutex never unlocked", source) } + } catch (e: TimeoutCancellationException) { + fail("Mutex never unlocked", source) + } } fun NSStreamEvent.asString(): String { From 110e56c45d75f2c1fce6fd5452dcc1c689dc9796 Mon Sep 17 00:00:00 2001 From: Jeff Lockhart Date: Tue, 1 Aug 2023 16:55:40 -0600 Subject: [PATCH 47/51] Code review feedback and fixes --- core/apple/src/BuffersApple.kt | 7 +- core/apple/src/SinksApple.kt | 43 ++++--- core/apple/src/SourcesApple.kt | 57 +++++----- core/apple/test/SinkNSOutputStreamTest.kt | 12 +- core/apple/test/SourceNSInputStreamTest.kt | 125 ++++----------------- core/build.gradle.kts | 2 - 6 files changed, 81 insertions(+), 165 deletions(-) diff --git a/core/apple/src/BuffersApple.kt b/core/apple/src/BuffersApple.kt index db55b8c67..ca5d67de3 100644 --- a/core/apple/src/BuffersApple.kt +++ b/core/apple/src/BuffersApple.kt @@ -11,9 +11,7 @@ import kotlinx.cinterop.* import platform.Foundation.* import platform.darwin.ByteVar import platform.darwin.NSUIntegerMax -import platform.posix.malloc -import platform.posix.memcpy -import platform.posix.uint8_tVar +import platform.posix.* internal fun Buffer.write(source: CPointer, maxLength: Int) { require(maxLength >= 0) { "maxLength ($maxLength) must not be negative" } @@ -58,7 +56,8 @@ internal fun Buffer.snapshotAsNSData(): NSData { check(size.toULong() <= NSUIntegerMax) { "Buffer is too long ($size) to be converted into NSData." } - val bytes = malloc(size.convert())!!.reinterpret() + val bytes = malloc(size.convert())?.reinterpret() + ?: throw Error("malloc failed: ${strerror(errno)?.toKString()}") var curr = head var index = 0 do { diff --git a/core/apple/src/SinksApple.kt b/core/apple/src/SinksApple.kt index 1043a28d9..496afd18a 100644 --- a/core/apple/src/SinksApple.kt +++ b/core/apple/src/SinksApple.kt @@ -40,12 +40,13 @@ private class SinkNSOutputStream( sink.close() } - override fun streamStatus() = if (isClosed()) NSStreamStatusClosed else status + override fun streamStatus() = if (status != NSStreamStatusError && isClosed()) NSStreamStatusClosed else status override fun streamError() = error override fun open() { if (status == NSStreamStatusNotOpen) { + status = NSStreamStatusOpening status = NSStreamStatusOpen postEvent(NSStreamEventOpenCompleted) postEvent(NSStreamEventHasSpaceAvailable) @@ -53,33 +54,37 @@ private class SinkNSOutputStream( } override fun close() { - if (status == NSStreamStatusError) return + if (status == NSStreamStatusError || status == NSStreamStatusNotOpen) return status = NSStreamStatusClosed - sink.close() runLoop = null runLoopModes = listOf() + sink.close() } @OptIn(DelicateIoApi::class) override fun write(buffer: CPointer?, maxLength: NSUInteger): NSInteger { - try { - if (isClosed()) throw IOException("Underlying sink is closed.") - if (status != NSStreamStatusOpen) return -1 - if (buffer == null) return -1 - - status = NSStreamStatusWriting + if (streamStatus != NSStreamStatusOpen || buffer == null) return -1 + status = NSStreamStatusWriting + val toWrite = minOf(maxLength, Int.MAX_VALUE.convert()).toInt() + return try { sink.writeToInternalBuffer { - it.write(buffer, maxLength.toInt()) + it.write(buffer, toWrite) } status = NSStreamStatusOpen - return maxLength.convert() + toWrite.convert() } catch (e: Exception) { error = e.toNSError() - return -1 + -1 } } - override fun hasSpaceAvailable() = !isClosed() + override fun hasSpaceAvailable() = !isFinished + + private val isFinished + get() = when (streamStatus) { + NSStreamStatusClosed, NSStreamStatusError -> true + else -> false + } @OptIn(InternalIoApi::class) override fun propertyForKey(key: NSStreamPropertyKey): Any? = when (key) { @@ -89,7 +94,9 @@ private class SinkNSOutputStream( override fun setProperty(property: Any?, forKey: NSStreamPropertyKey) = false - private var _delegate = WeakReference(this) + // WeakReference as delegate should not be retained + // https://developer.apple.com/documentation/foundation/nsstream/1418423-delegate + private var _delegate: WeakReference? = null private var runLoop: NSRunLoop? = null private var runLoopModes = listOf() @@ -97,15 +104,17 @@ private class SinkNSOutputStream( val runLoop = runLoop ?: return runLoop.performInModes(runLoopModes) { if (runLoop == this.runLoop) { - delegate?.stream(this, event) + delegateOrSelf.stream(this, event) } } } - override fun delegate() = _delegate.value + override fun delegate() = _delegate?.value + + private val delegateOrSelf get() = delegate ?: this override fun setDelegate(delegate: NSStreamDelegateProtocol?) { - _delegate = WeakReference(delegate ?: this) + _delegate = delegate?.let { WeakReference(it) } } override fun stream(aStream: NSStream, handleEvent: NSStreamEvent) { diff --git a/core/apple/src/SourcesApple.kt b/core/apple/src/SourcesApple.kt index 42ead56ab..73b2d558f 100644 --- a/core/apple/src/SourcesApple.kt +++ b/core/apple/src/SourcesApple.kt @@ -41,12 +41,13 @@ private class SourceNSInputStream( source.close() } - override fun streamStatus() = if (isClosed()) NSStreamStatusClosed else status + override fun streamStatus() = if (status != NSStreamStatusError && isClosed()) NSStreamStatusClosed else status override fun streamError() = error override fun open() { if (status == NSStreamStatusNotOpen) { + status = NSStreamStatusOpening status = NSStreamStatusOpen postEvent(NSStreamEventOpenCompleted) checkBytes() @@ -54,27 +55,22 @@ private class SourceNSInputStream( } override fun close() { - if (status == NSStreamStatusError) return + if (status == NSStreamStatusError || status == NSStreamStatusNotOpen) return status = NSStreamStatusClosed - source.close() runLoop = null runLoopModes = listOf() + source.close() } override fun read(buffer: CPointer?, maxLength: NSUInteger): NSInteger { + if (streamStatus != NSStreamStatusOpen && streamStatus != NSStreamStatusAtEnd || buffer == null) return -1 + status = NSStreamStatusReading try { - if (isClosed()) throw IOException("Underlying source is closed.") - if (status != NSStreamStatusOpen && status != NSStreamStatusAtEnd) { - return -1 - } if (source.exhausted()) { status = NSStreamStatusAtEnd return 0 } - if (buffer == null) return -1 - - status = NSStreamStatusReading - val toRead = minOf(maxLength.toInt(), source.buffer.size).toInt() + val toRead = minOf(maxLength.toLong(), source.buffer.size, Int.MAX_VALUE.toLong()).toInt() val read = source.buffer.readAtMostTo(buffer, toRead).convert() status = NSStreamStatusOpen checkBytes() @@ -87,13 +83,21 @@ private class SourceNSInputStream( override fun getBuffer(buffer: CPointer>?, length: CPointer?) = false - override fun hasBytesAvailable() = !source.exhausted() + override fun hasBytesAvailable() = !isFinished + + private val isFinished + get() = when (streamStatus) { + NSStreamStatusClosed, NSStreamStatusError -> true + else -> false + } override fun propertyForKey(key: NSStreamPropertyKey): Any? = null override fun setProperty(property: Any?, forKey: NSStreamPropertyKey) = false - private var _delegate = WeakReference(this) + // WeakReference as delegate should not be retained + // https://developer.apple.com/documentation/foundation/nsstream/1418423-delegate + private var _delegate: WeakReference? = null private var runLoop: NSRunLoop? = null private var runLoopModes = listOf() @@ -101,38 +105,35 @@ private class SourceNSInputStream( val runLoop = runLoop ?: return runLoop.performInModes(runLoopModes) { if (runLoop == this.runLoop) { - delegate?.stream(this, event) + delegateOrSelf.stream(this, event) } } } - private fun checkBytes(sendEndEvent: Boolean = true) { + private fun checkBytes() { val runLoop = runLoop ?: return runLoop.performInModes(runLoopModes) { - if (runLoop != this.runLoop) return@performInModes + if (runLoop != this.runLoop || isFinished) return@performInModes try { - if (source.exhausted()) { - if (sendEndEvent) delegate?.stream(this, NSStreamEventEndEncountered) - val timer = NSTimer.timerWithTimeInterval(0.1, false) { - checkBytes(sendEndEvent = false) - } - runLoopModes.forEach { mode -> - runLoop.addTimer(timer, mode) - } + val event = if (source.exhausted()) { + status = NSStreamStatusAtEnd + NSStreamEventEndEncountered } else { - status = NSStreamStatusOpen - delegate?.stream(this, NSStreamEventHasBytesAvailable) + NSStreamEventHasBytesAvailable } + delegateOrSelf.stream(this, event) } catch (e: IllegalStateException) { // ignore closed } } } - override fun delegate() = _delegate.value + override fun delegate() = _delegate?.value + + private val delegateOrSelf get() = delegate ?: this override fun setDelegate(delegate: NSStreamDelegateProtocol?) { - _delegate = WeakReference(delegate ?: this) + _delegate = delegate?.let { WeakReference(it) } } override fun stream(aStream: NSStream, handleEvent: NSStreamEvent) { diff --git a/core/apple/test/SinkNSOutputStreamTest.kt b/core/apple/test/SinkNSOutputStreamTest.kt index c42c01477..235298a2f 100644 --- a/core/apple/test/SinkNSOutputStreamTest.kt +++ b/core/apple/test/SinkNSOutputStreamTest.kt @@ -74,8 +74,7 @@ class SinkNSOutputStreamTest { val cPtr = it.addressOf(0).reinterpret() assertEquals(-1, out.write(cPtr, 4U)) - assertNotNull(out.streamError) - assertEquals("Underlying sink is closed.", out.streamError?.localizedDescription) + assertNull(out.streamError) assertTrue(sink.buffer.readByteArray().isEmpty()) } } @@ -85,7 +84,6 @@ class SinkNSOutputStreamTest { val runLoop = startRunLoop() fun produceWithDelegate(out: NSOutputStream, data: String) { - println("produceWithDelegate(${out::class}, $data)") val opened = Mutex(true) val written = atomic(0) val completed = Mutex(true) @@ -93,7 +91,6 @@ class SinkNSOutputStreamTest { out.delegate = object : NSObject(), NSStreamDelegateProtocol { val source = data.encodeToByteArray() override fun stream(aStream: NSStream, handleEvent: NSStreamEvent) { - println("${out::class} $data event = ${handleEvent.asString()} (${NSThread.currentThread.name})") assertEquals("run-loop", NSThread.currentThread.name) when (handleEvent) { NSStreamEventOpenCompleted -> opened.unlock() @@ -112,6 +109,7 @@ class SinkNSOutputStreamTest { out.close() completed.unlock() } + else -> fail("unexpected event ${handleEvent.asString()}") } } } @@ -124,9 +122,7 @@ class SinkNSOutputStreamTest { assertEquals(data.length, written.value) } - produceWithDelegate(NSOutputStream.outputStreamToMemory(), "default") produceWithDelegate(Buffer().asNSOutputStream(), "custom") - produceWithDelegate(NSOutputStream.outputStreamToMemory(), "") produceWithDelegate(Buffer().asNSOutputStream(), "") CFRunLoopStop(runLoop.getCFRunLoop()) } @@ -136,16 +132,15 @@ class SinkNSOutputStreamTest { val runLoop = startRunLoop() fun subscribeAfterOpen(out: NSOutputStream) { - println("subscribeAfterOpen(${out::class})") val available = Mutex(true) out.delegate = object : NSObject(), NSStreamDelegateProtocol { override fun stream(aStream: NSStream, handleEvent: NSStreamEvent) { - println("${out::class} event = ${handleEvent.asString()} (${NSThread.currentThread.name})") assertEquals("run-loop", NSThread.currentThread.name) when (handleEvent) { NSStreamEventOpenCompleted -> fail("opened before subscribe") NSStreamEventHasSpaceAvailable -> available.unlock() + else -> fail("unexpected event ${handleEvent.asString()}") } } } @@ -157,7 +152,6 @@ class SinkNSOutputStreamTest { out.close() } - subscribeAfterOpen(NSOutputStream.outputStreamToMemory()) subscribeAfterOpen(Buffer().asNSOutputStream()) CFRunLoopStop(runLoop.getCFRunLoop()) } diff --git a/core/apple/test/SourceNSInputStreamTest.kt b/core/apple/test/SourceNSInputStreamTest.kt index 9a75bafea..c09a67bf9 100644 --- a/core/apple/test/SourceNSInputStreamTest.kt +++ b/core/apple/test/SourceNSInputStreamTest.kt @@ -5,10 +5,6 @@ package kotlinx.io -import io.ktor.server.cio.* -import io.ktor.server.engine.* -import io.ktor.server.routing.* -import io.ktor.utils.io.core.* import kotlinx.atomicfu.atomic import kotlinx.atomicfu.locks.reentrantLock import kotlinx.atomicfu.locks.withLock @@ -20,8 +16,6 @@ import platform.CoreFoundation.CFRunLoopStop import platform.Foundation.* import platform.darwin.NSObject import platform.posix.uint8_tVar -import platform.posix.usleep -import kotlin.random.Random import kotlin.test.* @OptIn(UnsafeNumber::class) @@ -122,8 +116,7 @@ class SourceNSInputStreamTest { byteArray.fill(-5) assertEquals(-1, input.read(cPtr, 4U)) - assertNotNull(input.streamError) - assertEquals("Underlying source is closed.", input.streamError?.localizedDescription) + assertNull(input.streamError) assertEquals("[-5, -5, -5, -5]", byteArray.contentToString()) } } @@ -133,7 +126,6 @@ class SourceNSInputStreamTest { val runLoop = startRunLoop() fun consumeWithDelegate(input: NSInputStream, data: String) { - println("consumeWithDelegate(${input::class}, $data)") val opened = Mutex(true) val read = atomic(0) val completed = Mutex(true) @@ -141,7 +133,6 @@ class SourceNSInputStreamTest { input.delegate = object : NSObject(), NSStreamDelegateProtocol { val sink = ByteArray(data.length) override fun stream(aStream: NSStream, handleEvent: NSStreamEvent) { - println("${input::class} $data event = ${handleEvent.asString()} (${NSThread.currentThread.name})") assertEquals("run-loop", NSThread.currentThread.name) when (handleEvent) { NSStreamEventOpenCompleted -> opened.unlock() @@ -156,6 +147,7 @@ class SourceNSInputStreamTest { input.close() completed.unlock() } + else -> fail("unexpected event ${handleEvent.asString()}") } } } @@ -168,101 +160,17 @@ class SourceNSInputStreamTest { assertEquals(data.length, read.value) } - consumeWithDelegate(NSInputStream(data = "default".encodeToByteArray().toNSData()), "default") consumeWithDelegate(Buffer().apply { writeString("custom") }.asNSInputStream(), "custom") - consumeWithDelegate(NSInputStream(data = NSData.data()), "") consumeWithDelegate(Buffer().asNSInputStream(), "") CFRunLoopStop(runLoop.getCFRunLoop()) } - @Test - fun uploadTest() { - val port = Random.nextInt(4000, 8000) - fun tryUpload(stream: NSInputStream) { - println("tryUpload(${stream::class})") - val request = NSMutableURLRequest.requestWithURL( - NSURL(string = "http://127.0.0.1:$port") - ) - request.HTTPMethod = "POST" - request.HTTPBodyStream = stream - request.setValue("application/octet-stream", "Content-Type") - - val session = NSURLSession.sharedSession - val task = session.uploadTaskWithStreamedRequest(request) - task.resume() - while (stream.streamStatus != NSStreamStatusAtEnd && - stream.streamStatus != NSStreamStatusClosed && - stream.streamStatus != NSStreamStatusError - ) { - usleep(100_000U) - } - } - @Suppress("ExtractKtorModule") - val server = embeddedServer(CIO, port = port, host = "localhost") { - routing { - post { - this.context.request.receiveChannel().readAvailable(1) { - println(it.readText()) - } - } - } - } - server.start(false) - tryUpload(NSInputStream(data = "default".encodeToByteArray().toNSData())) - tryUpload(Buffer().apply { writeString("custom") }.asNSInputStream()) - server.stop() - } - - @Test - fun testRunLoopPolling() { - val runLoop = startRunLoop() - - val opened = Mutex(true) - val read = atomic(0) - val end = Mutex(true) - - val buffer = Buffer() - val input = buffer.asNSInputStream() - val sink = ByteArray(6) - input.delegate = object : NSObject(), NSStreamDelegateProtocol { - override fun stream(aStream: NSStream, handleEvent: NSStreamEvent) { - println("event = ${handleEvent.asString()} (${NSThread.currentThread.name})") - when (handleEvent) { - NSStreamEventOpenCompleted -> opened.unlock() - NSStreamEventHasBytesAvailable -> { - sink.usePinned { - assertEquals(3, input.read(it.addressOf(read.value).reinterpret(), 3U)) - read.value += 3 - } - } - NSStreamEventEndEncountered -> end.unlock() - } - } - } - input.scheduleInRunLoop(runLoop, NSDefaultRunLoopMode) - input.open() - runBlocking { - opened.lockWithTimeout() - end.lockWithTimeout() - buffer.writeString("abc") - end.lockWithTimeout() - assertEquals(3, read.value) - buffer.writeString("123") - end.lockWithTimeout() - } - assertEquals(6, read.value) - assertEquals("abc123", sink.decodeToString()) - input.close() - CFRunLoopStop(runLoop.getCFRunLoop()) - } - @Test fun testRunLoopSwitch() { val runLoop1 = startRunLoop("run-loop-1") val runLoop2 = startRunLoop("run-loop-2") fun consumeSwitching(input: NSInputStream, data: String) { - println("consumeSwitching(${input::class}, $data)") val opened = Mutex(true) val readLock = reentrantLock() var read = 0 @@ -271,22 +179,24 @@ class SourceNSInputStreamTest { input.delegate = object : NSObject(), NSStreamDelegateProtocol { val sink = ByteArray(data.length) override fun stream(aStream: NSStream, handleEvent: NSStreamEvent) { + // Ensure thread safe access to `read` between scheduled run loops readLock.withLock { - println("${input::class} $data event = ${handleEvent.asString()} (${NSThread.currentThread.name})") if (read == 0) { + // until first read assertEquals("run-loop-1", NSThread.currentThread.name) } else { + // after first read assertEquals("run-loop-2", NSThread.currentThread.name) } when (handleEvent) { NSStreamEventOpenCompleted -> opened.unlock() NSStreamEventHasBytesAvailable -> { if (read == 0) { - // switch to other run loop + // switch to other run loop before first read input.removeFromRunLoop(runLoop1, NSDefaultRunLoopMode) input.scheduleInRunLoop(runLoop2, NSDefaultRunLoopMode) } else if (read >= data.length - 3) { - // unsubscribe + // unsubscribe before last read input.removeFromRunLoop(runLoop2, NSDefaultRunLoopMode) } sink.usePinned { @@ -300,6 +210,7 @@ class SourceNSInputStreamTest { } } NSStreamEventEndEncountered -> fail("$data shouldn't be subscribed") + else -> fail("unexpected event ${handleEvent.asString()}") } } } @@ -315,7 +226,6 @@ class SourceNSInputStreamTest { input.close() } - consumeSwitching(NSInputStream(data = "default".encodeToByteArray().toNSData()), "default") consumeSwitching(Buffer().apply { writeString("custom") }.asNSInputStream(), "custom") CFRunLoopStop(runLoop1.getCFRunLoop()) CFRunLoopStop(runLoop2.getCFRunLoop()) @@ -325,17 +235,24 @@ class SourceNSInputStreamTest { fun testSubscribeAfterOpen() { val runLoop = startRunLoop() - fun subscribeAfterOpen(input: NSInputStream) { - println("subscribeAfterOpen(${input::class})") + fun subscribeAfterOpen(input: NSInputStream, data: String) { val available = Mutex(true) input.delegate = object : NSObject(), NSStreamDelegateProtocol { override fun stream(aStream: NSStream, handleEvent: NSStreamEvent) { - println("${input::class} event = ${handleEvent.asString()} (${NSThread.currentThread.name})") assertEquals("run-loop", NSThread.currentThread.name) when (handleEvent) { NSStreamEventOpenCompleted -> fail("opened before subscribe") - NSStreamEventHasBytesAvailable -> available.unlock() + NSStreamEventHasBytesAvailable -> { + val sink = ByteArray(data.length) + sink.usePinned { + assertEquals(data.length.convert(), input.read(it.addressOf(0).reinterpret(), data.length.convert())) + } + assertEquals(data, sink.decodeToString()) + input.close() + available.unlock() + } + else -> fail("unexpected event ${handleEvent.asString()}") } } } @@ -344,11 +261,9 @@ class SourceNSInputStreamTest { runBlocking { available.lockWithTimeout() } - input.close() } - subscribeAfterOpen(NSInputStream(data = "default".encodeToByteArray().toNSData())) - subscribeAfterOpen(Buffer().apply { writeString("custom") }.asNSInputStream()) + subscribeAfterOpen(Buffer().apply { writeString("custom") }.asNSInputStream(), "custom") CFRunLoopStop(runLoop.getCFRunLoop()) } } diff --git a/core/build.gradle.kts b/core/build.gradle.kts index 8aec29692..200369de1 100644 --- a/core/build.gradle.kts +++ b/core/build.gradle.kts @@ -38,8 +38,6 @@ kotlin { } appleTest { dependencies { - implementation("io.ktor:ktor-server-core:2.3.2") - implementation("io.ktor:ktor-server-cio:2.3.2") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.2") } } From 6d034e9f462c4f0090f96d4ba263695f46208119 Mon Sep 17 00:00:00 2001 From: Jeff Lockhart Date: Wed, 2 Aug 2023 11:08:25 -0600 Subject: [PATCH 48/51] Check for NSStreamStatusAtEnd after read --- core/apple/src/SourcesApple.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/apple/src/SourcesApple.kt b/core/apple/src/SourcesApple.kt index 73b2d558f..8f1b26871 100644 --- a/core/apple/src/SourcesApple.kt +++ b/core/apple/src/SourcesApple.kt @@ -72,7 +72,7 @@ private class SourceNSInputStream( } val toRead = minOf(maxLength.toLong(), source.buffer.size, Int.MAX_VALUE.toLong()).toInt() val read = source.buffer.readAtMostTo(buffer, toRead).convert() - status = NSStreamStatusOpen + status = if (source.exhausted()) NSStreamStatusAtEnd else NSStreamStatusOpen checkBytes() return read } catch (e: Exception) { From ed5902ec5edf09000c06f7c0609b1ecca519295b Mon Sep 17 00:00:00 2001 From: Jeff Lockhart Date: Wed, 2 Aug 2023 11:20:56 -0600 Subject: [PATCH 49/51] Post NSStreamEventErrorOccurred on exhausted error --- core/apple/src/SourcesApple.kt | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/core/apple/src/SourcesApple.kt b/core/apple/src/SourcesApple.kt index 8f1b26871..f85d2d4ad 100644 --- a/core/apple/src/SourcesApple.kt +++ b/core/apple/src/SourcesApple.kt @@ -37,7 +37,6 @@ private class SourceNSInputStream( set(value) { status = NSStreamStatusError field = value - postEvent(NSStreamEventErrorOccurred) source.close() } @@ -77,6 +76,7 @@ private class SourceNSInputStream( return read } catch (e: Exception) { error = e.toNSError() + postEvent(NSStreamEventErrorOccurred) return -1 } } @@ -114,17 +114,18 @@ private class SourceNSInputStream( val runLoop = runLoop ?: return runLoop.performInModes(runLoopModes) { if (runLoop != this.runLoop || isFinished) return@performInModes - try { - val event = if (source.exhausted()) { + val event = try { + if (source.exhausted()) { status = NSStreamStatusAtEnd NSStreamEventEndEncountered } else { NSStreamEventHasBytesAvailable } - delegateOrSelf.stream(this, event) - } catch (e: IllegalStateException) { - // ignore closed + } catch (e: Exception) { + error = e.toNSError() + NSStreamEventErrorOccurred } + delegateOrSelf.stream(this, event) } } From 07318d0f3f297ee6f461266a41fba05eb99e94d1 Mon Sep 17 00:00:00 2001 From: Jeff Lockhart Date: Wed, 2 Aug 2023 11:45:18 -0600 Subject: [PATCH 50/51] Revert check for NSStreamStatusAtEnd after read --- core/apple/src/SourcesApple.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/apple/src/SourcesApple.kt b/core/apple/src/SourcesApple.kt index f85d2d4ad..5967c35af 100644 --- a/core/apple/src/SourcesApple.kt +++ b/core/apple/src/SourcesApple.kt @@ -71,7 +71,7 @@ private class SourceNSInputStream( } val toRead = minOf(maxLength.toLong(), source.buffer.size, Int.MAX_VALUE.toLong()).toInt() val read = source.buffer.readAtMostTo(buffer, toRead).convert() - status = if (source.exhausted()) NSStreamStatusAtEnd else NSStreamStatusOpen + status = NSStreamStatusOpen checkBytes() return read } catch (e: Exception) { From 4897741a9c915a5cc8fad750a290335bf790c3f3 Mon Sep 17 00:00:00 2001 From: Jeff Lockhart Date: Mon, 7 Aug 2023 10:25:09 -0600 Subject: [PATCH 51/51] Add suggested doc comments --- core/apple/src/SinksApple.kt | 11 +++++++++++ core/apple/src/SourcesApple.kt | 10 ++++++++++ 2 files changed, 21 insertions(+) diff --git a/core/apple/src/SinksApple.kt b/core/apple/src/SinksApple.kt index 496afd18a..f4fbe9d6d 100644 --- a/core/apple/src/SinksApple.kt +++ b/core/apple/src/SinksApple.kt @@ -15,6 +15,17 @@ import kotlin.native.ref.WeakReference /** * Returns an output stream that writes to this sink. Closing the stream will also close this sink. * + * The stream supports both polling and run-loop scheduling, please check + * [Apple's documentation](https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/Streams/Articles/PollingVersusRunloop.html) + * for information about stream events handling. + * + * The stream does not implement initializers + * ([NSOutputStream.initToBuffer](https://developer.apple.com/documentation/foundation/nsoutputstream/1410805-inittobuffer), + * [NSOutputStream.initToMemory](https://developer.apple.com/documentation/foundation/nsoutputstream/1409909-inittomemory), + * [NSOutputStream.initWithURL](https://developer.apple.com/documentation/foundation/nsoutputstream/1414446-initwithurl), + * [NSOutputStream.initToFileAtPath](https://developer.apple.com/documentation/foundation/nsoutputstream/1416367-inittofileatpath)), + * their use will result in a runtime error. + * * @sample kotlinx.io.samples.KotlinxIoSamplesApple.asStream */ public fun Sink.asNSOutputStream(): NSOutputStream = SinkNSOutputStream(this) diff --git a/core/apple/src/SourcesApple.kt b/core/apple/src/SourcesApple.kt index 5967c35af..f03a7af7a 100644 --- a/core/apple/src/SourcesApple.kt +++ b/core/apple/src/SourcesApple.kt @@ -16,6 +16,16 @@ import kotlin.native.ref.WeakReference /** * Returns an input stream that reads from this source. Closing the stream will also close this source. * + * The stream supports both polling and run-loop scheduling, please check + * [Apple's documentation](https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/Streams/Articles/PollingVersusRunloop.html) + * for information about stream events handling. + * + * The stream does not implement initializers + * ([NSInputStream.initWithURL](https://developer.apple.com/documentation/foundation/nsinputstream/1417891-initwithurl), + * [NSInputStream.initWithData](https://developer.apple.com/documentation/foundation/nsinputstream/1412470-initwithdata), + * [NSInputStream.initWithFileAtPath](https://developer.apple.com/documentation/foundation/nsinputstream/1408976-initwithfileatpath)), + * their use will result in a runtime error. + * * @sample kotlinx.io.samples.KotlinxIoSamplesApple.asStream */ public fun Source.asNSInputStream(): NSInputStream = SourceNSInputStream(this)