WIP tests
diff --git a/kotlin-native/backend.native/tests/interop/objc/tests/noAutorelease.h b/kotlin-native/backend.native/tests/interop/objc/tests/noAutorelease.h
index 610da07..eb55b2d 100644
--- a/kotlin-native/backend.native/tests/interop/objc/tests/noAutorelease.h
+++ b/kotlin-native/backend.native/tests/interop/objc/tests/noAutorelease.h
@@ -2,17 +2,26 @@
#import <Foundation/NSObject.h>
#import <Foundation/NSString.h>
+@interface ObjCLivenessTracker : NSObject
+-(void)add:(id)obj;
+-(Boolean)isEmpty;
+-(Boolean)objectsAreAlive;
+-(Boolean)objectsAreDead;
+@end;
+
@interface NoAutoreleaseCustomObject : NSObject
@end;
@protocol NoAutoreleaseHelper
@required
--(void)sendObject:(id)obj;
--(id)sameObject:(id)obj;
+@property id kotlinObject;
--(void)sendCustomObject:(NoAutoreleaseCustomObject*)customObject;
--(NoAutoreleaseCustomObject*)receiveCustomObject;
+-(void)sendKotlinObject:(id)kotlinObject;
+-(id)receiveKotlinObject;
+
+-(void)sendObjCObject:(NoAutoreleaseCustomObject*)objCObject;
+-(NoAutoreleaseCustomObject*)receiveObjCObject;
-(void)sendArray:(NSArray*)array;
-(NSArray*)receiveArray;
@@ -22,10 +31,76 @@
-(void)sendBlock:(int (^)(void))block;
-(int (^)(void))receiveBlock;
-
--(Boolean)weakIsNull;
@end;
-id<NoAutoreleaseHelper> getNoAutoreleaseHelperImpl(void);
+id<NoAutoreleaseHelper> getNoAutoreleaseHelperImpl(ObjCLivenessTracker*);
-NSString* testSend
\ No newline at end of file
+void callSendKotlinObject(id<NoAutoreleaseHelper> helper, id obj, ObjCLivenessTracker* tracker) {
+ [helper sendKotlinObject:obj];
+ [helper sendKotlinObject:obj];
+ [tracker add:obj];
+}
+
+void callReceiveKotlinObject(id<NoAutoreleaseHelper> helper, ObjCLivenessTracker* tracker) {
+ [tracker add:[helper receiveKotlinObject]];
+ [tracker add:[helper receiveKotlinObject]];
+}
+
+void callSendObjCObject(id<NoAutoreleaseHelper> helper, ObjCLivenessTracker* tracker) {
+ NoAutoreleaseCustomObject* obj = [NoAutoreleaseCustomObject new];
+
+ [helper sendObjCObject:obj];
+ [helper sendObjCObject:obj];
+ [tracker add:obj];
+}
+
+void callReceiveObjCObject(id<NoAutoreleaseHelper> helper, ObjCLivenessTracker* tracker) {
+ [tracker add:[helper receiveObjCObject]];
+ [tracker add:[helper receiveObjCObject]];
+}
+
+void callSendArray(id<NoAutoreleaseHelper> helper, ObjCLivenessTracker* tracker) {
+ NSArray* array = @[[NSObject new]];
+
+ [helper sendArray:array];
+ [helper sendArray:array];
+ [tracker add:array];
+}
+
+void callReceiveArray(id<NoAutoreleaseHelper> helper, ObjCLivenessTracker* tracker) {
+ [tracker add:[helper receiveArray]];
+ [tracker add:[helper receiveArray]];
+}
+
+void callSendString(id<NoAutoreleaseHelper> helper, ObjCLivenessTracker* tracker) {
+ NSString* string = [[NSObject new] description]; // To make it dynamic.
+
+ [helper sendString:string];
+ [helper sendString:string];
+ [tracker add:string];
+}
+
+void callReceiveString(id<NoAutoreleaseHelper> helper, ObjCLivenessTracker* tracker) {
+ [tracker add:[helper receiveString]];
+ [tracker add:[helper receiveString]];
+}
+
+extern int blockResult;
+
+int (^createBlock(void))() {
+ int localBlockResult = blockResult;
+ return ^{ return localBlockResult; }; // Try to make it capturing thus dynamic.
+}
+
+void callSendBlock(id<NoAutoreleaseHelper> helper, ObjCLivenessTracker* tracker) {
+ int (^block)(void) = createBlock(); // Try to make it heap-allocated.
+
+ [helper sendBlock:block];
+ [helper sendBlock:block];
+ [tracker add:block];
+}
+
+void callReceiveBlock(id<NoAutoreleaseHelper> helper, ObjCLivenessTracker* tracker) {
+ [tracker add:[helper receiveBlock]];
+ [tracker add:[helper receiveBlock]];
+}
diff --git a/kotlin-native/backend.native/tests/interop/objc/tests/noAutorelease.kt b/kotlin-native/backend.native/tests/interop/objc/tests/noAutorelease.kt
index 101653a..9ec4ad1 100644
--- a/kotlin-native/backend.native/tests/interop/objc/tests/noAutorelease.kt
+++ b/kotlin-native/backend.native/tests/interop/objc/tests/noAutorelease.kt
@@ -1,119 +1,251 @@
import kotlinx.cinterop.*
+import kotlin.native.internal.GC
import kotlin.native.ref.*
import kotlin.test.*
import objcTests.*
-private fun <T> testSend(
+// The tests below make best efforts to ensure that objects don't "leak" to autoreleasepool
+// when simply passing them between Kotlin and Objective-C.
+
+private class KotlinLivenessTracker {
+ val refs = mutableListOf<WeakReference<Any>>()
+
+ fun add(obj: Any) {
+ refs += WeakReference(obj)
+ }
+
+ fun isEmpty() = refs.isEmpty()
+ fun objectsAreAlive() = refs.all { it.value !== null }
+ fun objectsAreDead() = refs.all { it.value === null }
+}
+
+private fun test(
+ kotlinPeerRetainsObjC: Boolean = true,
+ block: (kotlinLivenessTracker: KotlinLivenessTracker, objCLivenessTracker: ObjCLivenessTracker) -> Unit
+) = repeat(2) {
+ val kotlinLivenessTracker = KotlinLivenessTracker()
+ val objCLivenessTracker = ObjCLivenessTracker()
+
+ GC.collect() // Make predictable
+
+ autoreleasepool { // FIXME: remove!!
+ block(kotlinLivenessTracker, objCLivenessTracker)
+ }
+
+ assertFalse(kotlinLivenessTracker.isEmpty())
+ assertTrue(kotlinLivenessTracker.objectsAreAlive())
+
+ assertFalse(objCLivenessTracker.isEmpty())
+ if (kotlinPeerRetainsObjC) {
+ assertTrue(objCLivenessTracker.objectsAreAlive())
+ } else {
+ assertTrue(objCLivenessTracker.objectsAreDead())
+ }
+
+ GC.collect()
+
+ assertTrue(kotlinLivenessTracker.objectsAreDead())
+ assertTrue(objCLivenessTracker.objectsAreDead())
+
+ // TODO: can we create an autoreleasepool and actually check that it is not used?
+}
+
+private fun <T> testSendToObjC(
createObject: () -> T,
kotlinPeerRetainsObjC: Boolean = true,
sendObject: NoAutoreleaseHelperProtocol.(T) -> Unit
-) = repeat(2) {
- val helper1 = getNoAutoreleaseHelperImpl()!!
- val helper2 = getNoAutoreleaseHelperImpl()!!
+) = test(kotlinPeerRetainsObjC) { kotlinLivenessTracker, objCLivenessTracker ->
+ val helper = getNoAutoreleaseHelperImpl(objCLivenessTracker)!!
- val weakRef = runNoInline {
- val obj = createObject()
+ val obj = createObject()!!
+ kotlinLivenessTracker.add(obj)
- helper1.sendObject(obj)
- helper2.sendObject(obj)
-
- WeakReference(obj)
- }
-
- runNoInline {
- assertNotNull(weakRef.value)
- assertEquals(!kotlinPeerRetainsObjC, helper1.weakIsNull())
- assertEquals(!kotlinPeerRetainsObjC, helper2.weakIsNull())
- }
-
- kotlin.native.internal.GC.collect()
-
- runNoInline {
- assertNull(weakRef.value)
- assertTrue(helper1.weakIsNull())
- assertTrue(helper2.weakIsNull())
- }
+ helper.sendObject(obj)
+ helper.sendObject(obj)
}
-private fun <T> testReceive(
+private fun <T> testReceiveFromObjC(
kotlinPeerRetainsObjC: Boolean = true, // FIXME: is it used?
receiveObject: NoAutoreleaseHelperProtocol.() -> T
-) = repeat(2) {
- val helper = getNoAutoreleaseHelperImpl()!!
+) = test(kotlinPeerRetainsObjC) { kotlinLivenessTracker, objCLivenessTracker ->
+ val helper = getNoAutoreleaseHelperImpl(objCLivenessTracker)!!
- val (weakRef1, weakRef2) = runNoInline {
- val obj1 = helper.receiveObject()
- val obj2 = helper.receiveObject()
+ val obj1 = helper.receiveObject()!!
+ val obj2 = helper.receiveObject()!!
- helper.clear()
+ kotlinLivenessTracker.add(obj1)
+ kotlinLivenessTracker.add(obj2)
+}
- Pair(WeakReference(obj1), WeakReference(obj2))
+private fun testCallToKotlin(
+ kotlinPeerRetainsObjC: Boolean = true, // FIXME: is it used?
+ callObjC: NoAutoreleaseHelperProtocol.(ObjCLivenessTracker) -> Unit
+) = test { kotlinLivenessTracker, objCLivenessTracker ->
+
+ val helper = object : NSObject(), NoAutoreleaseHelperProtocol {
+ val myKotlinObject = KotlinObject4()
+ val objCObject = NoAutoreleaseCustomObject()
+ val array = listOf(Any())
+ val string = Any().toString()
+ val block = createLambda()
+
+ override fun kotlinObject(): Any? {
+ error("should not be used")
+ }
+
+ override fun setKotlinObject(value: Any?) {
+ error("should not be used")
+ }
+
+ override fun sendKotlinObject(kotlinObject: Any?) {
+ kotlinLivenessTracker.add(kotlinObject!!)
+ }
+
+ override fun receiveKotlinObject(): Any? {
+ val result = myKotlinObject
+ kotlinLivenessTracker.add(result)
+ return result
+ }
+
+ override fun sendObjCObject(objCObject: NoAutoreleaseCustomObject?) {
+ kotlinLivenessTracker.add(objCObject!!)
+ }
+
+ override fun receiveObjCObject(): NoAutoreleaseCustomObject? {
+ val result = objCObject
+ kotlinLivenessTracker.add(result)
+ return result
+ }
+
+ override fun sendArray(array: List<*>?) {
+ kotlinLivenessTracker.add(array!!)
+ }
+
+ override fun receiveArray(): List<*>? {
+ val result = array
+ kotlinLivenessTracker.add(result)
+ return result
+ }
+
+ override fun sendString(string: String?) {
+ kotlinLivenessTracker.add(string!!)
+ }
+
+ override fun receiveString(): String? {
+ val result = string
+ kotlinLivenessTracker.add(result)
+ return result
+ }
+
+ override fun sendBlock(block: (() -> Int)?) {
+ kotlinLivenessTracker.add(block!!)
+ }
+
+ override fun receiveBlock(): (() -> Int)? {
+ val result = block
+ kotlinLivenessTracker.add(result)
+ return result
+ }
}
- runNoInline {
- assertNotNull(weakRef1.value)
- assertNotNull(weakRef2.value)
- assertEquals(!kotlinPeerRetainsObjC, helper.weakIsNull())
- }
+ helper.callObjC(objCLivenessTracker)
+}
- kotlin.native.internal.GC.collect()
+private class KotlinObject1
+private class KotlinObject2
+private class KotlinObject3
+private class KotlinObject4
- runNoInline {
- assertNull(weakRef1.value)
- assertNull(weakRef2.value)
- assertTrue(helper.weakIsNull())
+@Test fun testSendKotlinObjectToObjC() = testSendToObjC({ KotlinObject1() }) {
+ sendKotlinObject(it)
+}
+
+@Test fun testReceiveKotlinObjectFromObjC() {
+ val obj = KotlinObject2()
+ testReceiveFromObjC {
+ this.kotlinObject = obj
+ receiveKotlinObject()
}
}
-private class KotlinObject
-
-@Test fun testSendObject() = testSend({ KotlinObject() }) {
- sendObject(it)
+@Test fun testSendObjCObjectToObjC() = testSendToObjC({ NoAutoreleaseCustomObject() }) {
+ sendObjCObject(it)
}
-@Test
-fun testSameObject() {
- val obj = KotlinObject()
- testReceive {
- sameObject(obj)
- }
+@Test fun testReceiveObjCObjectFromObjC() = testReceiveFromObjC {
+ receiveObjCObject()
}
-@Test fun testSendCustomObject() = testSend({ NoAutoreleaseCustomObject() }) {
- sendCustomObject(it)
-}
-
-@Test fun testReceiveCustomObject() = testReceive {
- receiveCustomObject()
-}
-
-@Test fun testSendArray() = testSend({ listOf(KotlinObject()) }, kotlinPeerRetainsObjC = false) {
+@Test fun testSendArrayToObjC() = testSendToObjC({ listOf(KotlinObject1()) }, kotlinPeerRetainsObjC = false) {
sendArray(it)
}
-@Test fun testReceiveArray() = testReceive {
+@Test fun testReceiveArrayFromObjC() = testReceiveFromObjC {
receiveArray()
}
-@Test fun testSendString() = testSend({ Any().toString() }) {
+@Test fun testSendStringToObjC() = testSendToObjC({ Any().toString() }) {
sendString(it)
}
-@Test fun testReceiveString() = testReceive {
+@Test fun testReceiveStringFromObjC() = testReceiveFromObjC {
receiveString()
}
-@Test fun testSendBlock() = testSend({
- val lambdaResult = 123
- { lambdaResult } // make it capturing
-}, kotlinPeerRetainsObjC = false) {
+@Test fun testSendBlockToObjC() = testSendToObjC({ createLambda() }, kotlinPeerRetainsObjC = false) {
sendBlock(it)
}
-@Test fun testReceiveBlock() = testReceive {
+@Test fun testReceiveBlockFromObjC() = testReceiveFromObjC {
receiveBlock()
}
+@Test fun testSendKotlinObjectToKotlin() = testCallToKotlin {
+ callSendKotlinObject(this, KotlinObject3(), it)
+}
+
+@Test fun testReceiveKotlinObjectFromKotlin() = testCallToKotlin {
+ callReceiveKotlinObject(this, it)
+}
+
+@Test fun testSendObjCObjectToKotlin() = testCallToKotlin {
+ callSendObjCObject(this, it)
+}
+
+@Test fun testReceiveObjCObjectFromKotlin() = testCallToKotlin {
+ callReceiveObjCObject(this, it)
+}
+
+@Test fun testSendArrayToKotlin() = testCallToKotlin {
+ callSendArray(this, it)
+}
+
+@Test fun testReceiveArrayFromKotlin() = testCallToKotlin {
+ callReceiveArray(this, it)
+}
+
+@Test fun testSendStringToKotlin() = testCallToKotlin {
+ callSendString(this, it)
+}
+
+@Test fun testReceiveStringFromKotlin() = testCallToKotlin {
+ callReceiveString(this, it)
+}
+
+@Test fun testSendBlockToKotlin() = testCallToKotlin {
+ callSendBlock(this, it)
+}
+
+@Test fun testReceiveBlockFromKotlin() = testCallToKotlin {
+ callReceiveBlock(this, it)
+}
+
+private fun createLambda(): () -> Int {
+ val lambdaResult = 123
+ return { lambdaResult } // make it capturing
+}
+
+// FIXME: remove
// Note: this executes code with a separate stack frame,
// so no stack refs will remain after it, and GC will be able to collect the garbage.
private fun <R> runNoInline(block: () -> R) = block()
diff --git a/kotlin-native/backend.native/tests/interop/objc/tests/noAutorelease.m b/kotlin-native/backend.native/tests/interop/objc/tests/noAutorelease.m
index b4ad6ae..f387c29 100644
--- a/kotlin-native/backend.native/tests/interop/objc/tests/noAutorelease.m
+++ b/kotlin-native/backend.native/tests/interop/objc/tests/noAutorelease.m
@@ -4,68 +4,127 @@
@end;
@interface NoAutoreleaseHelperImpl : NSObject <NoAutoreleaseHelper>
-@property (weak) id weakRef;
+@property ObjCLivenessTracker* objCLivenessTracker;
+
+@property id kotlinObject;
+@property NoAutoreleaseCustomObject* objCObject;
+@property NSArray* array;
+@property NSString* string;
+@property int (^block)(void);
+
@end;
-int globalBlockResult = 123;
-
@implementation NoAutoreleaseHelperImpl
-
--(void)sendObject:(id)obj {
- self.weakRef = obj;
+-(instancetype)init {
+ if (self = [super init]) {
+ self.objCObject = [NoAutoreleaseCustomObject new];
+ self.array = @[[NSObject new]];
+ self.string = [[NSObject new] description];
+ self.block = createBlock();
+ }
+ return self;
}
--(id)sameObject:(id)obj {
- self.weakRef = obj;
- return obj;
+-(void)sendKotlinObject:(id)kotlinObject {
+ [self.objCLivenessTracker add:kotlinObject];
}
--(void)sendCustomObject:(NoAutoreleaseCustomObject*)customObject {
- self.weakRef = customObject;
+-(id)receiveKotlinObject {
+ id result = self.kotlinObject;
+ [self.objCLivenessTracker add:result];
+ return result;
}
--(NoAutoreleaseCustomObject*)receiveCustomObject {
- NoAutoreleaseCustomObject* result = [NoAutoreleaseCustomObject new];
- self.weakRef = result;
+-(void)sendObjCObject:(NoAutoreleaseCustomObject*)objCObject {
+ [self.objCLivenessTracker add:objCObject];
+}
+
+-(NoAutoreleaseCustomObject*)receiveObjCObject {
+ NoAutoreleaseCustomObject* result = self.objCObject;
+ [self.objCLivenessTracker add:result];
return result;
}
-(void)sendArray:(NSArray*)array {
- self.weakRef = array;
+ [self.objCLivenessTracker add:array];
}
-(NSArray*)receiveArray {
- NSArray* result = @[[NSObject new]];
- self.weakRef = result;
+ NSArray* result = self.array;
+ [self.objCLivenessTracker add:result];
return result;
}
-(void)sendString:(NSString*)string {
- self.weakRef = string;
+ [self.objCLivenessTracker add:string];
}
-(NSString*)receiveString {
- NSString* result = [NSObject new].description;
- self.weakRef = result;
+ NSString* result = self.string;
+ [self.objCLivenessTracker add:result];
return result;
}
-(void)sendBlock:(int (^)(void))block {
- self.weakRef = block;
+ [self.objCLivenessTracker add:block];
}
-(int (^)(void))receiveBlock {
- int blockResult = globalBlockResult; // To make block capturing.
- int (^result)(void) = ^{ return blockResult; };
- self.weakRef = result;
+ int (^result)(void) = self.block;
+ [self.objCLivenessTracker add:result];
return result;
}
--(Boolean)weakIsNull {
- return self.weakRef == nil;
-}
@end;
-id<NoAutoreleaseHelper> getNoAutoreleaseHelperImpl(void) {
- return [NoAutoreleaseHelperImpl new];
+id<NoAutoreleaseHelper> getNoAutoreleaseHelperImpl(ObjCLivenessTracker* objCLivenessTracker) {
+ NoAutoreleaseHelperImpl* result = [NoAutoreleaseHelperImpl new];
+ result.objCLivenessTracker = objCLivenessTracker;
+ return result;
}
+
+int blockResult = 42;
+
+@interface ObjCWeakRef : NSObject
+@property (weak) id value;
+@end;
+
+@implementation ObjCWeakRef;
+@end;
+
+@implementation ObjCLivenessTracker {
+ NSMutableArray<ObjCWeakRef*>* weakRefs;
+}
+
+-(instancetype)init {
+ if (self = [super init]) {
+ self->weakRefs = [NSMutableArray new];
+ }
+ return self;
+}
+
+-(void)add:(id)obj {
+ ObjCWeakRef* weakRef = [ObjCWeakRef new];
+ weakRef.value = obj;
+ [weakRefs addObject:weakRef];
+}
+
+-(Boolean)isEmpty {
+ return [weakRefs count] == 0;
+}
+
+-(Boolean)objectsAreAlive {
+ for (ObjCWeakRef* weakRef in weakRefs) {
+ if (weakRef.value == nil) return NO;
+ }
+ return YES;
+}
+
+-(Boolean)objectsAreDead {
+ for (ObjCWeakRef* weakRef in weakRefs) {
+ if (weakRef.value != nil) return NO;
+ }
+ return YES;
+}
+
+@end;