diff --git a/kotlin-native/backend.native/cli.bc/src/org/jetbrains/kotlin/cli/bc/K2Native.kt b/kotlin-native/backend.native/cli.bc/src/org/jetbrains/kotlin/cli/bc/K2Native.kt index ba60bda4380..de0af80ea49 100644 --- a/kotlin-native/backend.native/cli.bc/src/org/jetbrains/kotlin/cli/bc/K2Native.kt +++ b/kotlin-native/backend.native/cli.bc/src/org/jetbrains/kotlin/cli/bc/K2Native.kt @@ -329,6 +329,15 @@ class K2Native : CLICompiler() { } }) put(PROPERTY_LAZY_INITIALIZATION, arguments.propertyLazyInitialization) + put(WORKER_EXCEPTION_HANDLING, when (arguments.workerExceptionHandling) { + null -> if (memoryModel == MemoryModel.EXPERIMENTAL) WorkerExceptionHandling.USE_HOOK else WorkerExceptionHandling.LEGACY + "legacy" -> WorkerExceptionHandling.LEGACY + "use-hook" -> WorkerExceptionHandling.USE_HOOK + else -> { + configuration.report(ERROR, "Unsupported worker exception handling mode ${arguments.workerExceptionHandling}") + WorkerExceptionHandling.LEGACY + } + }) } } } diff --git a/kotlin-native/backend.native/cli.bc/src/org/jetbrains/kotlin/cli/bc/K2NativeCompilerArguments.kt b/kotlin-native/backend.native/cli.bc/src/org/jetbrains/kotlin/cli/bc/K2NativeCompilerArguments.kt index 1e795c90812..185a782518a 100644 --- a/kotlin-native/backend.native/cli.bc/src/org/jetbrains/kotlin/cli/bc/K2NativeCompilerArguments.kt +++ b/kotlin-native/backend.native/cli.bc/src/org/jetbrains/kotlin/cli/bc/K2NativeCompilerArguments.kt @@ -320,6 +320,14 @@ class K2NativeCompilerArguments : CommonCompilerArguments() { @Argument(value="-Xruntime-asserts-mode", valueDescription = "", description = "Enable asserts in runtime. Possible values: 'ignore', 'log', 'panic'") var runtimeAssertsMode: String? = "ignore" + // TODO: Remove when legacy MM is gone. + @Argument( + value = "-Xworker-exception-handling", + valueDescription = "", + description = "Unhandled exception processing in Worker.executeAfter. Possible values: 'legacy', 'use-hook'. The default value is 'legacy', for -memory-model experimental the default value is 'use-hook'" + ) + var workerExceptionHandling: String? = null + override fun configureAnalysisFlags(collector: MessageCollector, languageVersion: LanguageVersion): MutableMap, Any> = super.configureAnalysisFlags(collector, languageVersion).also { val useExperimental = it[AnalysisFlags.useExperimental] as List<*> diff --git a/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/CAdapterGenerator.kt b/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/CAdapterGenerator.kt index ea479cd4ede..99bb6c714a4 100644 --- a/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/CAdapterGenerator.kt +++ b/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/CAdapterGenerator.kt @@ -465,7 +465,7 @@ private class ExportedElement(val kind: ElementKind, "result", cfunction[0], Direction.KOTLIN_TO_C, builder) builder.append(" return $result;\n") } - builder.append(" } catch (ExceptionObjHolder& e) { TerminateWithUnhandledException(e.GetExceptionObject()); } \n") + builder.append(" } catch (ExceptionObjHolder& e) { std::terminate(); } \n") builder.append("}\n") @@ -902,6 +902,8 @@ internal class CAdapterGenerator(val context: Context) : DeclarationDescriptorVi // Include header into C++ source. headerFile.forEachLine { it -> output(it) } + output("#include ") + output(""" |struct KObjHeader; |typedef struct KObjHeader KObjHeader; @@ -924,7 +926,6 @@ internal class CAdapterGenerator(val context: Context) : DeclarationDescriptorVi |void Kotlin_initRuntimeIfNeeded(); |void Kotlin_mm_switchThreadStateRunnable() RUNTIME_NOTHROW; |void Kotlin_mm_switchThreadStateNative() RUNTIME_NOTHROW; - |void TerminateWithUnhandledException(KObjHeader*) RUNTIME_NORETURN; | |KObjHeader* CreateStringFromCString(const char*, KObjHeader**); |char* CreateCStringFromString(const KObjHeader*); diff --git a/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/EntryPoint.kt b/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/EntryPoint.kt index bfc820ca7f0..723d5cb78ab 100644 --- a/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/EntryPoint.kt +++ b/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/EntryPoint.kt @@ -79,10 +79,12 @@ internal fun makeEntryPoint(context: Context): IrFunction { } catches += irCatch(context.irBuiltIns.throwableType).apply { result = irBlock { - +irCall(context.ir.symbols.onUnhandledException).apply { + +irCall(context.ir.symbols.processUnhandledException).apply { + putValueArgument(0, irGet(catchParameter)) + } + +irCall(context.ir.symbols.terminateWithUnhandledException).apply { putValueArgument(0, irGet(catchParameter)) } - +irReturn(irInt(1)) } } } diff --git a/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/KonanConfig.kt b/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/KonanConfig.kt index 3211085a6cf..c24e5144c29 100644 --- a/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/KonanConfig.kt +++ b/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/KonanConfig.kt @@ -50,6 +50,7 @@ class KonanConfig(val project: Project, val configuration: CompilerConfiguration val gc: GC get() = configuration.get(KonanConfigKeys.GARBAGE_COLLECTOR)!! val gcAggressive: Boolean get() = configuration.get(KonanConfigKeys.GARBAGE_COLLECTOR_AGRESSIVE)!! val runtimeAssertsMode: RuntimeAssertsMode get() = configuration.get(KonanConfigKeys.RUNTIME_ASSERTS_MODE)!! + val workerExceptionHandling: WorkerExceptionHandling get() = configuration.get(KonanConfigKeys.WORKER_EXCEPTION_HANDLING)!! val needVerifyIr: Boolean get() = configuration.get(KonanConfigKeys.VERIFY_IR) == true diff --git a/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/KonanConfigurationKeys.kt b/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/KonanConfigurationKeys.kt index 218e0ac3746..921b03679ef 100644 --- a/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/KonanConfigurationKeys.kt +++ b/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/KonanConfigurationKeys.kt @@ -164,6 +164,7 @@ class KonanConfigKeys { val RUNTIME_ASSERTS_MODE: CompilerConfigurationKey = CompilerConfigurationKey.create("enable runtime asserts") val PROPERTY_LAZY_INITIALIZATION: CompilerConfigurationKey = CompilerConfigurationKey.create("lazy top level properties initialization") + val WORKER_EXCEPTION_HANDLING: CompilerConfigurationKey = CompilerConfigurationKey.create("unhandled exception processing in Worker.executeAfter") } } diff --git a/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/WorkerExceptionHandling.kt b/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/WorkerExceptionHandling.kt new file mode 100644 index 00000000000..5132685d8f8 --- /dev/null +++ b/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/WorkerExceptionHandling.kt @@ -0,0 +1,11 @@ +/* + * Copyright 2010-2021 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 org.jetbrains.kotlin.backend.konan + +// Must match `WorkerExceptionHandling` in CompilerConstants.hpp +enum class WorkerExceptionHandling(val value: Int) { + LEGACY(0), + USE_HOOK(1), +} diff --git a/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/ir/Ir.kt b/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/ir/Ir.kt index 896459f0a3e..749fd26d570 100644 --- a/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/ir/Ir.kt +++ b/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/ir/Ir.kt @@ -117,7 +117,8 @@ internal class KonanSymbols( val objCMethodImp = symbolTable.referenceClass(context.interopBuiltIns.objCMethodImp) - val onUnhandledException = internalFunction("OnUnhandledException") + val processUnhandledException = irBuiltIns.findFunctions(Name.identifier("processUnhandledException"), "kotlin", "native").single() + val terminateWithUnhandledException = irBuiltIns.findFunctions(Name.identifier("terminateWithUnhandledException"), "kotlin", "native").single() val interopNativePointedGetRawPointer = symbolTable.referenceSimpleFunction(context.interopBuiltIns.nativePointedGetRawPointer) diff --git a/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/llvm/IrToBitcode.kt b/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/llvm/IrToBitcode.kt index 549f8e89126..d2a9828fca2 100644 --- a/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/llvm/IrToBitcode.kt +++ b/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/llvm/IrToBitcode.kt @@ -2631,6 +2631,7 @@ internal class CodeGeneratorVisitor(val context: Context, val lifetimes: Map + println("Hook") + }.freeze() + + val oldHook = setUnhandledExceptionHook(exceptionHook) + assertNull(oldHook) + val hook1 = getUnhandledExceptionHook() + assertEquals(exceptionHook, hook1) + val hook2 = getUnhandledExceptionHook() +} diff --git a/kotlin-native/backend.native/tests/runtime/exceptions/custom_hook_memory_leak.kt b/kotlin-native/backend.native/tests/runtime/exceptions/custom_hook_memory_leak.kt new file mode 100644 index 00000000000..e6d36f23778 --- /dev/null +++ b/kotlin-native/backend.native/tests/runtime/exceptions/custom_hook_memory_leak.kt @@ -0,0 +1,20 @@ +/* + * Copyright 2010-2021 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. + */ +import kotlin.test.* + +import kotlin.native.concurrent.* + +data class C(val x: Int) + +fun main() { + Platform.isMemoryLeakCheckerActive = true + + val c = C(42) + setUnhandledExceptionHook({ _: Throwable -> + println("Hook ${c.x}") + }.freeze()) + + throw Error("an error") +} diff --git a/kotlin-native/backend.native/tests/runtime/exceptions/custom_hook_no_reset.kt b/kotlin-native/backend.native/tests/runtime/exceptions/custom_hook_no_reset.kt new file mode 100644 index 00000000000..cd6382f1178 --- /dev/null +++ b/kotlin-native/backend.native/tests/runtime/exceptions/custom_hook_no_reset.kt @@ -0,0 +1,20 @@ +/* + * Copyright 2010-2021 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.* + +fun customExceptionHook(throwable: Throwable) { + println("Hook called") + assertEquals(::customExceptionHook, getUnhandledExceptionHook()) +} + +fun main() { + setUnhandledExceptionHook((::customExceptionHook).freeze()) + + throw Error("some error") +} diff --git a/kotlin-native/backend.native/tests/runtime/exceptions/custom_hook_terminate.kt b/kotlin-native/backend.native/tests/runtime/exceptions/custom_hook_terminate.kt new file mode 100644 index 00000000000..feed2442bee --- /dev/null +++ b/kotlin-native/backend.native/tests/runtime/exceptions/custom_hook_terminate.kt @@ -0,0 +1,18 @@ +/* + * Copyright 2010-2021 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.* + +fun main() { + setUnhandledExceptionHook({ t: Throwable -> + println("Hook called") + terminateWithUnhandledException(t) + }.freeze()) + + throw Error("some error") +} diff --git a/kotlin-native/backend.native/tests/runtime/exceptions/custom_hook_terminate_unhandled_exception.kt b/kotlin-native/backend.native/tests/runtime/exceptions/custom_hook_terminate_unhandled_exception.kt new file mode 100644 index 00000000000..681927c3c5f --- /dev/null +++ b/kotlin-native/backend.native/tests/runtime/exceptions/custom_hook_terminate_unhandled_exception.kt @@ -0,0 +1,20 @@ +/* + * Copyright 2010-2021 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.* + +fun main() { + setUnhandledExceptionHook({ t: Throwable -> + println("Hook called") + terminateWithUnhandledException(t) + }.freeze()) + + val exception = Error("some error") + processUnhandledException(exception) + println("Not going to happen") +} diff --git a/kotlin-native/backend.native/tests/runtime/exceptions/custom_hook_throws.kt b/kotlin-native/backend.native/tests/runtime/exceptions/custom_hook_throws.kt new file mode 100644 index 00000000000..31fcbad20a7 --- /dev/null +++ b/kotlin-native/backend.native/tests/runtime/exceptions/custom_hook_throws.kt @@ -0,0 +1,18 @@ +/* + * Copyright 2010-2021 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. + */ +import kotlin.test.* + +import kotlin.native.concurrent.* + +fun customExceptionHook(throwable: Throwable) { + println("Hook called") + throw Error("another error") +} + +fun main() { + setUnhandledExceptionHook((::customExceptionHook).freeze()) + + throw Error("some error") +} diff --git a/kotlin-native/backend.native/tests/runtime/exceptions/custom_hook_unhandled_exception.kt b/kotlin-native/backend.native/tests/runtime/exceptions/custom_hook_unhandled_exception.kt new file mode 100644 index 00000000000..b3c6741dbf3 --- /dev/null +++ b/kotlin-native/backend.native/tests/runtime/exceptions/custom_hook_unhandled_exception.kt @@ -0,0 +1,20 @@ +/* + * Copyright 2010-2021 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.* + +fun main() { + val called = AtomicInt(0) + setUnhandledExceptionHook({ _: Throwable -> + called.value = 1 + }.freeze()) + + val exception = Error("some error") + processUnhandledException(exception) + assertEquals(1, called.value) +} diff --git a/kotlin-native/backend.native/tests/runtime/exceptions/terminate.kt b/kotlin-native/backend.native/tests/runtime/exceptions/terminate.kt new file mode 100644 index 00000000000..55926607e26 --- /dev/null +++ b/kotlin-native/backend.native/tests/runtime/exceptions/terminate.kt @@ -0,0 +1,12 @@ +/* + * Copyright 2010-2021 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.* + +fun main() { + val exception = Error("some error") + terminateWithUnhandledException(exception) +} diff --git a/kotlin-native/backend.native/tests/runtime/exceptions/unhandled_exception.kt b/kotlin-native/backend.native/tests/runtime/exceptions/unhandled_exception.kt new file mode 100644 index 00000000000..526946def26 --- /dev/null +++ b/kotlin-native/backend.native/tests/runtime/exceptions/unhandled_exception.kt @@ -0,0 +1,12 @@ +/* + * Copyright 2010-2021 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.* + +fun main() { + val exception = Error("some error") + processUnhandledException(exception) +} diff --git a/kotlin-native/backend.native/tests/runtime/workers/worker_exceptions.kt b/kotlin-native/backend.native/tests/runtime/workers/worker_exceptions.kt new file mode 100644 index 00000000000..fa27d42052e --- /dev/null +++ b/kotlin-native/backend.native/tests/runtime/workers/worker_exceptions.kt @@ -0,0 +1,38 @@ +package runtime.workers.worker_exceptions + +import kotlin.test.* + +import kotlin.native.concurrent.* + +@Test +fun testExecuteAfterStartQuiet() { + val worker = Worker.start(errorReporting = false) + worker.executeAfter(0L, { + throw Error("testExecuteAfterStartQuiet error") + }.freeze()) + worker.requestTermination().result +} + +@Test +fun testExecuteStart() { + val worker = Worker.start() + val future = worker.execute(TransferMode.SAFE, {}) { + throw Error("testExecuteStart error") + } + assertFailsWith { + future.result + } + worker.requestTermination().result +} + +@Test +fun testExecuteStartQuiet() { + val worker = Worker.start(errorReporting = false) + val future = worker.execute(TransferMode.SAFE, {}) { + throw Error("testExecuteStartQuiet error") + } + assertFailsWith { + future.result + } + worker.requestTermination().result +} diff --git a/kotlin-native/backend.native/tests/runtime/workers/worker_exceptions_legacy.kt b/kotlin-native/backend.native/tests/runtime/workers/worker_exceptions_legacy.kt new file mode 100644 index 00000000000..917aee36c47 --- /dev/null +++ b/kotlin-native/backend.native/tests/runtime/workers/worker_exceptions_legacy.kt @@ -0,0 +1,26 @@ +package runtime.workers.worker_exceptions_legacy + +import kotlin.test.* + +import kotlin.native.concurrent.* + +@Test +fun testExecuteAfterStartLegacy() { + val worker = Worker.start() + worker.executeAfter(0L, { + throw Error("testExecuteAfterStartLegacy error") + }.freeze()) + worker.requestTermination().result +} + +@Test +fun testExecuteStartLegacy() { + val worker = Worker.start() + val future = worker.execute(TransferMode.SAFE, {}) { + throw Error("testExecuteStartLegacy error") + } + assertFailsWith { + future.result + } + worker.requestTermination().result +} diff --git a/kotlin-native/backend.native/tests/runtime/workers/worker_exceptions_terminate.kt b/kotlin-native/backend.native/tests/runtime/workers/worker_exceptions_terminate.kt new file mode 100644 index 00000000000..1d9d8a37684 --- /dev/null +++ b/kotlin-native/backend.native/tests/runtime/workers/worker_exceptions_terminate.kt @@ -0,0 +1,10 @@ +import kotlin.native.concurrent.* + +fun main() { + val worker = Worker.start() + worker.executeAfter(0L, { + throw Error("some error") + }.freeze()) + worker.requestTermination().result + println("Will not happen") +} diff --git a/kotlin-native/backend.native/tests/runtime/workers/worker_exceptions_terminate_current.kt b/kotlin-native/backend.native/tests/runtime/workers/worker_exceptions_terminate_current.kt new file mode 100644 index 00000000000..45e5cb81841 --- /dev/null +++ b/kotlin-native/backend.native/tests/runtime/workers/worker_exceptions_terminate_current.kt @@ -0,0 +1,9 @@ +import kotlin.native.concurrent.* + +fun main() { + Worker.current.executeAfter(0L, { + throw Error("some error") + }.freeze()) + Worker.current.processQueue() + println("Will not happen") +} diff --git a/kotlin-native/backend.native/tests/runtime/workers/worker_exceptions_terminate_hook.kt b/kotlin-native/backend.native/tests/runtime/workers/worker_exceptions_terminate_hook.kt new file mode 100644 index 00000000000..a12e97e4406 --- /dev/null +++ b/kotlin-native/backend.native/tests/runtime/workers/worker_exceptions_terminate_hook.kt @@ -0,0 +1,15 @@ +import kotlin.native.concurrent.* + +fun main() { + setUnhandledExceptionHook({ _: Throwable -> + println("hook called") + }.freeze()) + + + val worker = Worker.start() + worker.executeAfter(0L, { + throw Error("some error") + }.freeze()) + worker.requestTermination().result + println("Will happen") +} diff --git a/kotlin-native/backend.native/tests/runtime/workers/worker_exceptions_terminate_hook_current.kt b/kotlin-native/backend.native/tests/runtime/workers/worker_exceptions_terminate_hook_current.kt new file mode 100644 index 00000000000..c2ddc38fe87 --- /dev/null +++ b/kotlin-native/backend.native/tests/runtime/workers/worker_exceptions_terminate_hook_current.kt @@ -0,0 +1,13 @@ +import kotlin.native.concurrent.* + +fun main() { + setUnhandledExceptionHook({ _: Throwable -> + println("hook called") + }.freeze()) + + Worker.current.executeAfter(0L, { + throw Error("some error") + }.freeze()) + Worker.current.processQueue() + println("Will happen") +} diff --git a/kotlin-native/runtime/src/main/cpp/CompilerConstants.cpp b/kotlin-native/runtime/src/main/cpp/CompilerConstants.cpp index a09344c2ad5..e725640afe2 100644 --- a/kotlin-native/runtime/src/main/cpp/CompilerConstants.cpp +++ b/kotlin-native/runtime/src/main/cpp/CompilerConstants.cpp @@ -12,6 +12,7 @@ using namespace kotlin; // These are defined by overrideRuntimeGlobals in IrToBitcode.kt RUNTIME_WEAK int32_t Kotlin_destroyRuntimeMode = 1; RUNTIME_WEAK int32_t Kotiln_gcAggressive = 0; +RUNTIME_WEAK int32_t Kotlin_workerExceptionHandling = 0; ALWAYS_INLINE compiler::DestroyRuntimeMode compiler::destroyRuntimeMode() noexcept { return static_cast(Kotlin_destroyRuntimeMode); @@ -20,3 +21,7 @@ ALWAYS_INLINE compiler::DestroyRuntimeMode compiler::destroyRuntimeMode() noexce ALWAYS_INLINE bool compiler::gcAggressive() noexcept { return Kotiln_gcAggressive != 0; } + +ALWAYS_INLINE compiler::WorkerExceptionHandling compiler::workerExceptionHandling() noexcept { + return static_cast(Kotlin_workerExceptionHandling); +} diff --git a/kotlin-native/runtime/src/main/cpp/CompilerConstants.hpp b/kotlin-native/runtime/src/main/cpp/CompilerConstants.hpp index beb594d5b67..143a79404d2 100644 --- a/kotlin-native/runtime/src/main/cpp/CompilerConstants.hpp +++ b/kotlin-native/runtime/src/main/cpp/CompilerConstants.hpp @@ -32,6 +32,12 @@ enum class RuntimeAssertsMode : int32_t { kPanic = 2, }; +// Must match WorkerExceptionHandling in WorkerExceptionHandling.kt +enum class WorkerExceptionHandling : int32_t { + kLegacy = 0, + kUseHook = 1, +}; + DestroyRuntimeMode destroyRuntimeMode() noexcept; bool gcAggressive() noexcept; @@ -44,6 +50,8 @@ ALWAYS_INLINE inline RuntimeAssertsMode runtimeAssertsMode() noexcept { return static_cast(Kotlin_runtimeAssertsMode); } +WorkerExceptionHandling workerExceptionHandling() noexcept; + } // namespace compiler } // namespace kotlin diff --git a/kotlin-native/runtime/src/main/cpp/Exceptions.cpp b/kotlin-native/runtime/src/main/cpp/Exceptions.cpp index f5a4185263c..bc48f4981ba 100644 --- a/kotlin-native/runtime/src/main/cpp/Exceptions.cpp +++ b/kotlin-native/runtime/src/main/cpp/Exceptions.cpp @@ -30,6 +30,10 @@ #include "Utils.hpp" #include "ObjCExceptions.h" +// Defined in RuntimeUtils.kt +extern "C" void Kotlin_runUnhandledExceptionHook(KRef exception); +extern "C" void ReportUnhandledException(KRef exception); + void ThrowException(KRef exception) { RuntimeAssert(exception != nullptr && IsInstance(exception, theThrowableTypeInfo), "Throwing something non-throwable"); @@ -64,27 +68,32 @@ class { } } concurrentTerminateWrapper; -//! Process exception hook (if any) or just printStackTrace + write crash log -void processUnhandledKotlinException(KRef throwable) { - // Use the reentrant switch because both states are possible here: - // - runnable, if the exception occured in a pure Kotlin thread (except initialization of globals). - // - native, if the throwing code was called from ObjC/Swift or if the exception occured during initialization of globals. - kotlin::ThreadStateGuard guard(kotlin::ThreadState::kRunnable, /* reentrant = */ true); - OnUnhandledException(throwable); +void RUNTIME_NORETURN terminateWithUnhandledException(KRef exception) { + kotlin::AssertThreadState(kotlin::ThreadState::kRunnable); + concurrentTerminateWrapper([exception]() { + ReportUnhandledException(exception); #if KONAN_REPORT_BACKTRACE_TO_IOS_CRASH_LOG - ReportBacktraceToIosCrashLog(throwable); + ReportBacktraceToIosCrashLog(exception); +#endif + konan::abort(); + }); +} + +void processUnhandledException(KRef exception) noexcept { + kotlin::AssertThreadState(kotlin::ThreadState::kRunnable); +#if KONAN_NO_EXCEPTIONS + terminateWithUnhandledException(exception); +#else + try { + Kotlin_runUnhandledExceptionHook(exception); + } catch (ExceptionObjHolder& e) { + terminateWithUnhandledException(e.GetExceptionObject()); + } #endif } } // namespace -RUNTIME_NORETURN void TerminateWithUnhandledException(KRef throwable) { - concurrentTerminateWrapper([=]() { - processUnhandledKotlinException(throwable); - konan::abort(); - }); -} - ALWAYS_INLINE RUNTIME_NOTHROW OBJ_GETTER(Kotlin_getExceptionObject, void* holder) { #if !KONAN_NO_EXCEPTIONS RETURN_OBJ(static_cast(holder)->GetExceptionObject()); @@ -98,25 +107,33 @@ ALWAYS_INLINE RUNTIME_NOTHROW OBJ_GETTER(Kotlin_getExceptionObject, void* holder namespace { // Copy, move and assign would be safe, but not much useful, so let's delete all (rule of 5) class TerminateHandler : private kotlin::Pinned { + RUNTIME_NORETURN static void queuedHandler() { + concurrentTerminateWrapper([]() { + // Not a Kotlin exception - call default handler + instance().queuedHandler_(); + }); + } // In fact, it's safe to call my_handler directly from outside: it will do the job and then invoke original handler, // even if it has not been initialized yet. So one may want to make it public and/or not the class member RUNTIME_NORETURN static void kotlinHandler() { - concurrentTerminateWrapper([]() { if (auto currentException = std::current_exception()) { try { std::rethrow_exception(currentException); } catch (ExceptionObjHolder& e) { - processUnhandledKotlinException(e.GetExceptionObject()); - konan::abort(); + // Use the reentrant switch because both states are possible here: + // - runnable, if the exception occured in a pure Kotlin thread (except initialization of globals). + // - native, if the throwing code was called from ObjC/Swift or if the exception occured during initialization of globals. + kotlin::ThreadStateGuard guard(kotlin::ThreadState::kRunnable, /* reentrant = */ true); + processUnhandledException(e.GetExceptionObject()); + terminateWithUnhandledException(e.GetExceptionObject()); } catch (...) { // Not a Kotlin exception - call default handler - instance().queuedHandler_(); + queuedHandler(); } } // Come here in case of direct terminate() call or unknown exception - go to default terminate handler. - instance().queuedHandler_(); - }); + queuedHandler(); } using QH = __attribute__((noreturn)) void(*)(); @@ -154,3 +171,25 @@ void SetKonanTerminateHandler() { } #endif // !KONAN_NO_EXCEPTIONS + +extern "C" void RUNTIME_NORETURN Kotlin_terminateWithUnhandledException(KRef exception) { + kotlin::AssertThreadState(kotlin::ThreadState::kRunnable); + terminateWithUnhandledException(exception); +} + +extern "C" void Kotlin_processUnhandledException(KRef exception) { + kotlin::AssertThreadState(kotlin::ThreadState::kRunnable); + processUnhandledException(exception); +} + +void kotlin::ProcessUnhandledException(KRef exception) noexcept { + // This may be called from any state, do reentrant state switch to runnable. + kotlin::ThreadStateGuard guard(kotlin::ThreadState::kRunnable, /* reentrant = */ true); + processUnhandledException(exception); +} + +void RUNTIME_NORETURN kotlin::TerminateWithUnhandledException(KRef exception) noexcept { + // This may be called from any state, do reentrant state switch to runnable. + kotlin::ThreadStateGuard guard(kotlin::ThreadState::kRunnable, /* reentrant = */ true); + terminateWithUnhandledException(exception); +} diff --git a/kotlin-native/runtime/src/main/cpp/Exceptions.h b/kotlin-native/runtime/src/main/cpp/Exceptions.h index 066e501d62a..e9ac3c6c31e 100644 --- a/kotlin-native/runtime/src/main/cpp/Exceptions.h +++ b/kotlin-native/runtime/src/main/cpp/Exceptions.h @@ -26,11 +26,6 @@ extern "C" { // Throws arbitrary exception. void ThrowException(KRef exception); -// RuntimeUtils.kt -void OnUnhandledException(KRef throwable); - -RUNTIME_NORETURN void TerminateWithUnhandledException(KRef exception); - void SetKonanTerminateHandler(); RUNTIME_NOTHROW OBJ_GETTER(Kotlin_getExceptionObject, void* holder); @@ -68,4 +63,11 @@ void PrintThrowable(KRef); } // extern "C" #endif +namespace kotlin { + +void ProcessUnhandledException(KRef exception) noexcept; +void RUNTIME_NORETURN TerminateWithUnhandledException(KRef exception) noexcept; + +} // namespace kotlin + #endif // RUNTIME_NAMES_H diff --git a/kotlin-native/runtime/src/main/cpp/ExceptionsTest.cpp b/kotlin-native/runtime/src/main/cpp/ExceptionsTest.cpp new file mode 100644 index 00000000000..7213a2815ce --- /dev/null +++ b/kotlin-native/runtime/src/main/cpp/ExceptionsTest.cpp @@ -0,0 +1,287 @@ +/* + * Copyright 2010-2021 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 "Exceptions.h" + +#include "gtest/gtest.h" +#include "gmock/gmock.h" + +#include "ObjectTestSupport.hpp" +#include "TestSupportCompilerGenerated.hpp" +#include "TestSupport.hpp" + +using namespace kotlin; + +using ::testing::_; + +namespace { + +struct Payload { + int value = 0; + + using Field = ObjHeader* Payload::*; + static constexpr std::array kFields{}; +}; + +using Object = test_support::Object; + +} // namespace + +TEST(ExceptionTest, ProcessUnhandledException_WithHook) { + test_support::TypeInfoHolder typeHolder{test_support::TypeInfoHolder::ObjectBuilder().setSuperType(theThrowableTypeInfo)}; + kotlin::RunInNewThread([&typeHolder]() { + Object exception(typeHolder.typeInfo()); + exception.header()->typeInfoOrMeta_ = setPointerBits(exception.header()->typeInfoOrMeta_, OBJECT_TAG_PERMANENT_CONTAINER); + exception->value = 42; + auto reportUnhandledExceptionMock = ScopedReportUnhandledExceptionMock(); + auto Kotlin_runUnhandledExceptionHookMock = ScopedKotlin_runUnhandledExceptionHookMock(); + EXPECT_CALL(*reportUnhandledExceptionMock, Call(_)).Times(0); + EXPECT_CALL(*Kotlin_runUnhandledExceptionHookMock, Call(_)).WillOnce([](KRef exception) { + EXPECT_THAT(Object::FromObjHeader(exception)->value, 42); + }); + kotlin::ProcessUnhandledException(exception.header()); + }); +} + +TEST(ExceptionDeathTest, ProcessUnhandledException_NoHook) { + test_support::TypeInfoHolder typeHolder{test_support::TypeInfoHolder::ObjectBuilder().setSuperType(theThrowableTypeInfo)}; + kotlin::RunInNewThread([&typeHolder]() { + Object exception(typeHolder.typeInfo()); + exception.header()->typeInfoOrMeta_ = setPointerBits(exception.header()->typeInfoOrMeta_, OBJECT_TAG_PERMANENT_CONTAINER); + exception->value = 42; + auto reportUnhandledExceptionMock = ScopedReportUnhandledExceptionMock(); + auto Kotlin_runUnhandledExceptionHookMock = ScopedKotlin_runUnhandledExceptionHookMock(); + ON_CALL(*reportUnhandledExceptionMock, Call(_)).WillByDefault([](KRef exception) { + konan::consoleErrorf("Reporting %d\n", Object::FromObjHeader(exception)->value); + }); + ON_CALL(*Kotlin_runUnhandledExceptionHookMock, Call(_)).WillByDefault([](KRef exception) { + konan::consoleErrorf("Hook %d\n", Object::FromObjHeader(exception)->value); + // Kotlin_runUnhandledExceptionHookMock rethrows original exception when hook is unset. + ThrowException(exception); + }); + EXPECT_DEATH({ kotlin::ProcessUnhandledException(exception.header()); }, "Hook 42\nReporting 42\n"); + }); +} + +TEST(ExceptionDeathTest, ProcessUnhandledException_WithFailingHook) { + test_support::TypeInfoHolder typeHolder{test_support::TypeInfoHolder::ObjectBuilder().setSuperType(theThrowableTypeInfo)}; + kotlin::RunInNewThread([&typeHolder]() { + Object exception(typeHolder.typeInfo()); + exception.header()->typeInfoOrMeta_ = setPointerBits(exception.header()->typeInfoOrMeta_, OBJECT_TAG_PERMANENT_CONTAINER); + exception->value = 42; + Object hookException(typeHolder.typeInfo()); + hookException.header()->typeInfoOrMeta_ = setPointerBits(hookException.header()->typeInfoOrMeta_, OBJECT_TAG_PERMANENT_CONTAINER); + hookException->value = 13; + auto reportUnhandledExceptionMock = ScopedReportUnhandledExceptionMock(); + auto Kotlin_runUnhandledExceptionHookMock = ScopedKotlin_runUnhandledExceptionHookMock(); + ON_CALL(*reportUnhandledExceptionMock, Call(_)).WillByDefault([](KRef exception) { + konan::consoleErrorf("Reporting %d\n", Object::FromObjHeader(exception)->value); + }); + ON_CALL(*Kotlin_runUnhandledExceptionHookMock, Call(_)).WillByDefault([&hookException](KRef exception) { + konan::consoleErrorf("Hook %d\n", Object::FromObjHeader(exception)->value); + ThrowException(hookException.header()); + }); + EXPECT_DEATH({ kotlin::ProcessUnhandledException(exception.header()); }, "Hook 42\nReporting 13\n"); + }); +} + +TEST(ExceptionDeathTest, ProcessUnhandledException_WithTerminatingFailingHook) { + test_support::TypeInfoHolder typeHolder{test_support::TypeInfoHolder::ObjectBuilder().setSuperType(theThrowableTypeInfo)}; + kotlin::RunInNewThread([&typeHolder]() { + Object exception(typeHolder.typeInfo()); + exception.header()->typeInfoOrMeta_ = setPointerBits(exception.header()->typeInfoOrMeta_, OBJECT_TAG_PERMANENT_CONTAINER); + exception->value = 42; + Object hookException(typeHolder.typeInfo()); + hookException.header()->typeInfoOrMeta_ = setPointerBits(hookException.header()->typeInfoOrMeta_, OBJECT_TAG_PERMANENT_CONTAINER); + hookException->value = 13; + auto reportUnhandledExceptionMock = ScopedReportUnhandledExceptionMock(); + auto Kotlin_runUnhandledExceptionHookMock = ScopedKotlin_runUnhandledExceptionHookMock(); + ON_CALL(*reportUnhandledExceptionMock, Call(_)).WillByDefault([](KRef exception) { + konan::consoleErrorf("Reporting %d\n", Object::FromObjHeader(exception)->value); + }); + ON_CALL(*Kotlin_runUnhandledExceptionHookMock, Call(_)).WillByDefault([&hookException](KRef exception) { + konan::consoleErrorf("Hook %d\n", Object::FromObjHeader(exception)->value); + kotlin::TerminateWithUnhandledException(hookException.header()); + }); + EXPECT_DEATH({ kotlin::ProcessUnhandledException(exception.header()); }, "Hook 42\nReporting 13\n"); + }); +} + +TEST(ExceptionDeathTest, TerminateWithUnhandledException) { + test_support::TypeInfoHolder typeHolder{test_support::TypeInfoHolder::ObjectBuilder().setSuperType(theThrowableTypeInfo)}; + kotlin::RunInNewThread([&typeHolder]() { + Object exception(typeHolder.typeInfo()); + exception.header()->typeInfoOrMeta_ = setPointerBits(exception.header()->typeInfoOrMeta_, OBJECT_TAG_PERMANENT_CONTAINER); + exception->value = 42; + auto reportUnhandledExceptionMock = ScopedReportUnhandledExceptionMock(); + auto Kotlin_runUnhandledExceptionHookMock = ScopedKotlin_runUnhandledExceptionHookMock(); + ON_CALL(*reportUnhandledExceptionMock, Call(_)).WillByDefault([](KRef exception) { + konan::consoleErrorf("Reporting %d\n", Object::FromObjHeader(exception)->value); + }); + ON_CALL(*Kotlin_runUnhandledExceptionHookMock, Call(_)).WillByDefault([](KRef exception) { + konan::consoleErrorf("Hook %d\n", Object::FromObjHeader(exception)->value); + }); + EXPECT_DEATH({ kotlin::TerminateWithUnhandledException(exception.header()); }, "Reporting 42\n"); + }); +} + +TEST(ExceptionDeathTest, TerminateHandler_WithHook) { + test_support::TypeInfoHolder typeHolder{test_support::TypeInfoHolder::ObjectBuilder().setSuperType(theThrowableTypeInfo)}; + kotlin::RunInNewThread([&typeHolder]() { + Object exception(typeHolder.typeInfo()); + exception.header()->typeInfoOrMeta_ = setPointerBits(exception.header()->typeInfoOrMeta_, OBJECT_TAG_PERMANENT_CONTAINER); + exception->value = 42; + auto reportUnhandledExceptionMock = ScopedReportUnhandledExceptionMock(); + auto Kotlin_runUnhandledExceptionHookMock = ScopedKotlin_runUnhandledExceptionHookMock(); + ON_CALL(*reportUnhandledExceptionMock, Call(_)).WillByDefault([](KRef exception) { + konan::consoleErrorf("Reporting %d\n", Object::FromObjHeader(exception)->value); + }); + ON_CALL(*Kotlin_runUnhandledExceptionHookMock, Call(_)).WillByDefault([](KRef exception) { + konan::consoleErrorf("Hook %d\n", Object::FromObjHeader(exception)->value); + }); + EXPECT_DEATH( + { + std::set_terminate([]() { + konan::consoleErrorf("Custom terminate\n"); + if (auto exception = std::current_exception()) { + try { + std::rethrow_exception(exception); + } catch (int i) { + konan::consoleErrorf("Exception %d\n", i); + } catch (...) { + konan::consoleErrorf("Unknown Exception\n"); + } + } + }); + SetKonanTerminateHandler(); + try { + ThrowException(exception.header()); + } catch (...) { + std::terminate(); + } + }, + "Hook 42\n"); + }); +} + +TEST(ExceptionDeathTest, TerminateHandler_NoHook) { + test_support::TypeInfoHolder typeHolder{test_support::TypeInfoHolder::ObjectBuilder().setSuperType(theThrowableTypeInfo)}; + kotlin::RunInNewThread([&typeHolder]() { + Object exception(typeHolder.typeInfo()); + exception.header()->typeInfoOrMeta_ = setPointerBits(exception.header()->typeInfoOrMeta_, OBJECT_TAG_PERMANENT_CONTAINER); + exception->value = 42; + auto reportUnhandledExceptionMock = ScopedReportUnhandledExceptionMock(); + auto Kotlin_runUnhandledExceptionHookMock = ScopedKotlin_runUnhandledExceptionHookMock(); + ON_CALL(*reportUnhandledExceptionMock, Call(_)).WillByDefault([](KRef exception) { + konan::consoleErrorf("Reporting %d\n", Object::FromObjHeader(exception)->value); + }); + ON_CALL(*Kotlin_runUnhandledExceptionHookMock, Call(_)).WillByDefault([](KRef exception) { + konan::consoleErrorf("Hook %d\n", Object::FromObjHeader(exception)->value); + // Kotlin_runUnhandledExceptionHookMock rethrows original exception when hook is unset. + ThrowException(exception); + }); + EXPECT_DEATH( + { + std::set_terminate([]() { + konan::consoleErrorf("Custom terminate\n"); + if (auto exception = std::current_exception()) { + try { + std::rethrow_exception(exception); + } catch (int i) { + konan::consoleErrorf("Exception %d\n", i); + } catch (...) { + konan::consoleErrorf("Unknown Exception\n"); + } + } + }); + SetKonanTerminateHandler(); + try { + ThrowException(exception.header()); + } catch (...) { + std::terminate(); + } + }, + "Hook 42\nReporting 42\n"); + }); +} + +TEST(ExceptionDeathTest, TerminateHandler_WithFailingHook) { + test_support::TypeInfoHolder typeHolder{test_support::TypeInfoHolder::ObjectBuilder().setSuperType(theThrowableTypeInfo)}; + kotlin::RunInNewThread([&typeHolder]() { + Object exception(typeHolder.typeInfo()); + exception.header()->typeInfoOrMeta_ = setPointerBits(exception.header()->typeInfoOrMeta_, OBJECT_TAG_PERMANENT_CONTAINER); + exception->value = 42; + Object hookException(typeHolder.typeInfo()); + hookException.header()->typeInfoOrMeta_ = setPointerBits(hookException.header()->typeInfoOrMeta_, OBJECT_TAG_PERMANENT_CONTAINER); + hookException->value = 13; + auto reportUnhandledExceptionMock = ScopedReportUnhandledExceptionMock(); + auto Kotlin_runUnhandledExceptionHookMock = ScopedKotlin_runUnhandledExceptionHookMock(); + ON_CALL(*reportUnhandledExceptionMock, Call(_)).WillByDefault([](KRef exception) { + konan::consoleErrorf("Reporting %d\n", Object::FromObjHeader(exception)->value); + }); + ON_CALL(*Kotlin_runUnhandledExceptionHookMock, Call(_)).WillByDefault([&hookException](KRef exception) { + konan::consoleErrorf("Hook %d\n", Object::FromObjHeader(exception)->value); + ThrowException(hookException.header()); + }); + EXPECT_DEATH( + { + std::set_terminate([]() { + konan::consoleErrorf("Custom terminate\n"); + if (auto exception = std::current_exception()) { + try { + std::rethrow_exception(exception); + } catch (int i) { + konan::consoleErrorf("Exception %d\n", i); + } catch (...) { + konan::consoleErrorf("Unknown Exception\n"); + } + } + }); + SetKonanTerminateHandler(); + try { + ThrowException(exception.header()); + } catch (...) { + std::terminate(); + } + }, + "Hook 42\nReporting 13\n"); + }); +} + +TEST(ExceptionDeathTest, TerminateHandler_IgnoreHooks) { + kotlin::RunInNewThread([]() { + auto reportUnhandledExceptionMock = ScopedReportUnhandledExceptionMock(); + auto Kotlin_runUnhandledExceptionHookMock = ScopedKotlin_runUnhandledExceptionHookMock(); + ON_CALL(*reportUnhandledExceptionMock, Call(_)).WillByDefault([](KRef exception) { + konan::consoleErrorf("Reporting %d\n", Object::FromObjHeader(exception)->value); + }); + ON_CALL(*Kotlin_runUnhandledExceptionHookMock, Call(_)).WillByDefault([](KRef exception) { + konan::consoleErrorf("Hook %d\n", Object::FromObjHeader(exception)->value); + ThrowException(exception); + }); + EXPECT_DEATH( + { + std::set_terminate([]() { + konan::consoleErrorf("Custom terminate\n"); + if (auto exception = std::current_exception()) { + try { + std::rethrow_exception(exception); + } catch (int i) { + konan::consoleErrorf("Exception %d\n", i); + } catch (...) { + konan::consoleErrorf("Unknown Exception\n"); + } + } + }); + SetKonanTerminateHandler(); + try { + throw 3; + } catch (...) { + std::terminate(); + } + }, + "Custom terminate\nException 3\n"); + }); +} diff --git a/kotlin-native/runtime/src/main/cpp/ObjCExportErrors.mm b/kotlin-native/runtime/src/main/cpp/ObjCExportErrors.mm index e7fb2330e0f..22308f610de 100644 --- a/kotlin-native/runtime/src/main/cpp/ObjCExportErrors.mm +++ b/kotlin-native/runtime/src/main/cpp/ObjCExportErrors.mm @@ -40,7 +40,9 @@ extern "C" RUNTIME_NORETURN void Kotlin_ObjCExport_trapOnUndeclaredException(KRe "from Kotlin to Objective-C/Swift as NSError.\n" "It is considered unexpected and unhandled instead. Program will be terminated."); - TerminateWithUnhandledException(exception); + kotlin::ProcessUnhandledException(exception); + // Cannot safely continue, must terminate. + kotlin::TerminateWithUnhandledException(exception); } static char kotlinExceptionOriginChar; @@ -67,7 +69,9 @@ extern "C" id Kotlin_ObjCExport_ExceptionAsNSError(KRef exception, const TypeInf printlnMessage("Exception doesn't match @Throws-specified class list and thus isn't propagated " "from Kotlin to Objective-C/Swift as NSError.\n" "It is considered unexpected and unhandled instead. Program will be terminated."); - TerminateWithUnhandledException(exception); + kotlin::ProcessUnhandledException(exception); + // Cannot safely continue, must terminate. + kotlin::TerminateWithUnhandledException(exception); } return Kotlin_ObjCExport_WrapExceptionToNSError(exception); diff --git a/kotlin-native/runtime/src/main/cpp/ObjectTestSupport.hpp b/kotlin-native/runtime/src/main/cpp/ObjectTestSupport.hpp index 4a42459e89f..5e1841d0a9f 100644 --- a/kotlin-native/runtime/src/main/cpp/ObjectTestSupport.hpp +++ b/kotlin-native/runtime/src/main/cpp/ObjectTestSupport.hpp @@ -28,6 +28,7 @@ private: int32_t instanceSize_ = 0; KStdVector objOffsets_; int32_t flags_ = 0; + const TypeInfo* superType_ = nullptr; }; public: @@ -45,6 +46,11 @@ public: flags_ &= ~flag; return std::move(*this); } + + ObjectBuilder&& setSuperType(const TypeInfo* superType) noexcept { + superType_ = superType; + return std::move(*this); + } }; template @@ -75,6 +81,7 @@ public: typeInfo_.objOffsetsCount_ = objOffsets_.size(); } typeInfo_.flags_ = builder.flags_; + typeInfo_.superType_ = builder.superType_; } TypeInfo* typeInfo() noexcept { return &typeInfo_; } diff --git a/kotlin-native/runtime/src/main/cpp/Runtime.cpp b/kotlin-native/runtime/src/main/cpp/Runtime.cpp index 1f6833450bd..e06bc442d29 100644 --- a/kotlin-native/runtime/src/main/cpp/Runtime.cpp +++ b/kotlin-native/runtime/src/main/cpp/Runtime.cpp @@ -106,7 +106,7 @@ RuntimeState* initRuntime() { // Switch thread state because worker and globals inits require the runnable state. // This call may block if GC requested suspending threads. stateGuard = kotlin::ThreadStateGuard(result->memoryState, kotlin::ThreadState::kRunnable); - result->worker = WorkerInit(result->memoryState, true); + result->worker = WorkerInit(result->memoryState); firstRuntime = atomicAdd(&aliveRuntimesCount, 1) == 1; if (!kotlin::kSupportsMultipleMutators && !firstRuntime) { konan::consoleErrorf("This GC implementation does not support multiple mutator threads."); @@ -130,7 +130,7 @@ RuntimeState* initRuntime() { // Switch thread state because worker and globals inits require the runnable state. // This call may block if GC requested suspending threads. stateGuard = kotlin::ThreadStateGuard(result->memoryState, kotlin::ThreadState::kRunnable); - result->worker = WorkerInit(result->memoryState, true); + result->worker = WorkerInit(result->memoryState); } InitOrDeinitGlobalVariables(ALLOC_THREAD_LOCAL_GLOBALS, result->memoryState); diff --git a/kotlin-native/runtime/src/main/cpp/TestSupportCompilerGenerated.hpp b/kotlin-native/runtime/src/main/cpp/TestSupportCompilerGenerated.hpp index ef1e53df7a3..b53d0054ebf 100644 --- a/kotlin-native/runtime/src/main/cpp/TestSupportCompilerGenerated.hpp +++ b/kotlin-native/runtime/src/main/cpp/TestSupportCompilerGenerated.hpp @@ -60,3 +60,5 @@ private: ScopedStrictMockFunction ScopedCreateCleanerWorkerMock(); ScopedStrictMockFunction ScopedShutdownCleanerWorkerMock(); +ScopedStrictMockFunction ScopedReportUnhandledExceptionMock(); +ScopedStrictMockFunction ScopedKotlin_runUnhandledExceptionHookMock(); diff --git a/kotlin-native/runtime/src/main/cpp/Worker.cpp b/kotlin-native/runtime/src/main/cpp/Worker.cpp index e6ad1dca02b..f885f09ee0b 100644 --- a/kotlin-native/runtime/src/main/cpp/Worker.cpp +++ b/kotlin-native/runtime/src/main/cpp/Worker.cpp @@ -49,6 +49,25 @@ OBJ_GETTER(WorkerLaunchpad, KRef); } // extern "C" +namespace { + +enum class WorkerExceptionHandling { + kDefault, // Perform the default processing of unhandled exception. + kIgnore, // Do nothing on exception escaping job unit. + kLog, // Deprecated. +}; + +WorkerExceptionHandling workerExceptionHandling() noexcept { + switch (compiler::workerExceptionHandling()) { + case compiler::WorkerExceptionHandling::kLegacy: + return WorkerExceptionHandling::kLog; + case compiler::WorkerExceptionHandling::kUseHook: + return WorkerExceptionHandling::kDefault; + } +} + +} // namespace + #if WITH_WORKERS namespace { @@ -117,10 +136,10 @@ typedef KStdOrderedSet DelayedJobSet; class Worker { public: - Worker(KInt id, bool errorReporting, KRef customName, WorkerKind kind) + Worker(KInt id, WorkerExceptionHandling exceptionHandling, KRef customName, WorkerKind kind) : id_(id), kind_(kind), - errorReporting_(errorReporting) { + exceptionHandling_(exceptionHandling) { name_ = customName != nullptr ? CreateStablePointer(customName) : nullptr; pthread_mutex_init(&lock_, nullptr); pthread_cond_init(&cond_, nullptr); @@ -147,7 +166,7 @@ class Worker { KInt id() const { return id_; } - bool errorReporting() const { return errorReporting_; } + WorkerExceptionHandling exceptionHandling() const { return exceptionHandling_; } KNativePtr name() const { return name_; } @@ -173,7 +192,7 @@ class Worker { memoryState_ = state; } - friend Worker* WorkerInit(MemoryState* memoryState, KBoolean errorReporting); + friend Worker* WorkerInit(MemoryState* memoryState); KInt id_; WorkerKind kind_; @@ -184,8 +203,7 @@ class Worker { // Lock and condition for waiting on the queue. pthread_mutex_t lock_; pthread_cond_t cond_; - // If errors to be reported on console. - bool errorReporting_; + WorkerExceptionHandling exceptionHandling_; bool terminated_ = false; pthread_t thread_ = 0; // MemoryState for worker's thread. @@ -319,11 +337,11 @@ class State { pthread_cond_destroy(&cond_); } - Worker* addWorkerUnlocked(bool errorReporting, KRef customName, WorkerKind kind) { + Worker* addWorkerUnlocked(WorkerExceptionHandling exceptionHandling, KRef customName, WorkerKind kind) { Worker* worker = nullptr; { Locker locker(&lock_); - worker = konanConstructInstance(nextWorkerId(), errorReporting, customName, kind); + worker = konanConstructInstance(nextWorkerId(), exceptionHandling, customName, kind); if (worker == nullptr) return nullptr; workers_[worker->id()] = worker; } @@ -642,8 +660,8 @@ void Future::cancelUnlocked(MemoryState* memoryState) { // Defined in RuntimeUtils.kt. extern "C" void ReportUnhandledException(KRef e); -KInt startWorker(KBoolean errorReporting, KRef customName) { - Worker* worker = theState()->addWorkerUnlocked(errorReporting != 0, customName, WorkerKind::kNative); +KInt startWorker(WorkerExceptionHandling exceptionHandling, KRef customName) { + Worker* worker = theState()->addWorkerUnlocked(exceptionHandling, customName, WorkerKind::kNative); if (worker == nullptr) return -1; worker->startEventLoop(); return worker->id(); @@ -719,7 +737,7 @@ KNativePtr detachObjectGraphInternal(KInt transferMode, KRef producer) { #else -KInt startWorker(KBoolean errorReporting, KRef customName) { +KInt startWorker(WorkerExceptionHandling exceptionHandling, KRef customName) { ThrowWorkerUnsupported(); } @@ -787,13 +805,13 @@ KInt GetWorkerId(Worker* worker) { #endif // WITH_WORKERS } -Worker* WorkerInit(MemoryState* memoryState, KBoolean errorReporting) { +Worker* WorkerInit(MemoryState* memoryState) { #if WITH_WORKERS Worker* worker; if (::g_worker != nullptr) { worker = ::g_worker; } else { - worker = theState()->addWorkerUnlocked(errorReporting != 0, nullptr, WorkerKind::kOther); + worker = theState()->addWorkerUnlocked(workerExceptionHandling(), nullptr, WorkerKind::kOther); ::g_worker = worker; } worker->setThread(pthread_self()); @@ -1039,8 +1057,15 @@ JobKind Worker::processQueueElement(bool blocking) { #endif WorkerLaunchpad(obj, dummyHolder.slot()); } catch (ExceptionObjHolder& e) { - if (errorReporting()) - ReportUnhandledException(e.GetExceptionObject()); + switch (exceptionHandling()) { + case WorkerExceptionHandling::kIgnore: break; + case WorkerExceptionHandling::kDefault: + kotlin::ProcessUnhandledException(e.GetExceptionObject()); + break; + case WorkerExceptionHandling::kLog: + ReportUnhandledException(e.GetExceptionObject()); + break; + } } DisposeStablePointer(job.executeAfter.operation); break; @@ -1061,8 +1086,14 @@ JobKind Worker::processQueueElement(bool blocking) { result = transfer(&resultHolder, job.regularJob.transferMode); } catch (ExceptionObjHolder& e) { ok = false; - if (errorReporting()) - ReportUnhandledException(e.GetExceptionObject()); + switch (exceptionHandling()) { + case WorkerExceptionHandling::kIgnore: + break; + case WorkerExceptionHandling::kDefault: // TODO: Pass exception object into the future and do nothing in the default case. + case WorkerExceptionHandling::kLog: + ReportUnhandledException(e.GetExceptionObject()); + break; + } } // Notify the future. job.regularJob.future->storeResultUnlocked(result, ok); @@ -1079,8 +1110,8 @@ JobKind Worker::processQueueElement(bool blocking) { extern "C" { -KInt Kotlin_Worker_startInternal(KBoolean noErrorReporting, KRef customName) { - return startWorker(noErrorReporting, customName); +KInt Kotlin_Worker_startInternal(KBoolean errorReporting, KRef customName) { + return startWorker(errorReporting ? workerExceptionHandling() : WorkerExceptionHandling::kIgnore, customName); } KInt Kotlin_Worker_currentInternal() { diff --git a/kotlin-native/runtime/src/main/cpp/Worker.h b/kotlin-native/runtime/src/main/cpp/Worker.h index 963517bbea0..4de1cd615fe 100644 --- a/kotlin-native/runtime/src/main/cpp/Worker.h +++ b/kotlin-native/runtime/src/main/cpp/Worker.h @@ -8,7 +8,7 @@ class Worker; KInt GetWorkerId(Worker* worker); -Worker* WorkerInit(MemoryState* memoryState, KBoolean errorReporting); +Worker* WorkerInit(MemoryState* memoryState); void WorkerDeinit(Worker* worker); // Clean up all associated thread state, if this was a native worker. void WorkerDestroyThreadDataIfNeeded(KInt id); diff --git a/kotlin-native/runtime/src/main/kotlin/kotlin/native/Runtime.kt b/kotlin-native/runtime/src/main/kotlin/kotlin/native/Runtime.kt index cd263803f2f..2422aa3bbad 100644 --- a/kotlin-native/runtime/src/main/kotlin/kotlin/native/Runtime.kt +++ b/kotlin-native/runtime/src/main/kotlin/kotlin/native/Runtime.kt @@ -5,8 +5,11 @@ package kotlin.native import kotlin.native.concurrent.InvalidMutabilityException +import kotlin.native.internal.ExportForCppRuntime import kotlin.native.internal.GCUnsafeCall import kotlin.native.internal.UnhandledExceptionHookHolder +import kotlin.native.internal.runUnhandledExceptionHook +import kotlin.native.internal.ReportUnhandledException /** * Initializes Kotlin runtime for the current thread, if not inited already. @@ -51,8 +54,6 @@ public typealias ReportUnhandledExceptionHook = Function1 * Hook is invoked whenever there's uncaught exception reaching boundaries of the Kotlin world, * i.e. top level main(), or when Objective-C to Kotlin call not marked with @Throws throws an exception. * Hook must be a frozen lambda, so that it could be called from any thread/worker. - * Hook is invoked once, and is cleared afterwards, so that memory leak detection works as expected even - * with custom exception hooks. */ public fun setUnhandledExceptionHook(hook: ReportUnhandledExceptionHook): ReportUnhandledExceptionHook? { try { @@ -62,6 +63,38 @@ public fun setUnhandledExceptionHook(hook: ReportUnhandledExceptionHook): Report } } +/** + * Returns a user-defined uncaught exception handler set by [setUnhandledExceptionHook] or `null` if no user-defined handlers were set. + */ +@ExperimentalStdlibApi +@SinceKotlin("1.6") +public fun getUnhandledExceptionHook(): ReportUnhandledExceptionHook? { + return UnhandledExceptionHookHolder.hook.value +} + +/** + * Performs the default processing of unhandled exception. + * + * If user-defined hook set by [setUnhandledExceptionHook] is present, calls it and returns. + * If the hook is not present, calls [terminateWithUnhandledException] with [throwable]. + * If the hook fails with exception, calls [terminateWithUnhandledException] with exception from the hook. + */ +@ExperimentalStdlibApi +@SinceKotlin("1.6") +@GCUnsafeCall("Kotlin_processUnhandledException") +public external fun processUnhandledException(throwable: Throwable): Unit + +/* + * Terminates the program with the given [throwable] as an unhandled exception. + * User-defined hooks installed with [setUnhandledExceptionHook] are not invoked. + * + * `terminateWithUnhandledException` can be used to emulate an abrupt termination of the application with an uncaught exception. + */ +@ExperimentalStdlibApi +@SinceKotlin("1.6") +@GCUnsafeCall("Kotlin_terminateWithUnhandledException") +public external fun terminateWithUnhandledException(throwable: Throwable): Nothing + /** * Compute stable wrt potential object relocations by the memory manager identity hash code. * @return 0 for `null` object, identity hash code otherwise. diff --git a/kotlin-native/runtime/src/main/kotlin/kotlin/native/concurrent/Worker.kt b/kotlin-native/runtime/src/main/kotlin/kotlin/native/concurrent/Worker.kt index dd35d614c8c..a20eba64fb8 100644 --- a/kotlin-native/runtime/src/main/kotlin/kotlin/native/concurrent/Worker.kt +++ b/kotlin-native/runtime/src/main/kotlin/kotlin/native/concurrent/Worker.kt @@ -34,7 +34,7 @@ public inline class Worker @PublishedApi internal constructor(val id: Int) { * Typically new worker may be needed for computations offload to another core, for IO it may be * better to use non-blocking IO combined with more lightweight coroutines. * - * @param errorReporting controls if an uncaught exceptions in the worker will be printed out + * @param errorReporting controls if an uncaught exceptions in the worker will be reported. * @param name defines the optional name of this worker, if none - default naming is used. * @return worker object, usable across multiple concurrent contexts. */ @@ -99,6 +99,8 @@ public inline class Worker @PublishedApi internal constructor(val id: Int) { /** * Plan job for further execution in the worker. [operation] parameter must be either frozen, or execution to be * planned on the current worker. Otherwise [IllegalStateException] will be thrown. + * With -Xworker-exception-handling=use-hook, if the worker was created with `errorReporting` set to true, any exception escaping from [operation] will + * be handled by [processUnhandledException]. * * @param afterMicroseconds defines after how many microseconds delay execution shall happen, 0 means immediately, * @throws [IllegalArgumentException] on negative values of [afterMicroseconds]. diff --git a/kotlin-native/runtime/src/main/kotlin/kotlin/native/internal/ObjCExportCoroutines.kt b/kotlin-native/runtime/src/main/kotlin/kotlin/native/internal/ObjCExportCoroutines.kt index c8a6188dc5f..907a13ef096 100644 --- a/kotlin-native/runtime/src/main/kotlin/kotlin/native/internal/ObjCExportCoroutines.kt +++ b/kotlin-native/runtime/src/main/kotlin/kotlin/native/internal/ObjCExportCoroutines.kt @@ -29,10 +29,13 @@ private object EmptyCompletion : Continuation { override val context: CoroutineContext get() = EmptyCoroutineContext + @OptIn(ExperimentalStdlibApi::class) override fun resumeWith(result: Result) { val exception = result.exceptionOrNull() ?: return - TerminateWithUnhandledException(exception) - // Throwing the exception from [resumeWith] is not generally expected. + processUnhandledException(exception) + terminateWithUnhandledException(exception) + // Terminate even if unhandled exception hook has finished successfully, because + // throwing the exception from [resumeWith] is not generally expected. // Also terminating is consistent with other pieces of ObjCExport machinery. } } @@ -63,4 +66,4 @@ private external fun runCompletionSuccess(completionHolder: Any, result: Any?) @FilterExceptions @SymbolName("Kotlin_ObjCExport_runCompletionFailure") -private external fun runCompletionFailure(completionHolder: Any, exception: Throwable, exceptionTypes: NativePtr) \ No newline at end of file +private external fun runCompletionFailure(completionHolder: Any, exception: Throwable, exceptionTypes: NativePtr) diff --git a/kotlin-native/runtime/src/main/kotlin/kotlin/native/internal/RuntimeUtils.kt b/kotlin-native/runtime/src/main/kotlin/kotlin/native/internal/RuntimeUtils.kt index ebe401ea784..fa808818aca 100644 --- a/kotlin-native/runtime/src/main/kotlin/kotlin/native/internal/RuntimeUtils.kt +++ b/kotlin-native/runtime/src/main/kotlin/kotlin/native/internal/RuntimeUtils.kt @@ -123,9 +123,6 @@ internal fun ReportUnhandledException(throwable: Throwable) { throwable.printStackTrace() } -@GCUnsafeCall("TerminateWithUnhandledException") -internal external fun TerminateWithUnhandledException(throwable: Throwable) - // Using object to make sure that `hook` is initialized when it's needed instead of // in a normal global initialization flow. This is important if some global happens // to throw an exception during it's initialization before this hook would've been initialized. @@ -138,10 +135,11 @@ internal object UnhandledExceptionHookHolder { } } +// TODO: Can be removed only when native-mt coroutines stop using it. @PublishedApi @ExportForCppRuntime internal fun OnUnhandledException(throwable: Throwable) { - val handler = UnhandledExceptionHookHolder.hook.swap(null) + val handler = UnhandledExceptionHookHolder.hook.value if (handler == null) { ReportUnhandledException(throwable); return @@ -153,6 +151,12 @@ internal fun OnUnhandledException(throwable: Throwable) { } } +@ExportForCppRuntime("Kotlin_runUnhandledExceptionHook") +internal fun runUnhandledExceptionHook(throwable: Throwable) { + val handler = UnhandledExceptionHookHolder.hook.value ?: throw throwable + handler(throwable) +} + @ExportForCppRuntime internal fun TheEmptyString() = "" diff --git a/kotlin-native/runtime/src/test_support/cpp/CompilerGenerated.cpp b/kotlin-native/runtime/src/test_support/cpp/CompilerGenerated.cpp index 925373baea2..21f0f92f8d5 100644 --- a/kotlin-native/runtime/src/test_support/cpp/CompilerGenerated.cpp +++ b/kotlin-native/runtime/src/test_support/cpp/CompilerGenerated.cpp @@ -51,6 +51,8 @@ struct KBox { testing::StrictMock>* createCleanerWorkerMock = nullptr; testing::StrictMock>* shutdownCleanerWorkerMock = nullptr; +testing::StrictMock>* reportUnhandledExceptionMock = nullptr; +testing::StrictMock>* Kotlin_runUnhandledExceptionHookMock = nullptr; } // namespace @@ -190,15 +192,19 @@ void RUNTIME_NORETURN ThrowFreezingException(KRef toFreeze, KRef blocker) { } void ReportUnhandledException(KRef throwable) { - konan::consolePrintf("Uncaught Kotlin exception."); + if (!reportUnhandledExceptionMock) throw std::runtime_error("Not implemented for tests"); + + return reportUnhandledExceptionMock->Call(throwable); } RUNTIME_NORETURN OBJ_GETTER(DescribeObjectForDebugging, KConstNativePtr typeInfo, KConstNativePtr address) { throw std::runtime_error("Not implemented for tests"); } -void OnUnhandledException(KRef throwable) { - throw std::runtime_error("Not implemented for tests"); +void Kotlin_runUnhandledExceptionHook(KRef throwable) { + if (!Kotlin_runUnhandledExceptionHookMock) throw std::runtime_error("Not implemented for tests"); + + return Kotlin_runUnhandledExceptionHookMock->Call(throwable); } void Kotlin_WorkerBoundReference_freezeHook(KRef thiz) { @@ -285,3 +291,11 @@ ScopedStrictMockFunction ScopedCreateCleanerWorkerMock() { ScopedStrictMockFunction ScopedShutdownCleanerWorkerMock() { return ScopedStrictMockFunction(&shutdownCleanerWorkerMock); } + +ScopedStrictMockFunction ScopedReportUnhandledExceptionMock() { + return ScopedStrictMockFunction(&reportUnhandledExceptionMock); +} + +ScopedStrictMockFunction ScopedKotlin_runUnhandledExceptionHookMock() { + return ScopedStrictMockFunction(&Kotlin_runUnhandledExceptionHookMock); +}