diff --git a/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/llvm/objcexport/ObjCExportCodeGenerator.kt b/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/llvm/objcexport/ObjCExportCodeGenerator.kt index bde5124cd2339..5d34d6f30be5a 100644 --- a/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/llvm/objcexport/ObjCExportCodeGenerator.kt +++ b/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/llvm/objcexport/ObjCExportCodeGenerator.kt @@ -1375,6 +1375,15 @@ private fun ObjCExportCodeGenerator.createArrayConstructorAdapter( return objCToKotlinMethodAdapter(selectorName, methodBridge, imp) } +private fun ObjCExportCodeGenerator.createNSEnumAdapter( + symbol: IrSimpleFunctionSymbol, + methodBridge: MethodBridge, + selectorName: String +): ObjCToKotlinMethodAdapter { + val imp = generateObjCImp(symbol.owner.getLowered(), symbol.owner.getLowered(), methodBridge) + return objCToKotlinMethodAdapter(selectorName, methodBridge, imp) +} + private fun ObjCExportCodeGenerator.vtableIndex(irFunction: IrSimpleFunction): Int? { assert(irFunction.isOverridable) val irClass = irFunction.parentAsClass @@ -1430,6 +1439,9 @@ private fun ObjCExportCodeGenerator.createTypeAdapter( is ObjCInitMethodForKotlinConstructor -> { adapters += createConstructorAdapter(it.baseMethod) } + is ObjCGetterForNSEnumType -> { + adapters += createNSEnumAdapter(it.symbol, it.bridge, it.selector) + } is ObjCFactoryMethodForKotlinArrayConstructor -> { classAdapters += createArrayConstructorAdapter(it.baseMethod) } diff --git a/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/objcexport/ObjCExportCodeSpec.kt b/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/objcexport/ObjCExportCodeSpec.kt index 7e6f8f78705ee..d6fabe41039b9 100644 --- a/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/objcexport/ObjCExportCodeSpec.kt +++ b/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/objcexport/ObjCExportCodeSpec.kt @@ -9,11 +9,12 @@ import org.jetbrains.kotlin.backend.konan.descriptors.contributedMethods import org.jetbrains.kotlin.backend.konan.descriptors.enumEntries import org.jetbrains.kotlin.backend.konan.descriptors.isArray import org.jetbrains.kotlin.backend.konan.descriptors.isInterface +import org.jetbrains.kotlin.backend.konan.descriptors.propertyIfAccessor +import org.jetbrains.kotlin.backend.konan.objcexport.ObjCExportNamer.Companion.nsEnumPropertyName import org.jetbrains.kotlin.builtins.KotlinBuiltIns import org.jetbrains.kotlin.descriptors.* import org.jetbrains.kotlin.ir.ObsoleteDescriptorBasedAPI import org.jetbrains.kotlin.ir.symbols.* -import org.jetbrains.kotlin.ir.util.IdSignature import org.jetbrains.kotlin.ir.util.SymbolTable import org.jetbrains.kotlin.resolve.descriptorUtil.getSuperClassNotAny import java.io.PrintStream @@ -89,6 +90,14 @@ internal fun ObjCExportedInterface.createCodeSpec(symbolTable: SymbolTable): Obj } if (descriptor.kind == ClassKind.ENUM_CLASS) { + if (namer.getNSEnumTypeName(descriptor) != null) { + val superClass = descriptor.getSuperClassNotAny()!! // ordinal is declared in KotlinEnum + val ordinalDescriptor = superClass.contributedMethods.first { it.propertyIfAccessor.name.asString() == "ordinal" } + val symbol = symbolTable.descriptorExtension.referenceSimpleFunction(ordinalDescriptor) + val bridge = mapper.bridgeMethod(ordinalDescriptor) + methods += ObjCGetterForNSEnumType(symbol, bridge, nsEnumPropertyName) + } + descriptor.enumEntries.mapTo(methods) { ObjCGetterForKotlinEnumEntry(symbolTable.descriptorExtension.referenceEnumEntry(it), namer.getEnumEntrySelector(it)) } @@ -160,6 +169,7 @@ internal fun ObjCExportCodeSpec.dumpSelectorToSignatureMapping(path: String) { is ObjCClassMethodForKotlinEnumValuesOrEntries -> false is ObjCGetterForKotlinEnumEntry -> false is ObjCGetterForObjectInstance -> false + is ObjCGetterForNSEnumType -> true } fun ObjCMethodSpec.getMapping(objcClass: String): String? = when (this) { @@ -170,6 +180,7 @@ internal fun ObjCExportCodeSpec.dumpSelectorToSignatureMapping(path: String) { is ObjCInitMethodForKotlinConstructor -> "$objcClass.${baseMethod.selector},${baseMethod.symbol.signature}" is ObjCKotlinThrowableAsErrorMethod -> null is ObjCMethodForKotlinMethod -> "$objcClass.${baseMethod.selector},${baseMethod.symbol.signature}" + is ObjCGetterForNSEnumType -> null } out.println("\n# Instance methods mapping") for (type in types) { @@ -224,6 +235,17 @@ internal class ObjCGetterForKotlinEnumEntry( "ObjC spec of getter `$selector` for `$irEnumEntrySymbol`" } + +internal class ObjCGetterForNSEnumType( + val symbol: IrSimpleFunctionSymbol, + val bridge: MethodBridge, + val selector: String, +) : ObjCMethodSpec() { + override fun toString(): String = + "ObjC spec of $selector for $symbol" +} + + internal class ObjCClassMethodForKotlinEnumValuesOrEntries( val valuesFunctionSymbol: IrFunctionSymbol, val selector: String diff --git a/kotlin-native/runtime/src/main/kotlin/kotlin/native/Annotations.kt b/kotlin-native/runtime/src/main/kotlin/kotlin/native/Annotations.kt index c47c7e300472d..c63c84c66a0a1 100644 --- a/kotlin-native/runtime/src/main/kotlin/kotlin/native/Annotations.kt +++ b/kotlin-native/runtime/src/main/kotlin/kotlin/native/Annotations.kt @@ -6,6 +6,7 @@ package kotlin.native import kotlin.experimental.ExperimentalNativeApi +import kotlin.experimental.ExperimentalObjCEnum import kotlin.experimental.ExperimentalObjCName import kotlin.experimental.ExperimentalObjCRefinement @@ -116,6 +117,19 @@ public actual annotation class CName(actual val externName: String = "", actual @SinceKotlin("1.8") public actual annotation class ObjCName(actual val name: String = "", actual val swiftName: String = "", actual val exact: Boolean = false) +/** + * Instructs the Kotlin compiler to generate a NS_ENUM typedef for the annotated enum class. The name of the generated type will + * be the name of the enum type with "NSEnum" appended. This name can be overridden with the "name" parameter, which is treated + * as an exact name. The enum literals will be prefixed with the type name, as they live in a global namespace. + * Swift naming will automatically remove these prefixes. The native values are accessible via the "nsEnum" property. + */ +@Target(AnnotationTarget.CLASS) +@Retention(AnnotationRetention.BINARY) +@MustBeDocumented +@ExperimentalObjCEnum +@SinceKotlin("2.3") +public actual annotation class ObjCEnum(actual val name: String = "", actual val swiftName: String = "") + /** * Meta-annotation that instructs the Kotlin compiler to remove the annotated class, function or property from the public Objective-C API. * diff --git a/libraries/stdlib/src/kotlin/annotations/NativeAnnotations.kt b/libraries/stdlib/src/kotlin/annotations/NativeAnnotations.kt index 26328c074e7d5..afae21a76157a 100644 --- a/libraries/stdlib/src/kotlin/annotations/NativeAnnotations.kt +++ b/libraries/stdlib/src/kotlin/annotations/NativeAnnotations.kt @@ -1,11 +1,12 @@ /* - * Copyright 2010-2021 JetBrains s.r.o. and Kotlin Programming Language contributors. + * Copyright 2010-2025 JetBrains s.r.o. and Kotlin Programming Language contributors. * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. */ package kotlin.native import kotlin.experimental.ExperimentalNativeApi +import kotlin.experimental.ExperimentalObjCEnum import kotlin.experimental.ExperimentalObjCName import kotlin.experimental.ExperimentalObjCRefinement @@ -71,6 +72,22 @@ public expect annotation class FreezingIsDeprecated @SinceKotlin("1.8") public expect annotation class ObjCName(val name: String = "", val swiftName: String = "", val exact: Boolean = false) +/** + * Instructs the Kotlin compiler to generate a NS_ENUM typedef for the annotated enum class. The name of the generated type will + * be the name of the enum type with "NSEnum" appended. This name can be overridden with the "name" parameter, which is treated + * as an exact name. The enum literals will be prefixed with the type name, as they live in a global namespace. + * Swift naming will automatically remove these disambiguation prefixes. The NSEnum values are accessible via the "nsEnum" property. + */ +@Target( + AnnotationTarget.CLASS, +) +@Retention(AnnotationRetention.BINARY) +@MustBeDocumented +@OptionalExpectation +@ExperimentalObjCEnum +@SinceKotlin("2.3") +public expect annotation class ObjCEnum(val name: String = "", val swiftName: String = "") + /** * Meta-annotation that instructs the Kotlin compiler to remove the annotated class, function or property from the public Objective-C API. * diff --git a/libraries/stdlib/src/kotlin/experimental/ExperimentalObjCEnum.kt b/libraries/stdlib/src/kotlin/experimental/ExperimentalObjCEnum.kt new file mode 100644 index 0000000000000..d6da5fc65a3fb --- /dev/null +++ b/libraries/stdlib/src/kotlin/experimental/ExperimentalObjCEnum.kt @@ -0,0 +1,16 @@ +/* + * Copyright 2010-2025 JetBrains s.r.o. and Kotlin Programming Language contributors. + * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. + */ + +package kotlin.experimental + +/** + * This annotation marks the experimental [ObjCEnum][kotlin.native.ObjCEnum] annotation. + */ +@RequiresOptIn +@Target(AnnotationTarget.ANNOTATION_CLASS) +@Retention(AnnotationRetention.BINARY) +@MustBeDocumented +@SinceKotlin("2.3") +public annotation class ExperimentalObjCEnum diff --git a/libraries/tools/binary-compatibility-validator/klib-public-api/kotlin-stdlib.api b/libraries/tools/binary-compatibility-validator/klib-public-api/kotlin-stdlib.api index 036b12c19108d..a7fb6a3e14c33 100644 --- a/libraries/tools/binary-compatibility-validator/klib-public-api/kotlin-stdlib.api +++ b/libraries/tools/binary-compatibility-validator/klib-public-api/kotlin-stdlib.api @@ -53,6 +53,10 @@ open annotation class kotlin.experimental/ExperimentalNativeApi : kotlin/Annotat constructor () // kotlin.experimental/ExperimentalNativeApi.|(){}[0] } +open annotation class kotlin.experimental/ExperimentalObjCEnum : kotlin/Annotation { // kotlin.experimental/ExperimentalObjCEnum|null[0] + constructor () // kotlin.experimental/ExperimentalObjCEnum.|(){}[0] +} + open annotation class kotlin.experimental/ExperimentalObjCName : kotlin/Annotation { // kotlin.experimental/ExperimentalObjCName|null[0] constructor () // kotlin.experimental/ExperimentalObjCName.|(){}[0] } @@ -9829,6 +9833,16 @@ open annotation class kotlin.native/NoInline : kotlin/Annotation { // kotlin.nat constructor () // kotlin.native/NoInline.|(){}[0] } +// Targets: [native] +open annotation class kotlin.native/ObjCEnum : kotlin/Annotation { // kotlin.native/ObjCEnum|null[0] + constructor (kotlin/String = ..., kotlin/String = ...) // kotlin.native/ObjCEnum.|(kotlin.String;kotlin.String){}[0] + + final val name // kotlin.native/ObjCEnum.name|{}name[0] + final fun (): kotlin/String // kotlin.native/ObjCEnum.name.|(){}[0] + final val swiftName // kotlin.native/ObjCEnum.swiftName|{}swiftName[0] + final fun (): kotlin/String // kotlin.native/ObjCEnum.swiftName.|(){}[0] +} + // Targets: [native] open annotation class kotlin.native/ObjCName : kotlin/Annotation { // kotlin.native/ObjCName|null[0] constructor (kotlin/String = ..., kotlin/String = ..., kotlin/Boolean = ...) // kotlin.native/ObjCName.|(kotlin.String;kotlin.String;kotlin.Boolean){}[0] @@ -13739,6 +13753,16 @@ open annotation class kotlin.native/HidesFromObjC : kotlin/Annotation { // kotli constructor () // kotlin.native/HidesFromObjC.|(){}[1] } +// Targets: [js, wasmJs, wasmWasi] +open annotation class kotlin.native/ObjCEnum : kotlin/Annotation { // kotlin.native/ObjCEnum|null[1] + constructor (kotlin/String = ..., kotlin/String = ...) // kotlin.native/ObjCEnum.|(kotlin.String;kotlin.String){}[1] + + final val name // kotlin.native/ObjCEnum.name|{}name[1] + final fun (): kotlin/String // kotlin.native/ObjCEnum.name.|(){}[1] + final val swiftName // kotlin.native/ObjCEnum.swiftName|{}swiftName[1] + final fun (): kotlin/String // kotlin.native/ObjCEnum.swiftName.|(){}[1] +} + // Targets: [js, wasmJs, wasmWasi] open annotation class kotlin.native/ObjCName : kotlin/Annotation { // kotlin.native/ObjCName|null[1] constructor (kotlin/String = ..., kotlin/String = ..., kotlin/Boolean = ...) // kotlin.native/ObjCName.|(kotlin.String;kotlin.String;kotlin.Boolean){}[1] diff --git a/libraries/tools/binary-compatibility-validator/reference-public-api/kotlin-stdlib-runtime-merged.txt b/libraries/tools/binary-compatibility-validator/reference-public-api/kotlin-stdlib-runtime-merged.txt index 20c895329e8a2..8c168a719cdb5 100644 --- a/libraries/tools/binary-compatibility-validator/reference-public-api/kotlin-stdlib-runtime-merged.txt +++ b/libraries/tools/binary-compatibility-validator/reference-public-api/kotlin-stdlib-runtime-merged.txt @@ -3257,6 +3257,9 @@ public abstract interface annotation class kotlin/experimental/ExpectRefinement public abstract interface annotation class kotlin/experimental/ExperimentalNativeApi : java/lang/annotation/Annotation { } +public abstract interface annotation class kotlin/experimental/ExperimentalObjCEnum : java/lang/annotation/Annotation { +} + public abstract interface annotation class kotlin/experimental/ExperimentalObjCName : java/lang/annotation/Annotation { } diff --git a/native/base/src/main/kotlin/org/jetbrains/kotlin/backend/konan/KonanFqNames.kt b/native/base/src/main/kotlin/org/jetbrains/kotlin/backend/konan/KonanFqNames.kt index c06ccf588adbc..d25af6acedb2f 100644 --- a/native/base/src/main/kotlin/org/jetbrains/kotlin/backend/konan/KonanFqNames.kt +++ b/native/base/src/main/kotlin/org/jetbrains/kotlin/backend/konan/KonanFqNames.kt @@ -38,6 +38,7 @@ object KonanFqNames { val eagerInitialization = FqName("kotlin.native.EagerInitialization") val noReorderFields = FqName("kotlin.native.internal.NoReorderFields") val objCName = FqName("kotlin.native.ObjCName") + val objCEnum = FqName("kotlin.native.ObjCEnum") val hidesFromObjC = FqName("kotlin.native.HidesFromObjC") val refinesInSwift = FqName("kotlin.native.RefinesInSwift") val shouldRefineInSwift = FqName("kotlin.native.ShouldRefineInSwift") diff --git a/native/native.tests/testData/framework/objcexport/nativeEnum.kt b/native/native.tests/testData/framework/objcexport/nativeEnum.kt new file mode 100644 index 0000000000000..da206abf0db3b --- /dev/null +++ b/native/native.tests/testData/framework/objcexport/nativeEnum.kt @@ -0,0 +1,10 @@ +package nativeEnum + +import kotlin.native.ObjCEnum +import kotlin.experimental.ExperimentalObjCEnum + +@OptIn(kotlin.experimental.ExperimentalObjCEnum::class) +@ObjCEnum("OBJCFoo") +enum class MyKotlinEnum { + ALPHA, COPY, BAR_FOO +} \ No newline at end of file diff --git a/native/native.tests/testData/framework/objcexport/nativeEnum.swift b/native/native.tests/testData/framework/objcexport/nativeEnum.swift new file mode 100644 index 0000000000000..de244f984e516 --- /dev/null +++ b/native/native.tests/testData/framework/objcexport/nativeEnum.swift @@ -0,0 +1,21 @@ +import Kt + + +private func testNativeEnumValues() throws { + let ktEnum = MyKotlinEnum.alpha + let nsEnum = ktEnum.nsEnum + + switch(nsEnum) { + case .alpha: try assertEquals(actual: nsEnum, expected: ktEnum.nsEnum) + case .barFoo: try fail() + case .theCopy: try fail() + } +} + +class NativeEnumTests : SimpleTestProvider { + override init() { + super.init() + + test("TestNativeEnumValues", testNativeEnumValues) + } +} diff --git a/native/objcexport-header-generator/impl/analysis-api/src/org/jetbrains/kotlin/objcexport/ObjCExportTranslatedClass.kt b/native/objcexport-header-generator/impl/analysis-api/src/org/jetbrains/kotlin/objcexport/ObjCExportTranslatedClass.kt new file mode 100644 index 0000000000000..1bb1d7607002f --- /dev/null +++ b/native/objcexport-header-generator/impl/analysis-api/src/org/jetbrains/kotlin/objcexport/ObjCExportTranslatedClass.kt @@ -0,0 +1,17 @@ +/* + * Copyright 2010-2025 JetBrains s.r.o. and Kotlin Programming Language contributors. + * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. + */ + +package org.jetbrains.kotlin.objcexport + +import org.jetbrains.kotlin.backend.konan.objcexport.ObjCClass +import org.jetbrains.kotlin.backend.konan.objcexport.ObjCTopLevel + +class ObjCExportTranslatedClass( + val auxiliaryDeclarations: List, + val objCClass: ObjCClass, +) + +fun ObjCExportTranslatedClass(objCClass: ObjCClass?) = objCClass?.let { ObjCExportTranslatedClass(emptyList(), it) } + diff --git a/native/objcexport-header-generator/impl/analysis-api/src/org/jetbrains/kotlin/objcexport/ObjCPropertyNames.kt b/native/objcexport-header-generator/impl/analysis-api/src/org/jetbrains/kotlin/objcexport/ObjCPropertyNames.kt index 335f46f66319e..d6f66d046979f 100644 --- a/native/objcexport-header-generator/impl/analysis-api/src/org/jetbrains/kotlin/objcexport/ObjCPropertyNames.kt +++ b/native/objcexport-header-generator/impl/analysis-api/src/org/jetbrains/kotlin/objcexport/ObjCPropertyNames.kt @@ -11,4 +11,6 @@ internal object ObjCPropertyNames { @Suppress("unused") const val companionObjectPropertyName: String = "companion" + + const val nsEnumPropertyName: String = "nsEnum" } \ No newline at end of file diff --git a/native/objcexport-header-generator/impl/analysis-api/src/org/jetbrains/kotlin/objcexport/analysisApiUtils/getObjCDocumentedAnnotations.kt b/native/objcexport-header-generator/impl/analysis-api/src/org/jetbrains/kotlin/objcexport/analysisApiUtils/getObjCDocumentedAnnotations.kt index d165465de784e..91b16441fdc40 100644 --- a/native/objcexport-header-generator/impl/analysis-api/src/org/jetbrains/kotlin/objcexport/analysisApiUtils/getObjCDocumentedAnnotations.kt +++ b/native/objcexport-header-generator/impl/analysis-api/src/org/jetbrains/kotlin/objcexport/analysisApiUtils/getObjCDocumentedAnnotations.kt @@ -38,6 +38,7 @@ internal fun KaSession.getObjCDocumentedAnnotations(list: KaAnnotationList): Lis private val mustBeDocumentedAnnotationsStopList = setOf( StandardNames.FqNames.deprecated, + KonanFqNames.objCEnum, KonanFqNames.objCName, - KonanFqNames.shouldRefineInSwift + KonanFqNames.shouldRefineInSwift, ) \ No newline at end of file diff --git a/native/objcexport-header-generator/impl/analysis-api/src/org/jetbrains/kotlin/objcexport/getNSEnumTypeName.kt b/native/objcexport-header-generator/impl/analysis-api/src/org/jetbrains/kotlin/objcexport/getNSEnumTypeName.kt new file mode 100644 index 0000000000000..b137b7e368fbd --- /dev/null +++ b/native/objcexport-header-generator/impl/analysis-api/src/org/jetbrains/kotlin/objcexport/getNSEnumTypeName.kt @@ -0,0 +1,22 @@ +/* + * Copyright 2010-2025 JetBrains s.r.o. and Kotlin Programming Language contributors. + * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. + */ + +package org.jetbrains.kotlin.objcexport + +import org.jetbrains.kotlin.analysis.api.symbols.KaClassSymbol +import org.jetbrains.kotlin.backend.konan.KonanFqNames +import org.jetbrains.kotlin.backend.konan.objcexport.ObjCExportNSEnumTypeName +import org.jetbrains.kotlin.name.ClassId + +/** Returns the NSEnum type for the given enum type if the corresponding annotation is set; null otherwise */ +fun ObjCExportContext.getNSEnumTypeName(symbol: KaClassSymbol): ObjCExportNSEnumTypeName? { + val classId = ClassId.topLevel(KonanFqNames.objCEnum) + val annotation = symbol.annotations[classId].firstOrNull() ?: return null + + val name = annotation.findArgument("name")?.resolveStringConstantValue()?.ifEmpty { null } + ?: (getObjCClassOrProtocolName(symbol).objCName + "NSEnum") + val swiftName = annotation.findArgument("swiftName")?.resolveStringConstantValue()?.ifEmpty { null } ?: name + return ObjCExportNSEnumTypeName(swiftName = swiftName, objCName = name) +} \ No newline at end of file diff --git a/native/objcexport-header-generator/impl/analysis-api/src/org/jetbrains/kotlin/objcexport/resolveObjCNameAnnotation.kt b/native/objcexport-header-generator/impl/analysis-api/src/org/jetbrains/kotlin/objcexport/resolveObjCNameAnnotation.kt index c7331e26ce43e..3eb7adfbc4c85 100644 --- a/native/objcexport-header-generator/impl/analysis-api/src/org/jetbrains/kotlin/objcexport/resolveObjCNameAnnotation.kt +++ b/native/objcexport-header-generator/impl/analysis-api/src/org/jetbrains/kotlin/objcexport/resolveObjCNameAnnotation.kt @@ -53,11 +53,11 @@ internal fun KaAnnotatedSymbol.resolveObjCNameAnnotation(): ObjCExportObjCNameAn ) } -private fun KaAnnotation.findArgument(name: String): KaNamedAnnotationValue? { +internal fun KaAnnotation.findArgument(name: String): KaNamedAnnotationValue? { return arguments.find { it.name.identifier == name } } -private fun KaNamedAnnotationValue.resolveStringConstantValue(): String? { +internal fun KaNamedAnnotationValue.resolveStringConstantValue(): String? { return expression.let { it as? KaAnnotationValue.ConstantValue }?.value ?.let { it as? StringValue } ?.value diff --git a/native/objcexport-header-generator/impl/analysis-api/src/org/jetbrains/kotlin/objcexport/translateEnumMembers.kt b/native/objcexport-header-generator/impl/analysis-api/src/org/jetbrains/kotlin/objcexport/translateEnumMembers.kt index c2ec41e5d005f..a6e07dddd83af 100644 --- a/native/objcexport-header-generator/impl/analysis-api/src/org/jetbrains/kotlin/objcexport/translateEnumMembers.kt +++ b/native/objcexport-header-generator/impl/analysis-api/src/org/jetbrains/kotlin/objcexport/translateEnumMembers.kt @@ -78,7 +78,7 @@ private fun ObjCExportContext.getEnumEntriesProperty(symbol: KaClassSymbol): Obj /** * See K1 implementation as [org.jetbrains.kotlin.backend.konan.objcexport.ObjCExportNamerImpl.getEnumEntryName] */ -private fun ObjCExportContext.getEnumEntryName(symbol: KaEnumEntrySymbol, forSwift: Boolean): String { +internal fun ObjCExportContext.getEnumEntryName(symbol: KaEnumEntrySymbol, forSwift: Boolean): String { val name = getObjCPropertyName(symbol).run { when { diff --git a/native/objcexport-header-generator/impl/analysis-api/src/org/jetbrains/kotlin/objcexport/translateToNSEnum.kt b/native/objcexport-header-generator/impl/analysis-api/src/org/jetbrains/kotlin/objcexport/translateToNSEnum.kt new file mode 100644 index 0000000000000..8c5125e6a06c7 --- /dev/null +++ b/native/objcexport-header-generator/impl/analysis-api/src/org/jetbrains/kotlin/objcexport/translateToNSEnum.kt @@ -0,0 +1,47 @@ +/* + * Copyright 2010-2025 JetBrains s.r.o. and Kotlin Programming Language contributors. + * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. + */ + +package org.jetbrains.kotlin.objcexport + +import org.jetbrains.kotlin.analysis.api.symbols.KaClassSymbol +import org.jetbrains.kotlin.analysis.api.symbols.KaEnumEntrySymbol +import org.jetbrains.kotlin.backend.konan.objcexport.ObjCExportNSEnumTypeName +import org.jetbrains.kotlin.backend.konan.objcexport.ObjCNSEnum +import org.jetbrains.kotlin.backend.konan.objcexport.ObjCProperty +import org.jetbrains.kotlin.backend.konan.objcexport.ObjCRawType +import org.jetbrains.kotlin.backend.konan.objcexport.ObjCTopLevel +import org.jetbrains.kotlin.backend.konan.objcexport.ObjCExportNativeEnumEntry +import org.jetbrains.kotlin.backend.konan.objcexport.ObjCExportStubOrigin + +internal fun ObjCExportContext.translateNSEnum( + symbol: KaClassSymbol, + origin: ObjCExportStubOrigin, + nsEnumTypeName: ObjCExportNSEnumTypeName, + auxiliaryDeclarations: MutableList +): ObjCProperty { + auxiliaryDeclarations.add( + ObjCNSEnum(nsEnumTypeName.objCName, nsEnumTypeName.swiftName, origin, getNSEnumEntries(symbol, nsEnumTypeName.objCName))) + return ObjCProperty( + ObjCPropertyNames.nsEnumPropertyName, + null, + null, + ObjCRawType(nsEnumTypeName.objCName), + listOf("readonly") + ) +} + + +private fun ObjCExportContext.getNSEnumEntries(symbol: KaClassSymbol, objCTypeName: String): List { + val staticMembers = with(analysisSession) { symbol.staticDeclaredMemberScope }.callables.toList() + // Map the enum entries in declaration order, preserving the ordinal + return staticMembers.filterIsInstance().mapIndexed { ordinal, entry -> + ObjCExportNativeEnumEntry( + getEnumEntryName(entry, true), + objCTypeName + getEnumEntryName(entry, false).replaceFirstChar { it.uppercaseChar() }, + ordinal + ) + } +} + diff --git a/native/objcexport-header-generator/impl/analysis-api/src/org/jetbrains/kotlin/objcexport/translateToObjCClass.kt b/native/objcexport-header-generator/impl/analysis-api/src/org/jetbrains/kotlin/objcexport/translateToObjCClass.kt index 7232d036137d6..f910a2660b43e 100644 --- a/native/objcexport-header-generator/impl/analysis-api/src/org/jetbrains/kotlin/objcexport/translateToObjCClass.kt +++ b/native/objcexport-header-generator/impl/analysis-api/src/org/jetbrains/kotlin/objcexport/translateToObjCClass.kt @@ -15,7 +15,8 @@ import org.jetbrains.kotlin.analysis.api.export.utilities.isThrowable import org.jetbrains.kotlin.objcexport.analysisApiUtils.isVisibleInObjC -fun ObjCExportContext.translateToObjCClass(symbol: KaClassSymbol): ObjCClass? = withClassifierContext(symbol) { + +fun ObjCExportContext.translateToObjCClass(symbol: KaClassSymbol): ObjCExportTranslatedClass? = withClassifierContext(symbol) { require( symbol.classKind == KaClassKind.CLASS || symbol.classKind == KaClassKind.ENUM_CLASS || @@ -37,6 +38,7 @@ fun ObjCExportContext.translateToObjCClass(symbol: KaClassSymbol): ObjCClass? = val superClass = translateSuperClass(symbol) val superProtocols: List = superProtocols(symbol) + val auxiliaryDeclarations = mutableListOf() val members = buildList { /* The order of members tries to replicate the K1 implementation explicitly */ @@ -51,6 +53,9 @@ fun ObjCExportContext.translateToObjCClass(symbol: KaClassSymbol): ObjCClass? = .flatMap { translateToObjCExportStub(it) } if (symbol.classKind == KaClassKind.ENUM_CLASS) { + getNSEnumTypeName(symbol)?.let { + this += translateNSEnum(symbol, origin,it, auxiliaryDeclarations) + } this += translateEnumMembers(symbol) } @@ -69,17 +74,20 @@ fun ObjCExportContext.translateToObjCClass(symbol: KaClassSymbol): ObjCClass? = ) } - ObjCInterfaceImpl( - name = name.objCName, - comment = comment, - origin = origin, - attributes = attributes, - superProtocols = superProtocols, - members = members, - categoryName = categoryName, - generics = generics, - superClass = superClass.superClassName.objCName, - superClassGenerics = superClass.superClassGenerics + ObjCExportTranslatedClass( + auxiliaryDeclarations.toList(), + ObjCInterfaceImpl( + name = name.objCName, + comment = comment, + origin = origin, + attributes = attributes, + superProtocols = superProtocols, + members = members, + categoryName = categoryName, + generics = generics, + superClass = superClass.superClassName.objCName, + superClassGenerics = superClass.superClassGenerics + ) ) } diff --git a/native/objcexport-header-generator/impl/analysis-api/src/org/jetbrains/kotlin/objcexport/translateToObjCExportStub.kt b/native/objcexport-header-generator/impl/analysis-api/src/org/jetbrains/kotlin/objcexport/translateToObjCExportStub.kt index 9bf79d228769d..66e1e58edf372 100644 --- a/native/objcexport-header-generator/impl/analysis-api/src/org/jetbrains/kotlin/objcexport/translateToObjCExportStub.kt +++ b/native/objcexport-header-generator/impl/analysis-api/src/org/jetbrains/kotlin/objcexport/translateToObjCExportStub.kt @@ -6,7 +6,6 @@ package org.jetbrains.kotlin.objcexport import org.jetbrains.kotlin.analysis.api.symbols.* -import org.jetbrains.kotlin.backend.konan.objcexport.ObjCClass import org.jetbrains.kotlin.backend.konan.objcexport.ObjCExportStub import org.jetbrains.kotlin.utils.addIfNotNull @@ -32,11 +31,11 @@ internal fun ObjCExportContext.translateToObjCExportStub(symbol: KaCallableSymbo return result } -internal fun ObjCExportContext.translateToObjCExportStub(symbol: KaClassSymbol): ObjCClass? = when (symbol.classKind) { - KaClassKind.INTERFACE -> translateToObjCProtocol(symbol) +internal fun ObjCExportContext.translateToObjCExportStub(symbol: KaClassSymbol): ObjCExportTranslatedClass? = when (symbol.classKind) { + KaClassKind.INTERFACE -> ObjCExportTranslatedClass(translateToObjCProtocol(symbol)) KaClassKind.CLASS -> translateToObjCClass(symbol) - KaClassKind.OBJECT -> translateToObjCObject(symbol) + KaClassKind.OBJECT -> ObjCExportTranslatedClass(translateToObjCObject(symbol)) KaClassKind.ENUM_CLASS -> translateToObjCClass(symbol) - KaClassKind.COMPANION_OBJECT -> translateToObjCObject(symbol) + KaClassKind.COMPANION_OBJECT -> ObjCExportTranslatedClass(translateToObjCObject(symbol)) else -> null } diff --git a/native/objcexport-header-generator/impl/analysis-api/src/org/jetbrains/kotlin/objcexport/translateToObjCHeader.kt b/native/objcexport-header-generator/impl/analysis-api/src/org/jetbrains/kotlin/objcexport/translateToObjCHeader.kt index 12a8c34ffed23..a8dd797e7dd3c 100644 --- a/native/objcexport-header-generator/impl/analysis-api/src/org/jetbrains/kotlin/objcexport/translateToObjCHeader.kt +++ b/native/objcexport-header-generator/impl/analysis-api/src/org/jetbrains/kotlin/objcexport/translateToObjCHeader.kt @@ -147,7 +147,8 @@ private class KtObjCExportHeaderGenerator( * Translate: Note: Even if the result was 'null', the classId will still be marked as 'handled' by adding it * to the [objCStubsByClassId] index. */ - val objCClass = translateToObjCExportStub(symbol) + val translated = translateToObjCExportStub(symbol) + val objCClass = translated?.objCClass objCStubsByClassId[classId] = objCClass objCClass ?: return null @@ -170,9 +171,8 @@ private class KtObjCExportHeaderGenerator( } } - /* Note: It is important to add *this* stub to the result list only after translating/processing the superclass symbols */ - addObjCStubIfNotTranslated(objCClass, symbol.classId?.packageFqName?.asString()) + addObjCStubIfNotTranslated(objCClass, symbol.classId?.packageFqName?.asString(), translated.auxiliaryDeclarations) enqueueDependencyClasses(objCClass) return objCClass } @@ -202,6 +202,7 @@ private class KtObjCExportHeaderGenerator( is ObjCProperty -> listOf(childStub.type) is ObjCInterface -> childStub.superClassGenerics is ObjCTopLevel -> emptyList() + is ObjCNSEnum -> emptyList() } }.map { type -> /** @@ -284,11 +285,16 @@ private class KtObjCExportHeaderGenerator( * K1 also uses a dedicated hash map, but filtering out is spread across the translation traversal. * See the usage of [org.jetbrains.kotlin.backend.konan.objcexport.ObjCExportHeaderGenerator.generatedClasses]. */ - private fun addObjCStubIfNotTranslated(objCClass: ObjCClass, packageFqn: String? = "") { + private fun addObjCStubIfNotTranslated( + objCClass: ObjCClass, + packageFqn: String? = "", + auxiliaryDeclarations: List = emptyList() + ) { val key = ObjCClassKey(objCClass.name, packageFqn, (objCClass as? ObjCInterface)?.categoryName) val translatedClass = objCStubsByClassKey[key] if (translatedClass != null) return objCStubsByClassKey[key] = objCClass + objCStubs += auxiliaryDeclarations objCStubs += objCClass } } diff --git a/native/objcexport-header-generator/impl/analysis-api/test/org/jetbrains/kotlin/objcexport/tests/ObjCExportStubOriginTest.kt b/native/objcexport-header-generator/impl/analysis-api/test/org/jetbrains/kotlin/objcexport/tests/ObjCExportStubOriginTest.kt index 7f9c244a44e0b..6db02cfe41793 100644 --- a/native/objcexport-header-generator/impl/analysis-api/test/org/jetbrains/kotlin/objcexport/tests/ObjCExportStubOriginTest.kt +++ b/native/objcexport-header-generator/impl/analysis-api/test/org/jetbrains/kotlin/objcexport/tests/ObjCExportStubOriginTest.kt @@ -31,7 +31,7 @@ class ObjCExportStubOriginTest( ) { file -> val classSymbols = file.classifierSymbols val objectSymbol = classSymbols.single() - val objectStub = translateToObjCExportStub(objectSymbol) + val objectStub = translateToObjCExportStub(objectSymbol)?.objCClass assertIs(objectStub, "Object stub should be an interface") val sharedProperty = objectStub.members.firstOrNull { @@ -85,10 +85,10 @@ class ObjCExportStubOriginTest( } assertNotNull(classSymbol, "Class symbol should be present") - val companionObjectStub = translateToObjCExportStub(companionObjectSymbol) + val companionObjectStub = translateToObjCExportStub(companionObjectSymbol)?.objCClass assertIs(companionObjectStub, "Object stub should be an interface") - val classStub = translateToObjCExportStub(classSymbol) + val classStub = translateToObjCExportStub(classSymbol)?.objCClass assertIs(classStub, "Class stub should be an interface") val sharedProperty = companionObjectStub.members.firstOrNull { diff --git a/native/objcexport-header-generator/impl/analysis-api/test/org/jetbrains/kotlin/objcexport/tests/TypeParametersTranslationTests.kt b/native/objcexport-header-generator/impl/analysis-api/test/org/jetbrains/kotlin/objcexport/tests/TypeParametersTranslationTests.kt index 34dd6f67bc81c..9b7e91a19cceb 100644 --- a/native/objcexport-header-generator/impl/analysis-api/test/org/jetbrains/kotlin/objcexport/tests/TypeParametersTranslationTests.kt +++ b/native/objcexport-header-generator/impl/analysis-api/test/org/jetbrains/kotlin/objcexport/tests/TypeParametersTranslationTests.kt @@ -192,8 +192,8 @@ class TypeParametersTranslationTests( val foo = analysisSession.getClassOrFail(file, "Foo") val bar = analysisSession.getClassOrFail(file, "Bar") - val fooObjC = translateToObjCClass(foo) - val barObjC = translateToObjCClass(bar) + val fooObjC = translateToObjCClass(foo)?.objCClass + val barObjC = translateToObjCClass(bar)?.objCClass val initFoo = fooObjC?.members?.first { it.name.startsWith("initWith") } as? ObjCMethod ?: error("no initWith constructor were translate for Foo") diff --git a/native/objcexport-header-generator/impl/analysis-api/test/org/jetbrains/kotlin/objcexport/tests/mangling/ManglingConflictsTest.kt b/native/objcexport-header-generator/impl/analysis-api/test/org/jetbrains/kotlin/objcexport/tests/mangling/ManglingConflictsTest.kt index 21e2fb92ab671..d3ad224a47b6f 100644 --- a/native/objcexport-header-generator/impl/analysis-api/test/org/jetbrains/kotlin/objcexport/tests/mangling/ManglingConflictsTest.kt +++ b/native/objcexport-header-generator/impl/analysis-api/test/org/jetbrains/kotlin/objcexport/tests/mangling/ManglingConflictsTest.kt @@ -28,7 +28,7 @@ class ManglingConflictsTest( } """.trimMargin() ) { foo -> - val members = translateToObjCExportStub(foo)?.members ?: error("no translated members") + val members = translateToObjCExportStub(foo)?.objCClass?.members ?: error("no translated members") assertTrue(members.hasPropertiesConflicts()) } } @@ -43,7 +43,7 @@ class ManglingConflictsTest( } """.trimMargin() ) { foo -> - val stub = translateToObjCExportStub(foo) + val stub = translateToObjCExportStub(foo)?.objCClass assertTrue(stub?.members?.hasPropertiesConflicts() == false) } } @@ -58,7 +58,7 @@ class ManglingConflictsTest( } """.trimMargin() ) { foo -> - val stub = translateToObjCExportStub(foo) + val stub = translateToObjCExportStub(foo)?.objCClass assertTrue(stub?.members?.hasMethodConflicts() == true) } } @@ -73,7 +73,7 @@ class ManglingConflictsTest( } """.trimMargin() ) { foo -> - val stub = translateToObjCExportStub(foo) + val stub = translateToObjCExportStub(foo)?.objCClass assertTrue(stub?.members?.hasPropertiesConflicts() == false) } } diff --git a/native/objcexport-header-generator/impl/analysis-api/test/org/jetbrains/kotlin/objcexport/tests/mangling/MethodManglingTest.kt b/native/objcexport-header-generator/impl/analysis-api/test/org/jetbrains/kotlin/objcexport/tests/mangling/MethodManglingTest.kt index 30ca8dbc7b9a0..b0103737b58f3 100644 --- a/native/objcexport-header-generator/impl/analysis-api/test/org/jetbrains/kotlin/objcexport/tests/mangling/MethodManglingTest.kt +++ b/native/objcexport-header-generator/impl/analysis-api/test/org/jetbrains/kotlin/objcexport/tests/mangling/MethodManglingTest.kt @@ -29,7 +29,7 @@ class MethodManglingTest( } """.trimMargin() ) { foo -> - val stub = translateToObjCExportStub(foo) + val stub = translateToObjCExportStub(foo)?.objCClass val methods = stub?.members ?: error("no translated members") val mangledMethods = mangleObjCMethods(methods, stub).filterIsInstance().filter { it.name.startsWith("bar") } @@ -49,7 +49,7 @@ class MethodManglingTest( } """.trimMargin() ) { foo -> - val stub = translateToObjCExportStub(foo) + val stub = translateToObjCExportStub(foo)?.objCClass val methods = stub?.members ?: error("no translated members") val mangledMethods = mangleObjCMethods(methods, stub).filterIsInstance().filter { it.name.startsWith("bar") } diff --git a/native/objcexport-header-generator/impl/analysis-api/test/org/jetbrains/kotlin/objcexport/tests/mangling/PropertyManglingTest.kt b/native/objcexport-header-generator/impl/analysis-api/test/org/jetbrains/kotlin/objcexport/tests/mangling/PropertyManglingTest.kt index 997e66c981b8c..5f69caf3008d0 100644 --- a/native/objcexport-header-generator/impl/analysis-api/test/org/jetbrains/kotlin/objcexport/tests/mangling/PropertyManglingTest.kt +++ b/native/objcexport-header-generator/impl/analysis-api/test/org/jetbrains/kotlin/objcexport/tests/mangling/PropertyManglingTest.kt @@ -28,7 +28,7 @@ class PropertyManglingTest( } """.trimMargin() ) { foo -> - val properties = translateToObjCExportStub(foo)?.members ?: error("no translated members") + val properties = translateToObjCExportStub(foo)?.objCClass?.members ?: error("no translated members") val property = mangleObjCProperties(properties).filterIsInstance().first() assertEquals("bar", property.name) @@ -46,7 +46,7 @@ class PropertyManglingTest( } """.trimMargin() ) { foo -> - val properties = translateToObjCExportStub(foo)?.members ?: error("no translated members") + val properties = translateToObjCExportStub(foo)?.objCClass?.members ?: error("no translated members") val property = mangleObjCProperties(properties).filterIsInstance().first() assertEquals("bar", property.name) diff --git a/native/objcexport-header-generator/impl/k1/src/org/jetbrains/kotlin/backend/konan/objcexport/ObjCExportHeaderGenerator.kt b/native/objcexport-header-generator/impl/k1/src/org/jetbrains/kotlin/backend/konan/objcexport/ObjCExportHeaderGenerator.kt index 1b60e07c1b579..dbfc852141e80 100644 --- a/native/objcexport-header-generator/impl/k1/src/org/jetbrains/kotlin/backend/konan/objcexport/ObjCExportHeaderGenerator.kt +++ b/native/objcexport-header-generator/impl/k1/src/org/jetbrains/kotlin/backend/konan/objcexport/ObjCExportHeaderGenerator.kt @@ -198,7 +198,9 @@ abstract class ObjCExportHeaderGenerator @InternalKotlinNativeApi constructor( private fun generateClass(descriptor: ClassDescriptor) { if (!generatedClasses.add(descriptor)) return - stubs.add(translator.translateClass(descriptor)) + val translatedClass = translator.translateClass(descriptor) + stubs.addAll(translatedClass.auxiliaryDeclarations) + stubs.add(translatedClass.objCInterface) } private fun generateInterface(descriptor: ClassDescriptor) { diff --git a/native/objcexport-header-generator/impl/k1/src/org/jetbrains/kotlin/backend/konan/objcexport/ObjCExportLazy.kt b/native/objcexport-header-generator/impl/k1/src/org/jetbrains/kotlin/backend/konan/objcexport/ObjCExportLazy.kt index f3097839ba44d..8606dcb13c362 100644 --- a/native/objcexport-header-generator/impl/k1/src/org/jetbrains/kotlin/backend/konan/objcexport/ObjCExportLazy.kt +++ b/native/objcexport-header-generator/impl/k1/src/org/jetbrains/kotlin/backend/konan/objcexport/ObjCExportLazy.kt @@ -356,7 +356,7 @@ class ObjCExportLazyImpl( override val origin: ObjCExportStubOrigin? by lazy { ObjCExportStubOrigin(descriptor) } - override fun computeRealStub(): ObjCInterface = lazy.translator.translateClass(descriptor) + override fun computeRealStub(): ObjCInterface = lazy.translator.translateClass(descriptor).objCInterface } private class LazyObjCFileInterface( diff --git a/native/objcexport-header-generator/impl/k1/src/org/jetbrains/kotlin/backend/konan/objcexport/ObjCExportNamer.kt b/native/objcexport-header-generator/impl/k1/src/org/jetbrains/kotlin/backend/konan/objcexport/ObjCExportNamer.kt index 863a7a610597c..d7dc1487b864b 100644 --- a/native/objcexport-header-generator/impl/k1/src/org/jetbrains/kotlin/backend/konan/objcexport/ObjCExportNamer.kt +++ b/native/objcexport-header-generator/impl/k1/src/org/jetbrains/kotlin/backend/konan/objcexport/ObjCExportNamer.kt @@ -98,6 +98,15 @@ interface ObjCExportNamer { fun getCompanionObjectPropertySelector(descriptor: ClassDescriptor): String fun needsExplicitMethodFamily(name: String): Boolean + // Null means no NSEnum property. + fun getNSEnumTypeName(descriptor: ClassDescriptor): ObjCExportNSEnumTypeName? = + descriptor.annotations.findAnnotation(KonanFqNames.objCEnum)?.let { + val name = ((it.argumentValue("name")?.value as String?)?.ifEmpty { null }) + ?: "${getClassOrProtocolName(descriptor).objCName}NSEnum" + val swiftName = ((it.argumentValue("name")?.value as String?)?.ifEmpty { null }) ?: name + ObjCExportNSEnumTypeName(swiftName = swiftName, objCName = name) + } + companion object { @InternalKotlinNativeApi const val kotlinThrowableAsErrorMethodName: String = "asError" @@ -107,6 +116,9 @@ interface ObjCExportNamer { @InternalKotlinNativeApi const val companionObjectPropertyName: String = "companion" + + @InternalKotlinNativeApi + const val nsEnumPropertyName: String = "nsEnum" } } diff --git a/native/objcexport-header-generator/impl/k1/src/org/jetbrains/kotlin/backend/konan/objcexport/ObjCExportTranslator.kt b/native/objcexport-header-generator/impl/k1/src/org/jetbrains/kotlin/backend/konan/objcexport/ObjCExportTranslator.kt index 280f43af52df2..39868b36682e1 100644 --- a/native/objcexport-header-generator/impl/k1/src/org/jetbrains/kotlin/backend/konan/objcexport/ObjCExportTranslator.kt +++ b/native/objcexport-header-generator/impl/k1/src/org/jetbrains/kotlin/backend/konan/objcexport/ObjCExportTranslator.kt @@ -32,11 +32,16 @@ import org.jetbrains.kotlin.types.typeUtil.supertypes import org.jetbrains.kotlin.utils.addIfNotNull import kotlin.* +class ObjCExportTranslatedClass( + val auxiliaryDeclarations: List = emptyList(), + val objCInterface: ObjCInterface, +) + interface ObjCExportTranslator { fun generateBaseDeclarations(): List fun getClassIfExtension(receiverType: KotlinType): ClassDescriptor? fun translateFile(file: SourceFile, declarations: List): ObjCInterface - fun translateClass(descriptor: ClassDescriptor): ObjCInterface + fun translateClass(descriptor: ClassDescriptor): ObjCExportTranslatedClass fun translateInterface(descriptor: ClassDescriptor): ObjCProtocol fun translateExtensions(classDescriptor: ClassDescriptor, declarations: List): ObjCInterface } @@ -172,12 +177,13 @@ class ObjCExportTranslatorImpl( ) } - override fun translateClass(descriptor: ClassDescriptor): ObjCInterface { + override fun translateClass(descriptor: ClassDescriptor): ObjCExportTranslatedClass { require(!descriptor.isInterface) if (!mapper.shouldBeExposed(descriptor)) { - return translateUnexposedClassAsUnavailableStub(descriptor) + return ObjCExportTranslatedClass(objCInterface = translateUnexposedClassAsUnavailableStub(descriptor)) } + val auxiliaryDeclarations = mutableListOf() val genericExportScope = createGenericExportScope(descriptor) fun superClassGenerics(genericExportScope: ObjCExportScope): List { @@ -284,6 +290,37 @@ class ObjCExportTranslatorImpl( ClassKind.ENUM_CLASS -> { val type = mapType(descriptor.defaultType, ReferenceBridge, ObjCRootExportScope) + namer.getNSEnumTypeName(descriptor)?.let { nsEnumTypeName -> + // Map the enum entries in declaration order, preserving the ordinal + auxiliaryDeclarations.add( + ObjCNSEnum( + name = nsEnumTypeName.objCName, + swiftName = nsEnumTypeName.swiftName, + origin = ObjCExportStubOrigin(descriptor), + entries = descriptor.enumEntries.mapIndexed { ordinal, entry -> + ObjCExportNativeEnumEntry( + objCName = nsEnumTypeName.objCName + namer.getEnumEntrySelector(entry) + .replaceFirstChar { it.uppercaseChar() }, + swiftName = namer.getEnumEntrySwiftName(entry), + value = ordinal + ) + } + ) + ) + + add { + // TODO(KT-82581): If an enum already has a property named nsEnum, we'll get a name conflict + ObjCProperty( + "nsEnum", + null, + ObjCRawType(nsEnumTypeName.objCName), + listOf("readonly"), + declarationAttributes = emptyList(), + comment = null + ) + } + } + descriptor.enumEntries.forEach { val entryName = namer.getEnumEntrySelector(it) val swiftName = namer.getEnumEntrySwiftName(it) @@ -337,16 +374,19 @@ class ObjCExportTranslatorImpl( val generics = mapTypeConstructorParameters(descriptor) val superClassGenerics = superClassGenerics(genericExportScope) - return objCInterface( - name, - generics = generics, - descriptor = descriptor, - superClass = superName.objCName, - superClassGenerics = superClassGenerics, - superProtocols = superProtocols, - members = members, - attributes = attributes, - comment = objCCommentOrNull(mustBeDocumentedAttributeList(descriptor.annotations)) + return ObjCExportTranslatedClass( + auxiliaryDeclarations = auxiliaryDeclarations.toList(), + objCInterface = objCInterface( + name, + generics = generics, + descriptor = descriptor, + superClass = superName.objCName, + superClassGenerics = superClassGenerics, + superProtocols = superProtocols, + members = members, + attributes = attributes, + comment = objCCommentOrNull(mustBeDocumentedAttributeList(descriptor.annotations)) + ) ) } @@ -772,7 +812,9 @@ class ObjCExportTranslatorImpl( } private val mustBeDocumentedAnnotationsStopList = - setOf(StandardNames.FqNames.deprecated, KonanFqNames.objCName, KonanFqNames.shouldRefineInSwift) + setOf( + StandardNames.FqNames.deprecated, KonanFqNames.objCName, KonanFqNames.shouldRefineInSwift, KonanFqNames.objCEnum + ) private fun mustBeDocumentedAnnotations(annotations: Annotations): List { return annotations.mapNotNull { it -> diff --git a/native/objcexport-header-generator/src/org/jetbrains/kotlin/backend/konan/objcexport/ObjCExportName.kt b/native/objcexport-header-generator/src/org/jetbrains/kotlin/backend/konan/objcexport/ObjCExportName.kt index 8122553c2886f..e381a92b9c4af 100644 --- a/native/objcexport-header-generator/src/org/jetbrains/kotlin/backend/konan/objcexport/ObjCExportName.kt +++ b/native/objcexport-header-generator/src/org/jetbrains/kotlin/backend/konan/objcexport/ObjCExportName.kt @@ -22,6 +22,8 @@ interface ObjCExportFunctionName : ObjCExportName interface ObjCExportFileName : ObjCExportName +interface ObjCExportNSEnumTypeName : ObjCExportName + fun ObjCExportClassOrProtocolName( swiftName: String, objCName: String, @@ -62,6 +64,14 @@ fun ObjCExportFileName( objCName = objCName ) +fun ObjCExportNSEnumTypeName( + swiftName: String, + objCName: String, +): ObjCExportNSEnumTypeName = ObjCExportNSEnumTypeNameImpl( + swiftName = swiftName, + objCName = objCName +) + private data class ObjCExportPropertyNameImpl( override val swiftName: String, override val objCName: String, @@ -77,6 +87,10 @@ private data class ObjCExportFileNameImpl( override val objCName: String, ) : ObjCExportFileName +private data class ObjCExportNSEnumTypeNameImpl( + override val swiftName: String, + override val objCName: String, +) : ObjCExportNSEnumTypeName fun ObjCExportClassOrProtocolName.toNameAttributes(): List = listOfNotNull( binaryName.takeIf { it != objCName }?.let { objcRuntimeNameAttribute(it) }, diff --git a/native/objcexport-header-generator/src/org/jetbrains/kotlin/backend/konan/objcexport/StubRenderer.kt b/native/objcexport-header-generator/src/org/jetbrains/kotlin/backend/konan/objcexport/StubRenderer.kt index 7142753fa6186..f8fcb8bc68334 100644 --- a/native/objcexport-header-generator/src/org/jetbrains/kotlin/backend/konan/objcexport/StubRenderer.kt +++ b/native/objcexport-header-generator/src/org/jetbrains/kotlin/backend/konan/objcexport/StubRenderer.kt @@ -84,6 +84,9 @@ object StubRenderer { is ObjCProperty -> { +renderProperty(this) } + is ObjCNSEnum -> { + +renderNativeEnumType(this) + } else -> throw IllegalArgumentException("unsupported stub: " + stub::class) } } @@ -119,6 +122,32 @@ object StubRenderer { append(';') } + private fun renderNativeEnumType(nativeEnum: ObjCNSEnum): String = buildString { + append("typedef NS_ENUM(int32_t, ") + append(nativeEnum.name) + append(") {\n") + for (entry in nativeEnum.entries) { + appendNativeEnumEntry(entry) + } + append("}") + if (nativeEnum.name != nativeEnum.swiftName) { + append(" NS_SWIFT_NAME(") + append(nativeEnum.swiftName) + append(")") + } + append(";\n") + } + + private fun Appendable.appendNativeEnumEntry(entry: ObjCExportNativeEnumEntry) { + append(" ") + append(entry.objCName) + append(" NS_SWIFT_NAME(") + append(entry.swiftName) + append(") = ") + append(entry.value.toString()) + append(",\n") + } + private fun renderMethod(method: ObjCMethod): String = buildString { fun appendStaticness() { if (method.isInstanceMethod) { diff --git a/native/objcexport-header-generator/src/org/jetbrains/kotlin/backend/konan/objcexport/stubs.kt b/native/objcexport-header-generator/src/org/jetbrains/kotlin/backend/konan/objcexport/stubs.kt index d6fdf0fbad1dc..1549afb0f92d3 100644 --- a/native/objcexport-header-generator/src/org/jetbrains/kotlin/backend/konan/objcexport/stubs.kt +++ b/native/objcexport-header-generator/src/org/jetbrains/kotlin/backend/konan/objcexport/stubs.kt @@ -15,6 +15,7 @@ import org.jetbrains.kotlin.tooling.core.emptyExtras @Suppress("unused") typealias Stub<@Suppress("UNUSED_TYPEALIAS_PARAMETER") T> = ObjCExportStub + sealed interface ObjCExportStub : HasExtras { /** * The ObjC name of this entity; @@ -45,6 +46,23 @@ val ObjCExportStub.psiOrNull abstract class ObjCTopLevel : ObjCExportStub +class ObjCExportNativeEnumEntry( + val swiftName: String, + val objCName: String, + val value: Int +) + +class ObjCNSEnum( + override val name: String, + val swiftName: String, + override val origin: ObjCExportStubOrigin?, + val entries: List, +) : ObjCTopLevel() { + override val comment: ObjCComment? + get() = null + override val extras: Extras = emptyExtras() +} + sealed class ObjCClass : ObjCTopLevel() { abstract val attributes: List abstract val superProtocols: List diff --git a/native/objcexport-header-generator/test/org/jetbrains/kotlin/backend/konan/tests/ObjCExportHeaderGeneratorTest.kt b/native/objcexport-header-generator/test/org/jetbrains/kotlin/backend/konan/tests/ObjCExportHeaderGeneratorTest.kt index 6e2911fb41f9e..3a08507b44ea7 100644 --- a/native/objcexport-header-generator/test/org/jetbrains/kotlin/backend/konan/tests/ObjCExportHeaderGeneratorTest.kt +++ b/native/objcexport-header-generator/test/org/jetbrains/kotlin/backend/konan/tests/ObjCExportHeaderGeneratorTest.kt @@ -48,6 +48,16 @@ class ObjCExportHeaderGeneratorTest(private val generator: HeaderGenerator) { doTest(headersTestDataDir.resolve("simpleEnumClass")) } + @Test + fun `test - enumClassWithNamedObjCEnum`() { + doTest(headersTestDataDir.resolve("enumClassWithNamedObjCEnum")) + } + + @Test + fun `test - enumClassWithObjCEnum`() { + doTest(headersTestDataDir.resolve("enumClassWithObjCEnum")) + } + @Test fun `test - simpleObject`() { doTest(headersTestDataDir.resolve("simpleObject")) diff --git a/native/objcexport-header-generator/testData/headers/enumClassWithNamedObjCEnum/!enumClassWithNamedObjCEnum.h b/native/objcexport-header-generator/testData/headers/enumClassWithNamedObjCEnum/!enumClassWithNamedObjCEnum.h new file mode 100644 index 0000000000000..98c9b2dc8e47f --- /dev/null +++ b/native/objcexport-header-generator/testData/headers/enumClassWithNamedObjCEnum/!enumClassWithNamedObjCEnum.h @@ -0,0 +1,88 @@ +#import +#import +#import +#import +#import +#import +#import + +@class Foo, KotlinArray, KotlinEnum, KotlinEnumCompanion; + +@protocol KotlinComparable, KotlinIterator; + +NS_ASSUME_NONNULL_BEGIN +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wunknown-warning-option" +#pragma clang diagnostic ignored "-Wincompatible-property-type" +#pragma clang diagnostic ignored "-Wnullability" + +#pragma push_macro("_Nullable_result") +#if !__has_feature(nullability_nullable_result) +#undef _Nullable_result +#define _Nullable_result _Nullable +#endif + +@protocol KotlinComparable +@required +- (int32_t)compareToOther:(id _Nullable)other __attribute__((swift_name("compareTo(other:)"))); +@end + +@interface KotlinEnum : Base +- (instancetype)initWithName:(NSString *)name ordinal:(int32_t)ordinal __attribute__((swift_name("init(name:ordinal:)"))) __attribute__((objc_designated_initializer)); +@property (class, readonly, getter=companion) KotlinEnumCompanion *companion __attribute__((swift_name("companion"))); +- (int32_t)compareToOther:(E)other __attribute__((swift_name("compareTo(other:)"))); +- (BOOL)isEqual:(id _Nullable)other __attribute__((swift_name("isEqual(_:)"))); +- (NSUInteger)hash __attribute__((swift_name("hash()"))); +- (NSString *)description __attribute__((swift_name("description()"))); +@property (readonly) NSString *name __attribute__((swift_name("name"))); +@property (readonly) int32_t ordinal __attribute__((swift_name("ordinal"))); +@end + +typedef NS_ENUM(int32_t, OBJCFoo) { + OBJCFooAlpha NS_SWIFT_NAME(alpha) = 0, + OBJCFooTheCopy NS_SWIFT_NAME(theCopy) = 1, + OBJCFooBarFoo NS_SWIFT_NAME(barFoo) = 2, +} NS_SWIFT_NAME(SwiftFoo); + + +__attribute__((objc_subclassing_restricted)) +@interface Foo : KotlinEnum ++ (instancetype)alloc __attribute__((unavailable)); ++ (instancetype)allocWithZone:(struct _NSZone *)zone __attribute__((unavailable)); +- (instancetype)initWithName:(NSString *)name ordinal:(int32_t)ordinal __attribute__((swift_name("init(name:ordinal:)"))) __attribute__((objc_designated_initializer)) __attribute__((unavailable)); +@property (readonly) OBJCFoo nsEnum; +@property (class, readonly) Foo *alpha __attribute__((swift_name("alpha"))); +@property (class, readonly) Foo *theCopy __attribute__((swift_name("theCopy"))); +@property (class, readonly) Foo *barFoo __attribute__((swift_name("barFoo"))); ++ (KotlinArray *)values __attribute__((swift_name("values()"))); +@property (class, readonly) NSArray *entries __attribute__((swift_name("entries"))); +@end + +__attribute__((objc_subclassing_restricted)) +@interface KotlinEnumCompanion : Base ++ (instancetype)alloc __attribute__((unavailable)); ++ (instancetype)allocWithZone:(struct _NSZone *)zone __attribute__((unavailable)); ++ (instancetype)companion __attribute__((swift_name("init()"))); +@property (class, readonly, getter=shared) KotlinEnumCompanion *shared __attribute__((swift_name("shared"))); +@end + +__attribute__((objc_subclassing_restricted)) +@interface KotlinArray : Base ++ (instancetype)arrayWithSize:(int32_t)size init:(T _Nullable (^)(Int *))init __attribute__((swift_name("init(size:init:)"))); ++ (instancetype)alloc __attribute__((unavailable)); ++ (instancetype)allocWithZone:(struct _NSZone *)zone __attribute__((unavailable)); +- (T _Nullable)getIndex:(int32_t)index __attribute__((swift_name("get(index:)"))); +- (id)iterator __attribute__((swift_name("iterator()"))); +- (void)setIndex:(int32_t)index value:(T _Nullable)value __attribute__((swift_name("set(index:value:)"))); +@property (readonly) int32_t size __attribute__((swift_name("size"))); +@end + +@protocol KotlinIterator +@required +- (BOOL)hasNext __attribute__((swift_name("hasNext()"))); +- (id _Nullable)next __attribute__((swift_name("next()"))); +@end + +#pragma pop_macro("_Nullable_result") +#pragma clang diagnostic pop +NS_ASSUME_NONNULL_END diff --git a/native/objcexport-header-generator/testData/headers/enumClassWithNamedObjCEnum/Foo.kt b/native/objcexport-header-generator/testData/headers/enumClassWithNamedObjCEnum/Foo.kt new file mode 100644 index 0000000000000..6f8290b9a36fb --- /dev/null +++ b/native/objcexport-header-generator/testData/headers/enumClassWithNamedObjCEnum/Foo.kt @@ -0,0 +1,11 @@ +import kotlin.native.ObjCEnum +import kotlin.experimental.ExperimentalObjCEnum + +@file:OptIn(ExperimentalObjCEnum::class) + +@ObjCEnum(name = "OBJCFoo", swiftName = "SwiftFoo") +enum class Foo { + // Note that the order is not alphabetic on purpose, ensuring that tests fail if we can't rely on the + // order to be preserved for the ordinal value. + ALPHA, COPY, BAR_FOO, +} \ No newline at end of file diff --git a/native/objcexport-header-generator/testData/headers/enumClassWithObjCEnum/!enumClassWithObjCEnum.h b/native/objcexport-header-generator/testData/headers/enumClassWithObjCEnum/!enumClassWithObjCEnum.h new file mode 100644 index 0000000000000..d9cd6412f9d21 --- /dev/null +++ b/native/objcexport-header-generator/testData/headers/enumClassWithObjCEnum/!enumClassWithObjCEnum.h @@ -0,0 +1,88 @@ +#import +#import +#import +#import +#import +#import +#import + +@class Foo, KotlinArray, KotlinEnum, KotlinEnumCompanion; + +@protocol KotlinComparable, KotlinIterator; + +NS_ASSUME_NONNULL_BEGIN +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wunknown-warning-option" +#pragma clang diagnostic ignored "-Wincompatible-property-type" +#pragma clang diagnostic ignored "-Wnullability" + +#pragma push_macro("_Nullable_result") +#if !__has_feature(nullability_nullable_result) +#undef _Nullable_result +#define _Nullable_result _Nullable +#endif + +@protocol KotlinComparable +@required +- (int32_t)compareToOther:(id _Nullable)other __attribute__((swift_name("compareTo(other:)"))); +@end + +@interface KotlinEnum : Base +- (instancetype)initWithName:(NSString *)name ordinal:(int32_t)ordinal __attribute__((swift_name("init(name:ordinal:)"))) __attribute__((objc_designated_initializer)); +@property (class, readonly, getter=companion) KotlinEnumCompanion *companion __attribute__((swift_name("companion"))); +- (int32_t)compareToOther:(E)other __attribute__((swift_name("compareTo(other:)"))); +- (BOOL)isEqual:(id _Nullable)other __attribute__((swift_name("isEqual(_:)"))); +- (NSUInteger)hash __attribute__((swift_name("hash()"))); +- (NSString *)description __attribute__((swift_name("description()"))); +@property (readonly) NSString *name __attribute__((swift_name("name"))); +@property (readonly) int32_t ordinal __attribute__((swift_name("ordinal"))); +@end + +typedef NS_ENUM(int32_t, FooNSEnum) { + FooNSEnumAlpha NS_SWIFT_NAME(alpha) = 0, + FooNSEnumTheCopy NS_SWIFT_NAME(theCopy) = 1, + FooNSEnumBarFoo NS_SWIFT_NAME(barFoo) = 2, +}; + + +__attribute__((objc_subclassing_restricted)) +@interface Foo : KotlinEnum ++ (instancetype)alloc __attribute__((unavailable)); ++ (instancetype)allocWithZone:(struct _NSZone *)zone __attribute__((unavailable)); +- (instancetype)initWithName:(NSString *)name ordinal:(int32_t)ordinal __attribute__((swift_name("init(name:ordinal:)"))) __attribute__((objc_designated_initializer)) __attribute__((unavailable)); +@property (readonly) FooNSEnum nsEnum; +@property (class, readonly) Foo *alpha __attribute__((swift_name("alpha"))); +@property (class, readonly) Foo *theCopy __attribute__((swift_name("theCopy"))); +@property (class, readonly) Foo *barFoo __attribute__((swift_name("barFoo"))); ++ (KotlinArray *)values __attribute__((swift_name("values()"))); +@property (class, readonly) NSArray *entries __attribute__((swift_name("entries"))); +@end + +__attribute__((objc_subclassing_restricted)) +@interface KotlinEnumCompanion : Base ++ (instancetype)alloc __attribute__((unavailable)); ++ (instancetype)allocWithZone:(struct _NSZone *)zone __attribute__((unavailable)); ++ (instancetype)companion __attribute__((swift_name("init()"))); +@property (class, readonly, getter=shared) KotlinEnumCompanion *shared __attribute__((swift_name("shared"))); +@end + +__attribute__((objc_subclassing_restricted)) +@interface KotlinArray : Base ++ (instancetype)arrayWithSize:(int32_t)size init:(T _Nullable (^)(Int *))init __attribute__((swift_name("init(size:init:)"))); ++ (instancetype)alloc __attribute__((unavailable)); ++ (instancetype)allocWithZone:(struct _NSZone *)zone __attribute__((unavailable)); +- (T _Nullable)getIndex:(int32_t)index __attribute__((swift_name("get(index:)"))); +- (id)iterator __attribute__((swift_name("iterator()"))); +- (void)setIndex:(int32_t)index value:(T _Nullable)value __attribute__((swift_name("set(index:value:)"))); +@property (readonly) int32_t size __attribute__((swift_name("size"))); +@end + +@protocol KotlinIterator +@required +- (BOOL)hasNext __attribute__((swift_name("hasNext()"))); +- (id _Nullable)next __attribute__((swift_name("next()"))); +@end + +#pragma pop_macro("_Nullable_result") +#pragma clang diagnostic pop +NS_ASSUME_NONNULL_END diff --git a/native/objcexport-header-generator/testData/headers/enumClassWithObjCEnum/Foo.kt b/native/objcexport-header-generator/testData/headers/enumClassWithObjCEnum/Foo.kt new file mode 100644 index 0000000000000..a32df36750d3e --- /dev/null +++ b/native/objcexport-header-generator/testData/headers/enumClassWithObjCEnum/Foo.kt @@ -0,0 +1,9 @@ +import kotlin.native.ObjCEnum +import kotlin.experimental.ExperimentalObjCEnum + +@file:OptIn(ExperimentalObjCEnum::class) + +@ObjCEnum +enum class Foo { + ALPHA, COPY, BAR_FOO +} \ No newline at end of file