diff --git a/kotlin-native/backend.native/tests/interop/objc/tests/workerAutoreleasePool.h b/kotlin-native/backend.native/tests/interop/objc/tests/workerAutoreleasePool.h new file mode 100644 index 00000000000..0bbf79860ef --- /dev/null +++ b/kotlin-native/backend.native/tests/interop/objc/tests/workerAutoreleasePool.h @@ -0,0 +1,9 @@ +#import + +@interface CreateAutoreleaseDeallocated : NSObject +@property BOOL value; +@end; + +@interface CreateAutorelease : NSObject ++(void)createAutorelease:(CreateAutoreleaseDeallocated*)deallocated; +@end; diff --git a/kotlin-native/backend.native/tests/interop/objc/tests/workerAutoreleasePool.kt b/kotlin-native/backend.native/tests/interop/objc/tests/workerAutoreleasePool.kt new file mode 100644 index 00000000000..ad942a373f0 --- /dev/null +++ b/kotlin-native/backend.native/tests/interop/objc/tests/workerAutoreleasePool.kt @@ -0,0 +1,198 @@ +import kotlin.native.concurrent.* +import kotlinx.cinterop.* +import kotlin.test.* +import objcTests.* + +@Test +fun testExecuteInMainPark() = + test(Execute, InMain, Park) + +@Test +fun testExecuteInMainProcessQueue() = + test(Execute, InMain, ProcessQueue) + +@Test +fun testExecuteInWorkerPark() = + test(Execute, InWorker, Park) + +@Test +fun testExecuteInWorkerProcessQueue() = + test(Execute, InWorker, ProcessQueue) + +@Test +fun testExecuteInMainToWorkerNoYield() = + test(Execute, InMainToWorker, NoYield) + +@Test +fun testExecuteAfter0InMainPark() = + test(ExecuteAfter0, InMain, Park) + +@Test +fun testExecuteAfter0InMainProcessQueue() = + test(ExecuteAfter0, InMain, ProcessQueue) + +@Test +fun testExecuteAfter0InWorkerPark() = + test(ExecuteAfter0, InWorker, Park) + +@Test +fun testExecuteAfter0InWorkerProcessQueue() = + test(ExecuteAfter0, InWorker, ProcessQueue) + +@Test +fun testExecuteAfter0InMainToWorkerNoYield() = + test(ExecuteAfter0, InMainToWorker, NoYield) + +@Test +fun testExecuteAfter10InMainPark() = + testExecuteAfter10(InMain, Park) + +@Test +fun testExecuteAfter10InMainProcessQueue() = + testExecuteAfter10(InMain, ProcessQueue) + +@Test +fun testExecuteAfter10InWorkerPark() = + testExecuteAfter10(InWorker, Park) + +@Test +fun testExecuteAfter10InWorkerProcessQueue() = + testExecuteAfter10(InWorker, ProcessQueue) + +@Test +fun testExecuteAfter10InMainToWorkerNoYield() = + testExecuteAfter10(InMainToWorker, NoYield) + +private fun testExecuteAfter10(context: Context, yieldMethod: Yield) = + test(ExecuteAfter10(yieldMethod), context, yieldMethod) + +private fun test(method: ExecuteMethod, context: Context, yieldMethod: Yield) { + context.withWorker { worker -> + fun execute(block: () -> Unit) { + val future = method.submit(worker, block) + yieldMethod.yield() + method.wait(future) + } + + val deallocated = CreateAutoreleaseDeallocated() + + execute { + CreateAutorelease.createAutorelease(deallocated) + // Object is still in autorelease pool: + assertFalse(deallocated.value) + } + + // autorelease pool is processed after the job is finished, so the object should be deallocated; + // Checking in a job to make sure previous job is completely processed: + execute { + assertTrue(deallocated.value) + } + } +} + +interface ExecuteMethod { + fun submit(worker: Worker, block: () -> Unit): F + fun wait(future: F) +} + +object Execute : ExecuteMethod> { + override fun submit(worker: Worker, block: () -> Unit) = worker.execute(TransferMode.SAFE, { block.freeze() }) { + it() + } + + override fun wait(future: Future) { + future.result // Throws on failure. + } +} + +abstract class ExecuteAfter : ExecuteMethod> { + abstract val timeout: Long + + abstract fun sleepAndYield() + + override fun submit(worker: Worker, block: () -> Unit): AtomicReference { + val result = AtomicReference(null) + + worker.executeAfter(timeout, { + try { + block() + result.value = true + } catch (e: Throwable) { + result.value = e.freeze() + } + }.freeze()) + + return result + } + + override fun wait(future: AtomicReference) { + while (true) { + sleepAndYield() + when (val it = future.value) { + null -> continue + true -> return + else -> throw it as Throwable + } + } + } +} + +object ExecuteAfter0 : ExecuteAfter() { + override val timeout = 0L + + override fun sleepAndYield() { + // No sleep or additional yield required. + } +} + +class ExecuteAfter10(val yieldMethod: Yield) : ExecuteAfter() { + override val timeout = 10L + + override fun sleepAndYield() { + Worker.current.park(timeout + 1L, process = false) + yieldMethod.yield() + } +} + +interface Context { + fun withWorker(block: (worker: Worker) -> Unit) +} + +object InMain : Context { + override fun withWorker(block: (worker: Worker) -> Unit) = block(Worker.current) +} + +object InMainToWorker : Context { + override fun withWorker(block: (Worker) -> Unit) = kotlin.native.concurrent.withWorker { + block(this) + } +} + +object InWorker : Context { + override fun withWorker(block: (Worker) -> Unit) = kotlin.native.concurrent.withWorker { + val method = Execute + method.wait(method.submit(this) { + block(this) + }) + } +} + +interface Yield { + fun yield() +} + +object Park : Yield { + override fun yield() { + Worker.current.park(0L, process = true) + } +} + +object ProcessQueue : Yield { + override fun yield() { + Worker.current.processQueue() + } +} + +object NoYield : Yield { + override fun yield() {} +} diff --git a/kotlin-native/backend.native/tests/interop/objc/tests/workerAutoreleasePool.m b/kotlin-native/backend.native/tests/interop/objc/tests/workerAutoreleasePool.m new file mode 100644 index 00000000000..5fa2797d994 --- /dev/null +++ b/kotlin-native/backend.native/tests/interop/objc/tests/workerAutoreleasePool.m @@ -0,0 +1,22 @@ +#import "workerAutoreleasePool.h" + +@implementation CreateAutoreleaseDeallocated +@end; + +@implementation CreateAutorelease { + CreateAutoreleaseDeallocated* deallocated; +} ++(instancetype)create:(CreateAutoreleaseDeallocated*)deallocated { + CreateAutorelease* result = [self new]; + result->deallocated = deallocated; + return result; +} + ++(void)createAutorelease:(CreateAutoreleaseDeallocated*)deallocated { + [self create:deallocated]; +} + +-(void)dealloc { + deallocated.value = YES; +} +@end; diff --git a/kotlin-native/runtime/src/main/cpp/ObjCInterop.mm b/kotlin-native/runtime/src/main/cpp/ObjCInterop.mm index 73a48003f5d..bff9950996a 100644 --- a/kotlin-native/runtime/src/main/cpp/ObjCInterop.mm +++ b/kotlin-native/runtime/src/main/cpp/ObjCInterop.mm @@ -31,6 +31,7 @@ #include "Natives.h" #include "ObjCInterop.h" #include "ObjCExportPrivate.h" +#include "ObjCMMAPI.h" #include "Types.h" #include "Utils.h" @@ -309,6 +310,13 @@ id objc_allocWithZone(Class clazz); id objc_retain(id ptr); void objc_release(id ptr); +konan::AutoreleasePool::AutoreleasePool() + : handle(objc_autoreleasePoolPush()) {} + +konan::AutoreleasePool::~AutoreleasePool() { + objc_autoreleasePoolPop(handle); +} + void* Kotlin_objc_autoreleasePoolPush() { return objc_autoreleasePoolPush(); } diff --git a/kotlin-native/runtime/src/main/cpp/ObjCMMAPI.h b/kotlin-native/runtime/src/main/cpp/ObjCMMAPI.h index 2e7d00c95c9..de90e53140e 100644 --- a/kotlin-native/runtime/src/main/cpp/ObjCMMAPI.h +++ b/kotlin-native/runtime/src/main/cpp/ObjCMMAPI.h @@ -12,6 +12,22 @@ extern "C" ALWAYS_INLINE void Kotlin_ObjCExport_releaseAssociatedObject(void* associatedObject); +namespace konan { +class AutoreleasePool { + public: + AutoreleasePool(); + ~AutoreleasePool(); + + AutoreleasePool(const AutoreleasePool&) = delete; + AutoreleasePool(AutoreleasePool&&) = delete; + AutoreleasePool& operator=(const AutoreleasePool&) = delete; + AutoreleasePool& operator=(AutoreleasePool&&) = delete; + + private: + void* handle; +}; +} // namespace konan + #endif // KONAN_OBJC_INTEROP #endif // RUNTIME_OBJCMMAPI_H diff --git a/kotlin-native/runtime/src/main/cpp/Worker.cpp b/kotlin-native/runtime/src/main/cpp/Worker.cpp index 1da5f31ee9d..0f9279bcbf6 100644 --- a/kotlin-native/runtime/src/main/cpp/Worker.cpp +++ b/kotlin-native/runtime/src/main/cpp/Worker.cpp @@ -31,6 +31,7 @@ #include "Exceptions.h" #include "KAssert.h" #include "Memory.h" +#include "ObjCMMAPI.h" #include "Runtime.h" #include "Types.h" #include "Worker.h" @@ -956,6 +957,9 @@ JobKind Worker::processQueueElement(bool blocking) { ObjHolder operationHolder, dummyHolder; KRef obj = DerefStablePointer(job.executeAfter.operation, operationHolder.slot()); try { +#if KONAN_OBJC_INTEROP + konan::AutoreleasePool autoreleasePool; +#endif WorkerLaunchpad(obj, dummyHolder.slot()); } catch (ExceptionObjHolder& e) { if (errorReporting()) @@ -969,6 +973,9 @@ JobKind Worker::processQueueElement(bool blocking) { KNativePtr result = nullptr; bool ok = true; try { +#if KONAN_OBJC_INTEROP + konan::AutoreleasePool autoreleasePool; +#endif job.regularJob.function(argument, resultHolder.slot()); argumentHolder.clear(); // Transfer the result.