Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
6 changes: 5 additions & 1 deletion .github/workflows/run-integration-tests.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ jobs:
matrix:
platform: [ iOS, macOS, tvOS, watchOS ]
embeddable-compiler: [ true, false ]
swift-export: [ true, false ]
include:
- java: '17'
- xcode: '16.4'
Expand All @@ -29,7 +30,7 @@ jobs:
- platform: watchOS
scheme: watchOS Tests
destination: platform=watchOS Simulator,name=Apple Watch Series 10 (46mm)
name: ${{ format('{0} (embeddable {1})', matrix.platform, matrix.embeddable-compiler) }}
name: ${{ format('{0} (embeddable {1}, swiftExport {2})', matrix.platform, matrix.embeddable-compiler, matrix.swift-export) }}
runs-on: macos-15
defaults:
run:
Expand All @@ -55,10 +56,13 @@ jobs:
- name: Set kotlin.native.useEmbeddableCompilerJar
run: echo "kotlin.native.useEmbeddableCompilerJar=${{ matrix.embeddable-compiler }}" >> gradle.properties
- name: Run tests
env:
NATIVE_COROUTINES_SWIFT_EXPORT: ${{ matrix.swift-export }}
run: >-
set -o pipefail &&
xcodebuild test
-project KMPNativeCoroutinesSample.xcodeproj
-scheme "${{ matrix.scheme }}"
-destination "${{ matrix.destination }}"
'SWIFT_ACTIVE_COMPILATION_CONDITIONS=$(inherited) ${{ matrix.swift-export && 'NATIVE_COROUTINES_SWIFT_EXPORT' || '' }}'
| xcbeautify --renderer github-actions
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ So all the `Flow` interfaces lose their generic value type which make them hard

This library solves both of these limitations 😄.

> [!NOTE]
> Looking to try Swift export?
> Read about its current state and limitations in [SWIFT_EXPORT.md](SWIFT_EXPORT.md).

## Compatibility

The latest version of the library uses Kotlin version `2.2.21`.
Expand Down
60 changes: 60 additions & 0 deletions SWIFT_EXPORT.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# Swift Export compatibility and migration

