From e39f2f031a3b7318a7320efb1ad9bc91a2f8fa10 Mon Sep 17 00:00:00 2001 From: Rick Clephas Date: Fri, 24 Dec 2021 22:52:05 +0100 Subject: [PATCH 01/19] Update asNSError to use KN runtime functions --- kmp-nativecoroutines-core/build.gradle.kts | 1 + .../kmp/nativecoroutines/NSError.kt | 49 +++++++++++++------ .../kmp/nativecoroutines/NativeFlow.kt | 11 +++-- .../kmp/nativecoroutines/NativeSuspend.kt | 12 +++-- .../kmp/nativecoroutines/NSErrorTests.kt | 8 +-- .../kmp/nativecoroutines/NativeFlowTests.kt | 2 +- .../nativecoroutines/NativeSuspendTests.kt | 4 +- .../kmp/nativecoroutines/RandomException.kt | 2 +- 8 files changed, 60 insertions(+), 29 deletions(-) diff --git a/kmp-nativecoroutines-core/build.gradle.kts b/kmp-nativecoroutines-core/build.gradle.kts index 93a30d57..5e03076d 100644 --- a/kmp-nativecoroutines-core/build.gradle.kts +++ b/kmp-nativecoroutines-core/build.gradle.kts @@ -26,6 +26,7 @@ kotlin { sourceSets { all { languageSettings.optIn("kotlin.RequiresOptIn") + languageSettings.optIn("kotlin.native.internal.InternalForKotlinNative") } val commonMain by getting { dependencies { diff --git a/kmp-nativecoroutines-core/src/appleMain/kotlin/com/rickclephas/kmp/nativecoroutines/NSError.kt b/kmp-nativecoroutines-core/src/appleMain/kotlin/com/rickclephas/kmp/nativecoroutines/NSError.kt index 2b91a447..e0d9c75d 100644 --- a/kmp-nativecoroutines-core/src/appleMain/kotlin/com/rickclephas/kmp/nativecoroutines/NSError.kt +++ b/kmp-nativecoroutines-core/src/appleMain/kotlin/com/rickclephas/kmp/nativecoroutines/NSError.kt @@ -1,26 +1,43 @@ package com.rickclephas.kmp.nativecoroutines -import kotlinx.cinterop.UnsafeNumber -import kotlinx.cinterop.convert +import kotlinx.cinterop.* import platform.Foundation.NSError -import platform.Foundation.NSLocalizedDescriptionKey import kotlin.native.concurrent.freeze +import kotlin.native.internal.GCUnsafeCall +import kotlin.reflect.KClass /** - * Converts a [Throwable] to a [NSError]. + * Uses Kotlin Native runtime functions to convert a [Throwable] to a [NSError]. * - * The returned [NSError] has `KotlinException` as the [NSError.domain], `0` as the [NSError.code] and - * the [NSError.localizedDescription] is set to the [Throwable.message]. + * Warning: [Throwable]s that aren't of a [propagatedExceptions] type will terminate the program. * - * The Kotlin throwable can be retrieved from the [NSError.userInfo] with the key `KotlinException`. + * @param propagatedExceptions a list of [Throwable] types that should be propagated as [NSError]s. */ -@OptIn(UnsafeNumber::class) -internal fun Throwable.asNSError(): NSError { - val userInfo = mutableMapOf() - userInfo["KotlinException"] = this.freeze() - val message = message - if (message != null) { - userInfo[NSLocalizedDescriptionKey] = message +internal fun Throwable.asNSError( + propagatedExceptions: List> +): NSError { + freeze() + val shouldPropagate = propagatedExceptions.any { it.isInstance(this) } + return memScoped { + val error = alloc>() + val types = when (shouldPropagate) { + true -> allocArray>(2).apply { + val typeInfo = getTypeInfo(this@asNSError) + set(0, interpretCPointer(typeInfo)) + } + false -> allocArray(1) + } + rethrowExceptionAsNSError(this@asNSError, error.ptr, types) + error.value } - return NSError.errorWithDomain("KotlinException", 0.convert(), userInfo) -} \ No newline at end of file +} + +@GCUnsafeCall("Kotlin_Any_getTypeInfo") +private external fun getTypeInfo(obj: Any): NativePtr + +@GCUnsafeCall("Kotlin_ObjCExport_RethrowExceptionAsNSError") +private external fun rethrowExceptionAsNSError( + exception: Throwable, + error: CPointer>, + types: CArrayPointer> +) \ No newline at end of file diff --git a/kmp-nativecoroutines-core/src/appleMain/kotlin/com/rickclephas/kmp/nativecoroutines/NativeFlow.kt b/kmp-nativecoroutines-core/src/appleMain/kotlin/com/rickclephas/kmp/nativecoroutines/NativeFlow.kt index f1dce7e6..1953eff4 100644 --- a/kmp-nativecoroutines-core/src/appleMain/kotlin/com/rickclephas/kmp/nativecoroutines/NativeFlow.kt +++ b/kmp-nativecoroutines-core/src/appleMain/kotlin/com/rickclephas/kmp/nativecoroutines/NativeFlow.kt @@ -7,6 +7,7 @@ import kotlinx.coroutines.flow.collect import kotlinx.coroutines.launch import platform.Foundation.NSError import kotlin.native.concurrent.freeze +import kotlin.reflect.KClass /** * A function that collects a [Flow] via callbacks. @@ -20,10 +21,14 @@ typealias NativeFlow = (onItem: NativeCallback, onComplete: NativeCallback * Creates a [NativeFlow] for this [Flow]. * * @param scope the [CoroutineScope] to use for the collection, or `null` to use the [defaultCoroutineScope]. + * @param propagatedExceptions a list of [Throwable] types that should be propagated as [NSError]s. * @receiver the [Flow] to collect. * @see Flow.collect */ -fun Flow.asNativeFlow(scope: CoroutineScope? = null): NativeFlow { +fun Flow.asNativeFlow( + scope: CoroutineScope? = null, + propagatedExceptions: List> = listOf(CancellationException::class) +): NativeFlow { val coroutineScope = scope ?: defaultCoroutineScope return (collect@{ onItem: NativeCallback, onComplete: NativeCallback -> val job = coroutineScope.launch { @@ -35,13 +40,13 @@ fun Flow.asNativeFlow(scope: CoroutineScope? = null): NativeFlow { // this is required since the job could be cancelled before it is started throw e } catch (e: Throwable) { - onComplete(e.asNSError()) + onComplete(e.asNSError(propagatedExceptions)) } } 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.asNSError(propagatedExceptions)) } return@collect job.asNativeCancellable() }).freeze() diff --git a/kmp-nativecoroutines-core/src/appleMain/kotlin/com/rickclephas/kmp/nativecoroutines/NativeSuspend.kt b/kmp-nativecoroutines-core/src/appleMain/kotlin/com/rickclephas/kmp/nativecoroutines/NativeSuspend.kt index 676b38ca..1e3e1920 100644 --- a/kmp-nativecoroutines-core/src/appleMain/kotlin/com/rickclephas/kmp/nativecoroutines/NativeSuspend.kt +++ b/kmp-nativecoroutines-core/src/appleMain/kotlin/com/rickclephas/kmp/nativecoroutines/NativeSuspend.kt @@ -5,6 +5,7 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch import platform.Foundation.NSError import kotlin.native.concurrent.freeze +import kotlin.reflect.KClass /** * A function that awaits a suspend function via callbacks. @@ -18,9 +19,14 @@ typealias NativeSuspend = (onResult: NativeCallback, onError: NativeCallba * Creates a [NativeSuspend] for the provided suspend [block]. * * @param scope the [CoroutineScope] to run the [block] in, or `null` to use the [defaultCoroutineScope]. + * @param propagatedExceptions a list of [Throwable] types that should be propagated as [NSError]s. * @param block the suspend block to await. */ -fun nativeSuspend(scope: CoroutineScope? = null, block: suspend () -> T): NativeSuspend { +fun nativeSuspend( + scope: CoroutineScope? = null, + propagatedExceptions: List> = listOf(CancellationException::class), + block: suspend () -> T +): NativeSuspend { val coroutineScope = scope ?: defaultCoroutineScope return (collect@{ onResult: NativeCallback, onError: NativeCallback -> val job = coroutineScope.launch { @@ -31,13 +37,13 @@ fun 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.asNSError(propagatedExceptions)) } } 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.asNSError(propagatedExceptions)) } return@collect job.asNativeCancellable() }).freeze() diff --git a/kmp-nativecoroutines-core/src/appleTest/kotlin/com/rickclephas/kmp/nativecoroutines/NSErrorTests.kt b/kmp-nativecoroutines-core/src/appleTest/kotlin/com/rickclephas/kmp/nativecoroutines/NSErrorTests.kt index 50de7ef6..631a244c 100644 --- a/kmp-nativecoroutines-core/src/appleTest/kotlin/com/rickclephas/kmp/nativecoroutines/NSErrorTests.kt +++ b/kmp-nativecoroutines-core/src/appleTest/kotlin/com/rickclephas/kmp/nativecoroutines/NSErrorTests.kt @@ -11,7 +11,7 @@ class NSErrorTests { fun `ensure frozen`() { val exception = RandomException() assertFalse(exception.isFrozen, "Exception shouldn't be frozen yet") - val nsError = exception.asNSError() + val nsError = exception.asNSError(listOf(RandomException::class)) assertTrue(nsError.isFrozen, "NSError should be frozen") assertTrue(exception.isFrozen, "Exception should be frozen") } @@ -20,7 +20,7 @@ class NSErrorTests { @OptIn(UnsafeNumber::class) fun `ensure NSError domain and code are correct`() { val exception = RandomException() - val nsError = exception.asNSError() + val nsError = exception.asNSError(listOf(RandomException::class)) assertEquals("KotlinException", nsError.domain, "Incorrect NSError domain") assertEquals(0.convert(), nsError.code, "Incorrect NSError code") } @@ -28,7 +28,7 @@ class NSErrorTests { @Test fun `ensure localizedDescription is set to message`() { val exception = RandomException() - val nsError = exception.asNSError() + val nsError = exception.asNSError(listOf(RandomException::class)) assertEquals(exception.message, nsError.localizedDescription, "Localized description isn't set to message") } @@ -36,7 +36,7 @@ class NSErrorTests { @Test fun `ensure exception is part of user info`() { val exception = RandomException() - val nsError = exception.asNSError() + val nsError = exception.asNSError(listOf(RandomException::class)) assertSame(exception, nsError.userInfo["KotlinException"], "Exception isn't part of the user info") } } \ No newline at end of file diff --git a/kmp-nativecoroutines-core/src/appleTest/kotlin/com/rickclephas/kmp/nativecoroutines/NativeFlowTests.kt b/kmp-nativecoroutines-core/src/appleTest/kotlin/com/rickclephas/kmp/nativecoroutines/NativeFlowTests.kt index ce5dce3f..4947c748 100644 --- a/kmp-nativecoroutines-core/src/appleTest/kotlin/com/rickclephas/kmp/nativecoroutines/NativeFlowTests.kt +++ b/kmp-nativecoroutines-core/src/appleTest/kotlin/com/rickclephas/kmp/nativecoroutines/NativeFlowTests.kt @@ -37,7 +37,7 @@ class NativeFlowTests { val exception = RandomException() val flow = flow { throw exception } val job = Job() - val nativeFlow = flow.asNativeFlow(CoroutineScope(job)) + val nativeFlow = flow.asNativeFlow(CoroutineScope(job), listOf(RandomException::class)) val completionCount = AtomicInt(0) nativeFlow({ _, _ -> }, { error, _ -> assertNotNull(error, "Flow should complete with an error") diff --git a/kmp-nativecoroutines-core/src/appleTest/kotlin/com/rickclephas/kmp/nativecoroutines/NativeSuspendTests.kt b/kmp-nativecoroutines-core/src/appleTest/kotlin/com/rickclephas/kmp/nativecoroutines/NativeSuspendTests.kt index 40d14a7d..d9232574 100644 --- a/kmp-nativecoroutines-core/src/appleTest/kotlin/com/rickclephas/kmp/nativecoroutines/NativeSuspendTests.kt +++ b/kmp-nativecoroutines-core/src/appleTest/kotlin/com/rickclephas/kmp/nativecoroutines/NativeSuspendTests.kt @@ -52,7 +52,9 @@ class NativeSuspendTests { fun `ensure exceptions are received as errors`() = runBlocking { val exception = RandomException() val job = Job() - val nativeSuspend = nativeSuspend(CoroutineScope(job)) { delayAndThrow(100, exception) } + val nativeSuspend = nativeSuspend(CoroutineScope(job), listOf(RandomException::class)) { + delayAndThrow(100, exception) + } val receivedResultCount = AtomicInt(0) val receivedErrorCount = AtomicInt(0) nativeSuspend({ _, _ -> diff --git a/kmp-nativecoroutines-core/src/commonTest/kotlin/com/rickclephas/kmp/nativecoroutines/RandomException.kt b/kmp-nativecoroutines-core/src/commonTest/kotlin/com/rickclephas/kmp/nativecoroutines/RandomException.kt index 9d62a0ae..4e5619c4 100644 --- a/kmp-nativecoroutines-core/src/commonTest/kotlin/com/rickclephas/kmp/nativecoroutines/RandomException.kt +++ b/kmp-nativecoroutines-core/src/commonTest/kotlin/com/rickclephas/kmp/nativecoroutines/RandomException.kt @@ -6,5 +6,5 @@ import kotlin.random.Random * An exception with a message consisting of 20 random capital letter. */ internal class RandomException: Exception( - (1..20).map { Random.nextInt(65, 91).toChar() }.joinToString() + (1..20).map { Random.nextInt(65, 91).toChar() }.joinToString("") ) \ No newline at end of file From a61b5c3452eb1dd506d1efa0c1d731f25116abdb Mon Sep 17 00:00:00 2001 From: Rick Clephas Date: Sun, 26 Dec 2021 13:38:01 +0100 Subject: [PATCH 02/19] Update compiler to support exception propagation --- .../nativecoroutines/NativeCoroutineThrows.kt | 17 ++++++ .../compiler/CompilerConfigurationKeys.kt | 7 +++ ...KmpNativeCoroutinesCommandLineProcessor.kt | 16 ++++- .../KmpNativeCoroutinesComponentRegistrar.kt | 12 +++- ...mpNativeCoroutinesIrGenerationExtension.kt | 8 ++- .../KmpNativeCoroutinesIrTransformer.kt | 61 +++++++++++++++---- ...tiveCoroutinesSyntheticResolveExtension.kt | 5 +- .../compiler/utils/IrArrayOf.kt | 20 ++++++ .../nativecoroutines/compiler/utils/List.kt | 23 ------- .../compiler/utils/NativeCoroutineThrows.kt | 10 +++ .../nativecoroutines/compiler/utils/Throws.kt | 9 +++ .../kmp/nativecoroutines/NSError.kt | 4 +- .../kmp/nativecoroutines/NativeFlow.kt | 4 +- .../kmp/nativecoroutines/NativeSuspend.kt | 4 +- .../build.gradle.kts | 5 +- .../gradle/KmpNativeCoroutinesExtension.kt | 14 +++++ .../gradle/KmpNativeCoroutinesPlugin.kt | 9 ++- 17 files changed, 178 insertions(+), 50 deletions(-) create mode 100644 kmp-nativecoroutines-annotations/src/commonMain/kotlin/com/rickclephas/kmp/nativecoroutines/NativeCoroutineThrows.kt create mode 100644 kmp-nativecoroutines-compiler/src/main/kotlin/com/rickclephas/kmp/nativecoroutines/compiler/utils/IrArrayOf.kt delete mode 100644 kmp-nativecoroutines-compiler/src/main/kotlin/com/rickclephas/kmp/nativecoroutines/compiler/utils/List.kt create mode 100644 kmp-nativecoroutines-compiler/src/main/kotlin/com/rickclephas/kmp/nativecoroutines/compiler/utils/NativeCoroutineThrows.kt create mode 100644 kmp-nativecoroutines-compiler/src/main/kotlin/com/rickclephas/kmp/nativecoroutines/compiler/utils/Throws.kt diff --git a/kmp-nativecoroutines-annotations/src/commonMain/kotlin/com/rickclephas/kmp/nativecoroutines/NativeCoroutineThrows.kt b/kmp-nativecoroutines-annotations/src/commonMain/kotlin/com/rickclephas/kmp/nativecoroutines/NativeCoroutineThrows.kt new file mode 100644 index 00000000..04c1f38d --- /dev/null +++ b/kmp-nativecoroutines-annotations/src/commonMain/kotlin/com/rickclephas/kmp/nativecoroutines/NativeCoroutineThrows.kt @@ -0,0 +1,17 @@ +package com.rickclephas.kmp.nativecoroutines + +import kotlin.reflect.KClass + +/** + * Defines what exceptions should be propagated as `NSError`s. + * + * Exceptions which are instances of one of the [exceptionClasses] or their subclasses, + * are propagated as a `NSError`s. + * Other Kotlin exceptions are considered unhandled and cause program termination. + * + * @see Throws + */ +@Target(AnnotationTarget.PROPERTY, AnnotationTarget.FUNCTION) +@Retention(AnnotationRetention.SOURCE) +@MustBeDocumented +annotation class NativeCoroutineThrows(vararg val exceptionClasses: KClass) \ No newline at end of file diff --git a/kmp-nativecoroutines-compiler/src/main/kotlin/com/rickclephas/kmp/nativecoroutines/compiler/CompilerConfigurationKeys.kt b/kmp-nativecoroutines-compiler/src/main/kotlin/com/rickclephas/kmp/nativecoroutines/compiler/CompilerConfigurationKeys.kt index 26c028fa..4036e449 100644 --- a/kmp-nativecoroutines-compiler/src/main/kotlin/com/rickclephas/kmp/nativecoroutines/compiler/CompilerConfigurationKeys.kt +++ b/kmp-nativecoroutines-compiler/src/main/kotlin/com/rickclephas/kmp/nativecoroutines/compiler/CompilerConfigurationKeys.kt @@ -1,6 +1,13 @@ package com.rickclephas.kmp.nativecoroutines.compiler import org.jetbrains.kotlin.config.CompilerConfigurationKey +import org.jetbrains.kotlin.name.FqName internal const val SUFFIX_OPTION_NAME = "suffix" internal val SUFFIX_KEY = CompilerConfigurationKey(SUFFIX_OPTION_NAME) + +internal const val PROPAGATED_EXCEPTIONS_OPTION_NAME = "propagatedExceptions" +internal val PROPAGATED_EXCEPTIONS_KEY = CompilerConfigurationKey>(PROPAGATED_EXCEPTIONS_OPTION_NAME) + +internal const val USE_THROWS_ANNOTATION_OPTION_NAME = "useThrowsAnnotation" +internal val USE_THROWS_ANNOTATION_KEY = CompilerConfigurationKey(USE_THROWS_ANNOTATION_OPTION_NAME) diff --git a/kmp-nativecoroutines-compiler/src/main/kotlin/com/rickclephas/kmp/nativecoroutines/compiler/KmpNativeCoroutinesCommandLineProcessor.kt b/kmp-nativecoroutines-compiler/src/main/kotlin/com/rickclephas/kmp/nativecoroutines/compiler/KmpNativeCoroutinesCommandLineProcessor.kt index 0b6cf993..54d70664 100644 --- a/kmp-nativecoroutines-compiler/src/main/kotlin/com/rickclephas/kmp/nativecoroutines/compiler/KmpNativeCoroutinesCommandLineProcessor.kt +++ b/kmp-nativecoroutines-compiler/src/main/kotlin/com/rickclephas/kmp/nativecoroutines/compiler/KmpNativeCoroutinesCommandLineProcessor.kt @@ -4,13 +4,25 @@ import org.jetbrains.kotlin.compiler.plugin.AbstractCliOption import org.jetbrains.kotlin.compiler.plugin.CliOption import org.jetbrains.kotlin.compiler.plugin.CommandLineProcessor import org.jetbrains.kotlin.config.CompilerConfiguration +import org.jetbrains.kotlin.name.FqName class KmpNativeCoroutinesCommandLineProcessor: CommandLineProcessor { override val pluginId: String = "com.rickclephas.kmp.nativecoroutines" override val pluginOptions: Collection = listOf( - CliOption(SUFFIX_OPTION_NAME, "string", "suffix used for the generated functions", true) + CliOption(SUFFIX_OPTION_NAME, + valueDescription = "string", + description = "suffix used to generate the native function and property names"), + CliOption(PROPAGATED_EXCEPTIONS_OPTION_NAME, + valueDescription = "fqname", + description = "default Throwable classes that will be propagated as NSError", + required = false, + allowMultipleOccurrences = true), + CliOption(USE_THROWS_ANNOTATION_OPTION_NAME, + valueDescription = "boolean", + description = "indicates if the Throws annotation is used to generate the propagatedExceptions list", + required = false), ) override fun processOption( @@ -19,6 +31,8 @@ class KmpNativeCoroutinesCommandLineProcessor: CommandLineProcessor { configuration: CompilerConfiguration ) = when (option.optionName) { SUFFIX_OPTION_NAME -> configuration.put(SUFFIX_KEY, value) + PROPAGATED_EXCEPTIONS_OPTION_NAME -> configuration.add(PROPAGATED_EXCEPTIONS_KEY, FqName(value)) + USE_THROWS_ANNOTATION_OPTION_NAME -> configuration.put(USE_THROWS_ANNOTATION_KEY, value.toBooleanStrict()) else -> error("Unexpected config option ${option.optionName}") } } \ No newline at end of file diff --git a/kmp-nativecoroutines-compiler/src/main/kotlin/com/rickclephas/kmp/nativecoroutines/compiler/KmpNativeCoroutinesComponentRegistrar.kt b/kmp-nativecoroutines-compiler/src/main/kotlin/com/rickclephas/kmp/nativecoroutines/compiler/KmpNativeCoroutinesComponentRegistrar.kt index b4b15044..e23b6a4a 100644 --- a/kmp-nativecoroutines-compiler/src/main/kotlin/com/rickclephas/kmp/nativecoroutines/compiler/KmpNativeCoroutinesComponentRegistrar.kt +++ b/kmp-nativecoroutines-compiler/src/main/kotlin/com/rickclephas/kmp/nativecoroutines/compiler/KmpNativeCoroutinesComponentRegistrar.kt @@ -12,8 +12,14 @@ class KmpNativeCoroutinesComponentRegistrar: ComponentRegistrar { override fun registerProjectComponents(project: MockProject, configuration: CompilerConfiguration) { val suffix = configuration.get(SUFFIX_KEY) ?: return val nameGenerator = NameGenerator(suffix) - SyntheticResolveExtension.registerExtension(project, KmpNativeCoroutinesSyntheticResolveExtension(nameGenerator)) - SyntheticResolveExtension.registerExtension(project, KmpNativeCoroutinesSyntheticResolveExtension.RecursiveCallSyntheticResolveExtension()) - IrGenerationExtension.registerExtension(project, KmpNativeCoroutinesIrGenerationExtension(nameGenerator)) + KmpNativeCoroutinesSyntheticResolveExtension(nameGenerator) + .let { SyntheticResolveExtension.registerExtension(project, it) } + KmpNativeCoroutinesSyntheticResolveExtension.RecursiveCallSyntheticResolveExtension() + .let { SyntheticResolveExtension.registerExtension(project, it) } + KmpNativeCoroutinesIrGenerationExtension( + nameGenerator, + configuration.getList(PROPAGATED_EXCEPTIONS_KEY), + configuration.get(USE_THROWS_ANNOTATION_KEY, true) + ).let { IrGenerationExtension.registerExtension(project, it) } } } \ No newline at end of file diff --git a/kmp-nativecoroutines-compiler/src/main/kotlin/com/rickclephas/kmp/nativecoroutines/compiler/KmpNativeCoroutinesIrGenerationExtension.kt b/kmp-nativecoroutines-compiler/src/main/kotlin/com/rickclephas/kmp/nativecoroutines/compiler/KmpNativeCoroutinesIrGenerationExtension.kt index 6c922575..1647e178 100644 --- a/kmp-nativecoroutines-compiler/src/main/kotlin/com/rickclephas/kmp/nativecoroutines/compiler/KmpNativeCoroutinesIrGenerationExtension.kt +++ b/kmp-nativecoroutines-compiler/src/main/kotlin/com/rickclephas/kmp/nativecoroutines/compiler/KmpNativeCoroutinesIrGenerationExtension.kt @@ -4,13 +4,17 @@ import com.rickclephas.kmp.nativecoroutines.compiler.utils.NameGenerator import org.jetbrains.kotlin.backend.common.extensions.IrGenerationExtension import org.jetbrains.kotlin.backend.common.extensions.IrPluginContext import org.jetbrains.kotlin.ir.declarations.* +import org.jetbrains.kotlin.name.FqName internal class KmpNativeCoroutinesIrGenerationExtension( - private val nameGenerator: NameGenerator + private val nameGenerator: NameGenerator, + private val propagatedExceptions: List, + private val useThrowsAnnotation: Boolean ): IrGenerationExtension { override fun generate(moduleFragment: IrModuleFragment, pluginContext: IrPluginContext) { - moduleFragment.accept(KmpNativeCoroutinesIrTransformer(pluginContext, nameGenerator), null) + KmpNativeCoroutinesIrTransformer(pluginContext, nameGenerator, propagatedExceptions, useThrowsAnnotation) + .let { moduleFragment.accept(it, null) } } } diff --git a/kmp-nativecoroutines-compiler/src/main/kotlin/com/rickclephas/kmp/nativecoroutines/compiler/KmpNativeCoroutinesIrTransformer.kt b/kmp-nativecoroutines-compiler/src/main/kotlin/com/rickclephas/kmp/nativecoroutines/compiler/KmpNativeCoroutinesIrTransformer.kt index dfb655b3..ad9eedbe 100644 --- a/kmp-nativecoroutines-compiler/src/main/kotlin/com/rickclephas/kmp/nativecoroutines/compiler/KmpNativeCoroutinesIrTransformer.kt +++ b/kmp-nativecoroutines-compiler/src/main/kotlin/com/rickclephas/kmp/nativecoroutines/compiler/KmpNativeCoroutinesIrTransformer.kt @@ -9,27 +9,29 @@ import org.jetbrains.kotlin.backend.common.ir.passTypeArgumentsFrom import org.jetbrains.kotlin.backend.common.lower.DeclarationIrBuilder import org.jetbrains.kotlin.ir.IrStatement import org.jetbrains.kotlin.ir.builders.* +import org.jetbrains.kotlin.ir.declarations.IrDeclaration import org.jetbrains.kotlin.ir.declarations.IrFunction import org.jetbrains.kotlin.ir.declarations.IrProperty import org.jetbrains.kotlin.ir.declarations.IrSimpleFunction import org.jetbrains.kotlin.ir.expressions.IrBlockBody import org.jetbrains.kotlin.ir.expressions.IrExpression -import org.jetbrains.kotlin.ir.types.classifierOrFail +import org.jetbrains.kotlin.ir.expressions.IrVararg +import org.jetbrains.kotlin.ir.expressions.impl.IrClassReferenceImpl +import org.jetbrains.kotlin.ir.types.* import org.jetbrains.kotlin.ir.types.impl.IrSimpleTypeImpl -import org.jetbrains.kotlin.ir.types.typeOrNull +import org.jetbrains.kotlin.ir.types.impl.makeTypeProjection import org.jetbrains.kotlin.ir.util.* +import org.jetbrains.kotlin.name.FqName +import org.jetbrains.kotlin.types.Variance +import org.jetbrains.kotlin.utils.addToStdlib.ifTrue internal class KmpNativeCoroutinesIrTransformer( private val context: IrPluginContext, - private val nameGenerator: NameGenerator + private val nameGenerator: NameGenerator, + propagatedExceptions: List, + private val useThrowsAnnotation: Boolean ): IrElementTransformerVoidWithContext() { - private val nativeSuspendFunction = context.referenceNativeSuspendFunction() - private val nativeFlowFunction = context.referenceNativeFlowFunction() - private val stateFlowValueProperty = context.referenceStateFlowValueProperty() - private val sharedFlowReplayCacheProperty = context.referenceSharedFlowReplayCacheProperty() - private val listClass = context.referenceListClass() - override fun visitPropertyNew(declaration: IrProperty): IrStatement { if (declaration.isFakeOverride || declaration.getter?.body != null || declaration.setter != null) return super.visitPropertyNew(declaration) @@ -57,6 +59,8 @@ internal class KmpNativeCoroutinesIrTransformer( return super.visitPropertyNew(declaration) } + private val stateFlowValueProperty = context.referenceStateFlowValueProperty() + private fun createNativeValueBody(getter: IrFunction, originalGetter: IrSimpleFunction): IrBlockBody { val originalReturnType = originalGetter.returnType as? IrSimpleTypeImpl ?: throw IllegalStateException("Unsupported return type ${originalGetter.returnType}") @@ -72,6 +76,8 @@ internal class KmpNativeCoroutinesIrTransformer( } } + private val sharedFlowReplayCacheProperty = context.referenceSharedFlowReplayCacheProperty() + private fun createNativeReplayCacheBody(getter: IrFunction, originalGetter: IrSimpleFunction): IrBlockBody { val originalReturnType = originalGetter.returnType as? IrSimpleTypeImpl ?: throw IllegalStateException("Unsupported return type ${originalGetter.returnType}") @@ -79,7 +85,7 @@ internal class KmpNativeCoroutinesIrTransformer( originalGetter.startOffset, originalGetter.endOffset).irBlockBody { val valueType = originalReturnType.getSharedFlowValueTypeOrNull()?.typeOrNull as? IrSimpleTypeImpl ?: throw IllegalStateException("Unsupported StateFlow value type $originalReturnType") - val returnType = IrSimpleTypeImpl(listClass, false, listOf(valueType), emptyList()) + val returnType = context.irBuiltIns.listClass.typeWith(listOf(valueType)) val valueGetter = sharedFlowReplayCacheProperty.owner.getter?.symbol ?: throw IllegalStateException("Couldn't find StateFlow value getter") +irReturn(irCall(valueGetter, returnType).apply { @@ -102,6 +108,9 @@ internal class KmpNativeCoroutinesIrTransformer( return super.visitFunctionNew(declaration) } + private val nativeSuspendFunction = context.referenceNativeSuspendFunction() + private val nativeFlowFunction = context.referenceNativeFlowFunction() + private fun createNativeBody(declaration: IrFunction, originalFunction: IrSimpleFunction): IrBlockBody { val originalReturnType = originalFunction.returnType as? IrSimpleTypeImpl ?: throw IllegalStateException("Unsupported return type ${originalFunction.returnType}") @@ -110,8 +119,9 @@ internal class KmpNativeCoroutinesIrTransformer( // Call original function var returnType = originalReturnType var call: IrExpression = callOriginalFunction(declaration, originalFunction) - // Call nativeCoroutineScope + // Call nativeCoroutineScope and create propagatedExceptions array val nativeCoroutineScope = callNativeCoroutineScope(declaration) + val propagatedExceptions = createPropagatedExceptionsArray(originalFunction) // Convert Flow types to NativeFlow val flowValueType = returnType.getFlowValueTypeOrNull() if (flowValueType != null) { @@ -126,6 +136,7 @@ internal class KmpNativeCoroutinesIrTransformer( call = irCall(nativeFlowFunction, returnType).apply { putTypeArgument(0, valueType) putValueArgument(0, nativeCoroutineScope) + putValueArgument(1, propagatedExceptions) extensionReceiver = call } } @@ -149,7 +160,8 @@ internal class KmpNativeCoroutinesIrTransformer( call = irCall(nativeSuspendFunction, returnType).apply { putTypeArgument(0, lambda.function.returnType) putValueArgument(0, nativeCoroutineScope) - putValueArgument(1, lambda) + putValueArgument(1, propagatedExceptions) + putValueArgument(2, lambda) } } +irReturn(call) @@ -178,4 +190,29 @@ internal class KmpNativeCoroutinesIrTransformer( dispatchReceiver = function.dispatchReceiverParameter?.let { irGet(it) } } } + + private val propagatedExceptionClasses = propagatedExceptions.map { + context.referenceClass(it) ?: throw NoSuchElementException("Couldn't find $it symbol") + } + private val propagatedExceptionsArrayElementType = context.irBuiltIns.kClassClass.typeWithArguments(listOf( + makeTypeProjection(context.irBuiltIns.throwableType, Variance.OUT_VARIANCE) + )) + + private fun IrBuilderWithScope.createPropagatedExceptionsArray(originalDeclaration: IrDeclaration): IrExpression { + // Use the annotations on the declaration whenever possible + val annotation = originalDeclaration.annotations.findNativeCoroutineThrowsAnnotation() + ?: useThrowsAnnotation.ifTrue { originalDeclaration.annotations.findThrowsAnnotation() } + annotation?.getValueArgument(0)?.let { vararg -> + if (vararg !is IrVararg) throw IllegalArgumentException("Unexpected vararg: $vararg") + return irArrayOf(vararg.varargElementType, vararg.elements) + } + // Use the annotations on the parent class whenever possible + originalDeclaration.parentClassOrNull?.let { parentClass -> + return createPropagatedExceptionsArray(parentClass) + } + // Use the propagatedExceptionClasses list + return irArrayOf(propagatedExceptionsArrayElementType, propagatedExceptionClasses.map { + IrClassReferenceImpl(startOffset, endOffset, propagatedExceptionsArrayElementType, it, it.defaultType) + }) + } } \ No newline at end of file diff --git a/kmp-nativecoroutines-compiler/src/main/kotlin/com/rickclephas/kmp/nativecoroutines/compiler/KmpNativeCoroutinesSyntheticResolveExtension.kt b/kmp-nativecoroutines-compiler/src/main/kotlin/com/rickclephas/kmp/nativecoroutines/compiler/KmpNativeCoroutinesSyntheticResolveExtension.kt index 695cb7e0..f9aaee7e 100644 --- a/kmp-nativecoroutines-compiler/src/main/kotlin/com/rickclephas/kmp/nativecoroutines/compiler/KmpNativeCoroutinesSyntheticResolveExtension.kt +++ b/kmp-nativecoroutines-compiler/src/main/kotlin/com/rickclephas/kmp/nativecoroutines/compiler/KmpNativeCoroutinesSyntheticResolveExtension.kt @@ -16,6 +16,8 @@ import org.jetbrains.kotlin.resolve.lazy.descriptors.LazyClassMemberScope import org.jetbrains.kotlin.resolve.scopes.DescriptorKindFilter import org.jetbrains.kotlin.resolve.scopes.MemberScope import org.jetbrains.kotlin.types.KotlinType +import org.jetbrains.kotlin.types.KotlinTypeFactory +import org.jetbrains.kotlin.types.typeUtil.asTypeProjection import java.util.ArrayList internal class KmpNativeCoroutinesSyntheticResolveExtension( @@ -133,7 +135,8 @@ internal class KmpNativeCoroutinesSyntheticResolveExtension( ): PropertyDescriptor { val valueType = coroutinesPropertyDescriptor.getSharedFlowValueTypeOrNull()?.type ?: throw IllegalStateException("Coroutines property doesn't have a value type") - val type = thisDescriptor.module.createListType(valueType) + val type = KotlinTypeFactory.simpleType(thisDescriptor.builtIns.list.defaultType, + arguments = listOf(valueType.asTypeProjection())) return createPropertyDescriptor(thisDescriptor, coroutinesPropertyDescriptor.visibility, name, type, coroutinesPropertyDescriptor.dispatchReceiverParameter, coroutinesPropertyDescriptor.extensionReceiverParameter diff --git a/kmp-nativecoroutines-compiler/src/main/kotlin/com/rickclephas/kmp/nativecoroutines/compiler/utils/IrArrayOf.kt b/kmp-nativecoroutines-compiler/src/main/kotlin/com/rickclephas/kmp/nativecoroutines/compiler/utils/IrArrayOf.kt new file mode 100644 index 00000000..e3e8639c --- /dev/null +++ b/kmp-nativecoroutines-compiler/src/main/kotlin/com/rickclephas/kmp/nativecoroutines/compiler/utils/IrArrayOf.kt @@ -0,0 +1,20 @@ +package com.rickclephas.kmp.nativecoroutines.compiler.utils + +import org.jetbrains.kotlin.ir.builders.IrBuilderWithScope +import org.jetbrains.kotlin.ir.builders.irCall +import org.jetbrains.kotlin.ir.expressions.IrExpression +import org.jetbrains.kotlin.ir.expressions.IrVarargElement +import org.jetbrains.kotlin.ir.expressions.impl.IrVarargImpl +import org.jetbrains.kotlin.ir.types.IrType +import org.jetbrains.kotlin.ir.types.typeWith + +internal fun IrBuilderWithScope.irArrayOf( + elementType: IrType, + elements: List +): IrExpression { + val arrayType = context.irBuiltIns.arrayClass.typeWith(elementType) + val vararg = IrVarargImpl(startOffset, endOffset, arrayType, elementType, elements) + return irCall(context.irBuiltIns.arrayOf, arrayType, listOf(elementType)).apply { + putValueArgument(0, vararg) + } +} \ No newline at end of file diff --git a/kmp-nativecoroutines-compiler/src/main/kotlin/com/rickclephas/kmp/nativecoroutines/compiler/utils/List.kt b/kmp-nativecoroutines-compiler/src/main/kotlin/com/rickclephas/kmp/nativecoroutines/compiler/utils/List.kt deleted file mode 100644 index 80ff21ab..00000000 --- a/kmp-nativecoroutines-compiler/src/main/kotlin/com/rickclephas/kmp/nativecoroutines/compiler/utils/List.kt +++ /dev/null @@ -1,23 +0,0 @@ -package com.rickclephas.kmp.nativecoroutines.compiler.utils - -import org.jetbrains.kotlin.backend.common.extensions.IrPluginContext -import org.jetbrains.kotlin.descriptors.* -import org.jetbrains.kotlin.ir.symbols.IrClassSymbol -import org.jetbrains.kotlin.name.ClassId -import org.jetbrains.kotlin.name.FqName -import org.jetbrains.kotlin.types.KotlinType -import org.jetbrains.kotlin.types.KotlinTypeFactory -import org.jetbrains.kotlin.types.typeUtil.asTypeProjection - -private val listFqName = FqName("kotlin.collections.List") -private val listClassId = ClassId.topLevel(listFqName) - -internal fun ModuleDescriptor.findListClassifier(): ClassifierDescriptor = - findClassifierAcrossModuleDependencies(listClassId) - ?: throw NoSuchElementException("Couldn't find List classifier") - -internal fun ModuleDescriptor.createListType(valueType: KotlinType): KotlinType = - KotlinTypeFactory.simpleType(findListClassifier().defaultType, arguments = listOf(valueType.asTypeProjection())) - -internal fun IrPluginContext.referenceListClass(): IrClassSymbol = - referenceClass(listFqName) ?: throw NoSuchElementException("Couldn't find List symbol") diff --git a/kmp-nativecoroutines-compiler/src/main/kotlin/com/rickclephas/kmp/nativecoroutines/compiler/utils/NativeCoroutineThrows.kt b/kmp-nativecoroutines-compiler/src/main/kotlin/com/rickclephas/kmp/nativecoroutines/compiler/utils/NativeCoroutineThrows.kt new file mode 100644 index 00000000..662dd10e --- /dev/null +++ b/kmp-nativecoroutines-compiler/src/main/kotlin/com/rickclephas/kmp/nativecoroutines/compiler/utils/NativeCoroutineThrows.kt @@ -0,0 +1,10 @@ +package com.rickclephas.kmp.nativecoroutines.compiler.utils + +import org.jetbrains.kotlin.ir.expressions.IrConstructorCall +import org.jetbrains.kotlin.ir.util.findAnnotation +import org.jetbrains.kotlin.name.FqName + +private val nativeCoroutineThrowsFqName = FqName("com.rickclephas.kmp.nativecoroutines.NativeCoroutineThrows") + +internal fun List.findNativeCoroutineThrowsAnnotation() = + findAnnotation(nativeCoroutineThrowsFqName) \ No newline at end of file diff --git a/kmp-nativecoroutines-compiler/src/main/kotlin/com/rickclephas/kmp/nativecoroutines/compiler/utils/Throws.kt b/kmp-nativecoroutines-compiler/src/main/kotlin/com/rickclephas/kmp/nativecoroutines/compiler/utils/Throws.kt new file mode 100644 index 00000000..93a2263c --- /dev/null +++ b/kmp-nativecoroutines-compiler/src/main/kotlin/com/rickclephas/kmp/nativecoroutines/compiler/utils/Throws.kt @@ -0,0 +1,9 @@ +package com.rickclephas.kmp.nativecoroutines.compiler.utils + +import org.jetbrains.kotlin.ir.expressions.IrConstructorCall +import org.jetbrains.kotlin.ir.util.findAnnotation +import org.jetbrains.kotlin.name.FqName + +private val throwsFqName = FqName("kotlin.Throws") + +internal fun List.findThrowsAnnotation() = findAnnotation(throwsFqName) \ No newline at end of file diff --git a/kmp-nativecoroutines-core/src/appleMain/kotlin/com/rickclephas/kmp/nativecoroutines/NSError.kt b/kmp-nativecoroutines-core/src/appleMain/kotlin/com/rickclephas/kmp/nativecoroutines/NSError.kt index e0d9c75d..ad1b3f40 100644 --- a/kmp-nativecoroutines-core/src/appleMain/kotlin/com/rickclephas/kmp/nativecoroutines/NSError.kt +++ b/kmp-nativecoroutines-core/src/appleMain/kotlin/com/rickclephas/kmp/nativecoroutines/NSError.kt @@ -11,10 +11,10 @@ import kotlin.reflect.KClass * * Warning: [Throwable]s that aren't of a [propagatedExceptions] type will terminate the program. * - * @param propagatedExceptions a list of [Throwable] types that should be propagated as [NSError]s. + * @param propagatedExceptions an array of [Throwable] types that should be propagated as [NSError]s. */ internal fun Throwable.asNSError( - propagatedExceptions: List> + propagatedExceptions: Array> ): NSError { freeze() val shouldPropagate = propagatedExceptions.any { it.isInstance(this) } diff --git a/kmp-nativecoroutines-core/src/appleMain/kotlin/com/rickclephas/kmp/nativecoroutines/NativeFlow.kt b/kmp-nativecoroutines-core/src/appleMain/kotlin/com/rickclephas/kmp/nativecoroutines/NativeFlow.kt index 1953eff4..167ff676 100644 --- a/kmp-nativecoroutines-core/src/appleMain/kotlin/com/rickclephas/kmp/nativecoroutines/NativeFlow.kt +++ b/kmp-nativecoroutines-core/src/appleMain/kotlin/com/rickclephas/kmp/nativecoroutines/NativeFlow.kt @@ -21,13 +21,13 @@ typealias NativeFlow = (onItem: NativeCallback, onComplete: NativeCallback * Creates a [NativeFlow] for this [Flow]. * * @param scope the [CoroutineScope] to use for the collection, or `null` to use the [defaultCoroutineScope]. - * @param propagatedExceptions a list of [Throwable] types that should be propagated as [NSError]s. + * @param propagatedExceptions an array of [Throwable] types that should be propagated as [NSError]s. * @receiver the [Flow] to collect. * @see Flow.collect */ fun Flow.asNativeFlow( scope: CoroutineScope? = null, - propagatedExceptions: List> = listOf(CancellationException::class) + propagatedExceptions: Array> = arrayOf(CancellationException::class) ): NativeFlow { val coroutineScope = scope ?: defaultCoroutineScope return (collect@{ onItem: NativeCallback, onComplete: NativeCallback -> diff --git a/kmp-nativecoroutines-core/src/appleMain/kotlin/com/rickclephas/kmp/nativecoroutines/NativeSuspend.kt b/kmp-nativecoroutines-core/src/appleMain/kotlin/com/rickclephas/kmp/nativecoroutines/NativeSuspend.kt index 1e3e1920..0ac9ee1f 100644 --- a/kmp-nativecoroutines-core/src/appleMain/kotlin/com/rickclephas/kmp/nativecoroutines/NativeSuspend.kt +++ b/kmp-nativecoroutines-core/src/appleMain/kotlin/com/rickclephas/kmp/nativecoroutines/NativeSuspend.kt @@ -19,12 +19,12 @@ typealias NativeSuspend = (onResult: NativeCallback, onError: NativeCallba * Creates a [NativeSuspend] for the provided suspend [block]. * * @param scope the [CoroutineScope] to run the [block] in, or `null` to use the [defaultCoroutineScope]. - * @param propagatedExceptions a list of [Throwable] types that should be propagated as [NSError]s. + * @param propagatedExceptions an array of [Throwable] types that should be propagated as [NSError]s. * @param block the suspend block to await. */ fun nativeSuspend( scope: CoroutineScope? = null, - propagatedExceptions: List> = listOf(CancellationException::class), + propagatedExceptions: Array> = arrayOf(CancellationException::class), block: suspend () -> T ): NativeSuspend { val coroutineScope = scope ?: defaultCoroutineScope diff --git a/kmp-nativecoroutines-gradle-plugin/build.gradle.kts b/kmp-nativecoroutines-gradle-plugin/build.gradle.kts index 41b89353..ab46207e 100644 --- a/kmp-nativecoroutines-gradle-plugin/build.gradle.kts +++ b/kmp-nativecoroutines-gradle-plugin/build.gradle.kts @@ -1,7 +1,6 @@ plugins { `java-gradle-plugin` kotlin("jvm") - kotlin("kapt") `kmp-nativecoroutines-publish` id("com.gradle.plugin-publish") version "0.15.0" } @@ -14,6 +13,10 @@ val copyVersionTemplate by tasks.registering(Copy::class) { filteringCharset = "UTF-8" } +kotlin.sourceSets.all { + languageSettings.optIn("kotlin.RequiresOptIn") +} + tasks.compileKotlin { dependsOn(copyVersionTemplate) } diff --git a/kmp-nativecoroutines-gradle-plugin/src/main/kotlin/com/rickclephas/kmp/nativecoroutines/gradle/KmpNativeCoroutinesExtension.kt b/kmp-nativecoroutines-gradle-plugin/src/main/kotlin/com/rickclephas/kmp/nativecoroutines/gradle/KmpNativeCoroutinesExtension.kt index f2431b34..03c4afbe 100644 --- a/kmp-nativecoroutines-gradle-plugin/src/main/kotlin/com/rickclephas/kmp/nativecoroutines/gradle/KmpNativeCoroutinesExtension.kt +++ b/kmp-nativecoroutines-gradle-plugin/src/main/kotlin/com/rickclephas/kmp/nativecoroutines/gradle/KmpNativeCoroutinesExtension.kt @@ -1,5 +1,19 @@ package com.rickclephas.kmp.nativecoroutines.gradle open class KmpNativeCoroutinesExtension { + + /** + * The suffix used to generate the native function and property names. + */ var suffix: String = "Native" + + /** + * The default array of [Throwable] types that should be propagated as `NSError`s. + */ + var propagatedExceptions: Array = arrayOf("kotlin.coroutines.cancellation.CancellationException") + + /** + * Indicates if the [Throws] annotation is used to generate the [propagatedExceptions] list. + */ + var useThrowsAnnotation: Boolean = true } \ No newline at end of file diff --git a/kmp-nativecoroutines-gradle-plugin/src/main/kotlin/com/rickclephas/kmp/nativecoroutines/gradle/KmpNativeCoroutinesPlugin.kt b/kmp-nativecoroutines-gradle-plugin/src/main/kotlin/com/rickclephas/kmp/nativecoroutines/gradle/KmpNativeCoroutinesPlugin.kt index d2444414..0e25e293 100644 --- a/kmp-nativecoroutines-gradle-plugin/src/main/kotlin/com/rickclephas/kmp/nativecoroutines/gradle/KmpNativeCoroutinesPlugin.kt +++ b/kmp-nativecoroutines-gradle-plugin/src/main/kotlin/com/rickclephas/kmp/nativecoroutines/gradle/KmpNativeCoroutinesPlugin.kt @@ -24,12 +24,19 @@ class KmpNativeCoroutinesPlugin: KotlinCompilerPluginSupportPlugin { override fun isApplicable(kotlinCompilation: KotlinCompilation<*>): Boolean = kotlinCompilation.target.let { it is KotlinNativeTarget && it.konanTarget.family.isAppleFamily } + @OptIn(ExperimentalStdlibApi::class) override fun applyToCompilation(kotlinCompilation: KotlinCompilation<*>): Provider> { val project = kotlinCompilation.target.project val extension = project.extensions.findByType(KmpNativeCoroutinesExtension::class.java) ?: KmpNativeCoroutinesExtension() return project.provider { - listOf(SubpluginOption("suffix", extension.suffix)) + buildList { + add(SubpluginOption("suffix", extension.suffix)) + extension.propagatedExceptions.forEach { + add(SubpluginOption("propagatedExceptions", it)) + } + add(SubpluginOption("useThrowsAnnotation", extension.useThrowsAnnotation.toString())) + } } } From 32946d4ac5276233621c13a8652697afc5492262 Mon Sep 17 00:00:00 2001 From: Rick Clephas Date: Sun, 26 Dec 2021 13:49:17 +0100 Subject: [PATCH 03/19] Always propagate CancellationExceptions --- .../kotlin/com/rickclephas/kmp/nativecoroutines/NSError.kt | 5 ++++- .../nativecoroutines/gradle/KmpNativeCoroutinesExtension.kt | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/kmp-nativecoroutines-core/src/appleMain/kotlin/com/rickclephas/kmp/nativecoroutines/NSError.kt b/kmp-nativecoroutines-core/src/appleMain/kotlin/com/rickclephas/kmp/nativecoroutines/NSError.kt index ad1b3f40..b8f3a60b 100644 --- a/kmp-nativecoroutines-core/src/appleMain/kotlin/com/rickclephas/kmp/nativecoroutines/NSError.kt +++ b/kmp-nativecoroutines-core/src/appleMain/kotlin/com/rickclephas/kmp/nativecoroutines/NSError.kt @@ -2,6 +2,7 @@ package com.rickclephas.kmp.nativecoroutines import kotlinx.cinterop.* import platform.Foundation.NSError +import kotlin.coroutines.cancellation.CancellationException import kotlin.native.concurrent.freeze import kotlin.native.internal.GCUnsafeCall import kotlin.reflect.KClass @@ -10,6 +11,7 @@ import kotlin.reflect.KClass * Uses Kotlin Native runtime functions to convert a [Throwable] to a [NSError]. * * Warning: [Throwable]s that aren't of a [propagatedExceptions] type will terminate the program. + * Note: [CancellationException]s are always propagated. * * @param propagatedExceptions an array of [Throwable] types that should be propagated as [NSError]s. */ @@ -17,7 +19,8 @@ internal fun Throwable.asNSError( propagatedExceptions: Array> ): NSError { freeze() - val shouldPropagate = propagatedExceptions.any { it.isInstance(this) } + val shouldPropagate = CancellationException::class.isInstance(this) || + propagatedExceptions.any { it.isInstance(this) } return memScoped { val error = alloc>() val types = when (shouldPropagate) { diff --git a/kmp-nativecoroutines-gradle-plugin/src/main/kotlin/com/rickclephas/kmp/nativecoroutines/gradle/KmpNativeCoroutinesExtension.kt b/kmp-nativecoroutines-gradle-plugin/src/main/kotlin/com/rickclephas/kmp/nativecoroutines/gradle/KmpNativeCoroutinesExtension.kt index 03c4afbe..4772f243 100644 --- a/kmp-nativecoroutines-gradle-plugin/src/main/kotlin/com/rickclephas/kmp/nativecoroutines/gradle/KmpNativeCoroutinesExtension.kt +++ b/kmp-nativecoroutines-gradle-plugin/src/main/kotlin/com/rickclephas/kmp/nativecoroutines/gradle/KmpNativeCoroutinesExtension.kt @@ -10,7 +10,7 @@ open class KmpNativeCoroutinesExtension { /** * The default array of [Throwable] types that should be propagated as `NSError`s. */ - var propagatedExceptions: Array = arrayOf("kotlin.coroutines.cancellation.CancellationException") + var propagatedExceptions: Array = arrayOf() /** * Indicates if the [Throws] annotation is used to generate the [propagatedExceptions] list. From 115e147d152609e3f151c989c5b3b43f89b79230 Mon Sep 17 00:00:00 2001 From: Rick Clephas Date: Sun, 26 Dec 2021 13:49:41 +0100 Subject: [PATCH 04/19] Use custom BestExceptionEver --- .../kmp/nativecoroutines/sample/RandomLettersGenerator.kt | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/sample/shared/src/commonMain/kotlin/com/rickclephas/kmp/nativecoroutines/sample/RandomLettersGenerator.kt b/sample/shared/src/commonMain/kotlin/com/rickclephas/kmp/nativecoroutines/sample/RandomLettersGenerator.kt index 2c1165a0..eab512fb 100644 --- a/sample/shared/src/commonMain/kotlin/com/rickclephas/kmp/nativecoroutines/sample/RandomLettersGenerator.kt +++ b/sample/shared/src/commonMain/kotlin/com/rickclephas/kmp/nativecoroutines/sample/RandomLettersGenerator.kt @@ -1,16 +1,18 @@ package com.rickclephas.kmp.nativecoroutines.sample import kotlinx.coroutines.delay +import kotlin.coroutines.cancellation.CancellationException import kotlin.random.Random import kotlin.time.Duration.Companion.seconds object RandomLettersGenerator { + private class BestExceptionEver: RuntimeException("the best exception ever") + + @Throws(BestExceptionEver::class, CancellationException::class) suspend fun getRandomLetters(throwException: Boolean): String { delay(2.seconds) - if (throwException) { - throw RuntimeException("the best exception ever") - } + if (throwException) throw BestExceptionEver() val chars = mutableListOf() repeat(5) { chars.add(Random.nextInt(65, 91).toChar()) From e97ae38091f3e824fb6b980c0a261a33d0166bca Mon Sep 17 00:00:00 2001 From: Rick Clephas Date: Sun, 26 Dec 2021 14:11:08 +0100 Subject: [PATCH 05/19] Improve NSError tests --- .../kmp/nativecoroutines/NSErrorTests.kt | 29 +++++++++++++++---- .../kmp/nativecoroutines/NativeFlowTests.kt | 2 +- .../nativecoroutines/NativeSuspendTests.kt | 2 +- .../kmp/nativecoroutines/RandomException.kt | 3 +- .../kmp/nativecoroutines/RandomString.kt | 10 +++++++ 5 files changed, 36 insertions(+), 10 deletions(-) create mode 100644 kmp-nativecoroutines-core/src/commonTest/kotlin/com/rickclephas/kmp/nativecoroutines/RandomString.kt diff --git a/kmp-nativecoroutines-core/src/appleTest/kotlin/com/rickclephas/kmp/nativecoroutines/NSErrorTests.kt b/kmp-nativecoroutines-core/src/appleTest/kotlin/com/rickclephas/kmp/nativecoroutines/NSErrorTests.kt index 631a244c..6756eb8b 100644 --- a/kmp-nativecoroutines-core/src/appleTest/kotlin/com/rickclephas/kmp/nativecoroutines/NSErrorTests.kt +++ b/kmp-nativecoroutines-core/src/appleTest/kotlin/com/rickclephas/kmp/nativecoroutines/NSErrorTests.kt @@ -1,8 +1,11 @@ package com.rickclephas.kmp.nativecoroutines -import kotlinx.cinterop.UnsafeNumber import kotlinx.cinterop.convert +import platform.Foundation.NSError +import kotlin.coroutines.cancellation.CancellationException import kotlin.native.concurrent.isFrozen +import kotlin.native.internal.ObjCErrorException +import kotlin.random.Random import kotlin.test.* class NSErrorTests { @@ -11,16 +14,15 @@ class NSErrorTests { fun `ensure frozen`() { val exception = RandomException() assertFalse(exception.isFrozen, "Exception shouldn't be frozen yet") - val nsError = exception.asNSError(listOf(RandomException::class)) + val nsError = exception.asNSError(arrayOf(RandomException::class)) 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`() { val exception = RandomException() - val nsError = exception.asNSError(listOf(RandomException::class)) + val nsError = exception.asNSError(arrayOf(RandomException::class)) assertEquals("KotlinException", nsError.domain, "Incorrect NSError domain") assertEquals(0.convert(), nsError.code, "Incorrect NSError code") } @@ -28,7 +30,7 @@ class NSErrorTests { @Test fun `ensure localizedDescription is set to message`() { val exception = RandomException() - val nsError = exception.asNSError(listOf(RandomException::class)) + val nsError = exception.asNSError(arrayOf(RandomException::class)) assertEquals(exception.message, nsError.localizedDescription, "Localized description isn't set to message") } @@ -36,7 +38,22 @@ class NSErrorTests { @Test fun `ensure exception is part of user info`() { val exception = RandomException() - val nsError = exception.asNSError(listOf(RandomException::class)) + val nsError = exception.asNSError(arrayOf(RandomException::class)) assertSame(exception, nsError.userInfo["KotlinException"], "Exception isn't part of the user info") } + + @Test + fun `ensure CancellationException is always propagated`() { + val exception = CancellationException() + val nsError = exception.asNSError(arrayOf()) + assertSame(exception, nsError.userInfo["KotlinException"], "Exception isn't part of the user info") + } + + @Test + fun `ensure ObjCErrorException is always propagated`() { + val error = NSError.errorWithDomain(Random.nextString(), Random.nextInt().convert(), null) + val exception = ObjCErrorException(Random.nextString(), error) + val nsError = exception.asNSError(arrayOf()) + assertEquals(error, nsError, "NSError isn't equal") + } } \ No newline at end of file diff --git a/kmp-nativecoroutines-core/src/appleTest/kotlin/com/rickclephas/kmp/nativecoroutines/NativeFlowTests.kt b/kmp-nativecoroutines-core/src/appleTest/kotlin/com/rickclephas/kmp/nativecoroutines/NativeFlowTests.kt index 4947c748..bb338a18 100644 --- a/kmp-nativecoroutines-core/src/appleTest/kotlin/com/rickclephas/kmp/nativecoroutines/NativeFlowTests.kt +++ b/kmp-nativecoroutines-core/src/appleTest/kotlin/com/rickclephas/kmp/nativecoroutines/NativeFlowTests.kt @@ -37,7 +37,7 @@ class NativeFlowTests { val exception = RandomException() val flow = flow { throw exception } val job = Job() - val nativeFlow = flow.asNativeFlow(CoroutineScope(job), listOf(RandomException::class)) + val nativeFlow = flow.asNativeFlow(CoroutineScope(job), arrayOf(RandomException::class)) val completionCount = AtomicInt(0) nativeFlow({ _, _ -> }, { error, _ -> assertNotNull(error, "Flow should complete with an error") diff --git a/kmp-nativecoroutines-core/src/appleTest/kotlin/com/rickclephas/kmp/nativecoroutines/NativeSuspendTests.kt b/kmp-nativecoroutines-core/src/appleTest/kotlin/com/rickclephas/kmp/nativecoroutines/NativeSuspendTests.kt index d9232574..772c96db 100644 --- a/kmp-nativecoroutines-core/src/appleTest/kotlin/com/rickclephas/kmp/nativecoroutines/NativeSuspendTests.kt +++ b/kmp-nativecoroutines-core/src/appleTest/kotlin/com/rickclephas/kmp/nativecoroutines/NativeSuspendTests.kt @@ -52,7 +52,7 @@ class NativeSuspendTests { fun `ensure exceptions are received as errors`() = runBlocking { val exception = RandomException() val job = Job() - val nativeSuspend = nativeSuspend(CoroutineScope(job), listOf(RandomException::class)) { + val nativeSuspend = nativeSuspend(CoroutineScope(job), arrayOf(RandomException::class)) { delayAndThrow(100, exception) } val receivedResultCount = AtomicInt(0) diff --git a/kmp-nativecoroutines-core/src/commonTest/kotlin/com/rickclephas/kmp/nativecoroutines/RandomException.kt b/kmp-nativecoroutines-core/src/commonTest/kotlin/com/rickclephas/kmp/nativecoroutines/RandomException.kt index 4e5619c4..2121f935 100644 --- a/kmp-nativecoroutines-core/src/commonTest/kotlin/com/rickclephas/kmp/nativecoroutines/RandomException.kt +++ b/kmp-nativecoroutines-core/src/commonTest/kotlin/com/rickclephas/kmp/nativecoroutines/RandomException.kt @@ -5,6 +5,5 @@ import kotlin.random.Random /** * An exception with a message consisting of 20 random capital letter. */ -internal class RandomException: Exception( - (1..20).map { Random.nextInt(65, 91).toChar() }.joinToString("") +internal class RandomException: Exception(Random.nextString(20) ) \ No newline at end of file diff --git a/kmp-nativecoroutines-core/src/commonTest/kotlin/com/rickclephas/kmp/nativecoroutines/RandomString.kt b/kmp-nativecoroutines-core/src/commonTest/kotlin/com/rickclephas/kmp/nativecoroutines/RandomString.kt new file mode 100644 index 00000000..9b3c6c86 --- /dev/null +++ b/kmp-nativecoroutines-core/src/commonTest/kotlin/com/rickclephas/kmp/nativecoroutines/RandomString.kt @@ -0,0 +1,10 @@ +package com.rickclephas.kmp.nativecoroutines + +import kotlin.random.Random + +/** + * Generates a random string with the specified [length]. + */ +internal fun Random.nextString(length: Int = 10) = (1..length).map { + Random.nextInt(65, 91).toChar() +}.joinToString("") \ No newline at end of file From 118f814c94337e001220db9e6301b8b9443fb767 Mon Sep 17 00:00:00 2001 From: Rick Clephas Date: Sun, 26 Dec 2021 14:45:48 +0100 Subject: [PATCH 06/19] Add compiler tests for exceptions --- .../nativecoroutines/NativeCoroutineThrows.kt | 2 +- .../CompilerIntegrationTests.swift | 40 +++++++++++++++++++ .../sample/tests/CompilerIntegrationTests.kt | 26 ++++++++++++ .../sample/tests/FlowIntegrationTests.kt | 5 +++ .../sample/tests/IntegrationTests.kt | 5 --- .../sample/tests/SuspendIntegrationTests.kt | 5 +++ 6 files changed, 77 insertions(+), 6 deletions(-) diff --git a/kmp-nativecoroutines-annotations/src/commonMain/kotlin/com/rickclephas/kmp/nativecoroutines/NativeCoroutineThrows.kt b/kmp-nativecoroutines-annotations/src/commonMain/kotlin/com/rickclephas/kmp/nativecoroutines/NativeCoroutineThrows.kt index 04c1f38d..505379cf 100644 --- a/kmp-nativecoroutines-annotations/src/commonMain/kotlin/com/rickclephas/kmp/nativecoroutines/NativeCoroutineThrows.kt +++ b/kmp-nativecoroutines-annotations/src/commonMain/kotlin/com/rickclephas/kmp/nativecoroutines/NativeCoroutineThrows.kt @@ -11,7 +11,7 @@ import kotlin.reflect.KClass * * @see Throws */ -@Target(AnnotationTarget.PROPERTY, AnnotationTarget.FUNCTION) +@Target(AnnotationTarget.PROPERTY_GETTER, AnnotationTarget.FUNCTION) @Retention(AnnotationRetention.SOURCE) @MustBeDocumented annotation class NativeCoroutineThrows(vararg val exceptionClasses: KClass) \ No newline at end of file diff --git a/sample/IntegrationTests/CompilerIntegrationTests.swift b/sample/IntegrationTests/CompilerIntegrationTests.swift index 9f8dd6c5..f928ac98 100644 --- a/sample/IntegrationTests/CompilerIntegrationTests.swift +++ b/sample/IntegrationTests/CompilerIntegrationTests.swift @@ -12,6 +12,46 @@ import NativeCoroutinesSampleShared class CompilerIntegrationTests: XCTestCase { private typealias IntegrationTests = NativeCoroutinesSampleShared.CompilerIntegrationTests + private let testExceptionMessage = "com.rickclephas.kmp.nativecoroutines.sample.tests.TestException" + + func testThrowWithThrows() { + let integrationTests = IntegrationTests() + let errorExpectation = expectation(description: "Waiting for error") + _ = integrationTests.throwWithThrowsNative()({ _, unit in unit}, { error, unit in + let error = error as NSError + let exception = error.userInfo["KotlinException"] as! KotlinThrowable + XCTAssertEqual(exception.message, self.testExceptionMessage, "Received incorrect exception") + errorExpectation.fulfill() + return unit + }) + wait(for: [errorExpectation], timeout: 2) + } + + func testThrowWithNativeCoroutineThrows() { + let integrationTests = IntegrationTests() + let errorExpectation = expectation(description: "Waiting for error") + _ = integrationTests.throwWithNativeCoroutineThrowsNative()({ _, unit in unit}, { error, unit in + let error = error as NSError + let exception = error.userInfo["KotlinException"] as! KotlinThrowable + XCTAssertEqual(exception.message, self.testExceptionMessage, "Received incorrect exception") + errorExpectation.fulfill() + return unit + }) + wait(for: [errorExpectation], timeout: 2) + } + + func testFlowThrow() { + let integrationTests = IntegrationTests() + let errorExpectation = expectation(description: "Waiting for error") + _ = integrationTests.flowThrowNative({ _, unit in unit}, { error, unit in + let error = error! as NSError + let exception = error.userInfo["KotlinException"] as! KotlinThrowable + XCTAssertEqual(exception.message, self.testExceptionMessage, "Received incorrect exception") + errorExpectation.fulfill() + return unit + }) + wait(for: [errorExpectation], timeout: 2) + } func testReturnGenericClassValue() { let integrationTests = IntegrationTests() diff --git a/sample/shared/src/commonMain/kotlin/com/rickclephas/kmp/nativecoroutines/sample/tests/CompilerIntegrationTests.kt b/sample/shared/src/commonMain/kotlin/com/rickclephas/kmp/nativecoroutines/sample/tests/CompilerIntegrationTests.kt index 9cdbb377..e773ae43 100644 --- a/sample/shared/src/commonMain/kotlin/com/rickclephas/kmp/nativecoroutines/sample/tests/CompilerIntegrationTests.kt +++ b/sample/shared/src/commonMain/kotlin/com/rickclephas/kmp/nativecoroutines/sample/tests/CompilerIntegrationTests.kt @@ -1,9 +1,31 @@ package com.rickclephas.kmp.nativecoroutines.sample.tests import com.rickclephas.kmp.nativecoroutines.NativeCoroutinesIgnore +import com.rickclephas.kmp.nativecoroutines.NativeCoroutineThrows +import com.rickclephas.kmp.nativecoroutines.sample.utils.freeze +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flow +import kotlin.coroutines.cancellation.CancellationException class CompilerIntegrationTests: IntegrationTests() { + private class TestException: Exception("com.rickclephas.kmp.nativecoroutines.sample.tests.TestException") + + @Throws(TestException::class, CancellationException::class) + suspend fun throwWithThrows() { + throw TestException() + } + + @NativeCoroutineThrows(TestException::class) + suspend fun throwWithNativeCoroutineThrows() { + throw TestException() + } + + @get:NativeCoroutineThrows(TestException::class) + val flowThrow: Flow = flow { + throw TestException() + } + suspend fun returnGenericClassValue(value: V): V { return value } @@ -32,4 +54,8 @@ class CompilerIntegrationTests: IntegrationTests() { suspend fun returnIgnoredValue(value: Int): Int { return value } + + init { + freeze() + } } \ No newline at end of file diff --git a/sample/shared/src/commonMain/kotlin/com/rickclephas/kmp/nativecoroutines/sample/tests/FlowIntegrationTests.kt b/sample/shared/src/commonMain/kotlin/com/rickclephas/kmp/nativecoroutines/sample/tests/FlowIntegrationTests.kt index 86a475c9..04677632 100644 --- a/sample/shared/src/commonMain/kotlin/com/rickclephas/kmp/nativecoroutines/sample/tests/FlowIntegrationTests.kt +++ b/sample/shared/src/commonMain/kotlin/com/rickclephas/kmp/nativecoroutines/sample/tests/FlowIntegrationTests.kt @@ -1,5 +1,6 @@ package com.rickclephas.kmp.nativecoroutines.sample.tests +import com.rickclephas.kmp.nativecoroutines.sample.utils.freeze import kotlinx.coroutines.delay import kotlinx.coroutines.flow.flow @@ -42,4 +43,8 @@ class FlowIntegrationTests: IntegrationTests() { emit(it) } } + + init { + freeze() + } } \ No newline at end of file diff --git a/sample/shared/src/commonMain/kotlin/com/rickclephas/kmp/nativecoroutines/sample/tests/IntegrationTests.kt b/sample/shared/src/commonMain/kotlin/com/rickclephas/kmp/nativecoroutines/sample/tests/IntegrationTests.kt index b71fb341..48c40695 100644 --- a/sample/shared/src/commonMain/kotlin/com/rickclephas/kmp/nativecoroutines/sample/tests/IntegrationTests.kt +++ b/sample/shared/src/commonMain/kotlin/com/rickclephas/kmp/nativecoroutines/sample/tests/IntegrationTests.kt @@ -1,7 +1,6 @@ package com.rickclephas.kmp.nativecoroutines.sample.tests import com.rickclephas.kmp.nativecoroutines.NativeCoroutineScope -import com.rickclephas.kmp.nativecoroutines.sample.utils.freeze import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob @@ -17,8 +16,4 @@ abstract class IntegrationTests { val uncompletedJobCount: Int get() = job.children.count { !it.isCompleted } - - init { - freeze() - } } \ No newline at end of file diff --git a/sample/shared/src/commonMain/kotlin/com/rickclephas/kmp/nativecoroutines/sample/tests/SuspendIntegrationTests.kt b/sample/shared/src/commonMain/kotlin/com/rickclephas/kmp/nativecoroutines/sample/tests/SuspendIntegrationTests.kt index 6cb25cb0..140eae40 100644 --- a/sample/shared/src/commonMain/kotlin/com/rickclephas/kmp/nativecoroutines/sample/tests/SuspendIntegrationTests.kt +++ b/sample/shared/src/commonMain/kotlin/com/rickclephas/kmp/nativecoroutines/sample/tests/SuspendIntegrationTests.kt @@ -1,5 +1,6 @@ package com.rickclephas.kmp.nativecoroutines.sample.tests +import com.rickclephas.kmp.nativecoroutines.sample.utils.freeze import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flow @@ -40,4 +41,8 @@ class SuspendIntegrationTests: IntegrationTests() { } } } + + init { + freeze() + } } From c22943c6cd0a5480bfbba297955cd3ac1d55d252 Mon Sep 17 00:00:00 2001 From: Rick Clephas Date: Sun, 26 Dec 2021 14:55:29 +0100 Subject: [PATCH 07/19] Add compiler tests for Flow value and replayCache --- .../IntegrationTests/CompilerIntegrationTests.swift | 12 ++++++++++++ .../sample/tests/CompilerIntegrationTests.kt | 10 ++++++++-- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/sample/IntegrationTests/CompilerIntegrationTests.swift b/sample/IntegrationTests/CompilerIntegrationTests.swift index f928ac98..3c80ff51 100644 --- a/sample/IntegrationTests/CompilerIntegrationTests.swift +++ b/sample/IntegrationTests/CompilerIntegrationTests.swift @@ -53,6 +53,18 @@ class CompilerIntegrationTests: XCTestCase { wait(for: [errorExpectation], timeout: 2) } + func testStateFlowValue() { + let integrationTests = IntegrationTests() + let value = integrationTests.stateFlowNativeValue + XCTAssertEqual(value, 1, "Received inccorect value") + } + + func testSharedFlowReplayCache() { + let integrationTests = IntegrationTests() + let replayCache = integrationTests.sharedFlowNativeReplayCache + XCTAssertEqual(replayCache, [1, 2], "Received inccorect value") + } + func testReturnGenericClassValue() { let integrationTests = IntegrationTests() let valueExpectation = expectation(description: "Waiting for value") diff --git a/sample/shared/src/commonMain/kotlin/com/rickclephas/kmp/nativecoroutines/sample/tests/CompilerIntegrationTests.kt b/sample/shared/src/commonMain/kotlin/com/rickclephas/kmp/nativecoroutines/sample/tests/CompilerIntegrationTests.kt index e773ae43..3d5fb503 100644 --- a/sample/shared/src/commonMain/kotlin/com/rickclephas/kmp/nativecoroutines/sample/tests/CompilerIntegrationTests.kt +++ b/sample/shared/src/commonMain/kotlin/com/rickclephas/kmp/nativecoroutines/sample/tests/CompilerIntegrationTests.kt @@ -3,8 +3,7 @@ package com.rickclephas.kmp.nativecoroutines.sample.tests import com.rickclephas.kmp.nativecoroutines.NativeCoroutinesIgnore import com.rickclephas.kmp.nativecoroutines.NativeCoroutineThrows import com.rickclephas.kmp.nativecoroutines.sample.utils.freeze -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.* import kotlin.coroutines.cancellation.CancellationException class CompilerIntegrationTests: IntegrationTests() { @@ -26,6 +25,13 @@ class CompilerIntegrationTests: IntegrationTests() { throw TestException() } + val stateFlow: StateFlow = MutableStateFlow(1) + + val sharedFlow: SharedFlow = MutableSharedFlow(2).apply { + tryEmit(1) + tryEmit(2) + } + suspend fun returnGenericClassValue(value: V): V { return value } From 2218d3d96b12fea1ca11667dd0ba9220a5d4645d Mon Sep 17 00:00:00 2001 From: Rick Clephas Date: Sun, 26 Dec 2021 15:08:36 +0100 Subject: [PATCH 08/19] Remove unnecessary default propagatedException --- .../kotlin/com/rickclephas/kmp/nativecoroutines/NativeFlow.kt | 2 +- .../com/rickclephas/kmp/nativecoroutines/NativeSuspend.kt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/kmp-nativecoroutines-core/src/appleMain/kotlin/com/rickclephas/kmp/nativecoroutines/NativeFlow.kt b/kmp-nativecoroutines-core/src/appleMain/kotlin/com/rickclephas/kmp/nativecoroutines/NativeFlow.kt index 167ff676..16ee8d41 100644 --- a/kmp-nativecoroutines-core/src/appleMain/kotlin/com/rickclephas/kmp/nativecoroutines/NativeFlow.kt +++ b/kmp-nativecoroutines-core/src/appleMain/kotlin/com/rickclephas/kmp/nativecoroutines/NativeFlow.kt @@ -27,7 +27,7 @@ typealias NativeFlow = (onItem: NativeCallback, onComplete: NativeCallback */ fun Flow.asNativeFlow( scope: CoroutineScope? = null, - propagatedExceptions: Array> = arrayOf(CancellationException::class) + propagatedExceptions: Array> = arrayOf() ): NativeFlow { val coroutineScope = scope ?: defaultCoroutineScope return (collect@{ onItem: NativeCallback, onComplete: NativeCallback -> diff --git a/kmp-nativecoroutines-core/src/appleMain/kotlin/com/rickclephas/kmp/nativecoroutines/NativeSuspend.kt b/kmp-nativecoroutines-core/src/appleMain/kotlin/com/rickclephas/kmp/nativecoroutines/NativeSuspend.kt index 0ac9ee1f..2cd1d625 100644 --- a/kmp-nativecoroutines-core/src/appleMain/kotlin/com/rickclephas/kmp/nativecoroutines/NativeSuspend.kt +++ b/kmp-nativecoroutines-core/src/appleMain/kotlin/com/rickclephas/kmp/nativecoroutines/NativeSuspend.kt @@ -24,7 +24,7 @@ typealias NativeSuspend = (onResult: NativeCallback, onError: NativeCallba */ fun nativeSuspend( scope: CoroutineScope? = null, - propagatedExceptions: Array> = arrayOf(CancellationException::class), + propagatedExceptions: Array> = arrayOf(), block: suspend () -> T ): NativeSuspend { val coroutineScope = scope ?: defaultCoroutineScope From 64b1fb8a4141e4ae0a224c84a57961fbbf45f35a Mon Sep 17 00:00:00 2001 From: Rick Clephas Date: Sun, 26 Dec 2021 16:10:38 +0100 Subject: [PATCH 09/19] Support NativeCoroutineThrows annotation on classes --- .../nativecoroutines/NativeCoroutineThrows.kt | 2 +- .../KmpNativeCoroutinesIrTransformer.kt | 34 +++++++++---------- .../CompilerIntegrationTests.swift | 30 +++++++++++++++- sample/shared/build.gradle.kts | 4 +++ .../sample/tests/CompilerIntegrationTests.kt | 14 ++++++-- .../sample/utils/Exceptions.kt | 7 ++++ 6 files changed, 70 insertions(+), 21 deletions(-) create mode 100644 sample/shared/src/commonMain/kotlin/com/rickclephas/kmp/nativecoroutines/sample/utils/Exceptions.kt diff --git a/kmp-nativecoroutines-annotations/src/commonMain/kotlin/com/rickclephas/kmp/nativecoroutines/NativeCoroutineThrows.kt b/kmp-nativecoroutines-annotations/src/commonMain/kotlin/com/rickclephas/kmp/nativecoroutines/NativeCoroutineThrows.kt index 505379cf..ce7c9ddf 100644 --- a/kmp-nativecoroutines-annotations/src/commonMain/kotlin/com/rickclephas/kmp/nativecoroutines/NativeCoroutineThrows.kt +++ b/kmp-nativecoroutines-annotations/src/commonMain/kotlin/com/rickclephas/kmp/nativecoroutines/NativeCoroutineThrows.kt @@ -11,7 +11,7 @@ import kotlin.reflect.KClass * * @see Throws */ -@Target(AnnotationTarget.PROPERTY_GETTER, AnnotationTarget.FUNCTION) +@Target(AnnotationTarget.PROPERTY_GETTER, AnnotationTarget.FUNCTION, AnnotationTarget.CLASS) @Retention(AnnotationRetention.SOURCE) @MustBeDocumented annotation class NativeCoroutineThrows(vararg val exceptionClasses: KClass) \ No newline at end of file diff --git a/kmp-nativecoroutines-compiler/src/main/kotlin/com/rickclephas/kmp/nativecoroutines/compiler/KmpNativeCoroutinesIrTransformer.kt b/kmp-nativecoroutines-compiler/src/main/kotlin/com/rickclephas/kmp/nativecoroutines/compiler/KmpNativeCoroutinesIrTransformer.kt index ad9eedbe..201b0f26 100644 --- a/kmp-nativecoroutines-compiler/src/main/kotlin/com/rickclephas/kmp/nativecoroutines/compiler/KmpNativeCoroutinesIrTransformer.kt +++ b/kmp-nativecoroutines-compiler/src/main/kotlin/com/rickclephas/kmp/nativecoroutines/compiler/KmpNativeCoroutinesIrTransformer.kt @@ -13,9 +13,7 @@ import org.jetbrains.kotlin.ir.declarations.IrDeclaration import org.jetbrains.kotlin.ir.declarations.IrFunction import org.jetbrains.kotlin.ir.declarations.IrProperty import org.jetbrains.kotlin.ir.declarations.IrSimpleFunction -import org.jetbrains.kotlin.ir.expressions.IrBlockBody -import org.jetbrains.kotlin.ir.expressions.IrExpression -import org.jetbrains.kotlin.ir.expressions.IrVararg +import org.jetbrains.kotlin.ir.expressions.* import org.jetbrains.kotlin.ir.expressions.impl.IrClassReferenceImpl import org.jetbrains.kotlin.ir.types.* import org.jetbrains.kotlin.ir.types.impl.IrSimpleTypeImpl @@ -199,20 +197,22 @@ internal class KmpNativeCoroutinesIrTransformer( )) private fun IrBuilderWithScope.createPropagatedExceptionsArray(originalDeclaration: IrDeclaration): IrExpression { - // Use the annotations on the declaration whenever possible - val annotation = originalDeclaration.annotations.findNativeCoroutineThrowsAnnotation() - ?: useThrowsAnnotation.ifTrue { originalDeclaration.annotations.findThrowsAnnotation() } - annotation?.getValueArgument(0)?.let { vararg -> - if (vararg !is IrVararg) throw IllegalArgumentException("Unexpected vararg: $vararg") - return irArrayOf(vararg.varargElementType, vararg.elements) - } - // Use the annotations on the parent class whenever possible - originalDeclaration.parentClassOrNull?.let { parentClass -> - return createPropagatedExceptionsArray(parentClass) + // Find the annotation on the declaration (or a parent class) + fun IrDeclaration.getAnnotation(): IrConstructorCall? = + annotations.findNativeCoroutineThrowsAnnotation() ?: + useThrowsAnnotation.ifTrue { originalDeclaration.annotations.findThrowsAnnotation() } ?: + parentClassOrNull?.getAnnotation() + val annotation = originalDeclaration.getAnnotation() + // Combine the propagatedExceptionClasses list with the classes from the annotation + val propagatedExceptions = buildList { + propagatedExceptionClasses.forEach { + add(IrClassReferenceImpl(startOffset, endOffset, propagatedExceptionsArrayElementType, it, it.defaultType)) + } + annotation?.getValueArgument(0)?.let { vararg -> + if (vararg !is IrVararg) throw IllegalArgumentException("Unexpected vararg: $vararg") + addAll(vararg.elements) + } } - // Use the propagatedExceptionClasses list - return irArrayOf(propagatedExceptionsArrayElementType, propagatedExceptionClasses.map { - IrClassReferenceImpl(startOffset, endOffset, propagatedExceptionsArrayElementType, it, it.defaultType) - }) + return irArrayOf(propagatedExceptionsArrayElementType, propagatedExceptions) } } \ No newline at end of file diff --git a/sample/IntegrationTests/CompilerIntegrationTests.swift b/sample/IntegrationTests/CompilerIntegrationTests.swift index 3c80ff51..664cab58 100644 --- a/sample/IntegrationTests/CompilerIntegrationTests.swift +++ b/sample/IntegrationTests/CompilerIntegrationTests.swift @@ -12,7 +12,9 @@ import NativeCoroutinesSampleShared class CompilerIntegrationTests: XCTestCase { private typealias IntegrationTests = NativeCoroutinesSampleShared.CompilerIntegrationTests - private let testExceptionMessage = "com.rickclephas.kmp.nativecoroutines.sample.tests.TestException" + private let testExceptionMessage = "com.rickclephas.kmp.nativecoroutines.sample.utils.TestException" + private let classTestExceptionMessage = "com.rickclephas.kmp.nativecoroutines.sample.utils.ClassTestException" + private let moduleTestExceptionMessage = "com.rickclephas.kmp.nativecoroutines.sample.utils.ModuleTestException" func testThrowWithThrows() { let integrationTests = IntegrationTests() @@ -40,6 +42,32 @@ class CompilerIntegrationTests: XCTestCase { wait(for: [errorExpectation], timeout: 2) } + func testThrowWithNativeCoroutineThrowsOnClass() { + let integrationTests = IntegrationTests() + let errorExpectation = expectation(description: "Waiting for error") + _ = integrationTests.throwWithNativeCoroutineThrowsOnClassNative()({ _, unit in unit}, { error, unit in + let error = error as NSError + let exception = error.userInfo["KotlinException"] as! KotlinThrowable + XCTAssertEqual(exception.message, self.classTestExceptionMessage, "Received incorrect exception") + errorExpectation.fulfill() + return unit + }) + wait(for: [errorExpectation], timeout: 2) + } + + func testThrowWithPropagatedExceptionInModule() { + let integrationTests = IntegrationTests() + let errorExpectation = expectation(description: "Waiting for error") + _ = integrationTests.throwWithPropagatedExceptionInModuleNative()({ _, unit in unit}, { error, unit in + let error = error as NSError + let exception = error.userInfo["KotlinException"] as! KotlinThrowable + XCTAssertEqual(exception.message, self.moduleTestExceptionMessage, "Received incorrect exception") + errorExpectation.fulfill() + return unit + }) + wait(for: [errorExpectation], timeout: 2) + } + func testFlowThrow() { let integrationTests = IntegrationTests() let errorExpectation = expectation(description: "Waiting for error") diff --git a/sample/shared/build.gradle.kts b/sample/shared/build.gradle.kts index 77b28776..05216a79 100644 --- a/sample/shared/build.gradle.kts +++ b/sample/shared/build.gradle.kts @@ -72,4 +72,8 @@ afterEvaluate { tasks.withType(org.jetbrains.kotlin.gradle.tasks.FatFrameworkTask::class.java).forEach { it.baseName = "NativeCoroutinesSampleShared" } +} + +nativeCoroutines { + propagatedExceptions = arrayOf("com.rickclephas.kmp.nativecoroutines.sample.utils.ModuleTestException") } \ No newline at end of file diff --git a/sample/shared/src/commonMain/kotlin/com/rickclephas/kmp/nativecoroutines/sample/tests/CompilerIntegrationTests.kt b/sample/shared/src/commonMain/kotlin/com/rickclephas/kmp/nativecoroutines/sample/tests/CompilerIntegrationTests.kt index 3d5fb503..74e43456 100644 --- a/sample/shared/src/commonMain/kotlin/com/rickclephas/kmp/nativecoroutines/sample/tests/CompilerIntegrationTests.kt +++ b/sample/shared/src/commonMain/kotlin/com/rickclephas/kmp/nativecoroutines/sample/tests/CompilerIntegrationTests.kt @@ -2,14 +2,16 @@ package com.rickclephas.kmp.nativecoroutines.sample.tests import com.rickclephas.kmp.nativecoroutines.NativeCoroutinesIgnore import com.rickclephas.kmp.nativecoroutines.NativeCoroutineThrows +import com.rickclephas.kmp.nativecoroutines.sample.utils.ClassTestException +import com.rickclephas.kmp.nativecoroutines.sample.utils.ModuleTestException +import com.rickclephas.kmp.nativecoroutines.sample.utils.TestException import com.rickclephas.kmp.nativecoroutines.sample.utils.freeze import kotlinx.coroutines.flow.* import kotlin.coroutines.cancellation.CancellationException +@NativeCoroutineThrows(ClassTestException::class) class CompilerIntegrationTests: IntegrationTests() { - private class TestException: Exception("com.rickclephas.kmp.nativecoroutines.sample.tests.TestException") - @Throws(TestException::class, CancellationException::class) suspend fun throwWithThrows() { throw TestException() @@ -20,6 +22,14 @@ class CompilerIntegrationTests: IntegrationTests() { throw TestException() } + suspend fun throwWithNativeCoroutineThrowsOnClass() { + throw ClassTestException() + } + + suspend fun throwWithPropagatedExceptionInModule() { + throw ModuleTestException() + } + @get:NativeCoroutineThrows(TestException::class) val flowThrow: Flow = flow { throw TestException() diff --git a/sample/shared/src/commonMain/kotlin/com/rickclephas/kmp/nativecoroutines/sample/utils/Exceptions.kt b/sample/shared/src/commonMain/kotlin/com/rickclephas/kmp/nativecoroutines/sample/utils/Exceptions.kt new file mode 100644 index 00000000..6746d5ab --- /dev/null +++ b/sample/shared/src/commonMain/kotlin/com/rickclephas/kmp/nativecoroutines/sample/utils/Exceptions.kt @@ -0,0 +1,7 @@ +package com.rickclephas.kmp.nativecoroutines.sample.utils + +class TestException: Exception("com.rickclephas.kmp.nativecoroutines.sample.utils.TestException") + +class ClassTestException: Exception("com.rickclephas.kmp.nativecoroutines.sample.utils.ClassTestException") + +class ModuleTestException: Exception("com.rickclephas.kmp.nativecoroutines.sample.utils.ModuleTestException") From 0cb1ba539a1f0d0a5a157763262a2cfba1983808 Mon Sep 17 00:00:00 2001 From: Rick Clephas Date: Sun, 26 Dec 2021 16:54:25 +0100 Subject: [PATCH 10/19] Update README with exception propagation info --- README.md | 54 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/README.md b/README.md index d2ff1d2b..e6f4db22 100644 --- a/README.md +++ b/README.md @@ -144,6 +144,60 @@ class RandomLettersGenerator { The plugin is currently unable to generate native versions for global properties and functions. In such cases you have to manually create the native versions in your Kotlin native code. +#### Exception propagation + +> Note: for more information about ObjC interop and exceptions please take a look at the +> [Kotlin docs](https://kotlinlang.org/docs/native-objc-interop.html#errors-and-exceptions). + +KMP-NativeCoroutines uses the Kotlin Native runtime to propagate `Exception`s as `NSError`s to Swift/ObjC. +This means that by default only `CancellationException`s are propagated. + +To propagate other exceptions you should mark your functions with the `Throws` annotation. +E.g. use the following to propagate all types of `Throwable`s: +```kotlin +@Throws(Throwable::class) +suspend fun throwingSuspendFunction() { } +``` + +> **Note:** to ignore the `Throws` annotation set the `useThrowsAnnotation` config option to `false`. + +You can also use the `NativeCoroutineThrows` annotation: +```kotlin +@NativeCoroutineThrows(Throwable::class) +suspend fun throwingSuspendFunction() { } + +@get:NativeCoroutineThrows(Throwable::class) +val throwingFlow: Flow = flow { } +``` + +> **Note:** if both `Throws` and `NativeCoroutineThrows` are specified +> only the `NativeCoroutineThrows` one will be used by KMP-NativeCoroutines. + +To reduce code duplication you can also use the `NativeCoroutineThrows` annotation on a class. +```kotlin +@NativeCoroutineThrows(MyException::class) +class ExceptionThrower { + suspend fun throwMyException() { + // This exception will be propagated to Swift/ObjC + throw MyException() + } + + @Throws(MyOtherException::class) + suspend fun throwMyException() { + // Note that annotations on the function or property have a higher precedence. + // This means that the following exception will terminate the program. + throw MyException() + } +} +``` + +or you could specify the exceptions in your `build.gradle.kts`: +```kotlin +nativeCoroutines { + propagatedExceptions = arrayOf("my.company.MyException") +} +``` + #### Custom suffix If you don't like the naming of these generated properties/functions, you can easily change the suffix. From 3c6d768a86664911441a00839a07a53b87e28078 Mon Sep 17 00:00:00 2001 From: Rick Clephas Date: Sun, 26 Dec 2021 21:39:56 +0100 Subject: [PATCH 11/19] Fix tests --- .../sample/tests/FlowIntegrationTests.kt | 18 +++++++++++++----- .../sample/tests/SuspendIntegrationTests.kt | 3 +++ 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/sample/shared/src/commonMain/kotlin/com/rickclephas/kmp/nativecoroutines/sample/tests/FlowIntegrationTests.kt b/sample/shared/src/commonMain/kotlin/com/rickclephas/kmp/nativecoroutines/sample/tests/FlowIntegrationTests.kt index 04677632..d1e32eba 100644 --- a/sample/shared/src/commonMain/kotlin/com/rickclephas/kmp/nativecoroutines/sample/tests/FlowIntegrationTests.kt +++ b/sample/shared/src/commonMain/kotlin/com/rickclephas/kmp/nativecoroutines/sample/tests/FlowIntegrationTests.kt @@ -1,26 +1,31 @@ package com.rickclephas.kmp.nativecoroutines.sample.tests +import com.rickclephas.kmp.nativecoroutines.NativeCoroutineThrows import com.rickclephas.kmp.nativecoroutines.sample.utils.freeze import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flow class FlowIntegrationTests: IntegrationTests() { - fun getFlow(count: Int, delay: Long) = flow { + fun getFlow(count: Int, delay: Long): Flow = flow { repeat(count) { delay(delay) emit(it) } } - fun getFlowWithNull(count: Int, nullIndex: Int, delay: Long) = flow { + fun getFlowWithNull(count: Int, nullIndex: Int, delay: Long): Flow = flow { repeat(count) { delay(delay) emit(if (it == nullIndex) null else it) } } - fun getFlowWithException(count: Int, exceptionIndex: Int, message: String, delay: Long) = flow { +// TODO: Using Throws will fail the build with Kotlin_ObjCExport_RethrowExceptionAsNSError already exists error +// @Throws(Exception::class) + @NativeCoroutineThrows(Exception::class) + fun getFlowWithException(count: Int, exceptionIndex: Int, message: String, delay: Long): Flow = flow { repeat(count) { delay(delay) if (it == exceptionIndex) throw Exception(message) @@ -28,7 +33,10 @@ class FlowIntegrationTests: IntegrationTests() { } } - fun getFlowWithError(count: Int, errorIndex: Int, message: String, delay: Long) = flow { +// TODO: Using Throws will fail the build with Kotlin_ObjCExport_RethrowExceptionAsNSError already exists error +// @Throws(Error::class) + @NativeCoroutineThrows(Error::class) + fun getFlowWithError(count: Int, errorIndex: Int, message: String, delay: Long): Flow = flow { repeat(count) { delay(delay) if (it == errorIndex) throw Error(message) @@ -36,7 +44,7 @@ class FlowIntegrationTests: IntegrationTests() { } } - fun getFlowWithCallback(count: Int, callbackIndex: Int, delay: Long, callback: () -> Unit) = flow { + fun getFlowWithCallback(count: Int, callbackIndex: Int, delay: Long, callback: () -> Unit): Flow = flow { repeat(count) { delay(delay) if (it == callbackIndex) callback() diff --git a/sample/shared/src/commonMain/kotlin/com/rickclephas/kmp/nativecoroutines/sample/tests/SuspendIntegrationTests.kt b/sample/shared/src/commonMain/kotlin/com/rickclephas/kmp/nativecoroutines/sample/tests/SuspendIntegrationTests.kt index 140eae40..6deffbf9 100644 --- a/sample/shared/src/commonMain/kotlin/com/rickclephas/kmp/nativecoroutines/sample/tests/SuspendIntegrationTests.kt +++ b/sample/shared/src/commonMain/kotlin/com/rickclephas/kmp/nativecoroutines/sample/tests/SuspendIntegrationTests.kt @@ -4,6 +4,7 @@ import com.rickclephas.kmp.nativecoroutines.sample.utils.freeze import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flow +import kotlin.coroutines.cancellation.CancellationException class SuspendIntegrationTests: IntegrationTests() { @@ -17,11 +18,13 @@ class SuspendIntegrationTests: IntegrationTests() { return null } + @Throws(Exception::class) suspend fun throwException(message: String, delay: Long): Int { delay(delay) throw Exception(message) } + @Throws(Error::class, CancellationException::class) suspend fun throwError(message: String, delay: Long): Int { delay(delay) throw Error(message) From 7644c0b35190769e1b1252d5495dc8efaef3439e Mon Sep 17 00:00:00 2001 From: Rick Clephas Date: Sat, 19 Feb 2022 15:17:59 +0100 Subject: [PATCH 12/19] Update asNativeError function with propagatedExceptions --- .../rickclephas/kmp/nativecoroutines/NativeErrorApple.kt | 7 +++---- .../com/rickclephas/kmp/nativecoroutines/NativeError.kt | 8 +++++++- .../com/rickclephas/kmp/nativecoroutines/NativeFlow.kt | 6 +++--- .../com/rickclephas/kmp/nativecoroutines/NativeSuspend.kt | 6 +++--- 4 files changed, 16 insertions(+), 11 deletions(-) diff --git a/kmp-nativecoroutines-core/src/appleMain/kotlin/com/rickclephas/kmp/nativecoroutines/NativeErrorApple.kt b/kmp-nativecoroutines-core/src/appleMain/kotlin/com/rickclephas/kmp/nativecoroutines/NativeErrorApple.kt index d5f3949b..a4545aa7 100644 --- a/kmp-nativecoroutines-core/src/appleMain/kotlin/com/rickclephas/kmp/nativecoroutines/NativeErrorApple.kt +++ b/kmp-nativecoroutines-core/src/appleMain/kotlin/com/rickclephas/kmp/nativecoroutines/NativeErrorApple.kt @@ -17,7 +17,7 @@ actual typealias NativeError = NSError * * @param propagatedExceptions an array of [Throwable] types that should be propagated as [NSError]s. */ -internal fun Throwable.asNSError( +internal actual fun Throwable.asNativeError( propagatedExceptions: Array> ): NSError { freeze() @@ -27,12 +27,12 @@ internal fun Throwable.asNSError( val error = alloc>() val types = when (shouldPropagate) { true -> allocArray>(2).apply { - val typeInfo = getTypeInfo(this@asNSError) + val typeInfo = getTypeInfo(this@asNativeError) set(0, interpretCPointer(typeInfo)) } false -> allocArray(1) } - rethrowExceptionAsNSError(this@asNSError, error.ptr, types) + rethrowExceptionAsNSError(this@asNativeError, error.ptr, types) error.value } } @@ -46,4 +46,3 @@ private external fun rethrowExceptionAsNSError( error: CPointer>, types: CArrayPointer> ) -) diff --git a/kmp-nativecoroutines-core/src/nativeCoroutinesMain/kotlin/com/rickclephas/kmp/nativecoroutines/NativeError.kt b/kmp-nativecoroutines-core/src/nativeCoroutinesMain/kotlin/com/rickclephas/kmp/nativecoroutines/NativeError.kt index 0c44b12b..daab1d92 100644 --- a/kmp-nativecoroutines-core/src/nativeCoroutinesMain/kotlin/com/rickclephas/kmp/nativecoroutines/NativeError.kt +++ b/kmp-nativecoroutines-core/src/nativeCoroutinesMain/kotlin/com/rickclephas/kmp/nativecoroutines/NativeError.kt @@ -1,5 +1,7 @@ package com.rickclephas.kmp.nativecoroutines +import kotlin.reflect.KClass + /** * Represents an error in a way that the specific platform is able to handle */ @@ -7,5 +9,9 @@ expect class NativeError /** * Converts a [Throwable] to a [NativeError]. + * + * @param propagatedExceptions an array of [Throwable] types that should be propagated to ObjC/Swift. */ -internal expect fun Throwable.asNativeError(): NativeError +internal expect fun Throwable.asNativeError( + propagatedExceptions: Array> +): NativeError diff --git a/kmp-nativecoroutines-core/src/nativeCoroutinesMain/kotlin/com/rickclephas/kmp/nativecoroutines/NativeFlow.kt b/kmp-nativecoroutines-core/src/nativeCoroutinesMain/kotlin/com/rickclephas/kmp/nativecoroutines/NativeFlow.kt index 4a1927af..f606dafd 100644 --- a/kmp-nativecoroutines-core/src/nativeCoroutinesMain/kotlin/com/rickclephas/kmp/nativecoroutines/NativeFlow.kt +++ b/kmp-nativecoroutines-core/src/nativeCoroutinesMain/kotlin/com/rickclephas/kmp/nativecoroutines/NativeFlow.kt @@ -19,7 +19,7 @@ typealias NativeFlow = (onItem: NativeCallback, onComplete: NativeCallback * Creates a [NativeFlow] for this [Flow]. * * @param scope the [CoroutineScope] to use for the collection, or `null` to use the [defaultCoroutineScope]. - * @param propagatedExceptions an array of [Throwable] types that should be propagated as [NSError]s. + * @param propagatedExceptions an array of [Throwable] types that should be propagated to ObjC/Swift. * @receiver the [Flow] to collect. * @see Flow.collect */ @@ -38,13 +38,13 @@ fun Flow.asNativeFlow( // this is required since the job could be cancelled before it is started throw e } catch (e: Throwable) { - onComplete(e.asNSError(propagatedExceptions)) + onComplete(e.asNativeError(propagatedExceptions)) } } job.invokeOnCompletion { cause -> // Only handle CancellationExceptions, all other exceptions should be handled inside the job if (cause !is CancellationException) return@invokeOnCompletion - onComplete(cause.asNSError(propagatedExceptions)) + onComplete(cause.asNativeError(propagatedExceptions)) } return@collect job.asNativeCancellable() }).freeze() diff --git a/kmp-nativecoroutines-core/src/nativeCoroutinesMain/kotlin/com/rickclephas/kmp/nativecoroutines/NativeSuspend.kt b/kmp-nativecoroutines-core/src/nativeCoroutinesMain/kotlin/com/rickclephas/kmp/nativecoroutines/NativeSuspend.kt index e1a74085..648b1e78 100644 --- a/kmp-nativecoroutines-core/src/nativeCoroutinesMain/kotlin/com/rickclephas/kmp/nativecoroutines/NativeSuspend.kt +++ b/kmp-nativecoroutines-core/src/nativeCoroutinesMain/kotlin/com/rickclephas/kmp/nativecoroutines/NativeSuspend.kt @@ -17,7 +17,7 @@ typealias NativeSuspend = (onResult: NativeCallback, onError: NativeCallba * Creates a [NativeSuspend] for the provided suspend [block]. * * @param scope the [CoroutineScope] to run the [block] in, or `null` to use the [defaultCoroutineScope]. - * @param propagatedExceptions an array of [Throwable] types that should be propagated as [NSError]s. + * @param propagatedExceptions an array of [Throwable] types that should be propagated to ObjC/Swift. * @param block the suspend block to await. */ fun nativeSuspend( @@ -35,13 +35,13 @@ fun nativeSuspend( // this is required since the job could be cancelled before it is started throw e } catch (e: Throwable) { - onError(e.asNSError(propagatedExceptions)) + onError(e.asNativeError(propagatedExceptions)) } } job.invokeOnCompletion { cause -> // Only handle CancellationExceptions, all other exceptions should be handled inside the job if (cause !is CancellationException) return@invokeOnCompletion - onError(cause.asNSError(propagatedExceptions)) + onError(cause.asNativeError(propagatedExceptions)) } return@collect job.asNativeCancellable() }).freeze() From e908241b9e36dba30fe77442b25066d6f6588565 Mon Sep 17 00:00:00 2001 From: Rick Clephas Date: Sat, 19 Feb 2022 15:18:33 +0100 Subject: [PATCH 13/19] Code cleanup --- .../com/rickclephas/kmp/nativecoroutines/RandomException.kt | 3 +-- .../com/rickclephas/kmp/nativecoroutines/RandomString.kt | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/kmp-nativecoroutines-core/src/commonTest/kotlin/com/rickclephas/kmp/nativecoroutines/RandomException.kt b/kmp-nativecoroutines-core/src/commonTest/kotlin/com/rickclephas/kmp/nativecoroutines/RandomException.kt index 2121f935..714f92af 100644 --- a/kmp-nativecoroutines-core/src/commonTest/kotlin/com/rickclephas/kmp/nativecoroutines/RandomException.kt +++ b/kmp-nativecoroutines-core/src/commonTest/kotlin/com/rickclephas/kmp/nativecoroutines/RandomException.kt @@ -5,5 +5,4 @@ import kotlin.random.Random /** * An exception with a message consisting of 20 random capital letter. */ -internal class RandomException: Exception(Random.nextString(20) -) \ No newline at end of file +internal class RandomException: Exception(Random.nextString(20)) \ No newline at end of file diff --git a/kmp-nativecoroutines-core/src/commonTest/kotlin/com/rickclephas/kmp/nativecoroutines/RandomString.kt b/kmp-nativecoroutines-core/src/commonTest/kotlin/com/rickclephas/kmp/nativecoroutines/RandomString.kt index 9b3c6c86..46a3c835 100644 --- a/kmp-nativecoroutines-core/src/commonTest/kotlin/com/rickclephas/kmp/nativecoroutines/RandomString.kt +++ b/kmp-nativecoroutines-core/src/commonTest/kotlin/com/rickclephas/kmp/nativecoroutines/RandomString.kt @@ -6,5 +6,5 @@ import kotlin.random.Random * Generates a random string with the specified [length]. */ internal fun Random.nextString(length: Int = 10) = (1..length).map { - Random.nextInt(65, 91).toChar() + nextInt(65, 91).toChar() }.joinToString("") \ No newline at end of file From 9c92d44d91fffb8ff0077428eeab8cb389988f26 Mon Sep 17 00:00:00 2001 From: Rick Clephas Date: Sun, 20 Feb 2022 13:28:07 +0100 Subject: [PATCH 14/19] Use NSErrorKt --- build.gradle.kts | 2 ++ kmp-nativecoroutines-core/build.gradle.kts | 3 ++ .../kmp/nativecoroutines/NativeErrorApple.kt | 31 ++----------------- settings.gradle.kts | 1 + 4 files changed, 8 insertions(+), 29 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index 74a03fb0..98e00e6b 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -2,6 +2,7 @@ buildscript { repositories { gradlePluginPortal() mavenCentral() + mavenLocal() } dependencies { classpath(Dependencies.Kotlin.gradlePlugin) @@ -14,6 +15,7 @@ allprojects { repositories { mavenCentral() + mavenLocal() } } diff --git a/kmp-nativecoroutines-core/build.gradle.kts b/kmp-nativecoroutines-core/build.gradle.kts index c40d3739..78d01f69 100644 --- a/kmp-nativecoroutines-core/build.gradle.kts +++ b/kmp-nativecoroutines-core/build.gradle.kts @@ -47,6 +47,9 @@ kotlin { } val appleMain by creating { dependsOn(nativeCoroutinesMain) + dependencies { + api("com.rickclephas.kmp:nserror-kt:0.1.0-SNAPSHOT") + } } val appleTest by creating { dependsOn(nativeCoroutinesTest) diff --git a/kmp-nativecoroutines-core/src/appleMain/kotlin/com/rickclephas/kmp/nativecoroutines/NativeErrorApple.kt b/kmp-nativecoroutines-core/src/appleMain/kotlin/com/rickclephas/kmp/nativecoroutines/NativeErrorApple.kt index a4545aa7..71c42e2b 100644 --- a/kmp-nativecoroutines-core/src/appleMain/kotlin/com/rickclephas/kmp/nativecoroutines/NativeErrorApple.kt +++ b/kmp-nativecoroutines-core/src/appleMain/kotlin/com/rickclephas/kmp/nativecoroutines/NativeErrorApple.kt @@ -1,11 +1,10 @@ package com.rickclephas.kmp.nativecoroutines -import kotlinx.cinterop.* import platform.Foundation.NSError import kotlin.coroutines.cancellation.CancellationException import kotlin.native.concurrent.freeze -import kotlin.native.internal.GCUnsafeCall import kotlin.reflect.KClass +import com.rickclephas.kmp.nserrorkt.throwAsNSError actual typealias NativeError = NSError @@ -19,30 +18,4 @@ actual typealias NativeError = NSError */ internal actual fun Throwable.asNativeError( propagatedExceptions: Array> -): NSError { - freeze() - val shouldPropagate = CancellationException::class.isInstance(this) || - propagatedExceptions.any { it.isInstance(this) } - return memScoped { - val error = alloc>() - val types = when (shouldPropagate) { - true -> allocArray>(2).apply { - val typeInfo = getTypeInfo(this@asNativeError) - set(0, interpretCPointer(typeInfo)) - } - false -> allocArray(1) - } - rethrowExceptionAsNSError(this@asNativeError, error.ptr, types) - error.value - } -} - -@GCUnsafeCall("Kotlin_Any_getTypeInfo") -private external fun getTypeInfo(obj: Any): NativePtr - -@GCUnsafeCall("Kotlin_ObjCExport_RethrowExceptionAsNSError") -private external fun rethrowExceptionAsNSError( - exception: Throwable, - error: CPointer>, - types: CArrayPointer> -) +): NSError = freeze().throwAsNSError(*propagatedExceptions) diff --git a/settings.gradle.kts b/settings.gradle.kts index bd7149a8..99e9ba9a 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -2,6 +2,7 @@ pluginManagement { repositories { gradlePluginPortal() mavenCentral() + mavenLocal() } } From a2aae280cd8af1bfa096f84f47f6d7577b9dbecc Mon Sep 17 00:00:00 2001 From: Rick Clephas Date: Sun, 20 Feb 2022 13:29:01 +0100 Subject: [PATCH 15/19] Revert to Throws annotation --- .../sample/tests/FlowIntegrationTests.kt | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/sample/shared/src/commonMain/kotlin/com/rickclephas/kmp/nativecoroutines/sample/tests/FlowIntegrationTests.kt b/sample/shared/src/commonMain/kotlin/com/rickclephas/kmp/nativecoroutines/sample/tests/FlowIntegrationTests.kt index d1e32eba..464a77e7 100644 --- a/sample/shared/src/commonMain/kotlin/com/rickclephas/kmp/nativecoroutines/sample/tests/FlowIntegrationTests.kt +++ b/sample/shared/src/commonMain/kotlin/com/rickclephas/kmp/nativecoroutines/sample/tests/FlowIntegrationTests.kt @@ -1,6 +1,5 @@ package com.rickclephas.kmp.nativecoroutines.sample.tests -import com.rickclephas.kmp.nativecoroutines.NativeCoroutineThrows import com.rickclephas.kmp.nativecoroutines.sample.utils.freeze import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow @@ -22,9 +21,7 @@ class FlowIntegrationTests: IntegrationTests() { } } -// TODO: Using Throws will fail the build with Kotlin_ObjCExport_RethrowExceptionAsNSError already exists error -// @Throws(Exception::class) - @NativeCoroutineThrows(Exception::class) + @Throws(Exception::class) fun getFlowWithException(count: Int, exceptionIndex: Int, message: String, delay: Long): Flow = flow { repeat(count) { delay(delay) @@ -33,9 +30,7 @@ class FlowIntegrationTests: IntegrationTests() { } } -// TODO: Using Throws will fail the build with Kotlin_ObjCExport_RethrowExceptionAsNSError already exists error -// @Throws(Error::class) - @NativeCoroutineThrows(Error::class) + @Throws(Error::class) fun getFlowWithError(count: Int, errorIndex: Int, message: String, delay: Long): Flow = flow { repeat(count) { delay(delay) From dade4440bd8d04b38b7a27f8d3441094577c9465 Mon Sep 17 00:00:00 2001 From: Rick Clephas Date: Sun, 20 Feb 2022 13:38:32 +0100 Subject: [PATCH 16/19] Update tests for asNativeError --- kmp-nativecoroutines-core/build.gradle.kts | 1 - .../kmp/nativecoroutines/NativeErrorAppleTests.kt | 14 ++++++++------ 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/kmp-nativecoroutines-core/build.gradle.kts b/kmp-nativecoroutines-core/build.gradle.kts index 78d01f69..d0841092 100644 --- a/kmp-nativecoroutines-core/build.gradle.kts +++ b/kmp-nativecoroutines-core/build.gradle.kts @@ -26,7 +26,6 @@ kotlin { sourceSets { all { languageSettings.optIn("kotlin.RequiresOptIn") - languageSettings.optIn("kotlin.native.internal.InternalForKotlinNative") } val commonMain by getting { dependencies { diff --git a/kmp-nativecoroutines-core/src/appleTest/kotlin/com/rickclephas/kmp/nativecoroutines/NativeErrorAppleTests.kt b/kmp-nativecoroutines-core/src/appleTest/kotlin/com/rickclephas/kmp/nativecoroutines/NativeErrorAppleTests.kt index 611e7ec2..0911341a 100644 --- a/kmp-nativecoroutines-core/src/appleTest/kotlin/com/rickclephas/kmp/nativecoroutines/NativeErrorAppleTests.kt +++ b/kmp-nativecoroutines-core/src/appleTest/kotlin/com/rickclephas/kmp/nativecoroutines/NativeErrorAppleTests.kt @@ -1,5 +1,6 @@ package com.rickclephas.kmp.nativecoroutines +import kotlinx.cinterop.UnsafeNumber import kotlinx.cinterop.convert import platform.Foundation.NSError import kotlin.coroutines.cancellation.CancellationException @@ -14,7 +15,7 @@ class NativeErrorAppleTests { fun ensureFrozen() { val exception = RandomException() assertFalse(exception.isFrozen, "Exception shouldn't be frozen yet") - val nsError = exception.asNSError(arrayOf(RandomException::class)) + val nsError = exception.asNativeError(arrayOf(RandomException::class)) assertTrue(nsError.isFrozen, "NSError should be frozen") assertTrue(exception.isFrozen, "Exception should be frozen") } @@ -23,7 +24,7 @@ class NativeErrorAppleTests { @OptIn(UnsafeNumber::class) fun ensureNSErrorDomainAndCodeAreCorrect() { val exception = RandomException() - val nsError = exception.asNSError(arrayOf(RandomException::class)) + val nsError = exception.asNativeError(arrayOf(RandomException::class)) assertEquals("KotlinException", nsError.domain, "Incorrect NSError domain") assertEquals(0.convert(), nsError.code, "Incorrect NSError code") } @@ -31,7 +32,7 @@ class NativeErrorAppleTests { @Test fun ensureLocalizedDescriptionIsSetToMessage() { val exception = RandomException() - val nsError = exception.asNSError(arrayOf(RandomException::class)) + val nsError = exception.asNativeError(arrayOf(RandomException::class)) assertEquals(exception.message, nsError.localizedDescription, "Localized description isn't set to message") } @@ -39,7 +40,7 @@ class NativeErrorAppleTests { @Test fun ensureExceptionIsPartOfUserInfo() { val exception = RandomException() - val nsError = exception.asNSError(arrayOf(RandomException::class)) + val nsError = exception.asNativeError(arrayOf(RandomException::class)) assertSame(exception, nsError.userInfo["KotlinException"], "Exception isn't part of the user info") assertSame(exception, nsError.kotlinCause, "Incorrect kotlinCause") } @@ -47,15 +48,16 @@ class NativeErrorAppleTests { @Test fun `ensure CancellationException is always propagated`() { val exception = CancellationException() - val nsError = exception.asNSError(arrayOf()) + val nsError = exception.asNativeError(arrayOf()) assertSame(exception, nsError.userInfo["KotlinException"], "Exception isn't part of the user info") } @Test + @OptIn(UnsafeNumber::class) fun `ensure ObjCErrorException is always propagated`() { val error = NSError.errorWithDomain(Random.nextString(), Random.nextInt().convert(), null) val exception = ObjCErrorException(Random.nextString(), error) - val nsError = exception.asNSError(arrayOf()) + val nsError = exception.asNativeError(arrayOf()) assertEquals(error, nsError, "NSError isn't equal") } } From fc884eed17ba58c35620c99558a4acf2aa29ff05 Mon Sep 17 00:00:00 2001 From: Rick Clephas Date: Sun, 20 Feb 2022 13:44:54 +0100 Subject: [PATCH 17/19] Fix tests after merge --- .../com/rickclephas/kmp/nativecoroutines/NativeFlowTests.kt | 2 +- .../rickclephas/kmp/nativecoroutines/NativeSuspendTests.kt | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/kmp-nativecoroutines-core/src/nativeCoroutinesTest/kotlin/com/rickclephas/kmp/nativecoroutines/NativeFlowTests.kt b/kmp-nativecoroutines-core/src/nativeCoroutinesTest/kotlin/com/rickclephas/kmp/nativecoroutines/NativeFlowTests.kt index 3e5c63a2..a9769b0d 100644 --- a/kmp-nativecoroutines-core/src/nativeCoroutinesTest/kotlin/com/rickclephas/kmp/nativecoroutines/NativeFlowTests.kt +++ b/kmp-nativecoroutines-core/src/nativeCoroutinesTest/kotlin/com/rickclephas/kmp/nativecoroutines/NativeFlowTests.kt @@ -24,7 +24,7 @@ class NativeFlowTests { fun ensureExceptionsAreReceivedAsErrors() = runTest { val exception = RandomException() val flow = flow { throw exception } - val nativeFlow = flow.asNativeFlow(this) + val nativeFlow = flow.asNativeFlow(this, arrayOf(RandomException::class)) val completionCount = atomic(0) nativeFlow({ _, _ -> }, { error, _ -> assertNotNull(error, "Flow should complete with an error") diff --git a/kmp-nativecoroutines-core/src/nativeCoroutinesTest/kotlin/com/rickclephas/kmp/nativecoroutines/NativeSuspendTests.kt b/kmp-nativecoroutines-core/src/nativeCoroutinesTest/kotlin/com/rickclephas/kmp/nativecoroutines/NativeSuspendTests.kt index 229d1fc1..30073af1 100644 --- a/kmp-nativecoroutines-core/src/nativeCoroutinesTest/kotlin/com/rickclephas/kmp/nativecoroutines/NativeSuspendTests.kt +++ b/kmp-nativecoroutines-core/src/nativeCoroutinesTest/kotlin/com/rickclephas/kmp/nativecoroutines/NativeSuspendTests.kt @@ -36,7 +36,9 @@ class NativeSuspendTests { @Test fun ensureExceptionsAreReceivedAsErrors() = runTest { val exception = RandomException() - val nativeSuspend = nativeSuspend(this) { delayAndThrow(100, exception) } + val nativeSuspend = nativeSuspend(this, arrayOf(RandomException::class)) { + delayAndThrow(100, exception) + } val receivedResultCount = atomic(0) val receivedErrorCount = atomic(0) nativeSuspend({ _, _ -> From cf07edc0ba1ceea6697b8c335edcb9878f8eebd5 Mon Sep 17 00:00:00 2001 From: Rick Clephas Date: Sun, 20 Feb 2022 14:03:33 +0100 Subject: [PATCH 18/19] Always propagate CancellationExceptions --- .../com/rickclephas/kmp/nativecoroutines/NativeErrorApple.kt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/kmp-nativecoroutines-core/src/appleMain/kotlin/com/rickclephas/kmp/nativecoroutines/NativeErrorApple.kt b/kmp-nativecoroutines-core/src/appleMain/kotlin/com/rickclephas/kmp/nativecoroutines/NativeErrorApple.kt index 71c42e2b..bfde5b9f 100644 --- a/kmp-nativecoroutines-core/src/appleMain/kotlin/com/rickclephas/kmp/nativecoroutines/NativeErrorApple.kt +++ b/kmp-nativecoroutines-core/src/appleMain/kotlin/com/rickclephas/kmp/nativecoroutines/NativeErrorApple.kt @@ -18,4 +18,7 @@ actual typealias NativeError = NSError */ internal actual fun Throwable.asNativeError( propagatedExceptions: Array> -): NSError = freeze().throwAsNSError(*propagatedExceptions) +): NSError = freeze().throwAsNSError(*propagatedExceptions.run { + if (contains(CancellationException::class)) this + else plus(CancellationException::class) +}) From 3099f826b7d209ba61408f5caab0a47a6757b82e Mon Sep 17 00:00:00 2001 From: Rick Clephas Date: Sun, 20 Feb 2022 14:10:13 +0100 Subject: [PATCH 19/19] Add local maven repo to sample --- sample/build.gradle.kts | 2 ++ sample/settings.gradle.kts | 1 + 2 files changed, 3 insertions(+) diff --git a/sample/build.gradle.kts b/sample/build.gradle.kts index a2d9a51c..652099f4 100644 --- a/sample/build.gradle.kts +++ b/sample/build.gradle.kts @@ -2,6 +2,7 @@ buildscript { repositories { gradlePluginPortal() mavenCentral() + mavenLocal() } dependencies { classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.6.10") @@ -13,5 +14,6 @@ buildscript { allprojects { repositories { mavenCentral() + mavenLocal() } } diff --git a/sample/settings.gradle.kts b/sample/settings.gradle.kts index 3bc1adbb..09d98413 100644 --- a/sample/settings.gradle.kts +++ b/sample/settings.gradle.kts @@ -2,6 +2,7 @@ pluginManagement { repositories { gradlePluginPortal() mavenCentral() + mavenLocal() } }