Skip to content
Open
Show file tree
Hide file tree
Changes from 15 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 @@ -20,6 +20,7 @@ import org.jetbrains.kotlin.backend.konan.llvm.objcexport.KotlinToObjCMethodAdap
import org.jetbrains.kotlin.backend.konan.lower.getLoweredConstructorFunction
import org.jetbrains.kotlin.backend.konan.lower.getObjectClassInstanceFunction
import org.jetbrains.kotlin.backend.konan.objcexport.*
import org.jetbrains.kotlin.backend.konan.objcexport.ObjCMethodSpec.BaseMethod
import org.jetbrains.kotlin.descriptors.ClassKind
import org.jetbrains.kotlin.descriptors.Modality
import org.jetbrains.kotlin.incremental.components.NoLookupLocation
Expand Down Expand Up @@ -1375,6 +1376,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, 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 +1440,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 @@ -13,7 +13,6 @@ 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 +88,15 @@ internal fun ObjCExportedInterface.createCodeSpec(symbolTable: SymbolTable): Obj
}

if (descriptor.kind == ClassKind.ENUM_CLASS) {
if (namer.getNSEnumFunctionTypeName(descriptor) != null) {
val superClass = descriptor.getSuperClassNotAny()!! // ordinal is declared in KotlinEnum
val ordinalDescriptor = superClass.contributedMethods.first { it.name.asString() == "<get-ordinal>" }
val symbol = symbolTable.descriptorExtension.referenceSimpleFunction(ordinalDescriptor)
val bridge = mapper.bridgeMethod(ordinalDescriptor)
val method = ObjCMethodSpec.BaseMethod(symbol, bridge, "toNSEnum")
methods += ObjCGetterForNSEnumType(symbol, bridge, "toNSEnum", method)
}

descriptor.enumEntries.mapTo(methods) {
ObjCGetterForKotlinEnumEntry(symbolTable.descriptorExtension.referenceEnumEntry(it), namer.getEnumEntrySelector(it))
}
Expand Down Expand Up @@ -160,6 +168,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 +179,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 -> "$objcClass.${baseMethod.selector},${baseMethod.symbol.signature}"
}
out.println("\n# Instance methods mapping")
for (type in types) {
Expand Down Expand Up @@ -224,6 +234,18 @@ internal class ObjCGetterForKotlinEnumEntry(
"ObjC spec of getter `$selector` for `$irEnumEntrySymbol`"
}


internal class ObjCGetterForNSEnumType(
val symbol: IrSimpleFunctionSymbol,
val bridge: MethodBridge,
val selector: String,
val baseMethod: BaseMethod<IrSimpleFunctionSymbol>
) : ObjCMethodSpec() {
override fun toString(): String =
"ObjC spec of ${baseMethod.selector} for ${baseMethod.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 "Enum" 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.
*/
@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 = "")

/**
* Meta-annotation that instructs the Kotlin compiler to remove the annotated class, function or property from the public Objective-C API.
*
Expand Down
17 changes: 17 additions & 0 deletions libraries/stdlib/src/kotlin/annotations/NativeAnnotations.kt
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 @@ -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 "Enum" 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.
*/
@Target(
AnnotationTarget.CLASS,
)
@Retention(AnnotationRetention.BINARY)
@MustBeDocumented
@OptionalExpectation
@ExperimentalObjCEnum
@SinceKotlin("2.2.21")
public expect annotation class ObjCEnum(val name: 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-2022 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.2.21")
public annotation class ExperimentalObjCEnum
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 {
A, B, C
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import Kt


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

switch(nsEnum) {
case .a: try assertEquals(actual: nsEnum, expected: ktEnum.toNSEnum())
case .b: try fail()
case .c: try fail()
}
}

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

test("TestNativeEnumValues", testNativeEnumValues)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,7 @@ private class KtObjCExportHeaderGenerator(
is ObjCProperty -> listOf(childStub.type)
is ObjCInterface -> childStub.superClassGenerics
is ObjCTopLevel -> emptyList()
is ObjCNSEnum -> emptyList()
}
}.map { type ->
/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import org.jetbrains.kotlin.library.shortName
import org.jetbrains.kotlin.library.uniqueName
import org.jetbrains.kotlin.load.kotlin.PackagePartClassUtils
import org.jetbrains.kotlin.name.ClassId
import org.jetbrains.kotlin.name.FqName
import org.jetbrains.kotlin.name.Name
import org.jetbrains.kotlin.psi.*
import org.jetbrains.kotlin.psi.psiUtil.getStrictParentOfType
Expand Down Expand Up @@ -98,6 +99,13 @@ interface ObjCExportNamer {
fun getCompanionObjectPropertySelector(descriptor: ClassDescriptor): String
fun needsExplicitMethodFamily(name: String): Boolean

// Null means no NSEnum function.
fun getNSEnumFunctionTypeName(descriptor: ClassDescriptor): String? =
descriptor.annotations.findAnnotation(FqName("kotlin.native.ObjCEnum"))?.let {
val name = it.allValueArguments.entries.find { it.key.asString() == "name" }?.value?.value?.toString()
name ?: "${getClassOrProtocolName(descriptor).objCName}_Enum"
}

companion object {
@InternalKotlinNativeApi
const val kotlinThrowableAsErrorMethodName: String = "asError"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,17 @@ import org.jetbrains.kotlin.types.typeUtil.supertypes
import org.jetbrains.kotlin.utils.addIfNotNull
import kotlin.*


class TranslatedClass(
val auxiliaryDeclarations: List<ObjCExportStub> = emptyList(),
val objCInterface: ObjCInterface,
)

interface ObjCExportTranslator {
fun generateBaseDeclarations(): List<ObjCTopLevel>
fun getClassIfExtension(receiverType: KotlinType): ClassDescriptor?
fun translateFile(file: SourceFile, declarations: List<CallableMemberDescriptor>): ObjCInterface
fun translateClass(descriptor: ClassDescriptor): ObjCInterface
fun translateClass(descriptor: ClassDescriptor): TranslatedClass
fun translateInterface(descriptor: ClassDescriptor): ObjCProtocol
fun translateExtensions(classDescriptor: ClassDescriptor, declarations: List<CallableMemberDescriptor>): ObjCInterface
}
Expand Down Expand Up @@ -172,12 +178,13 @@ class ObjCExportTranslatorImpl(
)
}

override fun translateClass(descriptor: ClassDescriptor): ObjCInterface {
override fun translateClass(descriptor: ClassDescriptor): TranslatedClass {
require(!descriptor.isInterface)
if (!mapper.shouldBeExposed(descriptor)) {
return translateUnexposedClassAsUnavailableStub(descriptor)
return TranslatedClass(objCInterface = translateUnexposedClassAsUnavailableStub(descriptor))
}

val auxiliaryDeclarations = mutableListOf<ObjCExportStub>()
val genericExportScope = createGenericExportScope(descriptor)

fun superClassGenerics(genericExportScope: ObjCExportScope): List<ObjCNonNullReferenceType> {
Expand Down Expand Up @@ -284,6 +291,27 @@ class ObjCExportTranslatorImpl(
ClassKind.ENUM_CLASS -> {
val type = mapType(descriptor.defaultType, ReferenceBridge, ObjCRootExportScope)

namer.getNSEnumFunctionTypeName(descriptor)?.let { nsEnumTypeName ->
auxiliaryDeclarations.add(
ObjCNSEnum(nsEnumTypeName, descriptor.enumEntries.map {
ObjcExportNativeEnumEntryName(
objCName = namer.getEnumEntrySelector(it).replaceFirstChar { it.uppercaseChar() },
swiftName = namer.getEnumEntrySwiftName(it))
} ))

add {
ObjCMethod(
null,
null,
true,
ObjCRawType(nsEnumTypeName),
listOf("toNSEnum"),
emptyList<ObjCParameter>(),
emptyList()
)
}
}

descriptor.enumEntries.forEach {
val entryName = namer.getEnumEntrySelector(it)
val swiftName = namer.getEnumEntrySwiftName(it)
Expand Down Expand Up @@ -337,17 +365,18 @@ 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 TranslatedClass(
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))))
}

internal fun createGenericExportScope(descriptor: ClassDescriptor): ObjCExportScope = if (objcGenerics) {
Expand Down Expand Up @@ -772,7 +801,8 @@ 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<String> {
return annotations.mapNotNull { it ->
Expand Down
Loading