diff --git a/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/llvm/KotlinObjCClassInfoGenerator.kt b/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/llvm/KotlinObjCClassInfoGenerator.kt index 8b6953baff4..4b572ad567e 100644 --- a/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/llvm/KotlinObjCClassInfoGenerator.kt +++ b/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/llvm/KotlinObjCClassInfoGenerator.kt @@ -42,10 +42,13 @@ internal class KotlinObjCClassInfoGenerator(override val context: Context) : Con val bodySize = LLVMStoreSizeOfType(llvmTargetData, context.llvmDeclarations.forClass(irClass).bodyType).toInt() - val className = selectClassName(irClass)?.let { staticData.cStringLiteral(it) } ?: NullPointer(int8Type) + val exportedClassName = selectExportedClassName(irClass) + val className = exportedClassName ?: selectInternalClassName(irClass) + val classNameLiteral = className?.let { staticData.cStringLiteral(it) } ?: NullPointer(int8Type) val info = Struct(runtime.kotlinObjCClassInfo, - className, + classNameLiteral, + Int32(if (exportedClassName != null) 1 else 0), staticData.cStringLiteral(superclassName), staticData.placeGlobalConstArray("", int8TypePtr, @@ -91,16 +94,18 @@ internal class KotlinObjCClassInfoGenerator(override val context: Context) : Con } } - private fun selectClassName(irClass: IrClass): String? { + private fun selectExportedClassName(irClass: IrClass): String? { val exportObjCClassAnnotation = context.interopBuiltIns.exportObjCClass.fqNameSafe - return irClass.getAnnotationArgumentValue(exportObjCClassAnnotation, "name") - ?: if (irClass.annotations.hasAnnotation(exportObjCClassAnnotation)) - irClass.name.asString() - else if (irClass.isExported()) { - irClass.fqNameForIrSerialization.asString() - } else { - null // Generate as anonymous. - } + val explicitName = irClass.getAnnotationArgumentValue(exportObjCClassAnnotation, "name") + if (explicitName != null) return explicitName + + return if (irClass.annotations.hasAnnotation(exportObjCClassAnnotation)) irClass.name.asString() else null + } + + private fun selectInternalClassName(irClass: IrClass): String? = if (irClass.isExported()) { + irClass.fqNameForIrSerialization.asString() + } else { + null // Generate as anonymous. } private val impType = pointerType(functionType(int8TypePtr, true, int8TypePtr, int8TypePtr)) diff --git a/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/llvm/objcexport/ObjCExportCodeGenerator.kt b/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/llvm/objcexport/ObjCExportCodeGenerator.kt index 36fd88a3476..a66c1660d79 100644 --- a/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/llvm/objcexport/ObjCExportCodeGenerator.kt +++ b/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/llvm/objcexport/ObjCExportCodeGenerator.kt @@ -253,6 +253,13 @@ internal class ObjCExportCodeGenerator( emitTypeAdapters() + // Replace runtime global with weak linkage: + replaceExternalWeakOrCommonGlobal( + "Kotlin_ObjCInterop_uniquePrefix", + codegen.staticData.cStringLiteral(namer.topLevelNamePrefix), + context.standardLlvmSymbolsOrigin + ) + emitSelectorsHolder() emitStaticInitializers() diff --git a/backend.native/tests/build.gradle b/backend.native/tests/build.gradle index b5029c3c435..3287622c320 100644 --- a/backend.native/tests/build.gradle +++ b/backend.native/tests/build.gradle @@ -3540,7 +3540,8 @@ interopTest("interop_objc_smoke") { "5\n" + "String macro\n" + "CFString macro\n" + - "Deallocated\nDeallocated\n" + "Deallocated\nDeallocated\n" + + "Class TestExportObjCClass34 has multiple implementations. Which one will be used is undefined.\n" source = "interop/objc/smoke.kt" interop = 'objcSmoke' diff --git a/backend.native/tests/framework/multiple/framework1/first.kt b/backend.native/tests/framework/multiple/framework1/first.kt index ae516a444e4..f4bb6db5a33 100644 --- a/backend.native/tests/framework/multiple/framework1/first.kt +++ b/backend.native/tests/framework/multiple/framework1/first.kt @@ -15,6 +15,17 @@ class I1Impl : I1 { override fun getFortyTwo(): Int = 42 } +fun getI1() = object : I1 { + override fun getFortyTwo(): Int = 42 +} + class C -fun getUnit(): Unit? = Unit \ No newline at end of file +fun getUnit(): Unit? = Unit + +/* +// Disabled for now to avoid depending on platform libs. +fun getAnonymousObject() = object : platform.darwin.NSObject() {} +class NamedObject : platform.darwin.NSObject() +fun getNamedObject() = NamedObject() + */ \ No newline at end of file diff --git a/backend.native/tests/framework/multiple/framework2/second.kt b/backend.native/tests/framework/multiple/framework2/second.kt index aa03741da93..485fafd6326 100644 --- a/backend.native/tests/framework/multiple/framework2/second.kt +++ b/backend.native/tests/framework/multiple/framework2/second.kt @@ -13,6 +13,17 @@ interface I2 { fun getFortyTwoFrom(i2: I2): Int = i2.getFortyTwo() +fun getI2() = object : I2 { + override fun getFortyTwo(): Int = 42 +} + class C -fun isUnit(obj: Any?): Boolean = (obj === Unit) \ No newline at end of file +fun isUnit(obj: Any?): Boolean = (obj === Unit) + +/* +// Disabled for now to avoid depending on platform libs. +fun getAnonymousObject() = object : platform.darwin.NSObject() {} +class NamedObject : platform.darwin.NSObject() +fun getNamedObject() = NamedObject() + */ \ No newline at end of file diff --git a/backend.native/tests/framework/multiple/multiple.swift b/backend.native/tests/framework/multiple/multiple.swift index e1f6cb6fccf..e5604cd7cad 100644 --- a/backend.native/tests/framework/multiple/multiple.swift +++ b/backend.native/tests/framework/multiple/multiple.swift @@ -25,7 +25,7 @@ func testInteraction() throws { try assertEquals(actual: SecondKt.getFortyTwoFrom(i2: I1Impl()), expected: 42) } -func testIsolation() throws { +func testIsolation1() throws { try assertFalse(SecondKt.isUnit(obj: FirstKt.getUnit())) // Ensure frameworks don't share the same runtime (state): @@ -36,6 +36,20 @@ func testIsolation() throws { try assertTrue(Second.RuntimeState().consumeChange()) } +func testIsolation2() throws { + try assertEquals(actual: FirstKt.getI1().getFortyTwo(), expected: 42) + try assertEquals(actual: SecondKt.getI2().getFortyTwo(), expected: 42) +} + +func testIsolation3() throws { +#if false // Disabled for now to avoid depending on platform libs. + FirstKt.getAnonymousObject() + SecondKt.getAnonymousObject() + FirstKt.getNamedObject() + SecondKt.getNamedObject() +#endif +} + class MultipleFrameworksTests : TestProvider { var tests: [TestCase] = [] @@ -43,7 +57,9 @@ class MultipleFrameworksTests : TestProvider { tests = [ TestCase(name: "TestClashingNames", method: withAutorelease(testClashingNames)), TestCase(name: "TestInteraction", method: withAutorelease(testInteraction)), - TestCase(name: "TestIsolation", method: withAutorelease(testIsolation)), + TestCase(name: "TestIsolation1", method: withAutorelease(testIsolation1)), + TestCase(name: "TestIsolation2", method: withAutorelease(testIsolation2)), + TestCase(name: "TestIsolation3", method: withAutorelease(testIsolation3)), ] providers.append(self) } diff --git a/backend.native/tests/interop/objc/smoke.kt b/backend.native/tests/interop/objc/smoke.kt index 6506cbf234c..ed864770205 100644 --- a/backend.native/tests/interop/objc/smoke.kt +++ b/backend.native/tests/interop/objc/smoke.kt @@ -445,9 +445,16 @@ private const val TestExportObjCClass1Name = "TestExportObjCClass" @ExportObjCClass class TestExportObjCClass2 : NSObject() +const val TestExportObjCClass34Name = "TestExportObjCClass34" +@ExportObjCClass(TestExportObjCClass34Name) class TestExportObjCClass3 : NSObject() +@ExportObjCClass(TestExportObjCClass34Name) class TestExportObjCClass4 : NSObject() + fun testExportObjCClass() { assertEquals(TestExportObjCClass1Name, TestExportObjCClass1().objCClassName) assertEquals("TestExportObjCClass2", TestExportObjCClass2().objCClassName) + + assertTrue((TestExportObjCClass3().objCClassName == TestExportObjCClass34Name) + xor (TestExportObjCClass4().objCClassName == TestExportObjCClass34Name)) } private val Any.objCClassName: String diff --git a/runtime/src/main/cpp/ObjCExport.mm b/runtime/src/main/cpp/ObjCExport.mm index bab6022f322..446965d8116 100644 --- a/runtime/src/main/cpp/ObjCExport.mm +++ b/runtime/src/main/cpp/ObjCExport.mm @@ -17,6 +17,7 @@ #import "Types.h" #import "Memory.h" #include "Natives.h" +#include "ObjCInterop.h" #if KONAN_OBJC_INTEROP @@ -976,12 +977,13 @@ static void addVirtualAdapters(Class clazz, const ObjCTypeAdapter* typeAdapter) static Class createClass(const TypeInfo* typeInfo, Class superClass) { RuntimeAssert(typeInfo->superType_ != nullptr, ""); - char classNameBuffer[64]; - snprintf(classNameBuffer, sizeof(classNameBuffer), "kobjcc%d", anonymousClassNextId++); - const char* className = classNameBuffer; + int classIndex = (anonymousClassNextId++); + KStdString className = Kotlin_ObjCInterop_getUniquePrefix(); + className += "_kobjcc"; + className += std::to_string(classIndex); - Class result = objc_allocateClassPair(superClass, className, 0); - RuntimeAssert(result != nullptr, ""); + Class result = objc_allocateClassPair(superClass, className.c_str(), 0); + RuntimeCheck(result != nullptr, ""); // TODO: optimize by adding virtual adapters only for overridden methods. diff --git a/runtime/src/main/cpp/ObjCInterop.cpp b/runtime/src/main/cpp/ObjCInterop.cpp index 7596993a15a..c440eae2640 100644 --- a/runtime/src/main/cpp/ObjCInterop.cpp +++ b/runtime/src/main/cpp/ObjCInterop.cpp @@ -26,8 +26,19 @@ #include "MemoryPrivate.hpp" #include "Natives.h" +#include "ObjCInterop.h" +#include "Types.h" #include "Utils.h" +// Replaced in ObjCExportCodeGenerator. +__attribute__((weak)) const char* Kotlin_ObjCInterop_uniquePrefix = nullptr; + +const char* Kotlin_ObjCInterop_getUniquePrefix() { + auto result = Kotlin_ObjCInterop_uniquePrefix; + RuntimeCheck(result != nullptr, "unique prefix is not initialized"); + return result; +} + extern "C" { Class Kotlin_Interop_getObjCClass(const char* name); @@ -99,6 +110,7 @@ struct ObjCMethodDescription { struct KotlinObjCClassInfo { const char* name; + int exported; const char* superclassName; const char** protocolNames; @@ -129,6 +141,34 @@ static void AddMethods(Class clazz, const struct ObjCMethodDescription* methods, static SimpleMutex classCreationMutex; static int anonymousClassNextId = 0; +static Class allocateClass(const KotlinObjCClassInfo* info) { + Class superclass = Kotlin_Interop_getObjCClass(info->superclassName); + size_t extraBytes = sizeof(struct KotlinClassData); + + if (info->exported) { + RuntimeCheck(info->name != nullptr, "exported Objective-C class must have a name"); + Class result = objc_allocateClassPair(superclass, info->name, extraBytes); + if (result != nullptr) return result; + // Similar to how Objective-C runtime handles this: + fprintf(stderr, "Class %s has multiple implementations. Which one will be used is undefined.\n", info->name); + } + + KStdString className = Kotlin_ObjCInterop_getUniquePrefix(); + + if (info->name != nullptr) { + className += info->name; + } else { + className += "_kobjc"; + } + + int classId = anonymousClassNextId++; + className += std::to_string(classId); + + Class result = objc_allocateClassPair(superclass, className.c_str(), extraBytes); + RuntimeCheck(result != nullptr, "Failed to allocate Objective-C class"); + return result; +} + void* CreateKotlinObjCClass(const KotlinObjCClassInfo* info) { LockGuard lockGuard(classCreationMutex); @@ -137,15 +177,8 @@ void* CreateKotlinObjCClass(const KotlinObjCClassInfo* info) { return createdClass; } - char classNameBuffer[64]; - const char* className = info->name; - if (className == nullptr) { - snprintf(classNameBuffer, sizeof(classNameBuffer), "kobjc%d", anonymousClassNextId++); - className = classNameBuffer; - } + Class newClass = allocateClass(info); - Class superclass = Kotlin_Interop_getObjCClass(info->superclassName); - Class newClass = objc_allocateClassPair(superclass, className, sizeof(struct KotlinClassData)); RuntimeAssert(newClass != nullptr, "Failed to allocate Objective-C class"); Class newMetaclass = object_getClass(reinterpret_cast(newClass)); diff --git a/runtime/src/main/cpp/ObjCInterop.h b/runtime/src/main/cpp/ObjCInterop.h new file mode 100644 index 00000000000..30df929206a --- /dev/null +++ b/runtime/src/main/cpp/ObjCInterop.h @@ -0,0 +1,15 @@ +/* + * Copyright 2010-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license + * that can be found in the LICENSE file. + */ + +#ifndef RUNTIME_OBJCINTEROP_H +#define RUNTIME_OBJCINTEROP_H + +#if KONAN_OBJC_INTEROP + +const char* Kotlin_ObjCInterop_getUniquePrefix(); + +#endif // KONAN_OBJC_INTEROP + +#endif // RUNTIME_OBJCINTEROP_H