diff --git a/backend.native/tests/build.gradle b/backend.native/tests/build.gradle index bf8a5eaac8a..3e4753d7fae 100644 --- a/backend.native/tests/build.gradle +++ b/backend.native/tests/build.gradle @@ -2036,6 +2036,13 @@ task extend_exception(type: RunKonanTest) { source = "runtime/exceptions/extend0.kt" } +task custom_hook(type: RunStandaloneKonanTest) { + disabled = (project.testTarget == 'wasm32') // Uses exceptions. + goldValue = "value 42: Error\n" + expectedExitStatus = 1 + source = "runtime/exceptions/custom_hook.kt" +} + task runtime_math_exceptions(type: RunKonanTest) { disabled = (project.testTarget == 'wasm32') source = "stdlib_external/numbers/MathExceptionTest.kt" diff --git a/backend.native/tests/runtime/exceptions/custom_hook.kt b/backend.native/tests/runtime/exceptions/custom_hook.kt new file mode 100644 index 00000000000..edb2c682a6e --- /dev/null +++ b/backend.native/tests/runtime/exceptions/custom_hook.kt @@ -0,0 +1,22 @@ +/* + * Copyright 2010-2018 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 main(args : Array) { + assertFailsWith { + setUnhandledExceptionHook { _ -> println("wrong") } + } + + val x = 42 + val old = setUnhandledExceptionHook({ + throwable: Throwable -> println("value $x: ${throwable::class.simpleName}") + }.freeze()) + + assertNull(old) + + throw Error("an error") +} \ No newline at end of file diff --git a/runtime/src/launcher/kotlin/konan/start.kt b/runtime/src/launcher/kotlin/konan/start.kt index 658e168dfeb..e7dc25f19c8 100644 --- a/runtime/src/launcher/kotlin/konan/start.kt +++ b/runtime/src/launcher/kotlin/konan/start.kt @@ -12,18 +12,17 @@ import kotlin.native.internal.ExportForCppRuntime @SymbolName("EntryPointSelector") external fun EntryPointSelector(args: Array) +@SymbolName("OnUnhandledException") +external private fun OnUnhandledException(throwable: Throwable) + @ExportForCppRuntime private fun Konan_start(args: Array): Int { try { EntryPointSelector(args) - // Successfully finished: return 0 - } catch (e: Throwable) { - // TODO: may be add some more info. - print("Uncaught exception from Kotlin's main: ") - e.printStackTrace() + OnUnhandledException(e) return 1 } } diff --git a/runtime/src/main/cpp/Exceptions.cpp b/runtime/src/main/cpp/Exceptions.cpp index 14508b41e41..ad7911026dc 100644 --- a/runtime/src/main/cpp/Exceptions.cpp +++ b/runtime/src/main/cpp/Exceptions.cpp @@ -58,6 +58,13 @@ class AutoFree { } }; +// RuntimeUtiks.kt +extern "C" void ReportUnhandledException(KRef throwable); +extern "C" void ExceptionReporterLaunchpad(KRef reporter, KRef throwable); + +KRef currentUnhandledExceptionHook = nullptr; +int32_t currentUnhandledExceptionHookLock = 0; + #if USE_GCC_UNWIND struct Backtrace { Backtrace(int count, int skip) : index(0), skipCount(skip) { @@ -206,10 +213,24 @@ void ThrowException(KRef exception) { #endif } -void ReportUnhandledException(KRef e); +OBJ_GETTER(Kotlin_setUnhandledExceptionHook, KRef hook) { + RETURN_RESULT_OF(SwapRefLocked, + ¤tUnhandledExceptionHook, currentUnhandledExceptionHook, hook, ¤tUnhandledExceptionHookLock); +} -RUNTIME_NORETURN void TerminateWithUnhandledException(KRef e) { - ReportUnhandledException(e); +void OnUnhandledException(KRef throwable) { + ObjHolder handlerHolder; + auto* handler = SwapRefLocked(¤tUnhandledExceptionHook, currentUnhandledExceptionHook, nullptr, + ¤tUnhandledExceptionHookLock, handlerHolder.slot()); + if (handler == nullptr) { + ReportUnhandledException(throwable); + } else { + ExceptionReporterLaunchpad(handler, throwable); + } +} + +RUNTIME_NORETURN void TerminateWithUnhandledException(KRef throwable) { + OnUnhandledException(throwable); konan::abort(); } diff --git a/runtime/src/main/kotlin/kotlin/native/Runtime.kt b/runtime/src/main/kotlin/kotlin/native/Runtime.kt index edbd46a7d42..f8873c02da4 100644 --- a/runtime/src/main/kotlin/kotlin/native/Runtime.kt +++ b/runtime/src/main/kotlin/kotlin/native/Runtime.kt @@ -4,6 +4,9 @@ */ package kotlin.native +import kotlin.native.concurrent.isFrozen +import kotlin.native.concurrent.InvalidMutabilityException + /** * Initializes Kotlin runtime for the current thread, if not inited already. */ @@ -23,4 +26,27 @@ public class IncorrectDereferenceException : RuntimeException { constructor() : super() constructor(message: String) : super(message) -} \ No newline at end of file +} + +/** + * Typealias describing custom exception reporting hook. + */ +public typealias ReportUnhandledExceptionHook = Function1 + +/** + * Install custom unhandled exception hook. Returns old hook, or null if it was not specified. + * 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? { + if (!hook.isFrozen) { + throw InvalidMutabilityException("Unhandled exception hook must be frozen") + } + return setUnhandledExceptionHook0(hook) +} + +@SymbolName("Kotlin_setUnhandledExceptionHook") +external private fun setUnhandledExceptionHook0(hook: ReportUnhandledExceptionHook): ReportUnhandledExceptionHook? \ No newline at end of file diff --git a/runtime/src/main/kotlin/kotlin/native/internal/RuntimeUtils.kt b/runtime/src/main/kotlin/kotlin/native/internal/RuntimeUtils.kt index 79d383128ed..ba6be49d0ad 100644 --- a/runtime/src/main/kotlin/kotlin/native/internal/RuntimeUtils.kt +++ b/runtime/src/main/kotlin/kotlin/native/internal/RuntimeUtils.kt @@ -86,9 +86,18 @@ internal fun PrintThrowable(throwable: Throwable) { } @ExportForCppRuntime -internal fun ReportUnhandledException(e: Throwable) { +internal fun ReportUnhandledException(throwable: Throwable) { print("Uncaught Kotlin exception: ") - e.printStackTrace() + throwable.printStackTrace() +} + +@ExportForCppRuntime +internal fun ExceptionReporterLaunchpad(reporter: (Throwable) -> Unit, throwable: Throwable) { + try { + reporter(throwable) + } catch (t: Throwable) { + ReportUnhandledException(t) + } } @ExportForCppRuntime