Skip to content

Commit

Permalink
Merge branch 'master' into feature/GH-29-improve-exception-conversion
Browse files Browse the repository at this point in the history
  • Loading branch information
rickclephas committed Feb 19, 2022
2 parents 3c6d768 + cfe0201 commit 4781c30
Show file tree
Hide file tree
Showing 14 changed files with 94 additions and 42 deletions.
2 changes: 1 addition & 1 deletion KMPNativeCoroutinesAsync.podspec
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
Pod::Spec.new do |s|
s.name = 'KMPNativeCoroutinesAsync'
s.version = '0.11.1'
s.version = '0.11.3'
s.summary = 'Swift library for Kotlin Coroutines with Swift Async/Await'

s.homepage = 'https://github.com/rickclephas/KMP-NativeCoroutines'
Expand Down
2 changes: 1 addition & 1 deletion KMPNativeCoroutinesCombine.podspec
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
Pod::Spec.new do |s|
s.name = 'KMPNativeCoroutinesCombine'
s.version = '0.11.1'
s.version = '0.11.3'
s.summary = 'Swift library for Kotlin Coroutines with Combine'

s.homepage = 'https://github.com/rickclephas/KMP-NativeCoroutines'
Expand Down
2 changes: 1 addition & 1 deletion KMPNativeCoroutinesCore.podspec
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
Pod::Spec.new do |s|
s.name = 'KMPNativeCoroutinesCore'
s.version = '0.11.1'
s.version = '0.11.3'
s.summary = 'Swift library for Kotlin Coroutines'

s.homepage = 'https://github.com/rickclephas/KMP-NativeCoroutines'
Expand Down
2 changes: 1 addition & 1 deletion KMPNativeCoroutinesRxSwift.podspec
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
Pod::Spec.new do |s|
s.name = 'KMPNativeCoroutinesRxSwift'
s.version = '0.11.1'
s.version = '0.11.3'
s.summary = 'Swift library for Kotlin Coroutines with RxSwift'

