diff --git a/kotlin-native/Interop/Runtime/src/native/kotlin/kotlinx/cinterop/internal/Internal.kt b/kotlin-native/Interop/Runtime/src/native/kotlin/kotlinx/cinterop/internal/Internal.kt new file mode 100644 index 00000000000..ebd04b98c67 --- /dev/null +++ b/kotlin-native/Interop/Runtime/src/native/kotlin/kotlinx/cinterop/internal/Internal.kt @@ -0,0 +1,28 @@ +/* + * Copyright 2010-2023 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 kotlinx.cinterop.internal + +import kotlin.native.internal.GCUnsafeCall +import kotlin.native.internal.InternalForKotlinNative +import kotlinx.cinterop.BetaInteropApi +import kotlinx.cinterop.ObjCObject + +/** + * Detaches the Objective-C object from this Kotlin wrapper. More specifically, releases the Obj-C reference and zeroes + * the field where it is stored. + * + * This doesn't affect other possible Kotlin wrappers of this Objective-C object. Typically, when an Objective-C + * object gets into Kotlin, a new Kotlin wrapper is created, even if there is another wrapper already exists. To get + * the Objective-C object actually deallocated, each Kotlin wrapper should first be either GCed or detached with this + * function. + * + * If you use this object (Kotlin wrapper) after calling this function, the program behavior is undefined. + * In particular, it can crash. + */ +@InternalForKotlinNative +@GCUnsafeCall("Kotlin_objc_detachObjCObject") +@OptIn(BetaInteropApi::class) +public external fun detachObjCObject(obj: ObjCObject) diff --git a/kotlin-native/backend.native/tests/build.gradle b/kotlin-native/backend.native/tests/build.gradle index b1bdc21ae52..8e137490aaa 100644 --- a/kotlin-native/backend.native/tests/build.gradle +++ b/kotlin-native/backend.native/tests/build.gradle @@ -4599,7 +4599,7 @@ if (PlatformInfo.isAppleTarget(project)) { interopTestMultifile("interop_objc_tests") { source = "interop/objc/tests/" interop = 'objcTests' - flags = ['-tr', '-e', 'main'] + flags = ['-tr', '-e', 'main', '-opt-in=kotlin.native.internal.InternalForKotlinNative'] if (isNoopGC) { def exclude = [ diff --git a/kotlin-native/backend.native/tests/interop/objc/tests/detachObjCObject.h b/kotlin-native/backend.native/tests/interop/objc/tests/detachObjCObject.h new file mode 100644 index 00000000000..d0bf99b1e5c --- /dev/null +++ b/kotlin-native/backend.native/tests/interop/objc/tests/detachObjCObject.h @@ -0,0 +1,10 @@ +#import + +@interface DeallocFlagHolder : NSObject +@property BOOL deallocated; +@end + +@interface ObjectWithDeallocFlag : NSObject +@property (nonnull) DeallocFlagHolder* deallocFlagHolder; +- (instancetype _Nonnull)sameObject; +@end diff --git a/kotlin-native/backend.native/tests/interop/objc/tests/detachObjCObject.kt b/kotlin-native/backend.native/tests/interop/objc/tests/detachObjCObject.kt new file mode 100644 index 00000000000..614f13c3637 --- /dev/null +++ b/kotlin-native/backend.native/tests/interop/objc/tests/detachObjCObject.kt @@ -0,0 +1,77 @@ +/* + * Copyright 2010-2023 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. + */ + +import kotlin.test.* +import kotlin.native.ref.WeakReference +import kotlinx.cinterop.* +import kotlinx.cinterop.internal.detachObjCObject +import objcTests.* + +class detachObjCObjectTests { + @AfterTest + private fun gc() { + // An attempt to make sure the GC is fine after detachObjCObject. + kotlin.native.runtime.GC.collect() + kotlin.native.runtime.GC.collect() + } + + private fun checkThroughWeakRef(checkWeakRefBeforeReset: Boolean) { + val obj = NSObject() + val ref = WeakReference(obj) + + if (checkWeakRefBeforeReset) { + repeat(2) { + val refValue = ref.value + assertNotNull(refValue) + // refValue is actually a new wrapper, so we need to reset it as well: + detachObjCObject(refValue) + // The next iteration will check that the object is not yet removed. + } + } + + detachObjCObject(obj) + val refValue = ref.value + assertNull(refValue) + } + + @Test + fun checkThroughWeakRef() { + checkThroughWeakRef(checkWeakRefBeforeReset = false) + } + + @Test + fun checkThroughWeakRefWithMultipleWrappers() { + checkThroughWeakRef(checkWeakRefBeforeReset = true) + } + + @Test + fun checkThroughDeallocFlag() { + val obj = ObjectWithDeallocFlag() + val deallocFlagHolder = obj.deallocFlagHolder + + assertFalse(deallocFlagHolder.deallocated) + detachObjCObject(obj) + assertTrue(deallocFlagHolder.deallocated) + } + + @Test + fun checkThroughDeallocFlagWithMultipleWrappers() { + val obj = ObjectWithDeallocFlag() + val deallocFlagHolder = obj.deallocFlagHolder + + assertFalse(deallocFlagHolder.deallocated) + + val sameObj = obj.sameObject() // Same object, different wrapper + + detachObjCObject(obj) + assertFalse(deallocFlagHolder.deallocated) + + detachObjCObject(obj) + assertFalse(deallocFlagHolder.deallocated) + + detachObjCObject(sameObj) + assertTrue(deallocFlagHolder.deallocated) + } +} diff --git a/kotlin-native/backend.native/tests/interop/objc/tests/detachObjCObject.m b/kotlin-native/backend.native/tests/interop/objc/tests/detachObjCObject.m new file mode 100644 index 00000000000..b2a917526ae --- /dev/null +++ b/kotlin-native/backend.native/tests/interop/objc/tests/detachObjCObject.m @@ -0,0 +1,21 @@ +#import "detachObjCObject.h" + +@implementation DeallocFlagHolder +@end + +@implementation ObjectWithDeallocFlag +- (instancetype)init { + if (self = [super init]) { + self.deallocFlagHolder = [DeallocFlagHolder new]; + self.deallocFlagHolder.deallocated = NO; + } + return self; +} +- (void)dealloc { + self.deallocFlagHolder.deallocated = YES; +} + +- (instancetype _Nonnull)sameObject; { + return self; +} +@end \ No newline at end of file diff --git a/kotlin-native/runtime/src/main/cpp/ObjCInterop.mm b/kotlin-native/runtime/src/main/cpp/ObjCInterop.mm index 3f305e4619e..ed4aa181a9e 100644 --- a/kotlin-native/runtime/src/main/cpp/ObjCInterop.mm +++ b/kotlin-native/runtime/src/main/cpp/ObjCInterop.mm @@ -366,6 +366,19 @@ void Kotlin_objc_release(id ptr) { objc_release(ptr); } +void Kotlin_objc_detachObjCObject(KRef ref) { + id associatedObject = GetAssociatedObject(ref); + while (true) { + if (associatedObject == nullptr) break; + id actualAssociatedObject = AtomicCompareAndSwapAssociatedObject(ref, associatedObject, nullptr); + if (actualAssociatedObject == associatedObject) { + Kotlin_ObjCExport_releaseAssociatedObject(associatedObject); + break; + } + associatedObject = actualAssociatedObject; + } +} + } // extern "C" #else // KONAN_OBJC_INTEROP @@ -397,6 +410,10 @@ void Kotlin_objc_release(void* ptr) { RuntimeAssert(false, "Objective-C interop is disabled"); } +void Kotlin_objc_detachObjCObject(void* ref) { + RuntimeAssert(false, "Objective-C interop is disabled"); +} + } // extern "C" #endif // KONAN_OBJC_INTEROP