Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
e39f2f0
Update asNSError to use KN runtime functions
rickclephas Dec 24, 2021
a61b5c3
Update compiler to support exception propagation
rickclephas Dec 26, 2021
32946d4
Always propagate CancellationExceptions
rickclephas Dec 26, 2021
115e147
Use custom BestExceptionEver
rickclephas Dec 26, 2021
e97ae38
Improve NSError tests
rickclephas Dec 26, 2021
118f814
Add compiler tests for exceptions
rickclephas Dec 26, 2021
c22943c
Add compiler tests for Flow value and replayCache
rickclephas Dec 26, 2021
f369d5d
Merge branch 'master' into feature/GH-29-improve-exception-conversion
rickclephas Dec 26, 2021
2218d3d
Remove unnecessary default propagatedException
rickclephas Dec 26, 2021
64b1fb8
Support NativeCoroutineThrows annotation on classes
rickclephas Dec 26, 2021
0cb1ba5
Update README with exception propagation info
rickclephas Dec 26, 2021
3c6d768
Fix tests
rickclephas Dec 26, 2021
4781c30
Merge branch 'master' into feature/GH-29-improve-exception-conversion
rickclephas Feb 19, 2022
e99ddef
Merge branch 'master' into feature/GH-29-improve-exception-conversion
rickclephas Feb 19, 2022
7644c0b
Update asNativeError function with propagatedExceptions
rickclephas Feb 19, 2022
e908241
Code cleanup
rickclephas Feb 19, 2022
9c92d44
Use NSErrorKt
rickclephas Feb 20, 2022
a2aae28
Revert to Throws annotation
rickclephas Feb 20, 2022
dade444
Update tests for asNativeError
rickclephas Feb 20, 2022
3874964
Merge branch 'master' into feature/GH-29-improve-exception-conversion
rickclephas Feb 20, 2022
fc884ee
Fix tests after merge
rickclephas Feb 20, 2022
cf07edc
Always propagate CancellationExceptions
rickclephas Feb 20, 2022
3099f82
Add local maven repo to sample
rickclephas Feb 20, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 54 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,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<Int> = 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.
Expand Down
2 changes: 2 additions & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ buildscript {
repositories {
gradlePluginPortal()
mavenCentral()
mavenLocal()
}
dependencies {
classpath(Dependencies.Kotlin.gradlePlugin)
Expand All @@ -14,6 +15,7 @@ allprojects {

repositories {
mavenCentral()
mavenLocal()
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -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_GETTER, AnnotationTarget.FUNCTION, AnnotationTarget.CLASS)
@Retention(AnnotationRetention.SOURCE)
@MustBeDocumented
annotation class NativeCoroutineThrows(vararg val exceptionClasses: KClass<out Throwable>)
Original file line number Diff line number Diff line change
@@ -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<String>(SUFFIX_OPTION_NAME)

internal const val PROPAGATED_EXCEPTIONS_OPTION_NAME = "propagatedExceptions"
internal val PROPAGATED_EXCEPTIONS_KEY = CompilerConfigurationKey<List<FqName>>(PROPAGATED_EXCEPTIONS_OPTION_NAME)

internal const val USE_THROWS_ANNOTATION_OPTION_NAME = "useThrowsAnnotation"
internal val USE_THROWS_ANNOTATION_KEY = CompilerConfigurationKey<Boolean>(USE_THROWS_ANNOTATION_OPTION_NAME)
Original file line number Diff line number Diff line change
Expand Up @@ -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<AbstractCliOption> = 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(
Expand All @@ -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}")
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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) }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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<FqName>,
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) }
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -9,27 +9,27 @@ 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.*
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<FqName>,
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)
Expand Down Expand Up @@ -57,6 +57,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}")
Expand All @@ -72,14 +74,16 @@ 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}")
return DeclarationIrBuilder(context, getter.symbol,
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 {
Expand All @@ -102,6 +106,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}")
Expand All @@ -110,8 +117,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) {
Expand All @@ -126,6 +134,7 @@ internal class KmpNativeCoroutinesIrTransformer(
call = irCall(nativeFlowFunction, returnType).apply {
putTypeArgument(0, valueType)
putValueArgument(0, nativeCoroutineScope)
putValueArgument(1, propagatedExceptions)
extensionReceiver = call
}
}
Expand All @@ -149,7 +158,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)
Expand Down Expand Up @@ -178,4 +188,31 @@ 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 {
// Find the annotation on the declaration (or a parent class)
fun IrDeclaration.getAnnotation(): IrConstructorCall? =
annotations.findNativeCoroutineThrowsAnnotation() ?:
useThrowsAnnotation.ifTrue { originalDeclaration.annotations.findThrowsAnnotation() } ?:
parentClassOrNull?.getAnnotation()
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe this should just use all the annotations, would probably make more sense since the propagatedExceptions are also applied (even if the function has an annotation).

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)
}
}
return irArrayOf(propagatedExceptionsArrayElementType, propagatedExceptions)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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<IrVarargElement>
): 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)
}
}

This file was deleted.

Loading