diff --git a/kotlin-native/backend.native/tests/build.gradle b/kotlin-native/backend.native/tests/build.gradle index c4ebdde8e3e..8c69e6e086f 100644 --- a/kotlin-native/backend.native/tests/build.gradle +++ b/kotlin-native/backend.native/tests/build.gradle @@ -4447,6 +4447,10 @@ if (PlatformInfo.isAppleTarget(project)) { it.defFile 'interop/objc/kt56402/objclib.def' it.headers "$projectDir/interop/objc/kt56402/objclib.h" } + createInterop("objCAction") { + it.defFile 'interop/objc/objCAction/objclib.def' + it.headers "$projectDir/interop/objc/objCAction/objclib.h" + } } createInterop("withSpaces") { @@ -5316,6 +5320,24 @@ if (PlatformInfo.isAppleTarget(project)) { } } } + + interopTest("interop_objCAction") { + source = 'interop/objc/objCAction/main.kt' + interop = "objCAction" + flags = ["-tr"] + doBeforeBuild { + mkdir(buildDir) + project.extensions.execClang.execClangForCompilerTests(project.target) { + args "$projectDir/interop/objc/objCAction/objclib.m" + args "-lobjc", '-fobjc-arc' + args '-fPIC', '-shared', '-o', "$buildDir/libobjCAction.dylib" + } + if (UtilsKt.isSimulatorTarget(project, project.target)) { + UtilsKt.codesign(project, "$buildDir/libobjCAction.dylib") + } + } + UtilsKt.dependsOnPlatformLibs(it) + } } tasks.register("KT-50983", KonanDriverTest) { diff --git a/kotlin-native/backend.native/tests/interop/objc/objCAction/main.kt b/kotlin-native/backend.native/tests/interop/objc/objCAction/main.kt new file mode 100644 index 00000000000..4f26be70b97 --- /dev/null +++ b/kotlin-native/backend.native/tests/interop/objc/objCAction/main.kt @@ -0,0 +1,217 @@ +/* + * 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. + */ + +@file:OptIn(kotlin.ExperimentalStdlibApi::class) + +import objclib.* + +import platform.Foundation.* +import kotlin.test.* +import kotlinx.cinterop.* +import kotlin.concurrent.Volatile + +class CondVar { + @Volatile + @PublishedApi + internal var steps = 0 + + fun notify() { + steps += 1 + } + + inline fun runAndWait(block: () -> R) { + val prev = steps + val result = block() + // During block() execution someone somewhere does notify(). + // In this loop we wait to read the result of that notify(), which acts as a synchronization point. + while (steps <= prev) {} + } +} + +class Incrementor : NSObject() { + @ObjCOutlet + var counter: NSNumber = NSNumber.numberWithInt(0) + + val condVar = CondVar() + + @ObjCAction + fun increment() { // Only Unit is allowed for return value + println("I'm here to make sure this function generates a frame. Here's an object: ${Any()}") + counter = NSNumber.numberWithInt(counter.intValue + 1) + condVar.notify() + } + + @ObjCAction + fun incrementBy(amount: NSNumber) { // Only Unit is allowed for return value + println("I'm here to make sure this function generates a frame. Here's an object: ${Any()}") + counter = NSNumber.numberWithInt(counter.intValue + amount.intValue) + condVar.notify() + } + + @ObjCAction + fun incrementOtherBy(other: Incrementor, amount: NSNumber) { // Only Unit is allowed for return value + println("I'm here to make sure this function generates a frame. Here's an object: ${Any()}") + other.incrementBy(amount) + condVar.notify() + } +} + +class IncrementorViaObjC { + val impl = Incrementor() + + var counter: NSNumber + get() = impl.counter // ObjCOutlet only works for setters - not getters. + set(value) = setProperty(impl, "counter", value) + + fun increment() = performSelector0(impl, "increment") + + fun incrementBy(amount: NSNumber) = performSelector1(impl, "incrementBy:", amount) + + fun incrementOtherBy(other: Incrementor, amount: NSNumber) = performSelector2(impl, "incrementOtherBy:amount:", other, amount) +} + +class IncrementorViaObjCInNewThread { + val impl = Incrementor() + + var counter: NSNumber + get() = impl.counter // ObjCOutlet only works for setters - not getters. + set(value) = setProperty(impl, "counter", value) + + fun increment() = impl.condVar.runAndWait { performSelectorInNewThread0(impl, "increment") } + + fun incrementBy(amount: NSNumber) = impl.condVar.runAndWait { performSelectorInNewThread1(impl, "incrementBy:", amount) } + + fun incrementOtherBy(other: Incrementor, amount: NSNumber) = impl.condVar.runAndWait { performSelectorInNewThread2(impl, "incrementOtherBy:amount:", other, amount) } +} + +// Sanity checking that using functions and properties as regular kotlin properties works. +@Test +fun testIncrementorKt() { + val incrementor = Incrementor() + assertEquals(0, incrementor.counter.intValue) + + incrementor.increment() + assertEquals(1, incrementor.counter.intValue) + + incrementor.incrementBy(NSNumber.numberWithInt(3)) + assertEquals(4, incrementor.counter.intValue) + + incrementor.counter = NSNumber.numberWithInt(7) + assertEquals(7, incrementor.counter.intValue) + + incrementor.incrementBy(NSNumber.numberWithInt(2)) + assertEquals(9, incrementor.counter.intValue) + + val otherIncrementor = Incrementor() + incrementor.incrementOtherBy(otherIncrementor, NSNumber.numberWithInt(5)) + assertEquals(9, incrementor.counter.intValue) + assertEquals(5, otherIncrementor.counter.intValue) +} + +// Doing everything testIncrementorKt does, but via ObjC dynamic dispatch +@Test +fun testIncrementorObjC() { + val incrementor = IncrementorViaObjC() + assertEquals(0, incrementor.counter.intValue) + + incrementor.increment() + assertEquals(1, incrementor.counter.intValue) + + incrementor.incrementBy(NSNumber.numberWithInt(3)) + assertEquals(4, incrementor.counter.intValue) + + incrementor.counter = NSNumber.numberWithInt(7) + assertEquals(7, incrementor.counter.intValue) + + incrementor.incrementBy(NSNumber.numberWithInt(2)) + assertEquals(9, incrementor.counter.intValue) + + val otherIncrementor = Incrementor() + incrementor.incrementOtherBy(otherIncrementor, NSNumber.numberWithInt(5)) + assertEquals(9, incrementor.counter.intValue) + assertEquals(5, otherIncrementor.counter.intValue) +} + +// Doing everything testIncrementorKt does, but via ObjC dynamic dispatch and in a new NSThread +@Test +fun testIncrementorObjCInNewThread() { + if (!isExperimentalMM()) // Cross-thread stuff doesn't work with the legacy MM + return + + val incrementor = IncrementorViaObjCInNewThread() + assertEquals(0, incrementor.counter.intValue) + + incrementor.increment() + assertEquals(1, incrementor.counter.intValue) + + incrementor.incrementBy(NSNumber.numberWithInt(3)) + assertEquals(4, incrementor.counter.intValue) + + incrementor.counter = NSNumber.numberWithInt(7) + assertEquals(7, incrementor.counter.intValue) + + incrementor.incrementBy(NSNumber.numberWithInt(2)) + assertEquals(9, incrementor.counter.intValue) + + val otherIncrementor = Incrementor() + incrementor.incrementOtherBy(otherIncrementor, NSNumber.numberWithInt(5)) + assertEquals(9, incrementor.counter.intValue) + assertEquals(5, otherIncrementor.counter.intValue) +} + +// Mixing Kt and ObjC accesses +@Test +fun testIncrementorMix() { + val objc = IncrementorViaObjC() + val kt = objc.impl + assertEquals(0, kt.counter.intValue) + + kt.increment() + assertEquals(1, kt.counter.intValue) + + objc.increment() + assertEquals(2, kt.counter.intValue) + + kt.counter = NSNumber.numberWithInt(7) + assertEquals(7, kt.counter.intValue) + + objc.increment() + assertEquals(8, kt.counter.intValue) + + objc.counter = NSNumber.numberWithInt(11) + assertEquals(11, kt.counter.intValue) + + kt.increment() + assertEquals(12, kt.counter.intValue) +} + +// Mixing Kt and ObjC accesses when ObjC happens in a different thread +@Test +fun testIncrementorMixInNewThread() { + if (!isExperimentalMM()) // Cross-thread stuff doesn't work with the legacy MM + return + + val objc = IncrementorViaObjCInNewThread() + val kt = objc.impl + assertEquals(0, kt.counter.intValue) + + kt.increment() + assertEquals(1, kt.counter.intValue) + + objc.increment() + assertEquals(2, kt.counter.intValue) + + kt.counter = NSNumber.numberWithInt(7) + assertEquals(7, kt.counter.intValue) + + objc.increment() + assertEquals(8, kt.counter.intValue) + + objc.counter = NSNumber.numberWithInt(11) + assertEquals(11, kt.counter.intValue) + + kt.increment() + assertEquals(12, kt.counter.intValue) +} \ No newline at end of file diff --git a/kotlin-native/backend.native/tests/interop/objc/objCAction/objclib.def b/kotlin-native/backend.native/tests/interop/objc/objCAction/objclib.def new file mode 100644 index 00000000000..9bca35b833d --- /dev/null +++ b/kotlin-native/backend.native/tests/interop/objc/objCAction/objclib.def @@ -0,0 +1,3 @@ +language = Objective-C +headerFilter = **/objclib.h +linkerOpts = -lobjCAction diff --git a/kotlin-native/backend.native/tests/interop/objc/objCAction/objclib.h b/kotlin-native/backend.native/tests/interop/objc/objCAction/objclib.h new file mode 100644 index 00000000000..a686797b50e --- /dev/null +++ b/kotlin-native/backend.native/tests/interop/objc/objCAction/objclib.h @@ -0,0 +1,9 @@ +#import + +void performSelector0(id target, NSString* selectorName); +void performSelectorInNewThread0(id target, NSString* selectorName); +void performSelector1(id target, NSString* selectorName, id arg1); +void performSelectorInNewThread1(id target, NSString* selectorName, id arg1); +void performSelector2(id target, NSString* selectorName, id arg1, id arg2); +void performSelectorInNewThread2(id target, NSString* selectorName, id arg1, id arg2); +void setProperty(id target, NSString* propertyName, id value); \ No newline at end of file diff --git a/kotlin-native/backend.native/tests/interop/objc/objCAction/objclib.m b/kotlin-native/backend.native/tests/interop/objc/objCAction/objclib.m new file mode 100644 index 00000000000..b1e57cfccb7 --- /dev/null +++ b/kotlin-native/backend.native/tests/interop/objc/objCAction/objclib.m @@ -0,0 +1,47 @@ +#include "objclib.h" + +void performSelector0(id target, NSString* selectorName) { + NSLog(@"performSelector0(%@, %@)", target, selectorName); + // Ignoring return value, because ObjCAction does not allow one. + [target performSelector:NSSelectorFromString(selectorName)]; +} + +void performSelectorInNewThread0(id target, NSString* selectorName) { + NSLog(@"performSelectorInNewThread0(%@, %@)", target, selectorName); + NSThread* thread = [[NSThread alloc] initWithBlock:^{ + performSelector0(target, selectorName); + }]; + [thread start]; +} + +void performSelector1(id target, NSString* selectorName, id arg1) { + NSLog(@"performSelector1(%@, %@, %@)", target, selectorName, arg1); + // Ignoring return value, because ObjCAction does not allow one. + [target performSelector:NSSelectorFromString(selectorName) withObject:arg1]; +} + +void performSelectorInNewThread1(id target, NSString* selectorName, id arg1) { + NSLog(@"performSelectorInNewThread1(%@, %@, %@)", target, selectorName, arg1); + // Use NSThread method directly just for variety. + NSThread* thread = [[NSThread alloc] initWithTarget:target selector:NSSelectorFromString(selectorName) object:arg1]; + [thread start]; +} + +void performSelector2(id target, NSString* selectorName, id arg1, id arg2) { + NSLog(@"performSelector2(%@, %@, %@, %@)", target, selectorName, arg1, arg2); + // Ignoring return value, because ObjCAction does not allow one. + [target performSelector:NSSelectorFromString(selectorName) withObject:arg1 withObject:arg2]; +} + +void performSelectorInNewThread2(id target, NSString* selectorName, id arg1, id arg2) { + NSLog(@"performSelectorInNewThread2(%@, %@, %@, %@)", target, selectorName, arg1, arg2); + NSThread* thread = [[NSThread alloc] initWithBlock:^{ + performSelector2(target, selectorName, arg1, arg2); + }]; + [thread start]; +} + +void setProperty(id target, NSString* propertyName, id value) { + NSLog(@"setProperty(%@, %@, %@)", target, propertyName, value); + [target setValue:value forKey:propertyName]; +} \ No newline at end of file