Starting with Kotlin 2.2.20 you can use the (experimental)
[Swift export](https://kotlinlang.org/docs/native-swift-export.html), which provides direct Kotlin - Swift interoperability.

> [!WARNING]
> Swift export is still experimental and actively being developed! It doesn't yet support all language features,
> might still contain bugs, and its behaviour could change in future versions.
> It is NOT recommended to use Swift export in production just yet!

Once Swift export is complete and stable it'll likely completely remove the need for KMP-NativeCoroutines.

So to support you in your journey to Swift export:

* KMP-NativeCoroutines will try to be "compatible" with Swift export as much as possible,
allowing you to start testing with Swift export as soon as possible.
* A migration path will be provided once coroutines support in Swift export becomes stable.


# Known limitations

These are the known limitations with Swift export and KMP-NativeCoroutines.

## 🚨 `NativeSuspend` and `NativeFlow` are unsupported

At the moment Swift export doesn't support functional return types yet.

Unfortunately KMP-NativeCoroutines heavily relies on functional return types, making it incompatible with Swift Export.
For now the plugin just clones your original functions and properties to prevent your Kotlin builds from failing.

**Temporary workaround:**
You should disable any relevant code in Swift if you would like to try Swift export.

## ⚠️ `@ObjCName` is ignored

The `@ObjCName` annotation is (currently) ignored by Swift export.
This prevents KMP-NativeCoroutines from reusing your original function or property name.

**Temporary workaround:**
You should update your Swift code with the `Native` name suffix in order to access the generated declarations.

# Enabling Swift export

To enable Swift export with KMP-NativeCoroutines you start by following the
[official documentation](https://kotlinlang.org/docs/native-swift-export.html).

Once Swift export is enabled you'll need to activate the Swift export compatibility mode:
```kotlin
// build.gradle.kts
nativeCoroutines {
swiftExport = true
}
```

# Usage

At the moment you can't use any coroutines related code when Swift export is enabled.

You can still use the generated properties for the `StateFlow.value` and `SharedFlow.replayCache` values,
but keep in mind the `@ObjCName` limitation.
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,17 @@ public final class com/rickclephas/kmp/nativecoroutines/compiler/config/Suffixes
public static final fun getSUFFIX ()Lcom/rickclephas/kmp/nativecoroutines/compiler/config/ConfigOptionWithDefault;
}

public final class com/rickclephas/kmp/nativecoroutines/compiler/config/SwiftExport : java/lang/Enum {
public static final field NO_FUNC_RETURN_TYPES Lcom/rickclephas/kmp/nativecoroutines/compiler/config/SwiftExport;
public static fun getEntries ()Lkotlin/enums/EnumEntries;
public static fun valueOf (Ljava/lang/String;)Lcom/rickclephas/kmp/nativecoroutines/compiler/config/SwiftExport;
public static fun values ()[Lcom/rickclephas/kmp/nativecoroutines/compiler/config/SwiftExport;
}

public final class com/rickclephas/kmp/nativecoroutines/compiler/config/SwiftExportKt {
public static final fun getSWIFT_EXPORT ()Lcom/rickclephas/kmp/nativecoroutines/compiler/config/ConfigOptionWithDefault;
}

public final class com/rickclephas/kmp/nativecoroutines/compiler/fir/diagnostics/FirKmpNativeCoroutinesErrors : org/jetbrains/kotlin/diagnostics/KtDiagnosticsContainer {
public static final field INSTANCE Lcom/rickclephas/kmp/nativecoroutines/compiler/fir/diagnostics/FirKmpNativeCoroutinesErrors;
public final fun getCONFLICT_COROUTINES ()Lorg/jetbrains/kotlin/diagnostics/KtDiagnosticFactory0;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,17 @@ public final class com/rickclephas/kmp/nativecoroutines/compiler/config/Suffixes
public static final fun getSUFFIX ()Lcom/rickclephas/kmp/nativecoroutines/compiler/config/ConfigOptionWithDefault;
}

public final class com/rickclephas/kmp/nativecoroutines/compiler/config/SwiftExport : java/lang/Enum {
public static final field NO_FUNC_RETURN_TYPES Lcom/rickclephas/kmp/nativecoroutines/compiler/config/SwiftExport;
public static fun getEntries ()Lkotlin/enums/EnumEntries;
public static fun valueOf (Ljava/lang/String;)Lcom/rickclephas/kmp/nativecoroutines/compiler/config/SwiftExport;
public static fun values ()[Lcom/rickclephas/kmp/nativecoroutines/compiler/config/SwiftExport;
}

public final class com/rickclephas/kmp/nativecoroutines/compiler/config/SwiftExportKt {
public static final fun getSWIFT_EXPORT ()Lcom/rickclephas/kmp/nativecoroutines/compiler/config/ConfigOptionWithDefault;
}

public final class com/rickclephas/kmp/nativecoroutines/compiler/fir/diagnostics/FirKmpNativeCoroutinesErrors : org/jetbrains/kotlin/diagnostics/KtDiagnosticsContainer {
public static final field INSTANCE Lcom/rickclephas/kmp/nativecoroutines/compiler/fir/diagnostics/FirKmpNativeCoroutinesErrors;
public final fun getCONFLICT_COROUTINES ()Lorg/jetbrains/kotlin/diagnostics/KtDiagnosticFactory0;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ public class KmpNativeCoroutinesCommandLineProcessor: CommandLineProcessor {

override val pluginId: String = "com.rickclephas.kmp.nativecoroutines"
override val pluginOptions: Collection<AbstractCliOption> = listOf(
EXPOSED_SEVERITY, GENERATED_SOURCE_DIR,
EXPOSED_SEVERITY, GENERATED_SOURCE_DIR, SWIFT_EXPORT,
SUFFIX, FLOW_VALUE_SUFFIX, FLOW_REPLAY_CACHE_SUFFIX, STATE_SUFFIX, STATE_FLOW_SUFFIX
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,6 @@ public class KmpNativeCoroutinesCompilerPluginRegistrar: CompilerPluginRegistrar
StorageComponentContainerContributor.registerExtension(
KmpNativeCoroutinesStorageComponentContainerContributor(configuration)
)
IrGenerationExtension.registerExtension(KmpNativeCoroutinesIrGenerationExtension())
IrGenerationExtension.registerExtension(KmpNativeCoroutinesIrGenerationExtension(configuration))
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package com.rickclephas.kmp.nativecoroutines.compiler.config

import org.jetbrains.kotlin.utils.filterToSetOrEmpty

public enum class SwiftExport {
NO_FUNC_RETURN_TYPES,
}

public val SWIFT_EXPORT: ConfigOptionWithDefault<Set<SwiftExport>> =
object : ConfigOptionWithDefault<Set<SwiftExport>>("swiftExport") {
override val required: Boolean = false
override val description: String = "Specifies the Swift export compatibility version"
override val valueDescription: String = "0"
override val defaultValue: Set<SwiftExport> = emptySet()
override fun parse(value: String): Set<SwiftExport> {
val flags = value.toLong()
return SwiftExport.entries.filterToSetOrEmpty {
val flag = 1L shl it.ordinal
(flags and flag) == flag
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,22 @@ import com.rickclephas.kmp.nativecoroutines.compiler.fir.utils.asFirExpression
import com.rickclephas.kmp.nativecoroutines.compiler.utils.ClassIds
import com.rickclephas.kmp.nativecoroutines.compiler.utils.Names
import org.jetbrains.kotlin.fir.declarations.toAnnotationClassId
import org.jetbrains.kotlin.fir.declarations.utils.isCompanion
import org.jetbrains.kotlin.fir.expressions.*
import org.jetbrains.kotlin.fir.expressions.builder.buildAnnotation
import org.jetbrains.kotlin.fir.expressions.builder.buildAnnotationArgumentMapping
import org.jetbrains.kotlin.fir.expressions.builder.buildArgumentList
import org.jetbrains.kotlin.fir.expressions.builder.buildGetClassCall
import org.jetbrains.kotlin.fir.expressions.builder.buildResolvedQualifier
import org.jetbrains.kotlin.fir.expressions.builder.buildVarargArgumentsExpression
import org.jetbrains.kotlin.fir.extensions.FirExtension
import org.jetbrains.kotlin.fir.resolve.providers.symbolProvider
import org.jetbrains.kotlin.fir.types.ConeKotlinTypeProjectionOut
import org.jetbrains.kotlin.fir.types.builder.buildResolvedTypeRef
import org.jetbrains.kotlin.fir.types.constructClassLikeType
import org.jetbrains.kotlin.name.ClassId
import org.jetbrains.kotlin.name.Name
import org.jetbrains.kotlin.name.StandardClassIds

internal fun FirExtension.buildAnnotationsCopy(
originalAnnotations: List<FirAnnotation>,
Expand Down Expand Up @@ -44,6 +52,38 @@ internal fun buildAnnotation(
}
}

internal fun FirExtension.buildThrowsAnnotation(vararg classIds: ClassId): FirAnnotation {
val exceptionClasses = classIds.mapNotNull { classId ->
session.symbolProvider.getClassLikeSymbolByClassId(classId)
}.map { symbol ->
buildResolvedQualifier {
coneTypeOrNull = symbol.classId.constructClassLikeType()
packageFqName = symbol.classId.packageFqName
relativeClassFqName = symbol.classId.relativeClassName
this.symbol = symbol
resolvedToCompanionObject = symbol.isCompanion
}
}.map { resolvedQualifier ->
buildGetClassCall {
coneTypeOrNull = StandardClassIds.KClass.constructClassLikeType(arrayOf(
resolvedQualifier.classId!!.constructClassLikeType()
))
argumentList = buildArgumentList {
arguments.add(resolvedQualifier)
}
}
}
return buildAnnotation(ClassIds.throws, mapOf(
Names.Throws.exceptionClasses to buildVarargArgumentsExpression {
coneElementTypeOrNull = StandardClassIds.Throwable.constructClassLikeType()
coneTypeOrNull = StandardClassIds.Array.constructClassLikeType(arrayOf(
ConeKotlinTypeProjectionOut(coneElementTypeOrNull!!)
))
arguments.addAll(exceptionClasses)
}
))
}

private fun buildObjCNameAnnotationCopy(
annotation: FirAnnotation?,
objCName: String?,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.rickclephas.kmp.nativecoroutines.compiler.fir.codegen

import com.rickclephas.kmp.nativecoroutines.compiler.config.SwiftExport
import com.rickclephas.kmp.nativecoroutines.compiler.fir.utils.*
import com.rickclephas.kmp.nativecoroutines.compiler.utils.ClassIds
import com.rickclephas.kmp.nativecoroutines.compiler.utils.NativeCoroutinesAnnotation
Expand All @@ -21,7 +22,8 @@ import org.jetbrains.kotlin.name.CallableId
internal fun FirExtension.buildNativeFunction(
callableId: CallableId,
originalSymbol: FirNamedFunctionSymbol,
annotation: NativeCoroutinesAnnotation
annotation: NativeCoroutinesAnnotation,
swiftExport: Set<SwiftExport>,
): FirNamedFunctionSymbol? {
val firCallableSignature = originalSymbol.getCallableSignature(session) ?: return null
val callableSignature = firCallableSignature.signature
Expand All @@ -40,6 +42,9 @@ internal fun FirExtension.buildNativeFunction(

status = originalSymbol.getGeneratedDeclarationStatus(session)
?.copy(isInline = originalSymbol.isInline) ?: return null
if (SwiftExport.NO_FUNC_RETURN_TYPES in swiftExport) {
status = status.copy(isSuspend = callableSignature.isSuspend)
}

dispatchReceiverType = originalSymbol.dispatchReceiverType

Expand Down Expand Up @@ -68,6 +73,7 @@ internal fun FirExtension.buildNativeFunction(

returnTypeRef = firCallableSignature.getNativeType(
callableSignature.returnType,
swiftExport,
callableSignature.isSuspend
).let(typeParameters.substitutor::substituteOrSelf).toFirResolvedTypeRef()

Expand All @@ -78,6 +84,9 @@ internal fun FirExtension.buildNativeFunction(
if (annotation.shouldRefineInSwift) {
annotations.add(buildAnnotation(ClassIds.shouldRefineInSwift))
}
if (SwiftExport.NO_FUNC_RETURN_TYPES in swiftExport && callableSignature.isSuspend) {
annotations.add(buildThrowsAnnotation(ClassIds.exception))
}

body = session.buildCallableReferenceBlock(originalSymbol)
}.symbol
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.rickclephas.kmp.nativecoroutines.compiler.fir.codegen

import com.rickclephas.kmp.nativecoroutines.compiler.config.SwiftExport
import com.rickclephas.kmp.nativecoroutines.compiler.fir.utils.*
import com.rickclephas.kmp.nativecoroutines.compiler.utils.ClassIds
import com.rickclephas.kmp.nativecoroutines.compiler.utils.NativeCoroutinesAnnotation
Expand All @@ -24,6 +25,7 @@ internal fun FirExtension.buildNativeProperty(
annotation: NativeCoroutinesAnnotation,
objCName: String? = null,
objCNameSuffix: String? = null,
swiftExport: Set<SwiftExport>,
): FirPropertySymbol? {
val firCallableSignature = originalSymbol.getCallableSignature(session) ?: return null
val callableSignature = firCallableSignature.signature
Expand Down Expand Up @@ -61,7 +63,7 @@ internal fun FirExtension.buildNativeProperty(
typeParameters.substitutor
)

returnTypeRef = firCallableSignature.getNativeType(callableSignature.returnType)
returnTypeRef = firCallableSignature.getNativeType(callableSignature.returnType, swiftExport)
.let(typeParameters.substitutor::substituteOrSelf)
.toFirResolvedTypeRef()

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.rickclephas.kmp.nativecoroutines.compiler.fir.codegen

import com.rickclephas.kmp.nativecoroutines.compiler.config.SwiftExport
import com.rickclephas.kmp.nativecoroutines.compiler.fir.utils.*
import com.rickclephas.kmp.nativecoroutines.compiler.utils.CallableSignature
import com.rickclephas.kmp.nativecoroutines.compiler.utils.ClassIds
Expand All @@ -25,7 +26,8 @@ internal fun FirExtension.buildSharedFlowReplayCacheProperty(
callableId: CallableId,
originalSymbol: FirPropertySymbol,
annotation: NativeCoroutinesAnnotation,
objCNameSuffix: String?
objCNameSuffix: String?,
swiftExport: Set<SwiftExport>,
): FirPropertySymbol? {
val firCallableSignature = originalSymbol.getCallableSignature(session) ?: return null
val callableSignature = firCallableSignature.signature
Expand Down Expand Up @@ -65,7 +67,7 @@ internal fun FirExtension.buildSharedFlowReplayCacheProperty(
)

returnTypeRef = StandardClassIds.List.constructClassLikeType(
arrayOf(firCallableSignature.getNativeType(callableSignature.returnType.valueType)),
arrayOf(firCallableSignature.getNativeType(callableSignature.returnType.valueType, swiftExport)),
callableSignature.returnType.isNullable
).let(typeParameters.substitutor::substituteOrSelf).toFirResolvedTypeRef()

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.rickclephas.kmp.nativecoroutines.compiler.fir.codegen

import com.rickclephas.kmp.nativecoroutines.compiler.config.SwiftExport
import com.rickclephas.kmp.nativecoroutines.compiler.fir.utils.*
import com.rickclephas.kmp.nativecoroutines.compiler.utils.CallableSignature
import com.rickclephas.kmp.nativecoroutines.compiler.utils.ClassIds
Expand Down Expand Up @@ -27,6 +28,7 @@ internal fun FirExtension.buildStateFlowValueProperty(
annotation: NativeCoroutinesAnnotation,
objCName: String? = null,
objCNameSuffix: String? = null,
swiftExport: Set<SwiftExport>,
): FirPropertySymbol? {
val firCallableSignature = originalSymbol.getCallableSignature(session) ?: return null
val callableSignature = firCallableSignature.signature
Expand Down Expand Up @@ -65,7 +67,7 @@ internal fun FirExtension.buildStateFlowValueProperty(
typeParameters.substitutor
)

returnTypeRef = firCallableSignature.getNativeType(callableSignature.returnType.valueType)
returnTypeRef = firCallableSignature.getNativeType(callableSignature.returnType.valueType, swiftExport)
.applyIf(callableSignature.returnType.isNullable) {
withNullability(true, session.typeContext)
}
Expand Down
Loading
Loading