[K/N] Refactor unhandled exception handling API

* Do not reset unhandled exception hook
* Add processUnhandledException to perform default unhandled exception
  processing
* Add terminateWithUnhandledException to report the unhandled exception
  and terminate the program
* Use the default unhandled exception processing in entrypoint, interop
  boundaries and in Worker.executeAfter
* Add -Xworker-exception-handling to control exception processing of
  Worker.executeAfter. By default its the old behaviour with the old MM,
  and new behaviour with the new MM.
This commit is contained in:
Alexander Shabalin
2021-07-15 12:29:56 +03:00
committed by Space
parent 766857881a
commit 7e04bb4bf1
41 changed files with 983 additions and 71 deletions

View File

@@ -329,6 +329,15 @@ class K2Native : CLICompiler<K2NativeCompilerArguments>() {
}
})
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
}
})
}
}
}

View File

@@ -320,6 +320,14 @@ class K2NativeCompilerArguments : CommonCompilerArguments() {
@Argument(value="-Xruntime-asserts-mode", valueDescription = "<mode>", 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 = "<mode>",
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<AnalysisFlag<*>, Any> =
super.configureAnalysisFlags(collector, languageVersion).also {
val useExperimental = it[AnalysisFlags.useExperimental] as List<*>

View File

@@ -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 <exception>")
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*);

View File

@@ -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))
}
}
}

View File

@@ -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

View File

@@ -164,6 +164,7 @@ class KonanConfigKeys {
val RUNTIME_ASSERTS_MODE: CompilerConfigurationKey<RuntimeAssertsMode> = CompilerConfigurationKey.create("enable runtime asserts")
val PROPERTY_LAZY_INITIALIZATION: CompilerConfigurationKey<Boolean>
= CompilerConfigurationKey.create("lazy top level properties initialization")
val WORKER_EXCEPTION_HANDLING: CompilerConfigurationKey<WorkerExceptionHandling> = CompilerConfigurationKey.create("unhandled exception processing in Worker.executeAfter")
}
}

View File

@@ -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),
}

View File

@@ -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)

View File

@@ -2631,6 +2631,7 @@ internal class CodeGeneratorVisitor(val context: Context, val lifetimes: Map<IrE
overrideRuntimeGlobal("Kotlin_destroyRuntimeMode", Int32(context.config.destroyRuntimeMode.value))
overrideRuntimeGlobal("Kotlin_gcAggressive", Int32(if (context.config.gcAggressive) 1 else 0))
overrideRuntimeGlobal("Kotlin_workerExceptionHandling", Int32(context.config.workerExceptionHandling.value))
}
//-------------------------------------------------------------------------//

View File

