diff --git a/native/objcexport-header-generator/impl/analysis-api/src/org/jetbrains/kotlin/objcexport/KtObjCExportSession.kt b/native/objcexport-header-generator/impl/analysis-api/src/org/jetbrains/kotlin/objcexport/KtObjCExportSession.kt index 3679c5982d3..e693adb7cdc 100644 --- a/native/objcexport-header-generator/impl/analysis-api/src/org/jetbrains/kotlin/objcexport/KtObjCExportSession.kt +++ b/native/objcexport-header-generator/impl/analysis-api/src/org/jetbrains/kotlin/objcexport/KtObjCExportSession.kt @@ -5,15 +5,62 @@ package org.jetbrains.kotlin.objcexport -interface KtObjCExportSession { +import org.jetbrains.kotlin.utils.getOrPutNullable + + +sealed interface KtObjCExportSession { val configuration: KtObjCExportConfiguration } +private val KtObjCExportSession.private: KtObjCExportSessionPrivate + get() = when (this) { + is KtObjCExportSessionPrivate -> this + } + +private interface KtObjCExportSessionPrivate : KtObjCExportSession { + val cache: MutableMap +} + inline fun KtObjCExportSession( configuration: KtObjCExportConfiguration, block: KtObjCExportSession.() -> T, ): T { - return object : KtObjCExportSession { - override val configuration: KtObjCExportConfiguration = configuration - }.block() -} \ No newline at end of file + return KtObjCExportSessionImpl(configuration, hashMapOf()).block() +} + +@PublishedApi +internal class KtObjCExportSessionImpl( + override val configuration: KtObjCExportConfiguration, + override val cache: MutableMap, +) : KtObjCExportSessionPrivate + + +/** + * Will cache the given [computation] in the current [KtObjCExportSession]. + * Example Usage: Caching the ObjC name of 'MutableSet' + * + * ```kotlin + * context(KtObjCExportSession) + * val mutableSetObjCName get() = cached("mutableSetOfObjCName") { + * "MutableSet".getObjCKotlinStdlibClassOrProtocolName().objCName + * // ^ + * // Requires KtObjCExportSession + * } + * ``` + */ +context(KtObjCExportSession) +internal inline fun cached(key: Any, noinline computation: () -> T): T { + return cached(T::class.java, key, computation) +} + +/** + * @see cached + */ +context(KtObjCExportSession) +private fun cached(typeOfT: Class, key: Any, computation: () -> T): T { + val value = private.cache.getOrPutNullable(key) { + computation() + } + + return typeOfT.cast(value) +} diff --git a/native/objcexport-header-generator/impl/analysis-api/src/org/jetbrains/kotlin/objcexport/translateToMappedObjCType.kt b/native/objcexport-header-generator/impl/analysis-api/src/org/jetbrains/kotlin/objcexport/translateToMappedObjCType.kt new file mode 100644 index 00000000000..bdad28b2cef --- /dev/null +++ b/native/objcexport-header-generator/impl/analysis-api/src/org/jetbrains/kotlin/objcexport/translateToMappedObjCType.kt @@ -0,0 +1,56 @@ +/* + * Copyright 2010-2024 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.KtAnalysisSession +import org.jetbrains.kotlin.analysis.api.types.KtType +import org.jetbrains.kotlin.backend.konan.objcexport.NSNumberKind +import org.jetbrains.kotlin.backend.konan.objcexport.ObjCClassType +import org.jetbrains.kotlin.builtins.StandardNames +import org.jetbrains.kotlin.name.ClassId + +/** + * Will translate [this] type to the corresponding ObjC equivalent. + * e.g. `kotlin.String` -> `NSString` + * e.g. `kotlin.collections.List` -> `NSArray` + * + * This function will also look through supertypes (e.g., custom implementations of List will still be mapped to NSArray). + * Returns `null` if the type is not mapped to any ObjC equivalent + */ +context(KtAnalysisSession, KtObjCExportSession) +internal fun KtType.translateToMappedObjCTypeOrNull(): ObjCClassType? { + return listOf(this).plus(this.getAllSuperTypes()).firstNotNullOfOrNull find@{ type -> + val classId = type.expandedClassSymbol?.classIdIfNonLocal ?: return@find null + mappedObjCTypeNames[classId]?.let { mappedTypeName -> + return@find ObjCClassType(mappedTypeName, type.translateTypeArgumentsToObjC()) + } + } +} + + +context(KtAnalysisSession, KtObjCExportSession) +private val mappedObjCTypeNames: Map + get() = cached("mappedObjCTypeNames") { + buildMap { + this[ClassId.topLevel(StandardNames.FqNames.list)] = "NSArray" + this[ClassId.topLevel(StandardNames.FqNames.mutableList)] = "NSMutableArray" + this[ClassId.topLevel(StandardNames.FqNames.set)] = "NSSet" + this[ClassId.topLevel(StandardNames.FqNames.mutableSet)] = "MutableSet".getObjCKotlinStdlibClassOrProtocolName().objCName + this[ClassId.topLevel(StandardNames.FqNames.map)] = "NSDictionary" + this[ClassId.topLevel(StandardNames.FqNames.mutableMap)] = "MutableDictionary".getObjCKotlinStdlibClassOrProtocolName().objCName + this[ClassId.topLevel(StandardNames.FqNames.string.toSafe())] = "NSString" + + + NSNumberKind.entries.forEach { number -> + val numberClassId = number.mappedKotlinClassId + if (numberClassId != null) { + this[numberClassId] = numberClassId.shortClassName.asString().getObjCKotlinStdlibClassOrProtocolName().objCName + } + } + } + } + + diff --git a/native/objcexport-header-generator/impl/analysis-api/src/org/jetbrains/kotlin/objcexport/translateToObjCType.kt b/native/objcexport-header-generator/impl/analysis-api/src/org/jetbrains/kotlin/objcexport/translateToObjCType.kt index b1965c70a6a..f142c23e57c 100644 --- a/native/objcexport-header-generator/impl/analysis-api/src/org/jetbrains/kotlin/objcexport/translateToObjCType.kt +++ b/native/objcexport-header-generator/impl/analysis-api/src/org/jetbrains/kotlin/objcexport/translateToObjCType.kt @@ -11,8 +11,8 @@ import org.jetbrains.kotlin.analysis.api.types.KtErrorType import org.jetbrains.kotlin.analysis.api.types.KtNonErrorClassType import org.jetbrains.kotlin.analysis.api.types.KtType import org.jetbrains.kotlin.analysis.api.types.KtTypeParameterType +import org.jetbrains.kotlin.backend.konan.KonanPrimitiveType import org.jetbrains.kotlin.backend.konan.objcexport.* -import org.jetbrains.kotlin.builtins.StandardNames import org.jetbrains.kotlin.name.ClassId import org.jetbrains.kotlin.name.FqName import org.jetbrains.kotlin.name.StandardClassIds @@ -76,6 +76,16 @@ internal fun KtType.mapToReferenceTypeIgnoringNullability(): ObjCNonNullReferenc return ObjCIdType } + /* Priority: Check if this type is mapped into a known ObjCType (e.g. String -> NSString */ + translateToMappedObjCTypeOrNull()?.let { mappedObjCType -> + return mappedObjCType + } + + /* Kotlin Native Primitive Type cannot be mapped to a reference type here */ + if (classId in kotlinNativePrimitiveClassIds) { + return ObjCIdType + } + if (isObjCObjectType()) { return ObjCIdType } @@ -93,34 +103,9 @@ internal fun KtType.mapToReferenceTypeIgnoringNullability(): ObjCNonNullReferenc return inlineTargetType.mapToReferenceTypeIgnoringNullability() } - /** - * Simplified version of [org.jetbrains.kotlin.backend.konan.objcexport.CustomTypeMapper] - */ - val typesMap = mutableMapOf().apply { - this[ClassId.topLevel(StandardNames.FqNames.list)] = "NSArray" - this[ClassId.topLevel(StandardNames.FqNames.mutableList)] = "NSMutableArray" - this[ClassId.topLevel(StandardNames.FqNames.set)] = "NSSet" - this[ClassId.topLevel(StandardNames.FqNames.mutableSet)] = "MutableSet".getObjCKotlinStdlibClassOrProtocolName().objCName - this[ClassId.topLevel(StandardNames.FqNames.map)] = "NSDictionary" - this[ClassId.topLevel(StandardNames.FqNames.mutableMap)] = "MutableDictionary".getObjCKotlinStdlibClassOrProtocolName().objCName - this[ClassId.topLevel(StandardNames.FqNames.string.toSafe())] = "NSString" - } - - NSNumberKind.entries.forEach { number -> - val numberClassId = number.mappedKotlinClassId - if (numberClassId != null) { - typesMap[numberClassId] = numberClassId.shortClassName.asString().getObjCKotlinStdlibClassOrProtocolName().objCName - } - } - if (fullyExpandedType is KtNonErrorClassType) { - typesMap[classId]?.let { typeName -> - val typeArguments = translateToObjCTypeArguments() - return ObjCClassType(typeName, typeArguments, classId = null) // todo: not clear for reader why null is important here! - } - val typeName = fullyExpandedType.classId.shortClassName.asString().getObjCKotlinStdlibClassOrProtocolName().objCName - val typeArguments = translateToObjCTypeArguments() + val typeArguments = translateTypeArgumentsToObjC() // TODO NOW: create type translation test if (classSymbol?.classKind == KtClassKind.INTERFACE) { @@ -152,7 +137,7 @@ internal fun KtType.mapToReferenceTypeIgnoringNullability(): ObjCNonNullReferenc context(KtAnalysisSession, KtObjCExportSession) -private fun KtType.translateToObjCTypeArguments(): List { +internal fun KtType.translateTypeArgumentsToObjC(): List { if (this !is KtNonErrorClassType) return emptyList() /* See special casing below */ @@ -197,10 +182,10 @@ private fun KtType.isBinaryRepresentationNullable(): Boolean { /** - * Types to be "hidden" during mapping, i.e. represented as `id`. + * Types to be "hidden" during mapping, i.e., represented as `id`. * - * Currently contains super types of classes handled by custom type mappers. - * Note: can be generated programmatically, but requires stdlib in this case. + * Currently, it contains super types of classes handled by custom type mappers. + * Note: It can be generated programmatically, but requires stdlib in this case. */ private val hiddenClassIds: Set = listOf( "kotlin.Any", @@ -214,6 +199,10 @@ private val hiddenClassIds: Set = listOf( "kotlin.collections.MutableIterable" ).map { ClassId.topLevel(FqName(it)) }.toSet() + +private val kotlinNativePrimitiveClassIds: Set = + KonanPrimitiveType.entries.map { it.classId }.toSet() + private val collectionClassIds = setOf( StandardClassIds.List, StandardClassIds.MutableList, StandardClassIds.Set, StandardClassIds.MutableSet, diff --git a/native/objcexport-header-generator/impl/analysis-api/test/org/jetbrains/kotlin/objcexport/tests/KtObjCExportSessionCacheTest.kt b/native/objcexport-header-generator/impl/analysis-api/test/org/jetbrains/kotlin/objcexport/tests/KtObjCExportSessionCacheTest.kt new file mode 100644 index 00000000000..23b0f5c997f --- /dev/null +++ b/native/objcexport-header-generator/impl/analysis-api/test/org/jetbrains/kotlin/objcexport/tests/KtObjCExportSessionCacheTest.kt @@ -0,0 +1,34 @@ +/* + * Copyright 2010-2024 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.tests + +import org.jetbrains.kotlin.objcexport.KtObjCExportConfiguration +import org.jetbrains.kotlin.objcexport.KtObjCExportSession +import org.jetbrains.kotlin.objcexport.cached +import org.junit.jupiter.api.Test +import kotlin.test.assertEquals +import kotlin.test.assertSame +import kotlin.test.fail + +class KtObjCExportSessionCacheTest { + + data class Payload(val key: Any) + + @Test + fun `test - cache with string key`() { + + KtObjCExportSession(KtObjCExportConfiguration()) { + val cachedInstance1 = cached("instance1") { Payload("1") } + val cachedInstance2 = cached("instance2") { Payload("2") } + + assertSame(cachedInstance1, cached("instance1") { fail("Expected instance1 to be cached") }) + assertSame(cachedInstance2, cached("instance2") { fail("Expected instance2 to be cached") }) + + assertEquals(cachedInstance1, Payload("1")) + assertEquals(cachedInstance2, Payload("2")) + } + } +} diff --git a/native/objcexport-header-generator/test/org/jetbrains/kotlin/backend/konan/tests/ObjCExportTypeTranslationTest.kt b/native/objcexport-header-generator/test/org/jetbrains/kotlin/backend/konan/tests/ObjCExportTypeTranslationTest.kt index 7dff0b0b43a..19b8f27e17b 100644 --- a/native/objcexport-header-generator/test/org/jetbrains/kotlin/backend/konan/tests/ObjCExportTypeTranslationTest.kt +++ b/native/objcexport-header-generator/test/org/jetbrains/kotlin/backend/konan/tests/ObjCExportTypeTranslationTest.kt @@ -334,17 +334,6 @@ class ObjCExportTypeTranslationTest( assertEquals("C *(^)(A *, B *) -> void", header.renderTypesOfSymbol("foo")) } - @Test - fun `test - function type returning char`() { - val header = header( - """ - val foo: () -> Char - """.trimIndent() - ) - - assertEquals("id (^)(void)", header.renderTypesOfSymbol("foo")) - } - @Test fun `test - function type - receiver`() { val header = header( @@ -555,6 +544,29 @@ class ObjCExportTypeTranslationTest( assertEquals(" -> unichar", header.renderTypesOfSymbol("foo")) } + @Test + fun `test - function type returning char`() { + val header = header( + """ + val foo: () -> Char + """.trimIndent() + ) + + assertEquals("id (^)(void)", header.renderTypesOfSymbol("foo")) + } + + @Test + fun `test - custom List implementation`() { + val header = header( + """ + interface MyList: List + val foo: MyList get() = error("stub") + """.trimIndent() + ) + + assertEquals("NSArray *", header.renderTypesOfSymbol("foo")) + } + private fun header( @Language("kotlin") vararg sourceCode: String, configuration: HeaderGenerator.Configuration = HeaderGenerator.Configuration(),