From ac1a466cc9cb71bb901a72112435bf0c42a29380 Mon Sep 17 00:00:00 2001 From: Alexander Shabalin Date: Mon, 19 Oct 2020 11:57:24 +0300 Subject: [PATCH] Generalize finalization with Cleaner (#4362) --- .../kotlin/backend/konan/KonanFqNames.kt | 1 + .../jetbrains/kotlin/backend/konan/ir/Ir.kt | 6 + .../backend/konan/llvm/RTTIGenerator.kt | 5 + .../backend/konan/lower/InteropLowering.kt | 38 +++ backend.native/tests/build.gradle | 92 ++++++ .../tests/interop/cleaners/cleaners.kt | 26 ++ .../tests/interop/cleaners/leak.cpp | 11 + .../tests/interop/cleaners/main_thread.cpp | 12 + .../tests/interop/cleaners/second_thread.cpp | 15 + .../tests/runtime/basic/cleaner_basic.kt | 219 ++++++++++++++ .../basic/cleaner_in_main_with_checker.kt | 21 ++ .../basic/cleaner_in_main_without_checker.kt | 21 ++ .../basic/cleaner_in_tls_main_with_checker.kt | 26 ++ .../cleaner_in_tls_main_without_checker.kt | 26 ++ .../runtime/basic/cleaner_in_tls_worker.kt | 33 ++ .../tests/runtime/basic/cleaner_leak.kt | 24 ++ .../basic/cleaner_leak_with_checker.kt | 26 ++ .../basic/cleaner_leak_without_checker.kt | 26 ++ .../tests/runtime/basic/cleaner_workers.kt | 281 ++++++++++++++++++ .../kotlin/testing/native/NativeTest.kt | 4 +- runtime/build.gradle.kts | 8 +- runtime/src/launcher/cpp/launcher.cpp | 15 +- runtime/src/main/cpp/Cleaner.cpp | 130 ++++++++ runtime/src/main/cpp/Cleaner.h | 20 ++ runtime/src/main/cpp/CleanerTest.cpp | 80 +++++ runtime/src/main/cpp/Memory.cpp | 25 +- runtime/src/main/cpp/Memory.h | 3 + runtime/src/main/cpp/Runtime.cpp | 14 + runtime/src/main/cpp/Runtime.h | 2 + .../main/cpp/TestSupportCompilerGenerated.hpp | 66 ++++ runtime/src/main/cpp/TypeInfo.h | 2 + runtime/src/main/cpp/Types.h | 1 + runtime/src/main/cpp/Worker.cpp | 66 +++- runtime/src/main/cpp/Worker.h | 8 +- runtime/src/main/cpp/WorkerBoundReference.h | 4 +- .../src/main/kotlin/kotlin/native/Platform.kt | 10 + .../native/concurrent/WorkerBoundReference.kt | 1 + .../kotlin/native/internal/Annotations.kt | 13 + .../kotlin/kotlin/native/internal/Cleaner.kt | 123 ++++++++ .../test_support/cpp/CompilerGenerated.cpp | 27 ++ .../test_support/cpp/CompilerGeneratedObjC.mm | 4 +- 41 files changed, 1506 insertions(+), 29 deletions(-) create mode 100644 backend.native/tests/interop/cleaners/cleaners.kt create mode 100644 backend.native/tests/interop/cleaners/leak.cpp create mode 100644 backend.native/tests/interop/cleaners/main_thread.cpp create mode 100644 backend.native/tests/interop/cleaners/second_thread.cpp create mode 100644 backend.native/tests/runtime/basic/cleaner_basic.kt create mode 100644 backend.native/tests/runtime/basic/cleaner_in_main_with_checker.kt create mode 100644 backend.native/tests/runtime/basic/cleaner_in_main_without_checker.kt create mode 100644 backend.native/tests/runtime/basic/cleaner_in_tls_main_with_checker.kt create mode 100644 backend.native/tests/runtime/basic/cleaner_in_tls_main_without_checker.kt create mode 100644 backend.native/tests/runtime/basic/cleaner_in_tls_worker.kt create mode 100644 backend.native/tests/runtime/basic/cleaner_leak.kt create mode 100644 backend.native/tests/runtime/basic/cleaner_leak_with_checker.kt create mode 100644 backend.native/tests/runtime/basic/cleaner_leak_without_checker.kt create mode 100644 backend.native/tests/runtime/basic/cleaner_workers.kt create mode 100644 runtime/src/main/cpp/Cleaner.cpp create mode 100644 runtime/src/main/cpp/Cleaner.h create mode 100644 runtime/src/main/cpp/CleanerTest.cpp create mode 100644 runtime/src/main/cpp/TestSupportCompilerGenerated.hpp create mode 100644 runtime/src/main/kotlin/kotlin/native/internal/Cleaner.kt diff --git a/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/KonanFqNames.kt b/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/KonanFqNames.kt index 6f5c8f20f98..0ec960f9c12 100644 --- a/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/KonanFqNames.kt +++ b/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/KonanFqNames.kt @@ -30,4 +30,5 @@ object KonanFqNames { val canBePrecreated = FqName("kotlin.native.internal.CanBePrecreated") val typedIntrinsic = FqName("kotlin.native.internal.TypedIntrinsic") val objCMethod = FqName("kotlinx.cinterop.ObjCMethod") + val hasFinalizer = FqName("kotlin.native.internal.HasFinalizer") } diff --git a/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/ir/Ir.kt b/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/ir/Ir.kt index fbe731a24b0..4fb3485e615 100644 --- a/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/ir/Ir.kt +++ b/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/ir/Ir.kt @@ -239,6 +239,12 @@ internal class KonanSymbols( .single() ) + val createCleaner = symbolTable.referenceSimpleFunction( + builtIns.builtInsModule.getPackage(FqName("kotlin.native.internal")).memberScope + .getContributedFunctions(Name.identifier("createCleaner"), NoLookupLocation.FROM_BACKEND) + .single() + ) + val areEqualByValue = context.getKonanInternalFunctions("areEqualByValue").map { symbolTable.referenceSimpleFunction(it) }.associateBy { it.descriptor.valueParameters[0].type.computePrimitiveBinaryTypeOrNull()!! } diff --git a/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/llvm/RTTIGenerator.kt b/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/llvm/RTTIGenerator.kt index 7869da77641..9d8f144df31 100644 --- a/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/llvm/RTTIGenerator.kt +++ b/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/llvm/RTTIGenerator.kt @@ -70,6 +70,10 @@ internal class RTTIGenerator(override val context: Context) : ContextUtils { result = result or TF_SUSPEND_FUNCTION } + if (irClass.hasAnnotation(KonanFqNames.hasFinalizer)) { + result = result or TF_HAS_FINALIZER + } + return result } @@ -629,4 +633,5 @@ private const val TF_INTERFACE = 4 private const val TF_OBJC_DYNAMIC = 8 private const val TF_LEAK_DETECTOR_CANDIDATE = 16 private const val TF_SUSPEND_FUNCTION = 32 +private const val TF_HAS_FINALIZER = 64 diff --git a/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/lower/InteropLowering.kt b/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/lower/InteropLowering.kt index 708062aadc5..f6ea1d3586d 100644 --- a/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/lower/InteropLowering.kt +++ b/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/lower/InteropLowering.kt @@ -65,6 +65,33 @@ internal class InteropLowering(context: Context) : FileLoweringPass { } } +private fun IrExpression.isNonCapturingFunction(): Boolean { + if (!type.isFunctionTypeOrSubtype()) + return false + + val fromContainerExpression = fun(expr: IrExpression): IrConstructorCall? { + if (expr !is IrContainerExpression) + return null + if (expr.statements.size != 2) + return null + + val firstStatement = expr.statements[0] + if (firstStatement !is IrContainerExpression || firstStatement.statements.size != 0) { + return null + } + + val secondStatement = expr.statements[1] + + return secondStatement as? IrConstructorCall + } + + val constructorCall = this as? IrConstructorCall + ?: fromContainerExpression(this) + ?: return false + + return constructorCall.valueArgumentsCount == 0 +} + private abstract class BaseInteropIrTransformer(private val context: Context) : IrBuildingTransformer(context) { protected inline fun generateWithStubs(element: IrElement? = null, block: KotlinStubs.() -> T): T = @@ -1203,6 +1230,17 @@ private class InteropTransformer(val context: Context, override val irFile: IrFi builder.irCall(symbols.interopCPointerGetRawValue).apply { extensionReceiver = expression.dispatchReceiver } + // TODO: Move this check out of InteropLowering. + symbols.createCleaner.owner -> { + val irCallableReference = expression.getValueArgument(1) + if (irCallableReference == null || !irCallableReference.isNonCapturingFunction()) { + context.reportCompilationError( + "${function.fqNameForIrSerialization} must take an unbound, non-capturing function or lambda", + irFile, expression + ) + } + expression + } else -> expression } } diff --git a/backend.native/tests/build.gradle b/backend.native/tests/build.gradle index a7a2e03ec9b..1baf49c063c 100644 --- a/backend.native/tests/build.gradle +++ b/backend.native/tests/build.gradle @@ -896,6 +896,74 @@ task empty_substring(type: KonanLocalTest) { source = "runtime/basic/empty_substring.kt" } +standaloneTest("cleaner_basic") { + enabled = (project.testTarget != 'wasm32') // Cleaners need workers + source = "runtime/basic/cleaner_basic.kt" + flags = ['-tr', '-Xopt-in=kotlin.native.internal.InternalForKotlinNative'] +} + +standaloneTest("cleaner_workers") { + enabled = (project.testTarget != 'wasm32') // Cleaners need workers + source = "runtime/basic/cleaner_workers.kt" + flags = ['-tr', '-Xopt-in=kotlin.native.internal.InternalForKotlinNative'] +} + +standaloneTest("cleaner_in_main_with_checker") { + enabled = (project.testTarget != 'wasm32') // Cleaners need workers + source = "runtime/basic/cleaner_in_main_with_checker.kt" + goldValue = "42\n" +} + +standaloneTest("cleaner_in_main_without_checker") { + enabled = (project.testTarget != 'wasm32') // Cleaners need workers + source = "runtime/basic/cleaner_in_main_without_checker.kt" + goldValue = "" +} + +standaloneTest("cleaner_leak_release") { + enabled = !project.globalTestArgs.contains('-g') && (project.testTarget != 'wasm32') // Cleaners need workers + source = "runtime/basic/cleaner_leak.kt" + goldValue = "" +} + +standaloneTest("cleaner_leak_debug") { + enabled = !project.globalTestArgs.contains('-opt') && (project.testTarget != 'wasm32') // Cleaners need workers + source = "runtime/basic/cleaner_leak.kt" + flags = ['-g'] + expectedExitStatusChecker = { it != 0 } + outputChecker = { s -> (s =~ /Cleaner (0x)?[0-9a-fA-F]+ was disposed during program exit/).find() } +} + +standaloneTest("cleaner_leak_without_checker") { + enabled = (project.testTarget != 'wasm32') // Cleaners need workers + source = "runtime/basic/cleaner_leak_without_checker.kt" + goldValue = "" +} + +standaloneTest("cleaner_leak_with_checker") { + enabled = (project.testTarget != 'wasm32') // Cleaners need workers + source = "runtime/basic/cleaner_leak_with_checker.kt" + expectedExitStatusChecker = { it != 0 } + outputChecker = { s -> (s =~ /Cleaner (0x)?[0-9a-fA-F]+ was disposed during program exit/).find() } +} + +standaloneTest("cleaner_in_tls_main_without_checker") { + enabled = (project.testTarget != 'wasm32') // Cleaners need workers + source = "runtime/basic/cleaner_in_tls_main_without_checker.kt" +} + +standaloneTest("cleaner_in_tls_main_with_checker") { + enabled = (project.testTarget != 'wasm32') // Cleaners need workers + source = "runtime/basic/cleaner_in_tls_main_with_checker.kt" + expectedExitStatusChecker = { it != 0 } + outputChecker = { s -> (s =~ /Cleaner (0x)?[0-9a-fA-F]+ was disposed during program exit/).find() } +} + +standaloneTest("cleaner_in_tls_worker") { + enabled = (project.testTarget != 'wasm32') // Cleaners need workers + source = "runtime/basic/cleaner_in_tls_worker.kt" +} + standaloneTest("worker_bound_reference0") { enabled = (project.testTarget != 'wasm32') // Workers need pthreads. source = "runtime/concurrent/worker_bound_reference0.kt" @@ -4221,6 +4289,30 @@ dynamicTest("interop_kt42397") { flags = ['-g'] } +dynamicTest("interop_cleaners_main_thread") { + disabled = (project.testTarget != null && project.testTarget != project.hostName) + source = "interop/cleaners/cleaners.kt" + cSource = "$projectDir/interop/cleaners/main_thread.cpp" + clangTool = "clang++" + goldValue = "42\n" +} + +dynamicTest("interop_cleaners_second_thread") { + disabled = (project.testTarget != null && project.testTarget != project.hostName) + source = "interop/cleaners/cleaners.kt" + cSource = "$projectDir/interop/cleaners/second_thread.cpp" + clangTool = "clang++" + goldValue = "42\n" +} + +dynamicTest("interop_cleaners_leak") { + disabled = (project.testTarget != null && project.testTarget != project.hostName) + source = "interop/cleaners/cleaners.kt" + cSource = "$projectDir/interop/cleaners/leak.cpp" + clangTool = "clang++" + goldValue = "" +} + task library_mismatch(type: KonanDriverTest) { // Does not work for cross targets yet. enabled = !(project.testTarget != null && project.target.name != project.hostName) diff --git a/backend.native/tests/interop/cleaners/cleaners.kt b/backend.native/tests/interop/cleaners/cleaners.kt new file mode 100644 index 00000000000..a00050c863a --- /dev/null +++ b/backend.native/tests/interop/cleaners/cleaners.kt @@ -0,0 +1,26 @@ +/* + * Copyright 2010-2020 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. + */ +@file:OptIn(ExperimentalStdlibApi::class) + +import kotlin.native.internal.* + +fun createCleaner() { + createCleaner(42) { + println(it) + } +} + +fun performGC() { + GC.collect() + performGCOnCleanerWorker() +} + +private var globalCleaner: Cleaner? = null + +fun initializeGlobalCleaner() { + globalCleaner = createCleaner(11) { + println(it) + } +} diff --git a/backend.native/tests/interop/cleaners/leak.cpp b/backend.native/tests/interop/cleaners/leak.cpp new file mode 100644 index 00000000000..092bb70cc2c --- /dev/null +++ b/backend.native/tests/interop/cleaners/leak.cpp @@ -0,0 +1,11 @@ +/* + * Copyright 2010-2020 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. + */ + +#include "testlib_api.h" + +int main() { + testlib_symbols()->kotlin.root.initializeGlobalCleaner(); + return 0; +} diff --git a/backend.native/tests/interop/cleaners/main_thread.cpp b/backend.native/tests/interop/cleaners/main_thread.cpp new file mode 100644 index 00000000000..68777ada796 --- /dev/null +++ b/backend.native/tests/interop/cleaners/main_thread.cpp @@ -0,0 +1,12 @@ +/* + * Copyright 2010-2020 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. + */ + +#include "testlib_api.h" + +int main() { + testlib_symbols()->kotlin.root.createCleaner(); + testlib_symbols()->kotlin.root.performGC(); + return 0; +} diff --git a/backend.native/tests/interop/cleaners/second_thread.cpp b/backend.native/tests/interop/cleaners/second_thread.cpp new file mode 100644 index 00000000000..9ab0dda0941 --- /dev/null +++ b/backend.native/tests/interop/cleaners/second_thread.cpp @@ -0,0 +1,15 @@ +/* + * Copyright 2010-2020 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. + */ + +#include "testlib_api.h" + +#include + +int main() { + std::thread t([]() { testlib_symbols()->kotlin.root.createCleaner(); }); + t.join(); + testlib_symbols()->kotlin.root.performGC(); + return 0; +} diff --git a/backend.native/tests/runtime/basic/cleaner_basic.kt b/backend.native/tests/runtime/basic/cleaner_basic.kt new file mode 100644 index 00000000000..e68a78a5ea0 --- /dev/null +++ b/backend.native/tests/runtime/basic/cleaner_basic.kt @@ -0,0 +1,219 @@ +/* + * Copyright 2010-2020 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. + */ +@file:OptIn(ExperimentalTime::class, ExperimentalStdlibApi::class) + +package runtime.basic.cleaner_basic + +import kotlin.test.* + +import kotlin.native.internal.* +import kotlin.native.concurrent.* +import kotlin.native.ref.WeakReference +import kotlin.time.* + +class AtomicBoolean(initialValue: Boolean) { + private val impl = AtomicInt(if (initialValue) 1 else 0) + + init { + freeze() + } + + public var value: Boolean + get() = impl.value != 0 + set(new) { impl.value = if (new) 1 else 0 } +} + +class FunBox(private val impl: () -> Unit) { + fun call() { + impl() + } +} + +@Test +fun testCleanerLambda() { + val called = AtomicBoolean(false); + var funBoxWeak: WeakReference? = null + var cleanerWeak: WeakReference? = null + { + val cleaner = { + val funBox = FunBox { called.value = true }.freeze() + funBoxWeak = WeakReference(funBox) + createCleaner(funBox) { it.call() } + }() + GC.collect() // Make sure local funBox reference is gone + cleanerWeak = WeakReference(cleaner) + assertFalse(called.value) + }() + + GC.collect() + performGCOnCleanerWorker() + + assertNull(cleanerWeak!!.value) + assertTrue(called.value) + assertNull(funBoxWeak!!.value) +} + +@Test +fun testCleanerAnonymousFunction() { + val called = AtomicBoolean(false); + var funBoxWeak: WeakReference? = null + var cleanerWeak: WeakReference? = null + { + val cleaner = { + val funBox = FunBox { called.value = true }.freeze() + funBoxWeak = WeakReference(funBox) + createCleaner(funBox, fun (it: FunBox) { it.call() }) + }() + GC.collect() // Make sure local funBox reference is gone + cleanerWeak = WeakReference(cleaner) + assertFalse(called.value) + }() + + GC.collect() + performGCOnCleanerWorker() + + assertNull(cleanerWeak!!.value) + assertTrue(called.value) + assertNull(funBoxWeak!!.value) +} + +@Test +fun testCleanerFunctionReference() { + val called = AtomicBoolean(false); + var funBoxWeak: WeakReference? = null + var cleanerWeak: WeakReference? = null + { + val cleaner = { + val funBox = FunBox { called.value = true }.freeze() + funBoxWeak = WeakReference(funBox) + createCleaner(funBox, FunBox::call) + }() + GC.collect() // Make sure local funBox reference is gone + cleanerWeak = WeakReference(cleaner) + assertFalse(called.value) + }() + + GC.collect() + performGCOnCleanerWorker() + + assertNull(cleanerWeak!!.value) + assertTrue(called.value) + assertNull(funBoxWeak!!.value) +} + +@Test +fun testCleanerFailWithNonShareableArgument() { + val funBox = FunBox {} + assertFailsWith { + createCleaner(funBox) {} + } +} + +inline fun tryWithTimeout(timeoutSeconds: Int, f: () -> Unit): Unit { + val timeout = TimeSource.Monotonic.markNow() + timeoutSeconds.seconds + while (true) { + try { + f() + return + } catch (e: Throwable) { + if (timeout.hasPassedNow()) { + throw e + } + } + } +} + +@Test +fun testCleanerCleansWithoutGC() { + val called = AtomicBoolean(false); + var funBoxWeak: WeakReference? = null + var cleanerWeak: WeakReference? = null + { + val cleaner = { + val funBox = FunBox { called.value = true }.freeze() + funBoxWeak = WeakReference(funBox) + createCleaner(funBox) { it.call() } + }() + GC.collect() // Make sure local funBox reference is gone + cleaner.freeze() + cleanerWeak = WeakReference(cleaner) + assertFalse(called.value) + }() + + GC.collect() + + assertNull(cleanerWeak!!.value) + tryWithTimeout(3) { + GC.collect() // Collect local stack (from previous iteration) + assertTrue(called.value) + // If this fails, GC has somehow ran on the cleaners worker. + assertNotNull(funBoxWeak!!.value) + } +} + +val globalInt = AtomicInt(0) + +@Test +fun testCleanerWithInt() { + var cleanerWeak: WeakReference? = null + { + val cleaner = createCleaner(42) { + globalInt.value = it + }.freeze() + cleanerWeak = WeakReference(cleaner) + assertEquals(0, globalInt.value) + }() + + GC.collect() + performGCOnCleanerWorker() + + assertNull(cleanerWeak!!.value) + assertEquals(42, globalInt.value) +} + +val globalPtr = AtomicNativePtr(NativePtr.NULL) + +@Test +fun testCleanerWithNativePtr() { + var cleanerWeak: WeakReference? = null + { + val cleaner = createCleaner(NativePtr.NULL + 42L) { + globalPtr.value = it + } + cleanerWeak = WeakReference(cleaner) + assertEquals(NativePtr.NULL, globalPtr.value) + }() + + GC.collect() + performGCOnCleanerWorker() + + assertNull(cleanerWeak!!.value) + assertEquals(NativePtr.NULL + 42L, globalPtr.value) +} + +@Test +fun testCleanerWithException() { + val called = AtomicBoolean(false); + var funBoxWeak: WeakReference? = null + var cleanerWeak: WeakReference? = null + { + val funBox = FunBox { called.value = true }.freeze() + funBoxWeak = WeakReference(funBox) + val cleaner = createCleaner(funBox) { + it.call() + error("Cleaner block failed") + } + cleanerWeak = WeakReference(cleaner) + }() + + GC.collect() + performGCOnCleanerWorker() + + assertNull(cleanerWeak!!.value) + // Cleaners block started executing. + assertTrue(called.value) + // Even though the block failed, the captured funBox is freed. + assertNull(funBoxWeak!!.value) +} diff --git a/backend.native/tests/runtime/basic/cleaner_in_main_with_checker.kt b/backend.native/tests/runtime/basic/cleaner_in_main_with_checker.kt new file mode 100644 index 00000000000..e15f7a3210e --- /dev/null +++ b/backend.native/tests/runtime/basic/cleaner_in_main_with_checker.kt @@ -0,0 +1,21 @@ +/* + * Copyright 2010-2020 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. + */ +@file:OptIn(ExperimentalStdlibApi::class) + +import kotlin.native.internal.* +import kotlin.native.Platform + +fun main() { + // Cleaner holds onto a finalization lambda. If it doesn't get executed, + // the memory will leak. Suppress memory leak checker to check for cleaners + // leak only. + Platform.isMemoryLeakCheckerActive = false + Platform.isCleanersLeakCheckerActive = true + // This cleaner will run, because with the checker active this cleaner + // will get collected, block scheduled and executed before cleaners are disabled. + createCleaner(42) { + println(it) + } +} diff --git a/backend.native/tests/runtime/basic/cleaner_in_main_without_checker.kt b/backend.native/tests/runtime/basic/cleaner_in_main_without_checker.kt new file mode 100644 index 00000000000..ce163b605f7 --- /dev/null +++ b/backend.native/tests/runtime/basic/cleaner_in_main_without_checker.kt @@ -0,0 +1,21 @@ +/* + * Copyright 2010-2020 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. + */ +@file:OptIn(ExperimentalStdlibApi::class) + +import kotlin.native.internal.* +import kotlin.native.Platform + +fun main() { + // Cleaner holds onto a finalization lambda. If it doesn't get executed, + // the memory will leak. Suppress memory leak checker to check for cleaners + // leak only. + Platform.isMemoryLeakCheckerActive = false + Platform.isCleanersLeakCheckerActive = false + // This cleaner will not run, because with the checker inactive this cleaner + // will not get collected before cleaners are disabled. + createCleaner(42) { + println(it) + } +} diff --git a/backend.native/tests/runtime/basic/cleaner_in_tls_main_with_checker.kt b/backend.native/tests/runtime/basic/cleaner_in_tls_main_with_checker.kt new file mode 100644 index 00000000000..718017a823e --- /dev/null +++ b/backend.native/tests/runtime/basic/cleaner_in_tls_main_with_checker.kt @@ -0,0 +1,26 @@ +/* + * Copyright 2010-2020 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. + */ +@file:OptIn(ExperimentalStdlibApi::class) + +import kotlin.test.* + +import kotlin.native.concurrent.* +import kotlin.native.internal.* +import kotlin.native.Platform + +@ThreadLocal +var tlsCleaner: Cleaner? = null + +fun main() { + // Cleaner holds onto a finalization lambda. If it doesn't get executed, + // the memory will leak. Suppress memory leak checker to check for cleaners + // leak only. + Platform.isMemoryLeakCheckerActive = false + Platform.isCleanersLeakCheckerActive = true + // This cleaner won't be run + tlsCleaner = createCleaner(42) { + println(it) + } +} diff --git a/backend.native/tests/runtime/basic/cleaner_in_tls_main_without_checker.kt b/backend.native/tests/runtime/basic/cleaner_in_tls_main_without_checker.kt new file mode 100644 index 00000000000..54267f712c6 --- /dev/null +++ b/backend.native/tests/runtime/basic/cleaner_in_tls_main_without_checker.kt @@ -0,0 +1,26 @@ +/* + * Copyright 2010-2020 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. + */ +@file:OptIn(ExperimentalStdlibApi::class) + +import kotlin.test.* + +import kotlin.native.concurrent.* +import kotlin.native.internal.* +import kotlin.native.Platform + +@ThreadLocal +var tlsCleaner: Cleaner? = null + +fun main() { + // Cleaner holds onto a finalization lambda. If it doesn't get executed, + // the memory will leak. Suppress memory leak checker to check for cleaners + // leak only. + Platform.isMemoryLeakCheckerActive = false + Platform.isCleanersLeakCheckerActive = false + // This cleaner won't be run + tlsCleaner = createCleaner(42) { + println(it) + } +} diff --git a/backend.native/tests/runtime/basic/cleaner_in_tls_worker.kt b/backend.native/tests/runtime/basic/cleaner_in_tls_worker.kt new file mode 100644 index 00000000000..e47116352aa --- /dev/null +++ b/backend.native/tests/runtime/basic/cleaner_in_tls_worker.kt @@ -0,0 +1,33 @@ +/* + * Copyright 2010-2020 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. + */ +@file:OptIn(ExperimentalTime::class, ExperimentalStdlibApi::class) + +import kotlin.test.* + +import kotlin.native.concurrent.* +import kotlin.native.internal.* +import kotlin.time.* + +@ThreadLocal +var tlsCleaner: Cleaner? = null + +val value = AtomicInt(0) + +fun main() { + val worker = Worker.start() + + worker.execute(TransferMode.SAFE, {}) { + tlsCleaner = createCleaner(42) { + value.value = it + } + } + + worker.requestTermination().result + + val timeout = TimeSource.Monotonic.markNow() + 3.seconds + while (value.value == 0 || timeout.hasNotPassedNow()) {} + + assertEquals(42, value.value) +} diff --git a/backend.native/tests/runtime/basic/cleaner_leak.kt b/backend.native/tests/runtime/basic/cleaner_leak.kt new file mode 100644 index 00000000000..01f2a5277a2 --- /dev/null +++ b/backend.native/tests/runtime/basic/cleaner_leak.kt @@ -0,0 +1,24 @@ +/* + * Copyright 2010-2020 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. + */ +@file:OptIn(ExperimentalStdlibApi::class) + +import kotlin.test.* + +import kotlin.native.internal.* + +// This cleaner won't be run, because it's deinitialized with globals after +// cleaners are disabled. +val globalCleaner = createCleaner(42) { + println(it) +} + +fun main() { + // Cleaner holds onto a finalization lambda. If it doesn't get executed, + // the memory will leak. Suppress memory leak checker to check for cleaners + // leak only. + Platform.isMemoryLeakCheckerActive = false + // Make sure cleaner is initialized. + assertNotNull(globalCleaner) +} diff --git a/backend.native/tests/runtime/basic/cleaner_leak_with_checker.kt b/backend.native/tests/runtime/basic/cleaner_leak_with_checker.kt new file mode 100644 index 00000000000..053c1146d7c --- /dev/null +++ b/backend.native/tests/runtime/basic/cleaner_leak_with_checker.kt @@ -0,0 +1,26 @@ +/* + * Copyright 2010-2020 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. + */ +@file:OptIn(ExperimentalStdlibApi::class) + +import kotlin.test.* + +import kotlin.native.internal.* +import kotlin.native.Platform + +// This cleaner won't be run, because it's deinitialized with globals after +// cleaners are disabled. +val globalCleaner = createCleaner(42) { + println(it) +} + +fun main() { + // Cleaner holds onto a finalization lambda. If it doesn't get executed, + // the memory will leak. Suppress memory leak checker to check for cleaners + // leak only. + Platform.isMemoryLeakCheckerActive = false + Platform.isCleanersLeakCheckerActive = true + // Make sure cleaner is initialized. + assertNotNull(globalCleaner) +} diff --git a/backend.native/tests/runtime/basic/cleaner_leak_without_checker.kt b/backend.native/tests/runtime/basic/cleaner_leak_without_checker.kt new file mode 100644 index 00000000000..7391ef50de0 --- /dev/null +++ b/backend.native/tests/runtime/basic/cleaner_leak_without_checker.kt @@ -0,0 +1,26 @@ +/* + * Copyright 2010-2020 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. + */ +@file:OptIn(ExperimentalStdlibApi::class) + +import kotlin.test.* + +import kotlin.native.internal.* +import kotlin.native.Platform + +// This cleaner won't be run, because it's deinitialized with globals after +// cleaners are disabled. +val globalCleaner = createCleaner(42) { + println(it) +} + +fun main() { + // Cleaner holds onto a finalization lambda. If it doesn't get executed, + // the memory will leak. Suppress memory leak checker to check for cleaners + // leak only. + Platform.isMemoryLeakCheckerActive = false + Platform.isCleanersLeakCheckerActive = false + // Make sure cleaner is initialized. + assertNotNull(globalCleaner) +} diff --git a/backend.native/tests/runtime/basic/cleaner_workers.kt b/backend.native/tests/runtime/basic/cleaner_workers.kt new file mode 100644 index 00000000000..c7b5e35996a --- /dev/null +++ b/backend.native/tests/runtime/basic/cleaner_workers.kt @@ -0,0 +1,281 @@ +/* + * Copyright 2010-2020 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. + */ +@file:OptIn(ExperimentalTime::class, ExperimentalStdlibApi::class) + +package runtime.basic.cleaner_workers + +import kotlin.test.* + +import kotlin.native.internal.* +import kotlin.native.concurrent.* +import kotlin.native.ref.WeakReference +import kotlin.time.* + +class AtomicBoolean(initialValue: Boolean) { + private val impl = AtomicInt(if (initialValue) 1 else 0) + + init { + freeze() + } + + public var value: Boolean + get() = impl.value != 0 + set(new) { impl.value = if (new) 1 else 0 } +} + +class FunBox(private val impl: () -> Unit) { + fun call() { + impl() + } +} + +@Test +fun testCleanerDestroyInChild() { + val worker = Worker.start() + + val called = AtomicBoolean(false); + var funBoxWeak: WeakReference? = null + var cleanerWeak: WeakReference? = null + worker.execute(TransferMode.SAFE, { + val funBox = FunBox { called.value = true }.freeze() + funBoxWeak = WeakReference(funBox) + val cleaner = createCleaner(funBox) { it.call() } + cleanerWeak = WeakReference(cleaner) + Pair(called, cleaner) + }) { (called, cleaner) -> + assertFalse(called.value) + }.result + + GC.collect() + worker.execute(TransferMode.SAFE, {}) { GC.collect() }.result + performGCOnCleanerWorker() + + assertNull(cleanerWeak!!.value) + assertTrue(called.value) + assertNull(funBoxWeak!!.value) + + worker.requestTermination().result +} + +inline fun tryWithTimeout(timeoutSeconds: Int, f: () -> Unit): Unit { + val timeout = TimeSource.Monotonic.markNow() + timeoutSeconds.seconds + while (true) { + try { + f() + return + } catch (e: Throwable) { + if (timeout.hasPassedNow()) { + throw e + } + } + } +} + +@Test +fun testCleanerDestroyWithChild() { + val worker = Worker.start() + + val called = AtomicBoolean(false); + var funBoxWeak: WeakReference? = null + var cleanerWeak: WeakReference? = null + worker.execute(TransferMode.SAFE, { + val funBox = FunBox { called.value = true }.freeze() + funBoxWeak = WeakReference(funBox) + val cleaner = createCleaner(funBox) { it.call() } + cleanerWeak = WeakReference(cleaner) + Pair(called, cleaner) + }) { (called, cleaner) -> + assertFalse(called.value) + }.result + + GC.collect() + worker.requestTermination().result + + tryWithTimeout(3) { + GC.collect() // Collect local stack (from previous iteration) + performGCOnCleanerWorker() // Collect cleaners stack + + assertNull(cleanerWeak!!.value) + assertTrue(called.value) + assertNull(funBoxWeak!!.value) + } +} + +@Test +fun testCleanerDestroyFrozenWithChild() { + val worker = Worker.start() + + val called = AtomicBoolean(false); + var funBoxWeak: WeakReference? = null + var cleanerWeak: WeakReference? = null + worker.execute(TransferMode.SAFE, { + val funBox = FunBox { called.value = true }.freeze() + funBoxWeak = WeakReference(funBox) + val cleaner = createCleaner(funBox) { it.call() }.freeze() + cleanerWeak = WeakReference(cleaner) + Pair(called, cleaner) + }) { (called, cleaner) -> + assertFalse(called.value) + }.result + + GC.collect() + worker.requestTermination().result + + tryWithTimeout(3) { + GC.collect() // Collect local stack (from previous iteration) + performGCOnCleanerWorker() // Collect cleaners stack + + assertNull(cleanerWeak!!.value) + assertTrue(called.value) + assertNull(funBoxWeak!!.value) + } +} + +@Test +fun testCleanerDestroyFrozenInChild() { + val worker = Worker.start() + + val called = AtomicBoolean(false); + var funBoxWeak: WeakReference? = null + var cleanerWeak: WeakReference? = null + { + worker.execute(TransferMode.SAFE, { + val funBox = FunBox { called.value = true }.freeze() + funBoxWeak = WeakReference(funBox) + val cleaner = createCleaner(funBox) { it.call() }.freeze() + cleanerWeak = WeakReference(cleaner) + Pair(called, cleaner) + }) { (called, cleaner) -> + assertFalse(called.value) + }.result + }() + + GC.collect() + worker.execute(TransferMode.SAFE, {}) { GC.collect() }.result + performGCOnCleanerWorker() + + assertNull(cleanerWeak!!.value) + assertTrue(called.value) + assertNull(funBoxWeak!!.value) + + worker.requestTermination().result +} + +@Test +fun testCleanerDestroyInMain() { + val worker = Worker.start() + + val called = AtomicBoolean(false); + var funBoxWeak: WeakReference? = null + var cleanerWeak: WeakReference? = null + { + val result = worker.execute(TransferMode.SAFE, { called }) { called -> + val funBox = FunBox { called.value = true }.freeze() + val cleaner = createCleaner(funBox) { it.call() } + Triple(cleaner, WeakReference(funBox), WeakReference(cleaner)) + }.result + val cleaner = result.first + funBoxWeak = result.second + cleanerWeak = result.third + assertFalse(called.value) + }() + + GC.collect() + worker.execute(TransferMode.SAFE, {}) { GC.collect() }.result + performGCOnCleanerWorker() + + assertNull(cleanerWeak!!.value) + assertTrue(called.value) + assertNull(funBoxWeak!!.value) + + worker.requestTermination().result +} + +@Test +fun testCleanerDestroyFrozenInMain() { + val worker = Worker.start() + + val called = AtomicBoolean(false); + var funBoxWeak: WeakReference? = null + var cleanerWeak: WeakReference? = null + { + val result = worker.execute(TransferMode.SAFE, { called }) { called -> + val funBox = FunBox { called.value = true }.freeze() + val cleaner = createCleaner(funBox) { it.call() }.freeze() + Triple(cleaner, WeakReference(funBox), WeakReference(cleaner)) + }.result + val cleaner = result.first + funBoxWeak = result.second + cleanerWeak = result.third + assertFalse(called.value) + }() + + GC.collect() + worker.execute(TransferMode.SAFE, {}) { GC.collect() }.result + performGCOnCleanerWorker() + + assertNull(cleanerWeak!!.value) + assertTrue(called.value) + assertNull(funBoxWeak!!.value) + + worker.requestTermination().result +} + +@Test +fun testCleanerDestroyShared() { + val worker = Worker.start() + + val called = AtomicBoolean(false); + var funBoxWeak: WeakReference? = null + var cleanerWeak: WeakReference? = null + val cleanerHolder: AtomicReference = AtomicReference(null); + { + val funBox = FunBox { called.value = true }.freeze() + funBoxWeak = WeakReference(funBox) + val cleaner = createCleaner(funBox) { it.call() }.freeze() + cleanerWeak = WeakReference(cleaner) + cleanerHolder.value = cleaner + worker.execute(TransferMode.SAFE, { Pair(called, cleanerHolder) }) { (called, cleanerHolder) -> + cleanerHolder.value = null + assertFalse(called.value) + }.result + }() + + GC.collect() + worker.execute(TransferMode.SAFE, {}) { GC.collect() }.result + performGCOnCleanerWorker() + + assertNull(cleanerWeak!!.value) + assertTrue(called.value) + assertNull(funBoxWeak!!.value) + + worker.requestTermination().result +} + +@ThreadLocal +var tlsValue = 11 + +@Test +fun testCleanerWithTLS() { + val worker = Worker.start() + + tlsValue = 12 + + val value = AtomicInt(0) + worker.execute(TransferMode.SAFE, {value}) { + tlsValue = 13 + createCleaner(it) { + it.value = tlsValue + } + Unit + }.result + + worker.execute(TransferMode.SAFE, {}) { GC.collect() }.result + performGCOnCleanerWorker() + + assertEquals(11, value.value) + + worker.requestTermination().result +} diff --git a/build-tools/src/main/kotlin/org/jetbrains/kotlin/testing/native/NativeTest.kt b/build-tools/src/main/kotlin/org/jetbrains/kotlin/testing/native/NativeTest.kt index fda98bdc267..9ed0b8acbcb 100644 --- a/build-tools/src/main/kotlin/org/jetbrains/kotlin/testing/native/NativeTest.kt +++ b/build-tools/src/main/kotlin/org/jetbrains/kotlin/testing/native/NativeTest.kt @@ -154,7 +154,8 @@ open class LinkNativeTest @Inject constructor( fun createTestTask( project: Project, testTaskName: String, - testedTaskNames: List + testedTaskNames: List, + configureCompileToBitcode: CompileToBitcode.() -> Unit = {}, ): Task { val platformManager = project.rootProject.findProperty("platformManager") as PlatformManager val googleTestExtension = project.extensions.getByName(RuntimeTestingPlugin.GOOGLE_TEST_EXTENSION_NAME) as GoogleTestExtension @@ -179,6 +180,7 @@ fun createTestTask( dependsOn(it) compilerArgs.addAll(it.compilerArgs) headersDirs += googleTestExtension.headersDirs + this.configureCompileToBitcode() } if (task.inputFiles.count() == 0) null diff --git a/runtime/build.gradle.kts b/runtime/build.gradle.kts index ff748cc03da..03fc54cae3a 100644 --- a/runtime/build.gradle.kts +++ b/runtime/build.gradle.kts @@ -104,7 +104,9 @@ targetList.forEach { targetName -> "${targetName}Release", "${targetName}StdAlloc" ) - ) + ) { + includeRuntime() + } createTestTask( project, @@ -116,7 +118,9 @@ targetList.forEach { targetName -> "${targetName}Mimalloc", "${targetName}OptAlloc" ) - ) + ) { + includeRuntime() + } tasks.register("${targetName}RuntimeTests") { dependsOn("${targetName}StdAllocRuntimeTests") diff --git a/runtime/src/launcher/cpp/launcher.cpp b/runtime/src/launcher/cpp/launcher.cpp index 54e98291c42..c102a537106 100644 --- a/runtime/src/launcher/cpp/launcher.cpp +++ b/runtime/src/launcher/cpp/launcher.cpp @@ -14,6 +14,7 @@ * limitations under the License. */ +#include "Cleaner.h" #include "Memory.h" #include "Natives.h" #include "Runtime.h" @@ -57,9 +58,17 @@ extern "C" RUNTIME_USED int Init_and_run_start(int argc, const char** argv, int KInt exitStatus = Konan_run_start(argc, argv); if (memoryDeInit) { - if (Kotlin_memoryLeakCheckerEnabled()) - WaitNativeWorkersTermination(); - Kotlin_deinitRuntimeIfNeeded(); + if (Kotlin_cleanersLeakCheckerEnabled()) { + // Make sure to collect any lingering cleaners. + PerformFullGC(); + // Execute all the cleaner blocks and stop the Cleaner worker. + ShutdownCleaners(true); + } else { + // Stop the cleaner worker without executing remaining cleaner blocks. + ShutdownCleaners(false); + } + if (Kotlin_memoryLeakCheckerEnabled()) WaitNativeWorkersTermination(); + Kotlin_deinitRuntimeIfNeeded(); } return exitStatus; diff --git a/runtime/src/main/cpp/Cleaner.cpp b/runtime/src/main/cpp/Cleaner.cpp new file mode 100644 index 00000000000..5d624433aeb --- /dev/null +++ b/runtime/src/main/cpp/Cleaner.cpp @@ -0,0 +1,130 @@ +/* + * Copyright 2010-2020 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. + */ + +#include "Cleaner.h" + +#include "Memory.h" +#include "Runtime.h" +#include "Worker.h" + +// Defined in Cleaner.kt +extern "C" void Kotlin_CleanerImpl_shutdownCleanerWorker(KInt, bool); +extern "C" KInt Kotlin_CleanerImpl_createCleanerWorker(); + +namespace { + +struct CleanerImpl { + ObjHeader header; + KNativePtr cleanerStablePtr; +}; + +constexpr KInt kCleanerWorkerUninitialized = 0; +constexpr KInt kCleanerWorkerInitializing = -1; +constexpr KInt kCleanerWorkerShutdown = -2; + +KInt globalCleanerWorker = kCleanerWorkerUninitialized; + +void disposeCleaner(CleanerImpl* thiz) { + auto worker = atomicGet(&globalCleanerWorker); + RuntimeAssert( + worker != kCleanerWorkerUninitialized && worker != kCleanerWorkerInitializing, + "Cleaner worker must've been initialized by now"); + if (worker == kCleanerWorkerShutdown) { + if (Kotlin_cleanersLeakCheckerEnabled()) { + konan::consoleErrorf( + "Cleaner %p was disposed during program exit\n" + "Use `Platform.isCleanersLeakCheckerActive = false` to avoid this check.\n", + thiz); + RuntimeCheck(false, "Terminating now"); + } + return; + } + + RuntimeAssert(worker > 0, "Cleaner worker must be fully initialized here"); + + bool result = WorkerSchedule(worker, thiz->cleanerStablePtr); + RuntimeAssert(result, "Couldn't find Cleaner worker"); +} + +} // namespace + +RUNTIME_NOTHROW void DisposeCleaner(KRef thiz) { +#if KONAN_NO_EXCEPTIONS + disposeCleaner(reinterpret_cast(thiz)); +#else + try { + disposeCleaner(reinterpret_cast(thiz)); + } catch (...) { + // A trick to terminate with unhandled exception. This will print a stack trace + // and write to iOS crash log. + std::terminate(); + } +#endif +} + +void ShutdownCleaners(bool executeScheduledCleaners) { + KInt worker = 0; + do { + worker = atomicGet(&globalCleanerWorker); + RuntimeAssert(worker != kCleanerWorkerShutdown, "Cleaner worker must not be shutdown twice"); + if (worker == kCleanerWorkerUninitialized) { + if (!compareAndSet(&globalCleanerWorker, kCleanerWorkerUninitialized, kCleanerWorkerShutdown)) { + // Someone is trying to initialize the worker. Try again. + continue; + } + // worker was never initialized. Just return. + return; + } + if (worker == kCleanerWorkerInitializing) { + // Someone is trying to initialize the worker. Try again. + continue; + } + + // Worker is in some proper state. + break; + + } while (true); + + RuntimeAssert(worker > 0, "Cleaner worker must be fully initialized here"); + + atomicSet(&globalCleanerWorker, kCleanerWorkerShutdown); + Kotlin_CleanerImpl_shutdownCleanerWorker(worker, executeScheduledCleaners); + WaitNativeWorkerTermination(worker); +} + +extern "C" KInt Kotlin_CleanerImpl_getCleanerWorker() { + KInt worker = 0; + do { + worker = atomicGet(&globalCleanerWorker); + RuntimeAssert(worker != kCleanerWorkerShutdown, "Cleaner worker must not have been shutdown"); + if (worker == kCleanerWorkerUninitialized) { + if (!compareAndSet(&globalCleanerWorker, kCleanerWorkerUninitialized, kCleanerWorkerInitializing)) { + // Someone else is trying to initialize the worker. Try again. + continue; + } + worker = Kotlin_CleanerImpl_createCleanerWorker(); + if (!compareAndSet(&globalCleanerWorker, kCleanerWorkerInitializing, worker)) { + RuntimeCheck(false, "Someone interrupted worker initializing"); + } + // Worker is initialized. + break; + } + if (worker == kCleanerWorkerInitializing) { + // Someone is trying to initialize the worker. Try again. + continue; + } + + // Worker is in some proper state. + break; + } while (true); + + RuntimeAssert(worker > 0, "Cleaner worker must be fully initialized here"); + + return worker; +} + +void ResetCleanerWorkerForTests() { + atomicSet(&globalCleanerWorker, kCleanerWorkerUninitialized); +} diff --git a/runtime/src/main/cpp/Cleaner.h b/runtime/src/main/cpp/Cleaner.h new file mode 100644 index 00000000000..3b5b7f8f5fc --- /dev/null +++ b/runtime/src/main/cpp/Cleaner.h @@ -0,0 +1,20 @@ +/* + * Copyright 2010-2020 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_CLEANER_H +#define RUNTIME_CLEANER_H + +#include "Common.h" +#include "Types.h" + +RUNTIME_NOTHROW void DisposeCleaner(KRef thiz); + +void ShutdownCleaners(bool executeScheduledCleaners); + +extern "C" KInt Kotlin_CleanerImpl_getCleanerWorker(); + +void ResetCleanerWorkerForTests(); + +#endif // RUNTIME_CLEANER_H diff --git a/runtime/src/main/cpp/CleanerTest.cpp b/runtime/src/main/cpp/CleanerTest.cpp new file mode 100644 index 00000000000..4861b9b455f --- /dev/null +++ b/runtime/src/main/cpp/CleanerTest.cpp @@ -0,0 +1,80 @@ +/* + * Copyright 2010-2020 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. + */ + +#include "Cleaner.h" + +#include +#include + +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +#include "Atomic.h" +#include "TestSupportCompilerGenerated.hpp" + +using testing::_; + +// TODO: Also test disposal. (This requires extracting Worker interface) + +TEST(CleanerTest, ConcurrentCreation) { + ResetCleanerWorkerForTests(); + + constexpr int threadCount = 100; + constexpr KInt workerId = 42; + + auto createCleanerWorkerMock = ScopedCreateCleanerWorkerMock(); + + EXPECT_CALL(*createCleanerWorkerMock, Call()).Times(1).WillOnce(testing::Return(workerId)); + + int startedThreads = 0; + bool allowRunning = false; + std::vector> futures; + for (int i = 0; i < threadCount; ++i) { + auto future = std::async(std::launch::async, [&startedThreads, &allowRunning]() { + atomicAdd(&startedThreads, 1); + while (!atomicGet(&allowRunning)) { + } + return Kotlin_CleanerImpl_getCleanerWorker(); + }); + futures.push_back(std::move(future)); + } + while (atomicGet(&startedThreads) != threadCount) { + } + atomicSet(&allowRunning, true); + std::vector values; + for (auto& future : futures) { + values.push_back(future.get()); + } + + ASSERT_THAT(values.size(), threadCount); + EXPECT_THAT(values, testing::Each(workerId)); +} + +TEST(CleanerTest, ShutdownWithoutCreation) { + ResetCleanerWorkerForTests(); + + auto createCleanerWorkerMock = ScopedCreateCleanerWorkerMock(); + auto shutdownCleanerWorkerMock = ScopedShutdownCleanerWorkerMock(); + + EXPECT_CALL(*createCleanerWorkerMock, Call()).Times(0); + EXPECT_CALL(*shutdownCleanerWorkerMock, Call(_, _)).Times(0); + ShutdownCleaners(true); +} + +TEST(CleanerTest, ShutdownWithCreation) { + ResetCleanerWorkerForTests(); + + constexpr KInt workerId = 42; + constexpr bool executeScheduledCleaners = true; + + auto createCleanerWorkerMock = ScopedCreateCleanerWorkerMock(); + auto shutdownCleanerWorkerMock = ScopedShutdownCleanerWorkerMock(); + + EXPECT_CALL(*createCleanerWorkerMock, Call()).WillOnce(testing::Return(workerId)); + Kotlin_CleanerImpl_getCleanerWorker(); + + EXPECT_CALL(*shutdownCleanerWorkerMock, Call(workerId, executeScheduledCleaners)); + ShutdownCleaners(executeScheduledCleaners); +} diff --git a/runtime/src/main/cpp/Memory.cpp b/runtime/src/main/cpp/Memory.cpp index b84aa23315e..bcc70553a7c 100644 --- a/runtime/src/main/cpp/Memory.cpp +++ b/runtime/src/main/cpp/Memory.cpp @@ -33,6 +33,7 @@ #include "Alloc.h" #include "KAssert.h" #include "Atomic.h" +#include "Cleaner.h" #if USE_CYCLIC_GC #include "CyclicCollector.h" #endif // USE_CYCLIC_GC @@ -1063,14 +1064,26 @@ void freeAggregatingFrozenContainer(ContainerHeader* container) { MEMORY_LOG("Freeing subcontainers done\n"); } +// Not inlining this call as it affects deallocation performance for +// all types. +NO_INLINE RUNTIME_NOTHROW void runFinalizers(ObjHeader* obj) { + auto* type_info = obj->type_info(); + if (type_info == theCleanerImplTypeInfo) { + DisposeCleaner(obj); + } + if (type_info == theWorkerBoundReferenceTypeInfo) { + DisposeWorkerBoundReference(obj); + } +} + // This is called from 2 places where it's unconditionally called, // so better be inlined. ALWAYS_INLINE void runDeallocationHooks(ContainerHeader* container) { ObjHeader* obj = reinterpret_cast(container + 1); for (uint32_t index = 0; index < container->objectCount(); index++) { auto* type_info = obj->type_info(); - if (type_info == theWorkerBoundReferenceTypeInfo) { - DisposeWorkerBoundReference(obj); + if ((type_info->flags_ & TF_HAS_FINALIZER) != 0) { + runFinalizers(obj); } #if USE_CYCLIC_GC if ((type_info->flags_ & TF_LEAK_DETECTOR_CANDIDATE) != 0) { @@ -3543,4 +3556,12 @@ void Kotlin_native_internal_GC_setCyclicCollector(KRef gc, KBoolean value) { #endif // USE_CYCLIC_GC } +bool Kotlin_Any_isShareable(KRef thiz) { + return thiz == nullptr || isShareable(thiz->container()); +} + +void PerformFullGC() { + garbageCollect(::memoryState, true); +} + } // extern "C" diff --git a/runtime/src/main/cpp/Memory.h b/runtime/src/main/cpp/Memory.h index 46be88de207..9ee0a251607 100644 --- a/runtime/src/main/cpp/Memory.h +++ b/runtime/src/main/cpp/Memory.h @@ -558,6 +558,9 @@ void GC_RegisterWorker(void* worker) RUNTIME_NOTHROW; void GC_UnregisterWorker(void* worker) RUNTIME_NOTHROW; void GC_CollectorCallback(void* worker) RUNTIME_NOTHROW; +bool Kotlin_Any_isShareable(ObjHeader* thiz); +void PerformFullGC() RUNTIME_NOTHROW; + #ifdef __cplusplus } #endif diff --git a/runtime/src/main/cpp/Runtime.cpp b/runtime/src/main/cpp/Runtime.cpp index ecd5b4de9e8..e4eb23ef4a9 100644 --- a/runtime/src/main/cpp/Runtime.cpp +++ b/runtime/src/main/cpp/Runtime.cpp @@ -16,6 +16,7 @@ #include "Alloc.h" #include "Atomic.h" +#include "Cleaner.h" #include "Exceptions.h" #include "KAssert.h" #include "Memory.h" @@ -75,6 +76,7 @@ void InitOrDeinitGlobalVariables(int initialize, MemoryState* memory) { } KBoolean g_checkLeaks = KonanNeedDebugInfo; +KBoolean g_checkLeakedCleaners = KonanNeedDebugInfo; constexpr RuntimeState* kInvalidRuntime = nullptr; @@ -284,4 +286,16 @@ void Konan_Platform_setMemoryLeakChecker(KBoolean value) { g_checkLeaks = value; } +bool Kotlin_cleanersLeakCheckerEnabled() { + return g_checkLeakedCleaners; +} + +KBoolean Konan_Platform_getCleanersLeakChecker() { + return g_checkLeakedCleaners; +} + +void Konan_Platform_setCleanersLeakChecker(KBoolean value) { + g_checkLeakedCleaners = value; +} + } // extern "C" diff --git a/runtime/src/main/cpp/Runtime.h b/runtime/src/main/cpp/Runtime.h index 153b8d70a11..ed564e7056f 100644 --- a/runtime/src/main/cpp/Runtime.h +++ b/runtime/src/main/cpp/Runtime.h @@ -55,6 +55,8 @@ void Kotlin_zeroOutTLSGlobals(); bool Kotlin_memoryLeakCheckerEnabled(); +bool Kotlin_cleanersLeakCheckerEnabled(); + #ifdef __cplusplus } #endif diff --git a/runtime/src/main/cpp/TestSupportCompilerGenerated.hpp b/runtime/src/main/cpp/TestSupportCompilerGenerated.hpp new file mode 100644 index 00000000000..d685f32c16e --- /dev/null +++ b/runtime/src/main/cpp/TestSupportCompilerGenerated.hpp @@ -0,0 +1,66 @@ +/* + * Copyright 2010-2020 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. + */ + +#include +#include + +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +#include "Types.h" + +template +class ScopedStrictMockFunction { +public: + using Mock = testing::StrictMock>; + + explicit ScopedStrictMockFunction(Mock** globalMockLocation) : globalMockLocation_(globalMockLocation) { + RuntimeCheck(globalMockLocation != nullptr, "ScopedStrictMockFunction needs non-null global mock location"); + RuntimeCheck(*globalMockLocation == nullptr, "ScopedStrictMockFunction needs null global mock"); + // TODO: Use make_unique when sysroots on Linux get updated. + mock_ = std::unique_ptr(new Mock()); + *globalMockLocation_ = mock_.get(); + } + + ScopedStrictMockFunction(const ScopedStrictMockFunction&) = delete; + ScopedStrictMockFunction& operator=(const ScopedStrictMockFunction&) = delete; + + ScopedStrictMockFunction(ScopedStrictMockFunction&& rhs) : globalMockLocation_(rhs.globalMockLocation_), mock_(std::move(rhs.mock_)) { + rhs.globalMockLocation_ = nullptr; + } + + ScopedStrictMockFunction& operator=(ScopedStrictMockFunction&& rhs) { + ScopedStrictMockFunction tmp(std::move(rhs)); + swap(tmp); + return *this; + } + + ~ScopedStrictMockFunction() { + if (!globalMockLocation_) return; + + RuntimeCheck(*globalMockLocation_ == mock_.get(), "unexpected global mock location"); + + testing::Mock::VerifyAndClear(mock_.get()); + mock_.reset(); + + *globalMockLocation_ = nullptr; + } + + void swap(ScopedStrictMockFunction& other) { + std::swap(globalMockLocation_, other.globalMockLocation_); + std::swap(mock_, other.mock_); + } + + Mock& get() { return *mock_; } + Mock& operator*() { return *mock_; } + +private: + // Can be null if moved-out of. + Mock** globalMockLocation_; + std::unique_ptr mock_; +}; + +ScopedStrictMockFunction ScopedCreateCleanerWorkerMock(); +ScopedStrictMockFunction ScopedShutdownCleanerWorkerMock(); diff --git a/runtime/src/main/cpp/TypeInfo.h b/runtime/src/main/cpp/TypeInfo.h index daf011a5088..669962de6ce 100644 --- a/runtime/src/main/cpp/TypeInfo.h +++ b/runtime/src/main/cpp/TypeInfo.h @@ -54,6 +54,7 @@ enum Konan_RuntimeType { }; // Flags per type. +// Keep in sync with constants in RTTIGenerator. enum Konan_TypeFlags { TF_IMMUTABLE = 1 << 0, TF_ACYCLIC = 1 << 1, @@ -61,6 +62,7 @@ enum Konan_TypeFlags { TF_OBJC_DYNAMIC = 1 << 3, TF_LEAK_DETECTOR_CANDIDATE = 1 << 4, TF_SUSPEND_FUNCTION = 1 << 5, + TF_HAS_FINALIZER = 1 << 6, }; // Flags per object instance. diff --git a/runtime/src/main/cpp/Types.h b/runtime/src/main/cpp/Types.h index d05e7ae3404..d380584d206 100644 --- a/runtime/src/main/cpp/Types.h +++ b/runtime/src/main/cpp/Types.h @@ -106,6 +106,7 @@ extern const TypeInfo* theStringTypeInfo; extern const TypeInfo* theThrowableTypeInfo; extern const TypeInfo* theUnitTypeInfo; extern const TypeInfo* theWorkerBoundReferenceTypeInfo; +extern const TypeInfo* theCleanerImplTypeInfo; KBoolean IsInstance(const ObjHeader* obj, const TypeInfo* type_info) RUNTIME_PURE; KBoolean IsInstanceOfClassFast(const ObjHeader* obj, int32_t lo, int32_t hi) RUNTIME_PURE; diff --git a/runtime/src/main/cpp/Worker.cpp b/runtime/src/main/cpp/Worker.cpp index 830c490ca2a..8311f316e10 100644 --- a/runtime/src/main/cpp/Worker.cpp +++ b/runtime/src/main/cpp/Worker.cpp @@ -68,7 +68,7 @@ enum JobKind { // Order is important in sense that all job kinds after this one is considered // processed for APIs returning request process status. JOB_REGULAR = 2, - JOB_EXECUTE_AFTER = 3 + JOB_EXECUTE_AFTER = 3, }; enum class WorkerKind { @@ -358,6 +358,23 @@ class State { return true; } + bool scheduleJobInWorkerUnlocked(KInt id, KNativePtr operationStablePtr) { + Worker* worker = nullptr; + Locker locker(&lock_); + + auto it = workers_.find(id); + if (it == workers_.end()) { + return false; + } + worker = it->second; + + Job job; + job.kind = JOB_EXECUTE_AFTER; + job.executeAfter.operation = operationStablePtr; + worker->putJob(job, false); + return true; + } + // Returns `true` if something was indeed processed. bool processQueueUnlocked(KInt id) { // Can only process queue of the current worker. @@ -456,23 +473,28 @@ class State { terminating_native_workers_.erase(it); } - void waitNativeWorkersTerminationUnlocked() { - std::vector threadsToWait; - { - Locker locker(&lock_); + template + void waitNativeWorkersTerminationUnlocked(F waitForWorker) { + std::vector> workersToWait; + { + Locker locker(&lock_); - checkNativeWorkersLeakLocked(); + checkNativeWorkersLeakLocked(); - for (auto& kvp : terminating_native_workers_) { - RuntimeAssert(!pthread_equal(kvp.second, pthread_self()), "Native worker is joining with itself"); - threadsToWait.push_back(kvp.second); + for (auto& kvp : terminating_native_workers_) { + RuntimeAssert(!pthread_equal(kvp.second, pthread_self()), "Native worker is joining with itself"); + if (waitForWorker(kvp.first)) { + workersToWait.push_back(kvp); + } + } + for (auto worker : workersToWait) { + terminating_native_workers_.erase(worker.first); + } } - terminating_native_workers_.clear(); - } - for (auto thread : threadsToWait) { - pthread_join(thread, nullptr); - } + for (auto worker : workersToWait) { + pthread_join(worker.second, nullptr); + } } void checkNativeWorkersLeakLocked() { @@ -720,7 +742,13 @@ void WorkerDestroyThreadDataIfNeeded(KInt id) { void WaitNativeWorkersTermination() { #if WITH_WORKERS - theState()->waitNativeWorkersTerminationUnlocked(); + theState()->waitNativeWorkersTerminationUnlocked([](KInt worker) { return true; }); +#endif +} + +void WaitNativeWorkerTermination(KInt id) { +#if WITH_WORKERS + theState()->waitNativeWorkersTerminationUnlocked([id](KInt worker) { return worker == id; }); #endif } @@ -740,6 +768,14 @@ void WorkerResume(Worker* worker) { #endif // WITH_WORKERS } +bool WorkerSchedule(KInt id, KNativePtr jobStablePtr) { +#if WITH_WORKERS + return theState()->scheduleJobInWorkerUnlocked(id, jobStablePtr); +#else + return false; +#endif // WITH_WORKERS +} + #if WITH_WORKERS Worker::~Worker() { diff --git a/runtime/src/main/cpp/Worker.h b/runtime/src/main/cpp/Worker.h index 39792c96ba2..285d49ba092 100644 --- a/runtime/src/main/cpp/Worker.h +++ b/runtime/src/main/cpp/Worker.h @@ -12,10 +12,14 @@ Worker* WorkerInit(KBoolean errorReporting); void WorkerDeinit(Worker* worker); // Clean up all associated thread state, if this was a native worker. void WorkerDestroyThreadDataIfNeeded(KInt id); -// Wait until all terminating native workers finish termination. Expected to be called once. +// Wait until all terminating native workers finish termination. Expected to be called at most once. void WaitNativeWorkersTermination(); +// Wait until terminating native worker `id` finishes termination. Expected to be called at most once for each worker. +void WaitNativeWorkerTermination(KInt id); +// Schedule the job without the result. +bool WorkerSchedule(KInt id, KNativePtr jobStablePtr); Worker* WorkerSuspend(); void WorkerResume(Worker* worker); -#endif // RUNTIME_WORKER_H \ No newline at end of file +#endif // RUNTIME_WORKER_H diff --git a/runtime/src/main/cpp/WorkerBoundReference.h b/runtime/src/main/cpp/WorkerBoundReference.h index eff74e8c0ca..c1c7c51890e 100644 --- a/runtime/src/main/cpp/WorkerBoundReference.h +++ b/runtime/src/main/cpp/WorkerBoundReference.h @@ -9,9 +9,7 @@ #include "Common.h" #include "Types.h" -// Not inlining this call as it affects deallocation performance for -// all types. -RUNTIME_NOTHROW void DisposeWorkerBoundReference(KRef thiz) NO_INLINE; +RUNTIME_NOTHROW void DisposeWorkerBoundReference(KRef thiz); RUNTIME_NOTHROW void WorkerBoundReferenceFreezeHook(KRef thiz); diff --git a/runtime/src/main/kotlin/kotlin/native/Platform.kt b/runtime/src/main/kotlin/kotlin/native/Platform.kt index f031823e94d..6ca425e88fa 100644 --- a/runtime/src/main/kotlin/kotlin/native/Platform.kt +++ b/runtime/src/main/kotlin/kotlin/native/Platform.kt @@ -92,6 +92,10 @@ public object Platform { public var isMemoryLeakCheckerActive: Boolean get() = Platform_getMemoryLeakChecker() set(value) = Platform_setMemoryLeakChecker(value) + + public var isCleanersLeakCheckerActive: Boolean + get() = Platform_getCleanersLeakChecker() + set(value) = Platform_setCleanersLeakChecker(value) } @SymbolName("Konan_Platform_canAccessUnaligned") @@ -117,3 +121,9 @@ private external fun Platform_getMemoryLeakChecker(): Boolean @SymbolName("Konan_Platform_setMemoryLeakChecker") private external fun Platform_setMemoryLeakChecker(value: Boolean): Unit + +@SymbolName("Konan_Platform_getCleanersLeakChecker") +private external fun Platform_getCleanersLeakChecker(): Boolean + +@SymbolName("Konan_Platform_setCleanersLeakChecker") +private external fun Platform_setCleanersLeakChecker(value: Boolean): Unit diff --git a/runtime/src/main/kotlin/kotlin/native/concurrent/WorkerBoundReference.kt b/runtime/src/main/kotlin/kotlin/native/concurrent/WorkerBoundReference.kt index ca6e15e88a0..16e6e23a982 100644 --- a/runtime/src/main/kotlin/kotlin/native/concurrent/WorkerBoundReference.kt +++ b/runtime/src/main/kotlin/kotlin/native/concurrent/WorkerBoundReference.kt @@ -28,6 +28,7 @@ external private fun describeWorkerBoundReference(ref: NativePtr): String */ @NoReorderFields @ExportTypeInfo("theWorkerBoundReferenceTypeInfo") +@HasFinalizer public class WorkerBoundReference(value: T) { private var ptr = NativePtr.NULL diff --git a/runtime/src/main/kotlin/kotlin/native/internal/Annotations.kt b/runtime/src/main/kotlin/kotlin/native/internal/Annotations.kt index 432e416597f..90e60161a18 100644 --- a/runtime/src/main/kotlin/kotlin/native/internal/Annotations.kt +++ b/runtime/src/main/kotlin/kotlin/native/internal/Annotations.kt @@ -132,3 +132,16 @@ annotation class Independent @Target(AnnotationTarget.CLASS) @Retention(AnnotationRetention.SOURCE) public annotation class CanBePrecreated + +/** + * Marks a class that has a finalizer. + */ +@Target(AnnotationTarget.CLASS) +internal annotation class HasFinalizer + +/** + * Marks a declaration that is internal for Kotlin/Native and shouldn't be used externally. + */ +@RequiresOptIn(level = RequiresOptIn.Level.ERROR) +@Retention(value = AnnotationRetention.BINARY) +internal annotation class InternalForKotlinNative diff --git a/runtime/src/main/kotlin/kotlin/native/internal/Cleaner.kt b/runtime/src/main/kotlin/kotlin/native/internal/Cleaner.kt new file mode 100644 index 00000000000..b4f72e6675b --- /dev/null +++ b/runtime/src/main/kotlin/kotlin/native/internal/Cleaner.kt @@ -0,0 +1,123 @@ +/* + * Copyright 2010-2020 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. + */ + +package kotlin.native.internal + +import kotlin.native.concurrent.* +import kotlinx.cinterop.NativePtr + +public interface Cleaner + +/** + * Creates an object with a cleanup associated. + * + * After the resulting object ("cleaner") gets deallocated by memory manager, + * [block] is eventually called once with [argument]. + * + * Example of usage: + * ``` + * class ResourceWrapper { + * private val resource = Resource() + * + * private val cleaner = createCleaner(resource) { it.dispose() } + * } + * ``` + * + * When `ResourceWrapper` becomes unused and gets deallocated, its `cleaner` + * is also deallocated, and the resource is disposed later. + * + * It is not specified which thread runs [block], as well as whether two or more + * blocks from different cleaners can be run in parallel. + * + * Note: if [argument] refers (directly or indirectly) the cleaner, then both + * might leak, and the [block] will not be called in this case. + * For example, the code below has a leak: + * ``` + * class LeakingResourceWrapper { + * private val resource = Resource() + * private val cleaner = createCleaner(this) { it.resource.dispose() } + * } + * ``` + * In this case cleaner's argument (`LeakingResourceWrapper`) can't be deallocated + * until cleaner's block is executed, which can happen only strictly after + * the cleaner is deallocated, which can't happen until `LeakingResourceWrapper` + * is deallocated. So the requirements on object deallocations are contradictory + * in this case, which can't be handled gracefully. The cleaner's block + * is not executed then, and cleaner and its argument might leak + * (depending on the implementation). + * + * [block] should not use `@ThreadLocal` globals, because it may + * be executed on a different thread. + * + * If [block] throws an exception, the behavior is unspecified. + * + * Cleaners should not be kept in globals, because if cleaner is not deallocated + * before exiting main(), it'll never get executed. + * Use `Platform.isCleanersLeakCheckerActive` to warn about unexecuted cleaners. + * + * If cleaners are not GC'd before main() exits, then it's not guaranteed that + * they will be run. Moreover, it depends on `Platform.isCleanersLeakCheckerActive`. + * With the checker enabled, cleaners will be run (and therefore not reported as + * unexecuted cleaners); with the checker disabled - they might not get run. + * + * @param argument must be shareable + * @param block must not capture anything + */ +// TODO: Consider just annotating the lambda argument rather than hardcoding checking +// by function name in the compiler. +@ExperimentalStdlibApi +@ExportForCompiler +fun createCleaner(argument: T, block: (T) -> Unit): Cleaner { + if (!argument.isShareable()) + throw IllegalArgumentException("$argument must be shareable") + + val clean = { + // TODO: Maybe if this fails with exception, it should be (optionally) reported. + block(argument) + }.freeze() + + // Make sure there's an extra reference to clean, so it's definitely alive when CleanerImpl is destroyed. + val cleanPtr = createStablePointer(clean) + + // Make sure cleaner worker is initialized. + getCleanerWorker() + + return CleanerImpl(cleanPtr).freeze() +} + +/** + * Perform GC on a worker that executes Cleaner blocks. + */ +@InternalForKotlinNative +fun performGCOnCleanerWorker() = + getCleanerWorker().execute(TransferMode.SAFE, {}) { + GC.collect() + }.result + +@SymbolName("Kotlin_CleanerImpl_getCleanerWorker") +external private fun getCleanerWorker(): Worker + +@ExportForCppRuntime("Kotlin_CleanerImpl_shutdownCleanerWorker") +private fun shutdownCleanerWorker(worker: Worker, executeScheduledCleaners: Boolean) { + worker.requestTermination(executeScheduledCleaners).result +} + +@ExportForCppRuntime("Kotlin_CleanerImpl_createCleanerWorker") +private fun createCleanerWorker(): Worker { + return Worker.start(errorReporting = false, name = "Cleaner worker") +} + +@NoReorderFields +@ExportTypeInfo("theCleanerImplTypeInfo") +@HasFinalizer +private class CleanerImpl( + private val cleanPtr: NativePtr, +): Cleaner {} + +@SymbolName("Kotlin_Any_isShareable") +external private fun Any?.isShareable(): Boolean + +@SymbolName("CreateStablePointer") +external private fun createStablePointer(obj: Any): NativePtr diff --git a/runtime/src/test_support/cpp/CompilerGenerated.cpp b/runtime/src/test_support/cpp/CompilerGenerated.cpp index 706e9aa7857..16d087cf208 100644 --- a/runtime/src/test_support/cpp/CompilerGenerated.cpp +++ b/runtime/src/test_support/cpp/CompilerGenerated.cpp @@ -3,6 +3,8 @@ * that can be found in the LICENSE file. */ +#include "TestSupportCompilerGenerated.hpp" + #include "Types.h" namespace { @@ -26,6 +28,7 @@ TypeInfo theStringTypeInfoImpl = {}; TypeInfo theThrowableTypeInfoImpl = {}; TypeInfo theUnitTypeInfoImpl = {}; TypeInfo theWorkerBoundReferenceTypeInfoImpl = {}; +TypeInfo theCleanerImplTypeInfoImpl = {}; ArrayHeader theEmptyStringImpl = { &theStringTypeInfoImpl, /* element count */ 0 }; @@ -35,6 +38,9 @@ struct KBox { const T value; }; +testing::StrictMock>* createCleanerWorkerMock = nullptr; +testing::StrictMock>* shutdownCleanerWorkerMock = nullptr; + } // namespace extern "C" { @@ -61,6 +67,7 @@ extern const TypeInfo* theStringTypeInfo = &theStringTypeInfoImpl; extern const TypeInfo* theThrowableTypeInfo = &theThrowableTypeInfoImpl; extern const TypeInfo* theUnitTypeInfo = &theUnitTypeInfoImpl; extern const TypeInfo* theWorkerBoundReferenceTypeInfo = &theWorkerBoundReferenceTypeInfoImpl; +extern const TypeInfo* theCleanerImplTypeInfo = &theCleanerImplTypeInfoImpl; extern const ArrayHeader theEmptyArray = { &theArrayTypeInfoImpl, /* element count */0 }; @@ -230,4 +237,24 @@ RUNTIME_NORETURN OBJ_GETTER(Kotlin_Throwable_getMessage, KRef throwable) { throw std::runtime_error("Not implemented for tests"); } +void Kotlin_CleanerImpl_shutdownCleanerWorker(KInt worker, bool executeScheduledCleaners) { + if (!shutdownCleanerWorkerMock) throw std::runtime_error("Not implemented for tests"); + + return shutdownCleanerWorkerMock->Call(worker, executeScheduledCleaners); +} + +KInt Kotlin_CleanerImpl_createCleanerWorker() { + if (!createCleanerWorkerMock) throw std::runtime_error("Not implemented for tests"); + + return createCleanerWorkerMock->Call(); +} + } // extern "C" + +ScopedStrictMockFunction ScopedCreateCleanerWorkerMock() { + return ScopedStrictMockFunction(&createCleanerWorkerMock); +} + +ScopedStrictMockFunction ScopedShutdownCleanerWorkerMock() { + return ScopedStrictMockFunction(&shutdownCleanerWorkerMock); +} diff --git a/runtime/src/test_support/cpp/CompilerGeneratedObjC.mm b/runtime/src/test_support/cpp/CompilerGeneratedObjC.mm index ae57a4c7e12..6400daf30d7 100644 --- a/runtime/src/test_support/cpp/CompilerGeneratedObjC.mm +++ b/runtime/src/test_support/cpp/CompilerGeneratedObjC.mm @@ -5,6 +5,8 @@ #if KONAN_OBJC_INTEROP +#include "TestSupportCompilerGenerated.hpp" + #include #include @@ -67,4 +69,4 @@ void Kotlin_ObjCExport_resumeContinuationSuccess(KRef continuation, KRef result) } // extern "C" -#endif // KONAN_OBJC_INTEROP \ No newline at end of file +#endif // KONAN_OBJC_INTEROP