@@ -1008,6 +1008,98 @@ task worker_exception_messages(type: KonanLocalTest) {
source = "runtime/workers/worker_exception_messages.kt"
}
standaloneTest("worker_exceptions") {
flags = ["-tr", "-Xworker-exception-handling=use-hook"]
enabled = (project.testTarget != 'wasm32') // Workers need pthreads.
outputChecker = {
!it.contains("testExecuteAfterStartQuiet error") && it.contains("testExecuteStart error") && !it.contains("testExecuteStartQuiet error")
}
source = "runtime/workers/worker_exceptions.kt"
}
standaloneTest("worker_exceptions_legacy") {
flags = ["-tr", "-Xworker-exception-handling=legacy"]
enabled = (project.testTarget != 'wasm32') // Workers need pthreads.
outputChecker = {
it.contains("testExecuteAfterStartLegacy error") && it.contains("testExecuteStartLegacy error")
}
source = "runtime/workers/worker_exceptions_legacy.kt"
}
standaloneTest("worker_exceptions_terminate") {
flags = ["-Xworker-exception-handling=use-hook"]
enabled = (project.testTarget != 'wasm32') // Workers need pthreads.
expectedExitStatusChecker = { it != 0 }
outputChecker = {
it.contains("some error") && !it.contains("Will not happen")
}
source = "runtime/workers/worker_exceptions_terminate.kt"
}
standaloneTest("worker_exceptions_terminate_legacy") {
flags = ["-Xworker-exception-handling=legacy"]
enabled = (project.testTarget != 'wasm32') // Workers need pthreads.
outputChecker = {
it.contains("some error") && it.contains("Will not happen")
}
source = "runtime/workers/worker_exceptions_terminate.kt"
}
standaloneTest("worker_exceptions_terminate_hook") {
flags = ["-Xworker-exception-handling=use-hook"]
enabled = (project.testTarget != 'wasm32') // Workers need pthreads.
outputChecker = {
it.contains("hook called") && !it.contains("some error") && it.contains("Will happen")
}
source = "runtime/workers/worker_exceptions_terminate_hook.kt"
}
standaloneTest("worker_exceptions_terminate_hook_legacy") {
flags = ["-Xworker-exception-handling=legacy"]
enabled = (project.testTarget != 'wasm32') // Workers need pthreads.
outputChecker = {
!it.contains("hook called") && it.contains("some error") && it.contains("Will happen")
}
source = "runtime/workers/worker_exceptions_terminate_hook.kt"
}
standaloneTest("worker_exceptions_terminate_current") {
flags = ["-Xworker-exception-handling=use-hook"]
enabled = (project.testTarget != 'wasm32') // Workers need pthreads.
expectedExitStatusChecker = { it != 0 }
outputChecker = {
it.contains("some error") && !it.contains("Will not happen")
}
source = "runtime/workers/worker_exceptions_terminate_current.kt"
}
standaloneTest("worker_exceptions_terminate_current_legacy") {
flags = ["-Xworker-exception-handling=legacy"]
enabled = (project.testTarget != 'wasm32') // Workers need pthreads.
outputChecker = {
it.contains("some error") && it.contains("Will not happen")
}
source = "runtime/workers/worker_exceptions_terminate_current.kt"
}
standaloneTest("worker_exceptions_terminate_hook_current") {
flags = ["-Xworker-exception-handling=use-hook"]
enabled = (project.testTarget != 'wasm32') // Workers need pthreads.
outputChecker = {
it.contains("hook called") && !it.contains("some error") && it.contains("Will happen")
}
source = "runtime/workers/worker_exceptions_terminate_hook_current.kt"
}
standaloneTest("worker_exceptions_terminate_hook_current_legacy") {
flags = ["-Xworker-exception-handling=legacy"]
enabled = (project.testTarget != 'wasm32') // Workers need pthreads.
outputChecker = {
!it.contains("hook called") && it.contains("some error") && it.contains("Will happen")
}
source = "runtime/workers/worker_exceptions_terminate_hook_current.kt"
}
standaloneTest("worker_threadlocal_no_leak") {
disabled = (project.testTarget == 'wasm32') // Needs pthreads.
source = "runtime/workers/worker_threadlocal_no_leak.kt"
@@ -2548,11 +2640,68 @@ standaloneTest("kt-37572") {
standaloneTest("custom_hook") {
enabled = (project.testTarget != 'wasm32') // Uses exceptions.
goldValue = "value 42: Error\n"
expectedExitStatus = 1
outputChecker = {
it.contains("value 42: Error") && it.contains("Uncaught Kotlin exception: kotlin.Error: an error")
}
expectedExitStatusChecker = { it != 0 }
source = "runtime/exceptions/custom_hook.kt"
}
standaloneTest("custom_hook_memory_leak") {
enabled = (project.testTarget != 'wasm32') // Uses exceptions.
outputChecker = {
it.contains("Hook 42") && it.contains("Uncaught Kotlin exception: kotlin.Error: an error")
}
expectedExitStatusChecker = { it != 0 }
source = "runtime/exceptions/custom_hook_memory_leak.kt"
}
standaloneTest("custom_hook_get") {
source = "runtime/exceptions/custom_hook_get.kt"
}
standaloneTest("custom_hook_no_reset") {
enabled = (project.testTarget != 'wasm32') // Uses exceptions.
outputChecker = {
it.contains("Hook called") && it.contains("Uncaught Kotlin exception: kotlin.Error: some error")
}
expectedExitStatusChecker = { it != 0 }
source = "runtime/exceptions/custom_hook_no_reset.kt"
}
standaloneTest("custom_hook_throws") {
enabled = (project.testTarget != 'wasm32') // Uses exceptions.
outputChecker = {
it.contains("Hook called") && it.contains("Uncaught Kotlin exception: kotlin.Error: another error") && !it.contains("some error")
}
expectedExitStatusChecker = { it != 0 }
source = "runtime/exceptions/custom_hook_throws.kt"
}
standaloneTest("custom_hook_unhandled_exception") {
enabled = (project.testTarget != 'wasm32') // Uses exceptions.
goldValue = ""
source = "runtime/exceptions/custom_hook_unhandled_exception.kt"
}
standaloneTest("custom_hook_terminate") {
enabled = (project.testTarget != 'wasm32') // Uses exceptions.
outputChecker = {
it.contains("Hook called")
}
expectedExitStatusChecker = { it != 0 }
source = "runtime/exceptions/custom_hook_terminate.kt"
}
standaloneTest("custom_hook_terminate_unhandled_exception") {
enabled = (project.testTarget != 'wasm32') // Uses exceptions.
outputChecker = {
it.contains("Hook called") && it.contains("Uncaught Kotlin exception: kotlin.Error: some error") && !it.contains("Not going to happen")
}
expectedExitStatusChecker = { it != 0 }
source = "runtime/exceptions/custom_hook_terminate_unhandled_exception.kt"
}
standaloneTest("exception_in_global_init") {
enabled = (project.testTarget != 'wasm32') // Uses exceptions.
source = "runtime/exceptions/exception_in_global_init.kt"
@@ -2570,6 +2719,21 @@ task throw_from_catch(type: KonanLocalTest) {
source = "runtime/exceptions/throw_from_catch.kt"
}
standaloneTest("terminate") {
enabled = (project.testTarget != 'wasm32') // Uses exceptions.
expectedExitStatusChecker = { it != 0 }
source = "runtime/exceptions/terminate.kt"
}
standaloneTest("unhandled_exception") {
enabled = (project.testTarget != 'wasm32') // Uses exceptions.
outputChecker = {
it.contains("Uncaught Kotlin exception: kotlin.Error: some error")
}
expectedExitStatusChecker = { it != 0 }
source = "runtime/exceptions/unhandled_exception.kt"
}
standaloneTest("runtime_math_exceptions") {
enabled = (project.testTarget != 'wasm32')
source = "stdlib_external/numbers/MathExceptionTest.kt"
@@ -2789,7 +2953,7 @@ task runtime_basic_standard(type: KonanLocalTest) {
}
standaloneTest("runtime_basic_assert_failed") {
expectedExitStatus = 1
expectedExitStatusChecker = { it != 0 }
source = "runtime/basic/assert_failed.kt"
}

View File

@@ -0,0 +1,21 @@
/*
* 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 exceptionHook = { _: Throwable ->
println("Hook")
}.freeze()
val oldHook = setUnhandledExceptionHook(exceptionHook)
assertNull(oldHook)
val hook1 = getUnhandledExceptionHook()
assertEquals(exceptionHook, hook1)
val hook2 = getUnhandledExceptionHook()
}

View File

@@ -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")
}

View File

@@ -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")
}

View File

@@ -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")
}

View File

@@ -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")
}

View File

@@ -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")
}

View File

@@ -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)
}

View File

@@ -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)
}

View File

@@ -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)
}

View File

@@ -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<Throwable> {
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<Throwable> {
future.result
}
worker.requestTermination().result
}

View File

@@ -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<Throwable> {
future.result
}
worker.requestTermination().result
}

View File

@@ -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")
}

View File

@@ -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")
}

View File

@@ -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")
}

View File

@@ -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")
}

View File

@@ -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<compiler::DestroyRuntimeMode>(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<compiler::WorkerExceptionHandling>(Kotlin_workerExceptionHandling);
}

View File

@@ -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<RuntimeAssertsMode>(Kotlin_runtimeAssertsMode);
}
WorkerExceptionHandling workerExceptionHandling() noexcept;
} // namespace compiler
} // namespace kotlin

View File

@@ -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<ExceptionObjHolder*>(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);
}

View File

@@ -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

View File

@@ -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<Field, 0> kFields{};
};
using Object = test_support::Object<Payload>;
} // namespace
TEST(ExceptionTest, ProcessUnhandledException_WithHook) {
test_support::TypeInfoHolder typeHolder{test_support::TypeInfoHolder::ObjectBuilder<Payload>().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<Payload>().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<Payload>().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<Payload>().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<Payload>().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<Payload>().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<Payload>().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<Payload>().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");
});
}

View File

@@ -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);

View File

@@ -28,6 +28,7 @@ private:
int32_t instanceSize_ = 0;
KStdVector<int32_t> 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 <typename Payload>
@@ -75,6 +81,7 @@ public:
typeInfo_.objOffsetsCount_ = objOffsets_.size();
}
typeInfo_.flags_ = builder.flags_;
typeInfo_.superType_ = builder.superType_;
}
TypeInfo* typeInfo() noexcept { return &typeInfo_; }

View File

@@ -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);

View File

@@ -60,3 +60,5 @@ private:
ScopedStrictMockFunction<KInt()> ScopedCreateCleanerWorkerMock();
ScopedStrictMockFunction<void(KInt, bool)> ScopedShutdownCleanerWorkerMock();
ScopedStrictMockFunction<void(KRef)> ScopedReportUnhandledExceptionMock();
ScopedStrictMockFunction<void(KRef)> ScopedKotlin_runUnhandledExceptionHookMock();

View File

@@ -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<Job, JobCompare> 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<Worker>(nextWorkerId(), errorReporting, customName, kind);
worker = konanConstructInstance<Worker>(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() {

View File

@@ -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);

View File

@@ -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<Throwable, Unit>
* 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.

View File

@@ -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].

View File

@@ -29,10 +29,13 @@ private object EmptyCompletion : Continuation<Any?> {
override val context: CoroutineContext
get() = EmptyCoroutineContext
@OptIn(ExperimentalStdlibApi::class)
override fun resumeWith(result: Result<Any?>) {
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)
private external fun runCompletionFailure(completionHolder: Any, exception: Throwable, exceptionTypes: NativePtr)

View File

@@ -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() = ""

View File

@@ -51,6 +51,8 @@ struct KBox {
testing::StrictMock<testing::MockFunction<KInt()>>* createCleanerWorkerMock = nullptr;
testing::StrictMock<testing::MockFunction<void(KInt, bool)>>* shutdownCleanerWorkerMock = nullptr;
testing::StrictMock<testing::MockFunction<void(KRef)>>* reportUnhandledExceptionMock = nullptr;
testing::StrictMock<testing::MockFunction<void(KRef)>>* 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<KInt()> ScopedCreateCleanerWorkerMock() {
ScopedStrictMockFunction<void(KInt, bool)> ScopedShutdownCleanerWorkerMock() {
return ScopedStrictMockFunction<void(KInt, bool)>(&shutdownCleanerWorkerMock);
}
ScopedStrictMockFunction<void(KRef)> ScopedReportUnhandledExceptionMock() {
return ScopedStrictMockFunction<void(KRef)>(&reportUnhandledExceptionMock);
}
ScopedStrictMockFunction<void(KRef)> ScopedKotlin_runUnhandledExceptionHookMock() {
return ScopedStrictMockFunction<void(KRef)>(&Kotlin_runUnhandledExceptionHookMock);
}