s.homepage = 'https://github.com/rickclephas/KMP-NativeCoroutines'
Expand Down
17 changes: 13 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ plugins {

### Swift (Swift Package Manager)

All Swift implementations are also available via the Swift Package Manager.
The Swift implementations are available via the Swift Package Manager.
Just add it to your `Package.swift` file:
```swift
dependencies: [
Expand All @@ -85,8 +85,7 @@ Or add it in Xcode by going to `File` > `Add Packages...` and providing the URL:

### Swift (CocoaPods)

Now for Swift you can choose from a couple of implementations.
Add one or more of the following libraries to your `Podfile`:
If you use CocoaPods add one or more of the following libraries to your `Podfile`:
```ruby
pod 'KMPNativeCoroutinesAsync' # Swift 5.5 Async/Await implementation
pod 'KMPNativeCoroutinesCombine' # Combine implementation
Expand All @@ -96,10 +95,20 @@ pod 'KMPNativeCoroutinesRxSwift' # RxSwift implementation
## Usage

Using your Kotlin Coroutines code from Swift is almost as easy as calling the Kotlin code.
Just use the wrapper functions in Swift to get Observables, Publishers, AsyncStreams or async functions.
Just use the wrapper functions in Swift to get async functions, AsyncStreams, Publishers or Observables.

### Kotlin

> **WARNING:** The Kotlin part of this library consists of helper functions and a Kotlin compiler plugin.
> Using the plugin removes the boilerplate code from your project, however **Kotlin compiler plugins aren't stable**!
>
> The plugin is known to cause recursion errors in some scenarios such as in [#4][GH-4] and [#23][GH-23].
> To prevent such recursion errors it's best to explicitly define the (return) types of public
> properties and functions.
[GH-4]: https://github.com/rickclephas/KMP-NativeCoroutines/issues/4
[GH-23]: https://github.com/rickclephas/KMP-NativeCoroutines/issues/23

The plugin will automagically generate the necessary code for you! 🔮

Your `Flow` properties/functions get a `Native` version:
Expand Down
2 changes: 1 addition & 1 deletion build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ buildscript {

allprojects {
group = "com.rickclephas.kmp"
version = "0.11.1"
version = "0.11.3"

repositories {
mavenCentral()
Expand Down
2 changes: 2 additions & 0 deletions buildSrc/src/main/kotlin/Dependencies.kt
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,7 @@ object Dependencies {

object Kotlinx {
const val coroutinesCore = "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.0-native-mt"

const val atomicfu = "org.jetbrains.kotlinx:atomicfu:0.17.1"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@ internal class KmpNativeCoroutinesSyntheticResolveExtension(
}

private fun createPropertyDescriptor(
containingDeclaration: DeclarationDescriptor,
containingDeclaration: ClassDescriptor,
visibility: DescriptorVisibility,
name: Name,
outType: KotlinType,
Expand All @@ -153,7 +153,7 @@ internal class KmpNativeCoroutinesSyntheticResolveExtension(
): PropertyDescriptor = PropertyDescriptorImpl.create(
containingDeclaration,
Annotations.EMPTY,
Modality.FINAL,
if (containingDeclaration.kind.isInterface) Modality.OPEN else Modality.FINAL,
visibility,
false,
name,
Expand All @@ -176,7 +176,7 @@ internal class KmpNativeCoroutinesSyntheticResolveExtension(
PropertyGetterDescriptorImpl(
this,
Annotations.EMPTY,
Modality.FINAL,
modality,
visibility,
false,
false,
Expand Down Expand Up @@ -236,9 +236,10 @@ internal class KmpNativeCoroutinesSyntheticResolveExtension(
returnType = returnType.replaceFunctionGenerics(coroutinesFunctionDescriptor, typeParameters)

// Convert Flow types to NativeFlow
val flowValueType = coroutinesFunctionDescriptor.getFlowValueTypeOrNull()
val flowValueType = coroutinesFunctionDescriptor.getFlowValueTypeOrNull()?.type
?.replaceFunctionGenerics(coroutinesFunctionDescriptor, typeParameters)
if (flowValueType != null)
returnType = thisDescriptor.module.getExpandedNativeFlowType(flowValueType.type)
returnType = thisDescriptor.module.getExpandedNativeFlowType(flowValueType)

// Convert suspend function to NativeSuspend
if (coroutinesFunctionDescriptor.isSuspend)
Expand All @@ -250,7 +251,7 @@ internal class KmpNativeCoroutinesSyntheticResolveExtension(
typeParameters,
valueParameters,
returnType,
Modality.FINAL,
if (thisDescriptor.kind.isInterface) Modality.OPEN else Modality.FINAL,
coroutinesFunctionDescriptor.visibility
)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,21 +1,26 @@
package com.rickclephas.kmp.nativecoroutines.compiler.utils

import org.jetbrains.kotlin.descriptors.DeclarationDescriptor
import org.jetbrains.kotlin.descriptors.SourceElement
import org.jetbrains.kotlin.descriptors.TypeParameterDescriptor
import org.jetbrains.kotlin.descriptors.impl.TypeParameterDescriptorImpl

internal fun TypeParameterDescriptor.copyFor(
newContainingDeclaration: DeclarationDescriptor
): TypeParameterDescriptor =
TypeParameterDescriptorImpl.createWithDefaultBound(
TypeParameterDescriptorImpl.createForFurtherModification(
newContainingDeclaration,
annotations,
isReified,
variance,
name,
index,
SourceElement.NO_SOURCE,
storageManager
)
).apply {
this@copyFor.upperBounds.forEach(::addUpperBound)
setInitialized()
}

internal fun List<TypeParameterDescriptor>.copyFor(
newContainingDeclaration: DeclarationDescriptor
Expand Down
1 change: 1 addition & 0 deletions kmp-nativecoroutines-core/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ kotlin {
val commonTest by getting {
dependencies {
implementation(kotlin("test"))
implementation(Dependencies.Kotlinx.atomicfu)
}
}
val appleMain by creating {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
package com.rickclephas.kmp.nativecoroutines

import kotlinx.atomicfu.atomic
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.flow
import kotlin.native.concurrent.AtomicInt
import kotlin.native.concurrent.isFrozen
import kotlin.test.*

Expand All @@ -23,10 +23,10 @@ class NativeFlowTests {
val flow = flow<RandomValue> { }
val job = Job()
val nativeFlow = flow.asNativeFlow(CoroutineScope(job))
val completionCount = AtomicInt(0)
val completionCount = atomic(0)
nativeFlow({ _, _ -> }, { error, _ ->
assertNull(error, "Flow should complete without an error")
completionCount.increment()
completionCount.incrementAndGet()
})
job.children.forEach { it.join() } // Waits for the collection to complete
assertEquals(1, completionCount.value, "Completion callback should be called once")
Expand All @@ -38,12 +38,12 @@ class NativeFlowTests {
val flow = flow<RandomValue> { throw exception }
val job = Job()
val nativeFlow = flow.asNativeFlow(CoroutineScope(job), arrayOf(RandomException::class))
val completionCount = AtomicInt(0)
val completionCount = atomic(0)
nativeFlow({ _, _ -> }, { error, _ ->
assertNotNull(error, "Flow should complete with an error")
val kotlinException = error.userInfo["KotlinException"]
assertSame(exception, kotlinException, "Kotlin exception should be the same exception")
completionCount.increment()
completionCount.incrementAndGet()
})
job.children.forEach { it.join() } // Waits for the collection to complete
assertEquals(1, completionCount.value, "Completion callback should be called once")
Expand All @@ -55,10 +55,10 @@ class NativeFlowTests {
val flow = flow { values.forEach { emit(it) } }
val job = Job()
val nativeFlow = flow.asNativeFlow(CoroutineScope(job))
val receivedValueCount = AtomicInt(0)
val receivedValueCount = atomic(0)
nativeFlow({ value, _ ->
assertSame(values[receivedValueCount.value], value, "Received incorrect value")
receivedValueCount.increment()
receivedValueCount.incrementAndGet()
}, { _, _ -> })
job.children.forEach { it.join() } // Waits for the collection to complete
assertEquals(values.size, receivedValueCount.value, "Item callback should be called for every value")
Expand All @@ -69,16 +69,16 @@ class NativeFlowTests {
val flow = MutableSharedFlow<RandomValue>()
val job = Job()
val nativeFlow = flow.asNativeFlow(CoroutineScope(job))
val completionCount = AtomicInt(0)
val completionCount = atomic(0)
val cancel = nativeFlow({ _, _ -> }, { error, _ ->
assertNotNull(error, "Flow should complete with an error")
val exception = error.userInfo["KotlinException"]
assertIs<CancellationException>(exception, "Error should contain CancellationException")
completionCount.increment()
completionCount.incrementAndGet()
})
delay(100) // Gives the collection some time to start
cancel()
job.children.forEach { it.join() } // Waits for the collection to complete
assertEquals(1, completionCount.value, "Completion callback should be called once")
}
}
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
package com.rickclephas.kmp.nativecoroutines

import kotlinx.atomicfu.atomic
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.runBlocking
import kotlin.coroutines.cancellation.CancellationException
import kotlin.native.concurrent.AtomicInt
import kotlin.native.concurrent.isFrozen
import kotlin.test.*

Expand Down Expand Up @@ -35,13 +35,13 @@ class NativeSuspendTests {
val value = RandomValue()
val job = Job()
val nativeSuspend = nativeSuspend(CoroutineScope(job)) { delayAndReturn(100, value) }
val receivedResultCount = AtomicInt(0)
val receivedErrorCount = AtomicInt(0)
val receivedResultCount = atomic(0)
val receivedErrorCount = atomic(0)
nativeSuspend({ receivedValue, _ ->
assertSame(value, receivedValue, "Received incorrect value")
receivedResultCount.increment()
receivedResultCount.incrementAndGet()
}, { _, _ ->
receivedErrorCount.increment()
receivedErrorCount.incrementAndGet()
})
job.children.forEach { it.join() } // Waits for the function to complete
assertEquals(1, receivedResultCount.value, "Result callback should be called once")
Expand All @@ -55,15 +55,15 @@ class NativeSuspendTests {
val nativeSuspend = nativeSuspend(CoroutineScope(job), arrayOf(RandomException::class)) {
delayAndThrow(100, exception)
}
val receivedResultCount = AtomicInt(0)
val receivedErrorCount = AtomicInt(0)
val receivedResultCount = atomic(0)
val receivedErrorCount = atomic(0)
nativeSuspend({ _, _ ->
receivedResultCount.increment()
receivedResultCount.incrementAndGet()
}, { error, _ ->
assertNotNull(error, "Function should complete with an error")
val kotlinException = error.userInfo["KotlinException"]
assertSame(exception, kotlinException, "Kotlin exception should be the same exception")
receivedErrorCount.increment()
receivedErrorCount.incrementAndGet()
})
job.children.forEach { it.join() } // Waits for the function to complete
assertEquals(1, receivedErrorCount.value, "Error callback should be called once")
Expand All @@ -74,20 +74,20 @@ class NativeSuspendTests {
fun `ensure function is cancelled`() = runBlocking {
val job = Job()
val nativeSuspend = nativeSuspend(CoroutineScope(job)) { delayAndReturn(5_000, RandomValue()) }
val receivedResultCount = AtomicInt(0)
val receivedErrorCount = AtomicInt(0)
val receivedResultCount = atomic(0)
val receivedErrorCount = atomic(0)
val cancel = nativeSuspend({ _, _ ->
receivedResultCount.increment()
receivedResultCount.incrementAndGet()
}, { error, _ ->
assertNotNull(error, "Function should complete with an error")
val exception = error.userInfo["KotlinException"]
assertIs<CancellationException>(exception, "Error should contain CancellationException")
receivedErrorCount.increment()
receivedErrorCount.incrementAndGet()
})
delay(100) // Gives the function some time to start
cancel()
job.children.forEach { it.join() } // Waits for the function to complete
assertEquals(1, receivedErrorCount.value, "Error callback should be called once")
assertEquals(0, receivedResultCount.value, "Result callback shouldn't be called")
}
}
}
24 changes: 24 additions & 0 deletions sample/IntegrationTests/CompilerIntegrationTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,18 @@ class CompilerIntegrationTests: XCTestCase {
wait(for: [valueExpectation], timeout: 2)
}

func testReturnConstrainedGenericValue() {
let integrationTests = IntegrationTests()
let valueExpectation = expectation(description: "Waiting for value")
let sendValue = integrationTests.returnAppendable(value: randomString())
_ = integrationTests.returnConstrainedGenericValueNative(value: sendValue)({ value, unit in
XCTAssertIdentical(value, sendValue, "Received incorrect value")
valueExpectation.fulfill()
return unit
}, { _, unit in unit })
wait(for: [valueExpectation], timeout: 2)
}

func testReturnGenericValues() {
let integrationTests = IntegrationTests()
let valueExpectation = expectation(description: "Waiting for values")
Expand Down Expand Up @@ -166,4 +178,16 @@ class CompilerIntegrationTests: XCTestCase {
}, { _, unit in unit })
wait(for: [valueExpectation], timeout: 2)
}

func testReturnGenericFlow() {
let integrationTests = IntegrationTests()
let valueExpectation = expectation(description: "Waiting for value")
let sendValue = NSNumber(value: randomInt())
_ = integrationTests.returnGenericFlowNative(value: sendValue)({ value, unit in
XCTAssertEqual(value as! NSNumber, sendValue, "Received incorrect value")
valueExpectation.fulfill()
return unit
}, { _, unit in unit })
wait(for: [valueExpectation], timeout: 2)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,12 @@ class CompilerIntegrationTests<V>: IntegrationTests() {
return value
}

fun returnAppendable(value: String): Appendable = StringBuilder(value)

suspend fun <T: Appendable> returnConstrainedGenericValue(value: T): T {
return value
}

suspend fun <T> returnGenericValues(values: List<T>): List<T> {
return values
}
Expand All @@ -66,6 +72,10 @@ class CompilerIntegrationTests<V>: IntegrationTests() {
return value
}

fun <T> returnGenericFlow(value: T): Flow<T> = flow {
emit(value)
}

@NativeCoroutinesIgnore
suspend fun returnIgnoredValue(value: Int): Int {
return value
Expand All @@ -74,4 +84,4 @@ class CompilerIntegrationTests<V>: IntegrationTests() {
init {
freeze()
}
}
}

0 comments on commit 4781c30

Please sign in to comment.