Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
2ab6493
make 'something' visible end-to-end
stefanhaustein Sep 29, 2025
b824f55
End-to-end wireup with test; method body missing
stefanhaustein Oct 2, 2025
e180ad4
current state (incomplete)
stefanhaustein Oct 3, 2025
6ab07ae
cleanup
stefanhaustein Oct 8, 2025
b925ff2
cleanup
stefanhaustein Oct 8, 2025
e7ec42e
cleanup
stefanhaustein Oct 8, 2025
7e4fb66
ordinal should be in KotlinEnum
stefanhaustein Oct 8, 2025
b713914
refactored to avoid ObjCMethodForKotlinMethod
stefanhaustein Oct 13, 2025
b126cd4
move symbol resolution from ObjCExportCodeSpec to ObjCExportCodeGener…
stefanhaustein Oct 17, 2025
f51d934
swift test compiles
stefanhaustein Oct 20, 2025
854371f
remove footgun
stefanhaustein Oct 20, 2025
4013372
objc/switft name distinction / adjustment
stefanhaustein Oct 24, 2025
c9de8d8
Merge branch 'JetBrains:master' into master
stefanhaustein Oct 24, 2025
200acce
Merge branch 'JetBrains:master' into master
stefanhaustein Oct 29, 2025
29e8031
first round of review comments / snapshot before changing to property
stefanhaustein Oct 31, 2025
de01982
Merge branch 'JetBrains:master' into master
stefanhaustein Nov 4, 2025
0ccc118
Review comments addressed; mostly adding the missing AA impl
stefanhaustein Nov 4, 2025
60cca82
Make the enum entry value / order more explicit / add a clarifying co…
stefanhaustein Nov 5, 2025
8da8bb4
Naming consistency / fixes and test extensions / improvements
stefanhaustein Nov 10, 2025
6a52203
Move ObjCEnum to the auto documentartion exclude list for AA, matchin…
stefanhaustein Nov 12, 2025
bfb6921
first part of addressing 2nd round of review comments
stefanhaustein Nov 19, 2025
51cf02b
addressing review comments part 2
stefanhaustein Nov 20, 2025
32645cb
Add a comment about ordering
stefanhaustein Nov 20, 2025
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 @@ -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.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))
}
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 prefixes. The native 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.
*/
@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 @@ -9829,6 +9833,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 @@ -13739,6 +13753,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 @@ -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 {
}

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")
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.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)
}
}
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
Original file line number Diff line number Diff line change
@@ -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<ObjCTopLevel>
): 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<ObjCExportNativeEnumEntry> {
val staticMembers = with(analysisSession) { symbol.staticDeclaredMemberScope }.callables.toList()
// Map the enum entries in declaration order, preserving the ordinal
return staticMembers.filterIsInstance<KaEnumEntrySymbol>().mapIndexed { ordinal, entry ->
ObjCExportNativeEnumEntry(
getEnumEntryName(entry, true),
objCTypeName + getEnumEntryName(entry, false).replaceFirstChar { it.uppercaseChar() },
ordinal
)
}
}

Loading