Skip to content

Commit

Permalink
Merge branch 'master' into feature/GH-42-swift-to-kotlin-wrappers
Browse files Browse the repository at this point in the history
  • Loading branch information
rickclephas committed Feb 20, 2022
2 parents f113550 + c6b134a commit 615ee3e
Show file tree
Hide file tree
Showing 25 changed files with 249 additions and 143 deletions.
4 changes: 3 additions & 1 deletion buildSrc/src/main/kotlin/Dependencies.kt
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@ object Dependencies {
}

object Kotlinx {
const val coroutinesCore = "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.0-native-mt"
private const val coroutinesVersion = "1.6.0-native-mt"
const val coroutinesCore = "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutinesVersion"
const val coroutinesTest = "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutinesVersion"

const val atomicfu = "org.jetbrains.kotlinx:atomicfu:0.17.1"
}
Expand Down
11 changes: 9 additions & 2 deletions kmp-nativecoroutines-core/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -36,14 +36,21 @@ kotlin {
dependencies {
implementation(kotlin("test"))
implementation(Dependencies.Kotlinx.atomicfu)
implementation(Dependencies.Kotlinx.coroutinesTest)
}
}
val appleMain by creating {
val nativeCoroutinesMain by creating {
dependsOn(commonMain)
}
val appleTest by creating {
val nativeCoroutinesTest by creating {
dependsOn(commonTest)
}
val appleMain by creating {
dependsOn(nativeCoroutinesMain)
}
val appleTest by creating {
dependsOn(nativeCoroutinesTest)
}
listOf(
macosX64, macosArm64,
iosArm64, iosX64, iosSimulatorArm64,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.rickclephas.kmp.nativecoroutines

import kotlin.native.concurrent.freeze

actual fun <T> T.freeze(): T = this.freeze()
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import platform.Foundation.NSError
import platform.Foundation.NSLocalizedDescriptionKey
import kotlin.native.concurrent.freeze

actual typealias NativeError = NSError

/**
* Converts a [Throwable] to a [NSError].
*
Expand All @@ -15,12 +17,12 @@ import kotlin.native.concurrent.freeze
* The Kotlin throwable can be retrieved from the [NSError.userInfo] with the key `KotlinException`.
*/
@OptIn(UnsafeNumber::class)
internal fun Throwable.asNSError(): NSError {
internal actual fun Throwable.asNativeError(): NativeError {
val userInfo = mutableMapOf<Any?, Any>()
userInfo["KotlinException"] = this.freeze()
val message = message
if (message != null) {
userInfo[NSLocalizedDescriptionKey] = message
}
return NSError.errorWithDomain("KotlinException", 0.convert(), userInfo)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package com.rickclephas.kmp.nativecoroutines

internal actual val NativeError.kotlinCause: Throwable?
get() = this.userInfo["KotlinException"] as? Throwable
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package com.rickclephas.kmp.nativecoroutines

import kotlin.native.concurrent.isFrozen
import kotlin.test.*

class NativeCallbackAppleTests {

@Test
fun ensureFrozen() {
var receivedValue: RandomValue? = null
val callback: NativeCallback<RandomValue> = callback@{ value, unit ->
receivedValue = value
// This isn't required in Kotlin, but it is in Swift, so we'll test it anyway
return@callback unit
}
val value = RandomValue()
assertFalse(value.isFrozen, "Value shouldn't be frozen yet")
callback(value)
assertTrue(value.isFrozen, "Value should be frozen")
assertTrue(receivedValue?.isFrozen == true, "Received value should be frozen")
}
}

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -2,27 +2,16 @@ package com.rickclephas.kmp.nativecoroutines

import kotlinx.coroutines.Job
import kotlin.native.concurrent.isFrozen
import kotlin.test.Test
import kotlin.test.assertFalse
import kotlin.test.assertTrue
import kotlin.test.*

class NativeCancellableTests {
class NativeCancellableAppleTests {

@Test
fun `ensure frozen`() {
fun ensureFrozen() {
val job = Job()
assertFalse(job.isFrozen, "Job shouldn't be frozen yet")
val nativeCancellable = job.asNativeCancellable()
assertTrue(nativeCancellable.isFrozen, "NativeCancellable should be frozen")
assertTrue(job.isFrozen, "Job should be frozen after getting the NativeCancellable")
}

@Test
fun `ensure that the job gets cancelled`() {
val job = Job()
val nativeCancellable = job.asNativeCancellable()
assertFalse(job.isCancelled, "Job shouldn't be cancelled yet")
nativeCancellable()
assertTrue(job.isCancelled, "Job should be cancelled")
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,38 +5,39 @@ import kotlinx.cinterop.convert
import kotlin.native.concurrent.isFrozen
import kotlin.test.*

class NSErrorTests {
class NativeErrorAppleTests {

@Test
fun `ensure frozen`() {
fun ensureFrozen() {
val exception = RandomException()
assertFalse(exception.isFrozen, "Exception shouldn't be frozen yet")
val nsError = exception.asNSError()
val nsError = exception.asNativeError()
assertTrue(nsError.isFrozen, "NSError should be frozen")
assertTrue(exception.isFrozen, "Exception should be frozen")
}

@Test
@OptIn(UnsafeNumber::class)
fun `ensure NSError domain and code are correct`() {
fun ensureNSErrorDomainAndCodeAreCorrect() {
val exception = RandomException()
val nsError = exception.asNSError()
val nsError = exception.asNativeError()
assertEquals("KotlinException", nsError.domain, "Incorrect NSError domain")
assertEquals(0.convert(), nsError.code, "Incorrect NSError code")
}

@Test
fun `ensure localizedDescription is set to message`() {
fun ensureLocalizedDescriptionIsSetToMessage() {
val exception = RandomException()
val nsError = exception.asNSError()
val nsError = exception.asNativeError()
assertEquals(exception.message, nsError.localizedDescription,
"Localized description isn't set to message")
}

@Test
fun `ensure exception is part of user info`() {
fun ensureExceptionIsPartOfUserInfo() {
val exception = RandomException()
val nsError = exception.asNSError()
val nsError = exception.asNativeError()
assertSame(exception, nsError.userInfo["KotlinException"], "Exception isn't part of the user info")
assertSame(exception, nsError.kotlinCause, "Incorrect kotlinCause")
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.rickclephas.kmp.nativecoroutines

import kotlinx.coroutines.flow.flow
import kotlin.native.concurrent.isFrozen
import kotlin.test.*

class NativeFlowAppleTests {

@Test
fun ensureFrozen() {
val flow = flow<RandomValue> { }
assertFalse(flow.isFrozen, "Flow shouldn't be frozen yet")
val nativeFlow = flow.asNativeFlow()
assertTrue(nativeFlow.isFrozen, "NativeFlow should be frozen")
assertTrue(flow.isFrozen, "Flow should be frozen")
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package com.rickclephas.kmp.nativecoroutines

import kotlinx.coroutines.delay
import kotlin.native.concurrent.isFrozen
import kotlin.test.*

class NativeSuspendAppleTests {

private suspend fun delayAndReturn(delay: Long, value: RandomValue): RandomValue {
delay(delay)
return value
}

@Test
fun ensureFrozen() {
val value = RandomValue()
assertFalse(value.isFrozen, "Value shouldn't be frozen yet")
val nativeSuspend = nativeSuspend { delayAndReturn(0, value) }
assertTrue(nativeSuspend.isFrozen, "NativeSuspend should be frozen")
assertTrue(value.isFrozen, "Value should be frozen")
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.rickclephas.kmp.nativecoroutines

import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.runBlocking
import kotlin.coroutines.CoroutineContext

@Suppress("ACTUAL_WITHOUT_EXPECT")
actual typealias TestResult = Unit

internal actual fun runTest(
context: CoroutineContext,
block: suspend CoroutineScope.() -> Unit
): TestResult = runBlocking(context, block)

@Suppress("SuspendFunctionOnCoroutineScope")
internal actual suspend fun CoroutineScope.runCurrent() {
coroutineContext[Job]?.children?.forEach { it.join() }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.rickclephas.kmp.nativecoroutines

/**
* Freezes this object in Kotlin/Native.
*/
internal expect fun <T> T.freeze(): T
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
package com.rickclephas.kmp.nativecoroutines

import kotlin.native.concurrent.freeze

/**
* A callback with a single argument.
*
* We don't want the Swift code to known how to get the [Unit] object so we'll provide it as the second argument.
* We don't want the Swift code to known how to get the [Unit] object, so we'll provide it as the second argument.
* This way Swift can just return the value that it received without knowing what it is/how to get it.
*/
typealias NativeCallback<T> = (T, Unit) -> Unit
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package com.rickclephas.kmp.nativecoroutines

import kotlinx.coroutines.Job
import kotlin.native.concurrent.freeze

/**
* A function that cancels the coroutines [Job].
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.rickclephas.kmp.nativecoroutines

/**
* Represents an error in a way that the specific platform is able to handle
*/
expect class NativeError

/**
* Converts a [Throwable] to a [NativeError].
*/
internal expect fun Throwable.asNativeError(): NativeError
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,6 @@ import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch
import platform.Foundation.NSError
import kotlin.native.concurrent.freeze

/**
* A function that collects a [Flow] via callbacks.
Expand All @@ -31,7 +29,7 @@ typealias NativeFlow<T, Unit> = (
*/
fun <T> Flow<T>.asNativeFlow(scope: CoroutineScope? = null): NativeFlow<T, Unit> {
val coroutineScope = scope ?: defaultCoroutineScope
return (collect@{ onItem: NativeCallback<T>, onComplete: NativeCallback<NSError?> ->
return (collect@{ onItem: NativeCallback<T>, onComplete: NativeCallback<NativeError?> ->
val job = coroutineScope.launch {
try {
collect { onItem(it) }
Expand All @@ -41,13 +39,13 @@ fun <T> Flow<T>.asNativeFlow(scope: CoroutineScope? = null): NativeFlow<T, Unit>
// this is required since the job could be cancelled before it is started
throw e
} catch (e: Throwable) {
onComplete(e.asNSError())
onComplete(e.asNativeError())
}
}
job.invokeOnCompletion { cause ->
// Only handle CancellationExceptions, all other exceptions should be handled inside the job
if (cause !is CancellationException) return@invokeOnCompletion
onComplete(cause.asNSError())
onComplete(cause.asNativeError())
}
return@collect job.asNativeCancellable()
}).freeze()
Expand All @@ -64,4 +62,4 @@ fun <T, Unit> NativeFlow<T, Unit>.asFlow(): Flow<T> = callbackFlow {
{ error, _ -> close(error?.let { RuntimeException("NSError: $it") }) } // TODO: Convert NSError
)
awaitClose { cancellable() }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ typealias NativeSuspend<T, Unit> = (
*/
fun <T> nativeSuspend(scope: CoroutineScope? = null, block: suspend () -> T): NativeSuspend<T, Unit> {
val coroutineScope = scope ?: defaultCoroutineScope
return (collect@{ onResult: NativeCallback<T>, onError: NativeCallback<NSError> ->
return (collect@{ onResult: NativeCallback<T>, onError: NativeCallback<NativeError> ->
val job = coroutineScope.launch {
try {
onResult(block())
Expand All @@ -37,13 +37,13 @@ fun <T> nativeSuspend(scope: CoroutineScope? = null, block: suspend () -> T): Na
// this is required since the job could be cancelled before it is started
throw e
} catch (e: Throwable) {
onError(e.asNSError())
onError(e.asNativeError())
}
}
job.invokeOnCompletion { cause ->
// Only handle CancellationExceptions, all other exceptions should be handled inside the job
if (cause !is CancellationException) return@invokeOnCompletion
onError(cause.asNSError())
onError(cause.asNativeError())
}
return@collect job.asNativeCancellable()
}).freeze()
Expand All @@ -60,4 +60,4 @@ suspend fun <T, Unit> NativeSuspend<T, Unit>.await(): T = suspendCancellableCoro
{ error, _ -> cont.resumeWithException(RuntimeException("NSError: $error")) } // TODO: Convert NSError
)
cont.invokeOnCancellation { cancellable() }
}
}
Loading

0 comments on commit 615ee3e

Please sign in to comment.