-
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 20 commits
2ab6493
b824f55
e180ad4
6ab07ae
b925ff2
e7ec42e
7e4fb66
b713914
b126cd4
f51d934
854371f
4013372
c9de8d8
200acce
29e8031
de01982
0ccc118
60cca82
8da8bb4
6a52203
bfb6921
51cf02b
32645cb
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 |
|---|---|---|
|
|
@@ -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>" } | ||
stefanhaustein marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| 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}" | ||
|
||
| } | ||
| 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") | ||||
stefanhaustein marked this conversation as resolved.
Show resolved
Hide resolved
|
||||
| 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 |
|---|---|---|
| @@ -0,0 +1,16 @@ | ||
| /* | ||
| * Copyright 2010-2022 JetBrains s.r.o. and Kotlin Programming Language contributors. | ||
stefanhaustein marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| * 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( | ||
stefanhaustein marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| 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? { | ||
stefanhaustein marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| val classId = ClassId(FqName("kotlin.native"), FqName("ObjCEnum"), false) | ||
stefanhaustein marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| 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 | ||
stefanhaustein marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| } | ||
| } | ||
| 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), | ||
stefanhaustein marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| typeName + getEnumEntryName(entry, true).replaceFirstChar { it.uppercaseChar() }, | ||
| ordinal | ||
| ) | ||
| } | ||
| } | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.