Skip to content

Commit 99127a7

Browse files
Add opt-in support for NSEnum generation for enum classes for Kotlin Native iOS via an annotation
Addresses KT-48068 If not specified otherwise via @ObjCEnum, the NSEnum type name will match the Kotlin Enum ObjC / Swift name with "NSEnum" appended. The new type is accesible via the "nsEnum" property (the same way as the ordinal value via "ordinal")
1 parent 5952e77 commit 99127a7

File tree

40 files changed

+816
-57
lines changed

40 files changed

+816
-57
lines changed

analysis/low-level-api-fir/testData/lazyResolveStdlibSources/wrappedInt.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -824,7 +824,7 @@ FILE: [ResolvedTo(BODY_RESOLVE)] TypeReference.kt
824824
}
825825

826826
public open override [ResolvedTo(BODY_RESOLVE)] fun hashCode(): R|kotlin/Int| {
827-
^hashCode this@R|kotlin/jvm/internal/TypeReference|.R|kotlin/jvm/internal/TypeReference.classifier|.R|kotlin/Any.hashCode|().R|kotlin/Int.times|(Int(31)).R|kotlin/Int.plus|(this@R|kotlin/jvm/internal/TypeReference|.R|kotlin/jvm/internal/TypeReference.arguments|.R|kotlin/Any.hashCode|()).R|kotlin/Int.times|(Int(31)).R|kotlin/Int.plus|(this@R|kotlin/jvm/internal/TypeReference|.R|kotlin/jvm/internal/TypeReference.flags|.R|kotlin/Int.hashCode|())
827+
^hashCode this@R|kotlin/jvm/internal/TypeReference|.R|kotlin/jvm/internal/TypeReference.classifier|.R|kotlin/Any.hashCode|().R|kotlin/Int.times|(Int(31)).R|kotlin/Int.plus|(this@R|kotlin/jvm/internal/TypeReference|.R|kotlin/jvm/internal/TypeReference.arguments|.R|kotlin/Any.hashCode|()).R|kotlin/Int.times|(Int(31)).R|kotlin/Int.plus|(this@R|kotlin/jvm/internal/TypeReference|.R|kotlin/jvm/internal/TypeReference.flags|.R|kotlin/Any.hashCode|())
828828
}
829829

