-
Notifications
You must be signed in to change notification settings - Fork 6.1k
[KT-48068] Add opt-in support for NSEnum for Kotlin Native iOS via an annotation #5539
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
2ab6493
b824f55
e180ad4
6ab07ae
b925ff2
e7ec42e
7e4fb66
b713914
b126cd4
f51d934
854371f
4013372
c9de8d8
200acce
29e8031
de01982
0ccc118
60cca82
8da8bb4
6a52203
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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 | ||
|
|
@@ -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) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think both |
||
| return objCToKotlinMethodAdapter(selectorName, methodBridge, imp) | ||
| } | ||
|
|
||
| private fun ObjCExportCodeGenerator.vtableIndex(irFunction: IrSimpleFunction): Int? { | ||
| assert(irFunction.isOverridable) | ||
| val irClass = irFunction.parentAsClass | ||
|
|
@@ -1430,6 +1440,9 @@ private fun ObjCExportCodeGenerator.createTypeAdapter( | |
| is ObjCInitMethodForKotlinConstructor -> { | ||
| adapters += createConstructorAdapter(it.baseMethod) | ||
| } | ||
| is ObjCGetterForNSEnumType -> { | ||
| adapters += createNSEnumAdapter(it.symbol, it.bridge, it. selector) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Redundant space in |
||
| } | ||
| is ObjCFactoryMethodForKotlinArrayConstructor -> { | ||
| classAdapters += createArrayConstructorAdapter(it.baseMethod) | ||
| } | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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 | ||
|
|
@@ -89,6 +88,14 @@ 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>" } | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. A little bit less magical way to do that would be: superClass.contributedMethods.first { it.propertyIfAccessor.name.asString() == "ordinal" }or superClass.unsubstitutedMemberScope.getContributedVariables(Name.identifier("ordinal"), NoLookupLocation.FROM_BACKEND).single().getter |
||
| val symbol = symbolTable.descriptorExtension.referenceSimpleFunction(ordinalDescriptor) | ||
| val bridge = mapper.bridgeMethod(ordinalDescriptor) | ||
| methods += ObjCGetterForNSEnumType(symbol, bridge, "nsEnum") | ||
| } | ||
|
|
||
| descriptor.enumEntries.mapTo(methods) { | ||
| ObjCGetterForKotlinEnumEntry(symbolTable.descriptorExtension.referenceEnumEntry(it), namer.getEnumEntrySelector(it)) | ||
| } | ||
|
|
@@ -160,6 +167,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 +178,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.$selector,${symbol.signature}" | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Now, that's a bit tricky. The problem with this implementation is therefore the following: if the generated See these changes for more details: |
||
| } | ||
| out.println("\n# Instance methods mapping") | ||
| for (type in types) { | ||
|
|
@@ -224,6 +233,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 | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||
|---|---|---|---|---|
|
|
@@ -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) | ||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ideally, we should check that the annotation is not applied to non- Line 24 in 360c000
To avoid stalling this PR, this can be done separately. |
||||
| @Retention(AnnotationRetention.BINARY) | ||||
| @MustBeDocumented | ||||
stefanhaustein marked this conversation as resolved.
Show resolved
Hide resolved
|
||||
| @ExperimentalObjCEnum | ||||
| @SinceKotlin("2.3") | ||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This version differs from the one for the |
||||
| 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. | ||||
| * | ||||
|
|
||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -6,6 +6,7 @@ | |
| 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 native values are accessible via the "nsEnum" property. | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. "The native values" might be confusing: "native" is a very ambiguous term, and it is mentioned for the first time in this comment. |
||
| */ | ||
| @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. | ||
| * | ||
|
|
||
| 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. | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The year is a bit old. |
||
| * 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 |
|---|---|---|
| @@ -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, BAR_FOO, COPY | ||
| } |
| 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 TranslatedClass( | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The name is kind of too ambiguous. |
||
| val auxiliaryDeclarations: List<ObjCTopLevel>, | ||
| val objCClass: ObjCClass, | ||
| ) | ||
|
|
||
| fun TranslatedClass(objCClass: ObjCClass?) = objCClass?.let { TranslatedClass(emptyList(), it) } | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||
|---|---|---|---|---|
| @@ -0,0 +1,24 @@ | ||||
| /* | ||||
| * 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.annotations.KaAnnotationValue | ||||
| import org.jetbrains.kotlin.analysis.api.symbols.KaClassSymbol | ||||
| import org.jetbrains.kotlin.name.ClassId | ||||
| import org.jetbrains.kotlin.name.FqName | ||||
|
|
||||
| /** Returns the NSEnum type for the given enum type if the corresponding annotation is set; null otherwise */ | ||||
| fun ObjCExportContext.getNSEnumType(symbol: KaClassSymbol): String? { | ||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This function returns not a type but a type name. Let's name it accordingly? Like |
||||
| val classId = ClassId(FqName("kotlin.native"), FqName("ObjCEnum"), false) | ||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This can be written this way val classId = ClassId.topLevel(KonanFqNames.objCEnum)to reuse the existing constant and keep things clearer. |
||||
| val annotation = symbol.annotations[classId].firstOrNull() | ||||
| return if (annotation == null) { | ||||
| null | ||||
| } else if (annotation.arguments.isEmpty()) { | ||||
| getObjCClassOrProtocolName(symbol).objCName + "NSEnum" | ||||
| } else { | ||||
| (annotation.arguments[0].expression as KaAnnotationValue.ConstantValue).value.value as String | ||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Line 50 in a4fbe41
|
||||
| } | ||||
| } | ||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,39 @@ | ||
| /* | ||
| * 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.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 | ||
|
|
||
| internal fun ObjCExportContext.translateNSEnum(symbol: KaClassSymbol, nsEnumTypeName: String, auxiliaryDeclarations: MutableList<ObjCTopLevel>): ObjCProperty { | ||
| auxiliaryDeclarations.add(ObjCNSEnum(nsEnumTypeName, getNSEnumEntries(symbol, nsEnumTypeName))) | ||
| return ObjCProperty( | ||
| "nsEnum", | ||
| null, | ||
| null, | ||
| ObjCRawType(nsEnumTypeName), | ||
| listOf("readonly") | ||
| ) | ||
| } | ||
|
|
||
|
|
||
| private fun ObjCExportContext.getNSEnumEntries(symbol: KaClassSymbol, typeName: 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, false), | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why does this code passes |
||
| typeName + getEnumEntryName(entry, true).replaceFirstChar { it.uppercaseChar() }, | ||
| ordinal | ||
| ) | ||
| } | ||
| } | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unused import?