Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
Original file line number Diff line number Diff line change
Expand Up @@ -824,7 +824,7 @@ FILE: [ResolvedTo(BODY_RESOLVE)] TypeReference.kt
}

public open override [ResolvedTo(BODY_RESOLVE)] fun hashCode(): R|kotlin/Int| {
^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|())
^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|())
}

public open override [ResolvedTo(BODY_RESOLVE)] fun toString(): R|kotlin/String| {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<IrSimpleFunction>(), methodBridge)
return objCToKotlinMethodAdapter(selectorName, methodBridge, imp)
}

private fun ObjCExportCodeGenerator.vtableIndex(irFunction: IrSimpleFunction): Int? {
assert(irFunction.isOverridable)
val irClass = irFunction.parentAsClass
Expand Down Expand Up @@ -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)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.single() { 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))
}
Expand Down Expand Up @@ -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) {
Expand All @@ -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) {
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
package kotlin.native

import kotlin.experimental.ExperimentalNativeApi
import kotlin.experimental.ExperimentalObjCEnum
import kotlin.experimental.ExperimentalObjCName
import kotlin.experimental.ExperimentalObjCRefinement

Expand Down Expand Up @@ -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 disambiguation prefixes. The NSEnum values are accessible via the "nsEnum" property.
*/
@Target(AnnotationTarget.CLASS)
Copy link
Contributor

Choose a reason for hiding this comment

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

Ideally, we should check that the annotation is not applied to non-enum classes.
A new frontend checker is needed for that. See an example here:

object FirNativeThreadLocalChecker : FirBasicDeclarationChecker(MppCheckerKind.Platform) {
.

To avoid stalling this PR, this can be done separately.

@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.
*
Expand Down
19 changes: 18 additions & 1 deletion libraries/stdlib/src/kotlin/annotations/NativeAnnotations.kt
Original file line number Diff line number Diff line change
@@ -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

Expand Down Expand Up @@ -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.
*
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
Copy link
Contributor

Choose a reason for hiding this comment

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

Please add a description of what this status entails.

Also, is there a path to stabilization?

Copy link
Author

Choose a reason for hiding this comment

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

I have expanded the explanation based on the documentation for ExperimentalNativeApi

Not sure about the stabilization path, I'd expect something along the lines that this would lose experimental status if no issues are observed for a certain amount of time and there is significant usage, but I don't see something similar elsewere...

*/
@RequiresOptIn
@Target(AnnotationTarget.ANNOTATION_CLASS)
@Retention(AnnotationRetention.BINARY)
@MustBeDocumented
@SinceKotlin("2.3")
public annotation class ExperimentalObjCEnum
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,10 @@ open annotation class kotlin.experimental/ExperimentalNativeApi : kotlin/Annotat
constructor <init>() // kotlin.experimental/ExperimentalNativeApi.<init>|<init>(){}[0]
}

open annotation class kotlin.experimental/ExperimentalObjCEnum : kotlin/Annotation { // kotlin.experimental/ExperimentalObjCEnum|null[0]
constructor <init>() // kotlin.experimental/ExperimentalObjCEnum.<init>|<init>(){}[0]
}

open annotation class kotlin.experimental/ExperimentalObjCName : kotlin/Annotation { // kotlin.experimental/ExperimentalObjCName|null[0]
constructor <init>() // kotlin.experimental/ExperimentalObjCName.<init>|<init>(){}[0]
}
Expand Down Expand Up @@ -9847,6 +9851,16 @@ open annotation class kotlin.native/NoInline : kotlin/Annotation { // kotlin.nat
constructor <init>() // kotlin.native/NoInline.<init>|<init>(){}[0]
}

// Targets: [native]
open annotation class kotlin.native/ObjCEnum : kotlin/Annotation { // kotlin.native/ObjCEnum|null[0]
constructor <init>(kotlin/String = ..., kotlin/String = ...) // kotlin.native/ObjCEnum.<init>|<init>(kotlin.String;kotlin.String){}[0]

final val name // kotlin.native/ObjCEnum.name|{}name[0]
final fun <get-name>(): kotlin/String // kotlin.native/ObjCEnum.name.<get-name>|<get-name>(){}[0]
final val swiftName // kotlin.native/ObjCEnum.swiftName|{}swiftName[0]
final fun <get-swiftName>(): kotlin/String // kotlin.native/ObjCEnum.swiftName.<get-swiftName>|<get-swiftName>(){}[0]
}

// Targets: [native]
open annotation class kotlin.native/ObjCName : kotlin/Annotation { // kotlin.native/ObjCName|null[0]
constructor <init>(kotlin/String = ..., kotlin/String = ..., kotlin/Boolean = ...) // kotlin.native/ObjCName.<init>|<init>(kotlin.String;kotlin.String;kotlin.Boolean){}[0]
Expand Down Expand Up @@ -13769,6 +13783,16 @@ open annotation class kotlin.native/HidesFromObjC : kotlin/Annotation { // kotli
constructor <init>() // kotlin.native/HidesFromObjC.<init>|<init>(){}[1]
}

// Targets: [js, wasmJs, wasmWasi]
open annotation class kotlin.native/ObjCEnum : kotlin/Annotation { // kotlin.native/ObjCEnum|null[1]
constructor <init>(kotlin/String = ..., kotlin/String = ...) // kotlin.native/ObjCEnum.<init>|<init>(kotlin.String;kotlin.String){}[1]

final val name // kotlin.native/ObjCEnum.name|{}name[1]
final fun <get-name>(): kotlin/String // kotlin.native/ObjCEnum.name.<get-name>|<get-name>(){}[1]
final val swiftName // kotlin.native/ObjCEnum.swiftName|{}swiftName[1]
final fun <get-swiftName>(): kotlin/String // kotlin.native/ObjCEnum.swiftName.<get-swiftName>|<get-swiftName>(){}[1]
}

// Targets: [js, wasmJs, wasmWasi]
open annotation class kotlin.native/ObjCName : kotlin/Annotation { // kotlin.native/ObjCName|null[1]
constructor <init>(kotlin/String = ..., kotlin/String = ..., kotlin/Boolean = ...) // kotlin.native/ObjCName.<init>|<init>(kotlin.String;kotlin.String;kotlin.Boolean){}[1]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3264,6 +3264,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 {
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
10 changes: 10 additions & 0 deletions native/native.tests/testData/framework/objcexport/nativeEnum.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package nativeEnum

import kotlin.native.ObjCEnum
import kotlin.experimental.ExperimentalObjCEnum

@OptIn(kotlin.experimental.ExperimentalObjCEnum::class)
@ObjCEnum("OBJCFoo", swiftName="SwiftFoo")
enum class MyKotlinEnum {
ALPHA, COPY, BAR_FOO
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import Kt


private func testNativeEnumValues() throws {
let ktEnum = MyKotlinEnum.barFoo
let nsEnum = ktEnum.nsEnum

switch(nsEnum) {
case SwiftFoo.alpha: try fail()
case .barFoo: try assertEquals(actual: nsEnum, expected: ktEnum.nsEnum)
case .theCopy: try fail()
}
}

class NativeEnumTests : SimpleTestProvider {
override init() {
super.init()

test("TestNativeEnumValues", testNativeEnumValues)
}
}
Original file line number Diff line number Diff line change
@@ -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<ObjCTopLevel>,
val objCClass: ObjCClass,
)

fun ObjCExportTranslatedClass(objCClass: ObjCClass?) = objCClass?.let { ObjCExportTranslatedClass(emptyList(), it) }

Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,6 @@ internal object ObjCPropertyNames {

@Suppress("unused")
const val companionObjectPropertyName: String = "companion"

const val nsEnumPropertyName: String = "nsEnum"
}
Original file line number Diff line number Diff line change
Expand Up @@ -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,
)
Original file line number Diff line number Diff line change
@@ -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)
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Loading