830830
public open override [ResolvedTo(BODY_RESOLVE)] fun toString(): R|kotlin/String| {

kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/llvm/objcexport/ObjCExportCodeGenerator.kt

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1375,6 +1375,15 @@ private fun ObjCExportCodeGenerator.createArrayConstructorAdapter(
13751375
return objCToKotlinMethodAdapter(selectorName, methodBridge, imp)
13761376
}
13771377

1378+
private fun ObjCExportCodeGenerator.createNSEnumAdapter(
1379+
symbol: IrSimpleFunctionSymbol,
1380+
methodBridge: MethodBridge,
1381+
selectorName: String
1382+
): ObjCToKotlinMethodAdapter {
1383+
val imp = generateObjCImp(symbol.owner.getLowered(), symbol.owner.getLowered<IrSimpleFunction>(), methodBridge)
1384+
return objCToKotlinMethodAdapter(selectorName, methodBridge, imp)
1385+
}
1386+
13781387
private fun ObjCExportCodeGenerator.vtableIndex(irFunction: IrSimpleFunction): Int? {
13791388
assert(irFunction.isOverridable)
13801389
val irClass = irFunction.parentAsClass
@@ -1430,6 +1439,9 @@ private fun ObjCExportCodeGenerator.createTypeAdapter(
14301439
is ObjCInitMethodForKotlinConstructor -> {
14311440
adapters += createConstructorAdapter(it.baseMethod)
14321441
}
1442+
is ObjCGetterForNSEnumType -> {
1443+
adapters += createNSEnumAdapter(it.symbol, it.bridge, it.selector)
1444+
}
14331445
is ObjCFactoryMethodForKotlinArrayConstructor -> {
14341446
classAdapters += createArrayConstructorAdapter(it.baseMethod)
14351447
}

kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/objcexport/ObjCExportCodeSpec.kt

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,12 @@ import org.jetbrains.kotlin.backend.konan.descriptors.contributedMethods
99
import org.jetbrains.kotlin.backend.konan.descriptors.enumEntries
1010
import org.jetbrains.kotlin.backend.konan.descriptors.isArray
1111
import org.jetbrains.kotlin.backend.konan.descriptors.isInterface
12+
import org.jetbrains.kotlin.backend.konan.descriptors.propertyIfAccessor
13+
import org.jetbrains.kotlin.backend.konan.objcexport.ObjCExportNamer.Companion.nsEnumPropertyName
1214
import org.jetbrains.kotlin.builtins.KotlinBuiltIns
1315
import org.jetbrains.kotlin.descriptors.*
1416
import org.jetbrains.kotlin.ir.ObsoleteDescriptorBasedAPI
1517
import org.jetbrains.kotlin.ir.symbols.*
16-
import org.jetbrains.kotlin.ir.util.IdSignature
1718
import org.jetbrains.kotlin.ir.util.SymbolTable
1819
import org.jetbrains.kotlin.resolve.descriptorUtil.getSuperClassNotAny
1920
import java.io.PrintStream
@@ -89,6 +90,14 @@ internal fun ObjCExportedInterface.createCodeSpec(symbolTable: SymbolTable): Obj
8990
}
9091

9192
if (descriptor.kind == ClassKind.ENUM_CLASS) {
93+
if (namer.getNSEnumTypeName(descriptor) != null) {
94+
val superClass = descriptor.getSuperClassNotAny()!! // ordinal is declared in KotlinEnum
95+
val ordinalDescriptor = superClass.contributedMethods.single() { it.propertyIfAccessor.name.asString() == "ordinal" }
96+
val symbol = symbolTable.descriptorExtension.referenceSimpleFunction(ordinalDescriptor)
97+
val bridge = mapper.bridgeMethod(ordinalDescriptor)
98+
methods += ObjCGetterForNSEnumType(symbol, bridge, nsEnumPropertyName)
99+
}
100+
92101
descriptor.enumEntries.mapTo(methods) {
93102
ObjCGetterForKotlinEnumEntry(symbolTable.descriptorExtension.referenceEnumEntry(it), namer.getEnumEntrySelector(it))
94103
}
@@ -160,6 +169,7 @@ internal fun ObjCExportCodeSpec.dumpSelectorToSignatureMapping(path: String) {
160169
is ObjCClassMethodForKotlinEnumValuesOrEntries -> false
161170
is ObjCGetterForKotlinEnumEntry -> false
162171
is ObjCGetterForObjectInstance -> false
172+
is ObjCGetterForNSEnumType -> true
163173
}
164174

165175
fun ObjCMethodSpec.getMapping(objcClass: String): String? = when (this) {
@@ -170,6 +180,7 @@ internal fun ObjCExportCodeSpec.dumpSelectorToSignatureMapping(path: String) {
170180
is ObjCInitMethodForKotlinConstructor -> "$objcClass.${baseMethod.selector},${baseMethod.symbol.signature}"
171181
is ObjCKotlinThrowableAsErrorMethod -> null
172182
is ObjCMethodForKotlinMethod -> "$objcClass.${baseMethod.selector},${baseMethod.symbol.signature}"
183+
is ObjCGetterForNSEnumType -> null
173184
}
174185
out.println("\n# Instance methods mapping")
175186
for (type in types) {
@@ -224,6 +235,17 @@ internal class ObjCGetterForKotlinEnumEntry(
224235
"ObjC spec of getter `$selector` for `$irEnumEntrySymbol`"
225236
}
226237

238+
239+
internal class ObjCGetterForNSEnumType(
240+
val symbol: IrSimpleFunctionSymbol,
241+
val bridge: MethodBridge,
242+
val selector: String,
243+
) : ObjCMethodSpec() {
244+
override fun toString(): String =
245+
"ObjC spec of $selector for $symbol"
246+
}
247+
248+
227249
internal class ObjCClassMethodForKotlinEnumValuesOrEntries(
228250
val valuesFunctionSymbol: IrFunctionSymbol,
229251
val selector: String

kotlin-native/runtime/src/main/kotlin/kotlin/native/Annotations.kt

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
package kotlin.native
77

88
import kotlin.experimental.ExperimentalNativeApi
9+
import kotlin.experimental.ExperimentalObjCEnum
910
import kotlin.experimental.ExperimentalObjCName
1011
import kotlin.experimental.ExperimentalObjCRefinement
1112

@@ -116,6 +117,19 @@ public actual annotation class CName(actual val externName: String = "", actual
116117
@SinceKotlin("1.8")
117118
public actual annotation class ObjCName(actual val name: String = "", actual val swiftName: String = "", actual val exact: Boolean = false)
118119

120+
/**
121+
* Instructs the Kotlin compiler to generate a NS_ENUM typedef for the annotated enum class. The name of the generated type will
122+
* be the name of the enum type with "NSEnum" appended. This name can be overridden with the "name" parameter, which is treated
123+
* as an exact name. The enum literals will be prefixed with the type name, as they live in a global namespace.
124+
* Swift naming will automatically remove these disambiguation prefixes. The NSEnum values are accessible via the "nsEnum" property.
125+
*/
126+
@Target(AnnotationTarget.CLASS)
127+
@Retention(AnnotationRetention.BINARY)
128+
@MustBeDocumented
129+
@ExperimentalObjCEnum
130+
@SinceKotlin("2.3")
131+
public actual annotation class ObjCEnum(actual val name: String = "", actual val swiftName: String = "")
132+
119133
/**
120134
* Meta-annotation that instructs the Kotlin compiler to remove the annotated class, function or property from the public Objective-C API.
121135
*

libraries/stdlib/src/kotlin/annotations/NativeAnnotations.kt

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
/*
2-
* Copyright 2010-2021 JetBrains s.r.o. and Kotlin Programming Language contributors.
2+
* Copyright 2010-2025 JetBrains s.r.o. and Kotlin Programming Language contributors.
33
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
44
*/
55

66
package kotlin.native
77

88
import kotlin.experimental.ExperimentalNativeApi
9+
import kotlin.experimental.ExperimentalObjCEnum
910
import kotlin.experimental.ExperimentalObjCName
1011
import kotlin.experimental.ExperimentalObjCRefinement
1112

@@ -71,6 +72,22 @@ public expect annotation class FreezingIsDeprecated
7172
@SinceKotlin("1.8")
7273
public expect annotation class ObjCName(val name: String = "", val swiftName: String = "", val exact: Boolean = false)
7374

75+
/**
76+
* Instructs the Kotlin compiler to generate a NS_ENUM typedef for the annotated enum class. The name of the generated type will
77+
* be the name of the enum type with "NSEnum" appended. This name can be overridden with the "name" parameter, which is treated
78+
* as an exact name. The enum literals will be prefixed with the type name, as they live in a global namespace.
79+
* Swift naming will automatically remove these disambiguation prefixes. The NSEnum values are accessible via the "nsEnum" property.
80+
*/
81+
@Target(
82+
AnnotationTarget.CLASS,
83+
)
84+
@Retention(AnnotationRetention.BINARY)
85+
@MustBeDocumented
86+
@OptionalExpectation
87+
@ExperimentalObjCEnum
88+
@SinceKotlin("2.3")
89+
public expect annotation class ObjCEnum(val name: String = "", val swiftName: String = "")
90+
7491
/**
7592
* Meta-annotation that instructs the Kotlin compiler to remove the annotated class, function or property from the public Objective-C API.
7693
*
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
/*
2+
* Copyright 2010-2025 JetBrains s.r.o. and Kotlin Programming Language contributors.
3+
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
4+
*/
5+
6+
package kotlin.experimental
7+
8+
/**
9+
* This annotation marks the experimental [ObjCEnum][kotlin.native.ObjCEnum] annotation.
10+
*/
11+
@RequiresOptIn
12+
@Target(AnnotationTarget.ANNOTATION_CLASS)
13+
@Retention(AnnotationRetention.BINARY)
14+
@MustBeDocumented
15+
@SinceKotlin("2.3")
16+
public annotation class ExperimentalObjCEnum

libraries/tools/binary-compatibility-validator/klib-public-api/kotlin-stdlib.api

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,10 @@ open annotation class kotlin.experimental/ExperimentalNativeApi : kotlin/Annotat
5353
constructor <init>() // kotlin.experimental/ExperimentalNativeApi.<init>|<init>(){}[0]
5454
}
5555

56+
open annotation class kotlin.experimental/ExperimentalObjCEnum : kotlin/Annotation { // kotlin.experimental/ExperimentalObjCEnum|null[0]
57+
constructor <init>() // kotlin.experimental/ExperimentalObjCEnum.<init>|<init>(){}[0]
58+
}
59+
5660
open annotation class kotlin.experimental/ExperimentalObjCName : kotlin/Annotation { // kotlin.experimental/ExperimentalObjCName|null[0]
5761
constructor <init>() // kotlin.experimental/ExperimentalObjCName.<init>|<init>(){}[0]
5862
}
@@ -9847,6 +9851,16 @@ open annotation class kotlin.native/NoInline : kotlin/Annotation { // kotlin.nat
98479851
constructor <init>() // kotlin.native/NoInline.<init>|<init>(){}[0]
98489852
}
98499853

9854+
// Targets: [native]
9855+
open annotation class kotlin.native/ObjCEnum : kotlin/Annotation { // kotlin.native/ObjCEnum|null[0]
9856+
constructor <init>(kotlin/String = ..., kotlin/String = ...) // kotlin.native/ObjCEnum.<init>|<init>(kotlin.String;kotlin.String){}[0]
9857+
9858+
final val name // kotlin.native/ObjCEnum.name|{}name[0]
9859+
final fun <get-name>(): kotlin/String // kotlin.native/ObjCEnum.name.<get-name>|<get-name>(){}[0]
9860+
final val swiftName // kotlin.native/ObjCEnum.swiftName|{}swiftName[0]
9861+
final fun <get-swiftName>(): kotlin/String // kotlin.native/ObjCEnum.swiftName.<get-swiftName>|<get-swiftName>(){}[0]
9862+
}
9863+
98509864
// Targets: [native]
98519865
open annotation class kotlin.native/ObjCName : kotlin/Annotation { // kotlin.native/ObjCName|null[0]
98529866
constructor <init>(kotlin/String = ..., kotlin/String = ..., kotlin/Boolean = ...) // kotlin.native/ObjCName.<init>|<init>(kotlin.String;kotlin.String;kotlin.Boolean){}[0]
@@ -13769,6 +13783,16 @@ open annotation class kotlin.native/HidesFromObjC : kotlin/Annotation { // kotli
1376913783
constructor <init>() // kotlin.native/HidesFromObjC.<init>|<init>(){}[1]
1377013784
}
1377113785

13786+
// Targets: [js, wasmJs, wasmWasi]
13787+
open annotation class kotlin.native/ObjCEnum : kotlin/Annotation { // kotlin.native/ObjCEnum|null[1]
13788+
constructor <init>(kotlin/String = ..., kotlin/String = ...) // kotlin.native/ObjCEnum.<init>|<init>(kotlin.String;kotlin.String){}[1]
13789+
13790+
final val name // kotlin.native/ObjCEnum.name|{}name[1]
13791+
final fun <get-name>(): kotlin/String // kotlin.native/ObjCEnum.name.<get-name>|<get-name>(){}[1]
13792+
final val swiftName // kotlin.native/ObjCEnum.swiftName|{}swiftName[1]
13793+
final fun <get-swiftName>(): kotlin/String // kotlin.native/ObjCEnum.swiftName.<get-swiftName>|<get-swiftName>(){}[1]
13794+
}
13795+
1377213796
// Targets: [js, wasmJs, wasmWasi]
1377313797
open annotation class kotlin.native/ObjCName : kotlin/Annotation { // kotlin.native/ObjCName|null[1]
1377413798
constructor <init>(kotlin/String = ..., kotlin/String = ..., kotlin/Boolean = ...) // kotlin.native/ObjCName.<init>|<init>(kotlin.String;kotlin.String;kotlin.Boolean){}[1]

libraries/tools/binary-compatibility-validator/reference-public-api/kotlin-stdlib-runtime-merged.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3264,6 +3264,9 @@ public abstract interface annotation class kotlin/experimental/ExpectRefinement
32643264
public abstract interface annotation class kotlin/experimental/ExperimentalNativeApi : java/lang/annotation/Annotation {
32653265
}
32663266

3267+
public abstract interface annotation class kotlin/experimental/ExperimentalObjCEnum : java/lang/annotation/Annotation {
3268+
}
3269+
32673270
public abstract interface annotation class kotlin/experimental/ExperimentalObjCName : java/lang/annotation/Annotation {
32683271
}
32693272

native/base/src/main/kotlin/org/jetbrains/kotlin/backend/konan/KonanFqNames.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ object KonanFqNames {
3838
val eagerInitialization = FqName("kotlin.native.EagerInitialization")
3939
val noReorderFields = FqName("kotlin.native.internal.NoReorderFields")
4040
val objCName = FqName("kotlin.native.ObjCName")
41+
val objCEnum = FqName("kotlin.native.ObjCEnum")
4142
val hidesFromObjC = FqName("kotlin.native.HidesFromObjC")
4243
val refinesInSwift = FqName("kotlin.native.RefinesInSwift")
4344
val shouldRefineInSwift = FqName("kotlin.native.ShouldRefineInSwift")
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package nativeEnum
2+
3+
import kotlin.native.ObjCEnum
4+
import kotlin.experimental.ExperimentalObjCEnum
5+
6+
@OptIn(kotlin.experimental.ExperimentalObjCEnum::class)
7+
@ObjCEnum("OBJCFoo", swiftName="SwiftFoo")
8+
enum class MyKotlinEnum {
9+
ALPHA, COPY, BAR_FOO
10+
}

0 commit comments

Comments
 (0)