-
Notifications
You must be signed in to change notification settings - Fork 73
Add Source and Sink extensions for Apple's NSInputStream and NSOutputStream #174
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 3 commits
Commits
Show all changes
52 commits
Select commit
Hold shift + click to select a range
c0f042f
NSInputStream.asSource() and Source.asNSInputStream()
jeffdgr8 13bf3b1
Move Exception.toNSError() to -Util file
jeffdgr8 3386ab3
Make isClosed explicitly private
jeffdgr8 6b4bb80
Code review feedback
jeffdgr8 8ca65a2
Implement NSStreamStatus
jeffdgr8 2be70a6
Open NSInputStream on first read
jeffdgr8 54e247c
Unknown error when no streamError description
jeffdgr8 dccd22a
NSOutputStream.asSink() and Sink.asNSOutputStream()
jeffdgr8 2ba8bed
Support SinkNSOutputStream NSStreamDataWrittenToMemoryStreamKey
jeffdgr8 d7b8e1d
Override SourceNSInputStream.propertyForKey as no-op
jeffdgr8 62757c5
Mark status property @Volatile
jeffdgr8 613e2be
Code review feedback and fixes
jeffdgr8 e5f5c27
Fix reading byte as int
jeffdgr8 f5dfc1a
Buffer.snapshotAsNSData() for NSStreamDataWrittenToMemoryStreamKey
jeffdgr8 c9d1f44
Test SinkNSOutputStream with data longer than Segment
jeffdgr8 242fda0
Open streams on init
jeffdgr8 ead4f78
Update core/apple/src/-Util.kt
jeffdgr8 f9c9305
Update core/apple/src/BuffersApple.kt
jeffdgr8 18ff1d0
Update core/apple/test/NSOutputStreamSinkTest.kt
jeffdgr8 c4eb1b9
Update core/apple/test/SinkNSOutputStreamTest.kt
jeffdgr8 56bb0e4
Update core/apple/test/samples/samplesApple.kt
jeffdgr8 9d53c8e
Update core/apple/test/utilApple.kt
jeffdgr8 4e3ff87
Add samplesApple.kt to Dokka samples
jeffdgr8 aa5830c
Verify buffer != null and maxLength >= 0
jeffdgr8 6f076d9
Check isClosed() in SinkNSOutputStream.streamStatus
jeffdgr8 f789518
Use assertFailsWith
jeffdgr8 feb3145
Update core/apple/src/BuffersApple.kt
jeffdgr8 3508104
Update core/apple/src/SinksApple.kt
jeffdgr8 9e71d4b
Don't close a stream with the error status
jeffdgr8 f40d472
Use malloc and NSData.dataWithBytesNoCopy:length:
jeffdgr8 caba9b4
Better variable names
jeffdgr8 afab5b7
Use uint8_tVar (same typealias, but matches function signature)
jeffdgr8 365c354
Test SourceNSInputStream with long input data
jeffdgr8 a19c463
Add apple source set to bytestring module
jeffdgr8 0021412
Add NSInputStream from file test
jeffdgr8 211c5f5
Remove @Volatile annotations
jeffdgr8 ae33893
Remove getBuffer() implementation
jeffdgr8 5d14977
createTempFile() not working on Apple platforms
jeffdgr8 5cfafa2
Merge remote-tracking branch 'upstream/develop' into nsinputstream
jeffdgr8 e9fcaeb
Add apple source set
jeffdgr8 50fe63f
SourceNSInputStream run loop delegate support
jeffdgr8 89a4f80
Test subscribe after open
jeffdgr8 e296c01
Check run loop on postEvent()
jeffdgr8 7b3ab06
SinkNSOutputStream run loop delegate support
jeffdgr8 c74845e
lockWithTimeout() with better failure logging
jeffdgr8 d2d040d
Synchronize access to read variable for entire event handler
jeffdgr8 630423c
Only catch TimeoutCancellationException
jeffdgr8 110e56c
Code review feedback and fixes
jeffdgr8 6d034e9
Check for NSStreamStatusAtEnd after read
jeffdgr8 ed5902e
Post NSStreamEventErrorOccurred on exhausted error
jeffdgr8 07318d0
Revert check for NSStreamStatusAtEnd after read
jeffdgr8 4897741
Add suggested doc comments
jeffdgr8 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 | ||
| ) | ||
| ) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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<UInt8Var>() | ||
| 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)" | ||
jeffdgr8 marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,98 @@ | ||
| /* | ||
| * 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()) { | ||
fzhinkin marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| private val isClosed: () -> Boolean = when (source) { | ||
| is RealSource -> source::closed | ||
| is Buffer -> { | ||
| { false } | ||
| } | ||
| } | ||
|
|
||
| private var error: NSError? = null | ||
| private var pinnedBuffer: Pinned<ByteArray>? = null | ||
jeffdgr8 marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| override fun streamError(): NSError? = error | ||
|
|
||
| override fun open() { | ||
| // no-op | ||
| } | ||
|
|
||
| override fun read(buffer: CPointer<uint8_tVar>?, 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<CPointerVar<uint8_tVar>>?, length: CPointer<NSUIntegerVar>?): Boolean { | ||
fzhinkin marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| 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() = source.buffer.size > 0 | ||
jeffdgr8 marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| override fun close() { | ||
| pinnedBuffer?.unpin() | ||
| pinnedBuffer = null | ||
| source.close() | ||
| } | ||
|
|
||
| override fun description() = "$source.inputStream()" | ||
|
|
||
| private fun Buffer.readNative(sink: CPointer<uint8_tVar>?, maxLength: Int): Int { | ||
jeffdgr8 marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| 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 | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 | ||
| } | ||
jeffdgr8 marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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") | ||
fzhinkin marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| 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<UInt8Var>() | ||
|
|
||
| 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<CPointerVar<UInt8Var>>() | ||
| val lengthPtr = alloc<NSUIntegerVar>() | ||
| 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<UInt8Var>() | ||
|
|
||
| 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()) | ||
| } | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,17 @@ | ||
| package kotlinx.io.samples | ||
jeffdgr8 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| 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) | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,18 @@ | ||
| package kotlinx.io | ||
jeffdgr8 marked this conversation as resolved.
Show resolved
Hide resolved
fzhinkin marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| 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() | ||
| } | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.