diff --git a/kotlin-native/runtime/src/main/cpp/Natives.cpp b/kotlin-native/runtime/src/main/cpp/Natives.cpp index 22738ced0ec..44caaf60e3b 100644 --- a/kotlin-native/runtime/src/main/cpp/Natives.cpp +++ b/kotlin-native/runtime/src/main/cpp/Natives.cpp @@ -22,6 +22,7 @@ #include #include "KAssert.h" +#include "KString.h" #include "StackTrace.hpp" #include "Memory.h" #include "Natives.h" @@ -40,8 +41,37 @@ KInt Kotlin_Any_hashCode(KConstRef thiz) { return reinterpret_cast(thiz); } +NO_INLINE OBJ_GETTER0(Kotlin_getCurrentStackTrace) { + KStdVector stackTrace; + { + // Don't use `kotlin::CallWithThreadState` to avoid messing up callstack. + kotlin::ThreadStateGuard guard(kotlin::ThreadState::kNative); + // Skip this function and primary `Throwable` constructor. + stackTrace = kotlin::GetCurrentStackTrace(2); + } + + ObjHolder resultHolder; + ObjHeader* result = AllocArrayInstance(theNativePtrArrayTypeInfo, stackTrace.size(), resultHolder.slot()); + for (size_t index = 0; index < stackTrace.size(); ++index) { + Kotlin_NativePtrArray_set(result, index, stackTrace[index]); + } + RETURN_OBJ(result); +} + OBJ_GETTER(Kotlin_getStackTraceStrings, KConstRef stackTrace) { - RETURN_RESULT_OF(kotlin::GetStackTraceStrings, stackTrace); + const KNativePtr* array = PrimitiveArrayAddressOfElementAt(stackTrace->array(), 0); + size_t size = stackTrace->array()->count_; + auto stackTraceStrings = kotlin::CallWithThreadState(kotlin::GetStackTraceStrings, array, size); + ObjHolder resultHolder; + ObjHeader* strings = AllocArrayInstance(theArrayTypeInfo, stackTraceStrings.size(), resultHolder.slot()); + + for (size_t index = 0; index < stackTraceStrings.size(); ++index) { + ObjHolder holder; + CreateStringFromCString(stackTraceStrings[index].c_str(), holder.slot()); + UpdateHeapRef(ArrayAddressOfElementAt(strings->array(), index), holder.obj()); + } + + RETURN_OBJ(strings); } // TODO: consider handling it with compiler magic instead. diff --git a/kotlin-native/runtime/src/main/cpp/StackTrace.cpp b/kotlin-native/runtime/src/main/cpp/StackTrace.cpp index a24b36c6d2f..f09aecbc15a 100644 --- a/kotlin-native/runtime/src/main/cpp/StackTrace.cpp +++ b/kotlin-native/runtime/src/main/cpp/StackTrace.cpp @@ -17,10 +17,9 @@ #include "Common.h" #include "ExecFormat.h" -#include "Memory.h" -#include "KString.h" -#include "Natives.h" +#include "Porting.h" #include "SourceInfo.h" +#include "Types.h" #include "utf8.h" @@ -30,23 +29,18 @@ namespace { #if USE_GCC_UNWIND struct Backtrace { - Backtrace(int count, int skip) : index(0), skipCount(skip) { + Backtrace(int count, int skip) : skipCount(skip) { uint32_t size = count - skipCount; if (size < 0) { size = 0; } - auto result = AllocArrayInstance(theNativePtrArrayTypeInfo, size, arrayHolder.slot()); - // TODO: throw cached OOME? - RuntimeCheck(result != nullptr, "Cannot create backtrace array"); + array.reserve(size); } - void setNextElement(_Unwind_Ptr element) { Kotlin_NativePtrArray_set(obj(), index++, (KNativePtr)element); } + void setNextElement(_Unwind_Ptr element) { array.push_back(reinterpret_cast(element)); } - ObjHeader* obj() { return arrayHolder.obj(); } - - int index; int skipCount; - ObjHolder arrayHolder; + KStdVector array; }; _Unwind_Reason_Code depthCountCallback(struct _Unwind_Context* context, void* arg) { @@ -67,9 +61,6 @@ _Unwind_Reason_Code unwindCallback(struct _Unwind_Context* context, void* arg) { #else _Unwind_Ptr address = _Unwind_GetIP(context); #endif - // We run the unwinding process in the native thread state. But setting a next element - // requires writing to a Kotlin array which must be performed in the runnable thread state. - kotlin::ThreadStateGuard guard(kotlin::ThreadState::kRunnable); backtrace->setNextElement(address); return _URC_NO_REASON; @@ -79,9 +70,8 @@ _Unwind_Reason_Code unwindCallback(struct _Unwind_Context* context, void* arg) { THREAD_LOCAL_VARIABLE bool disallowSourceInfo = false; #if !KONAN_NO_BACKTRACE && !USE_GCC_UNWIND -SourceInfo getSourceInfo(KConstRef stackTrace, int32_t index) { - return disallowSourceInfo ? SourceInfo{.fileName = nullptr, .lineNumber = -1, .column = -1} - : Kotlin_getSourceInfo(*PrimitiveArrayAddressOfElementAt(stackTrace->array(), index)); +SourceInfo getSourceInfo(void* symbol) { + return disallowSourceInfo ? SourceInfo{.fileName = nullptr, .lineNumber = -1, .column = -1} : Kotlin_getSourceInfo(symbol); } #endif @@ -89,70 +79,65 @@ SourceInfo getSourceInfo(KConstRef stackTrace, int32_t index) { // TODO: this implementation is just a hack, e.g. the result is inexact; // however it is better to have an inexact stacktrace than not to have any. -extern "C" NO_INLINE OBJ_GETTER0(Kotlin_getCurrentStackTrace) { +NO_INLINE KStdVector kotlin::GetCurrentStackTrace(int extraSkipFrames) noexcept { #if KONAN_NO_BACKTRACE - return AllocArrayInstance(theNativePtrArrayTypeInfo, 0, OBJ_RESULT); + return {}; #else - // Skips first 2 elements as irrelevant: this function and primary Throwable constructor. - constexpr int kSkipFrames = 2; + // Skips this function frame + anything asked by the caller. + const int kSkipFrames = 1 + extraSkipFrames; #if USE_GCC_UNWIND int depth = 0; - CallWithThreadState(_Unwind_Backtrace, depthCountCallback, static_cast(&depth)); + _Unwind_Backtrace(depthCountCallback, static_cast(&depth)); Backtrace result(depth, kSkipFrames); - if (result.obj()->array()->count_ > 0) { - CallWithThreadState(_Unwind_Backtrace, unwindCallback, static_cast(&result)); + if (result.array.capacity() > 0) { + _Unwind_Backtrace(unwindCallback, static_cast(&result)); } - RETURN_OBJ(result.obj()); + return std::move(result.array); #else const int maxSize = 32; void* buffer[maxSize]; - int size = kotlin::CallWithThreadState(backtrace, buffer, maxSize); - if (size < kSkipFrames) return AllocArrayInstance(theNativePtrArrayTypeInfo, 0, OBJ_RESULT); + int size = backtrace(buffer, maxSize); + if (size < kSkipFrames) return {}; - ObjHolder resultHolder; - ObjHeader* result = AllocArrayInstance(theNativePtrArrayTypeInfo, size - kSkipFrames, resultHolder.slot()); + KStdVector result; + result.reserve(size - kSkipFrames); for (int index = kSkipFrames; index < size; ++index) { - Kotlin_NativePtrArray_set(result, index - kSkipFrames, buffer[index]); + result.push_back(buffer[index]); } - RETURN_OBJ(result); + return result; #endif #endif // !KONAN_NO_BACKTRACE } -OBJ_GETTER(kotlin::GetStackTraceStrings, KConstRef stackTrace) { +KStdVector kotlin::GetStackTraceStrings(void* const* stackTrace, size_t stackTraceSize) noexcept { #if KONAN_NO_BACKTRACE - ObjHeader* result = AllocArrayInstance(theArrayTypeInfo, 1, OBJ_RESULT); - ObjHolder holder; - CreateStringFromCString("", holder.slot()); - UpdateHeapRef(ArrayAddressOfElementAt(result->array(), 0), holder.obj()); - return result; + KStdVector strings; + strings.push_back(""); + return strings; #else - int32_t size = static_cast(stackTrace->array()->count_); - ObjHolder resultHolder; - ObjHeader* strings = AllocArrayInstance(theArrayTypeInfo, size, resultHolder.slot()); + KStdVector strings; + strings.reserve(stackTraceSize); #if USE_GCC_UNWIND - for (int32_t index = 0; index < size; ++index) { - KNativePtr address = Kotlin_NativePtrArray_get(stackTrace, index); + for (size_t index = 0; index < stackTraceSize; ++index) { + KNativePtr address = stackTrace[index]; char symbol[512]; - if (!CallWithThreadState(AddressToSymbol, (const void*)address, symbol, sizeof(symbol))) { + if (!AddressToSymbol(address, symbol, sizeof(symbol))) { // Make empty string: symbol[0] = '\0'; } char line[512]; konan::snprintf(line, sizeof(line) - 1, "%s (%p)", symbol, (void*)(intptr_t)address); - ObjHolder holder; - CreateStringFromCString(line, holder.slot()); - UpdateHeapRef(ArrayAddressOfElementAt(strings->array(), index), holder.obj()); + strings.push_back(line); } #else - if (size > 0) { - char** symbols = CallWithThreadState( - backtrace_symbols, PrimitiveArrayAddressOfElementAt(stackTrace->array(), 0), size); + if (stackTraceSize > 0) { + char** symbols = backtrace_symbols(stackTrace, static_cast(stackTraceSize)); RuntimeCheck(symbols != nullptr, "Not enough memory to retrieve the stacktrace"); - for (int32_t index = 0; index < size; ++index) { - auto sourceInfo = CallWithThreadState(getSourceInfo, stackTrace, index); + for (size_t index = 0; index < stackTraceSize; ++index) { + KNativePtr address = stackTrace[index]; + auto sourceInfo = getSourceInfo(address); const char* symbol = symbols[index]; const char* result; char line[1024]; @@ -167,15 +152,13 @@ OBJ_GETTER(kotlin::GetStackTraceStrings, KConstRef stackTrace) { } else { result = symbol; } - ObjHolder holder; - CreateStringFromCString(result, holder.slot()); - UpdateHeapRef(ArrayAddressOfElementAt(strings->array(), index), holder.obj()); + strings.push_back(result); } // Not konan::free. Used to free memory allocated in backtrace_symbols where malloc is used. free(symbols); } #endif - RETURN_OBJ(strings); + return strings; #endif // !KONAN_NO_BACKTRACE } @@ -183,23 +166,23 @@ void kotlin::DisallowSourceInfo() { disallowSourceInfo = true; } -void kotlin::PrintStackTraceStderr() { +NO_INLINE void kotlin::PrintStackTraceStderr() { + // NOTE: This might be called from both runnable and native states (including in uninitialized runtime) // TODO: This is intended for runtime use. Try to avoid memory allocations and signal unsafe functions. - kotlin::ThreadStateGuard guard(kotlin::ThreadState::kRunnable, true); - - ObjHolder stackTrace; - Kotlin_getCurrentStackTrace(stackTrace.slot()); - ObjHolder stackTraceStrings; - kotlin::GetStackTraceStrings(stackTrace.obj(), stackTraceStrings.slot()); - ArrayHeader* stackTraceStringsArray = stackTraceStrings.obj()->array(); - for (uint32_t i = 0; i < stackTraceStringsArray->count_; ++i) { - ArrayHeader* symbol = (*ArrayAddressOfElementAt(stackTraceStringsArray, i))->array(); - auto* utf16 = CharArrayAddressOfElementAt(symbol, 0); - KStdString utf8; - utf8::with_replacement::utf16to8(utf16, utf16 + symbol->count_, std::back_inserter(utf8)); - kotlin::ThreadStateGuard guard(kotlin::ThreadState::kNative); - konan::consoleErrorUtf8(utf8.c_str(), utf8.size()); + // TODO: This might have to go into `GetCurrentStackTrace`, but this changes the generated stacktrace for + // `Throwable`. +#if KONAN_WINDOWS + // Skip this function and `_Unwind_Backtrace`. + constexpr int kSkipFrames = 2; +#else + // Skip this function. + constexpr int kSkipFrames = 1; +#endif + auto stackTrace = GetCurrentStackTrace(kSkipFrames); + auto stackTraceStrings = GetStackTraceStrings(stackTrace.data(), stackTrace.size()); + for (auto& frame : stackTraceStrings) { + konan::consoleErrorUtf8(frame.c_str(), frame.size()); konan::consoleErrorf("\n"); } } diff --git a/kotlin-native/runtime/src/main/cpp/StackTrace.hpp b/kotlin-native/runtime/src/main/cpp/StackTrace.hpp index a35fa672f4d..973579687bc 100644 --- a/kotlin-native/runtime/src/main/cpp/StackTrace.hpp +++ b/kotlin-native/runtime/src/main/cpp/StackTrace.hpp @@ -8,7 +8,13 @@ namespace kotlin { -OBJ_GETTER(GetStackTraceStrings, KConstRef stackTrace); +// TODO: Instead of KStd* provide allocator-customizable versions, to allow stack memory allocation. +// TODO: Model API as in upcoming https://en.cppreference.com/w/cpp/utility/basic_stacktrace + +KStdVector GetCurrentStackTrace(int extraSkipFrames) noexcept; + +// TODO: This is asking for a span. +KStdVector GetStackTraceStrings(void* const* stackTrace, size_t stackTraceSize) noexcept; // It's not always safe to extract SourceInfo during unhandled exception termination. void DisallowSourceInfo(); @@ -16,6 +22,3 @@ void DisallowSourceInfo(); void PrintStackTraceStderr(); } // namespace kotlin - -// Returns current stacktrace as Array. -extern "C" OBJ_GETTER0(Kotlin_getCurrentStackTrace); diff --git a/kotlin-native/runtime/src/main/cpp/StackTraceTest.cpp b/kotlin-native/runtime/src/main/cpp/StackTraceTest.cpp index 1687793b410..69ad1ec9acd 100644 --- a/kotlin-native/runtime/src/main/cpp/StackTraceTest.cpp +++ b/kotlin-native/runtime/src/main/cpp/StackTraceTest.cpp @@ -5,6 +5,8 @@ #include "StackTrace.hpp" +#include + #include "gmock/gmock.h" #include "gtest/gtest.h" @@ -16,21 +18,63 @@ using namespace kotlin; namespace { -NO_INLINE void AbortWithStackTrace() { +NO_INLINE KStdVector GetStackTrace1(int skipFrames) { + return GetCurrentStackTrace(skipFrames); +} + +NO_INLINE KStdVector GetStackTrace2(int skipFrames) { + return GetStackTrace1(skipFrames); +} + +NO_INLINE void AbortWithStackTrace(int) { PrintStackTraceStderr(); konan::abort(); } } // namespace +TEST(StackTraceTest, StackTrace) { + // TODO: Consider incorporating extra skipping to `GetCurrentStackTrace` on windows. +#if KONAN_WINDOWS + constexpr int kSkip = 1; +#else + constexpr int kSkip = 0; +#endif + auto stackTrace = GetStackTrace2(kSkip); + auto symbolicStackTrace = GetStackTraceStrings(stackTrace.data(), stackTrace.size()); + ASSERT_GT(symbolicStackTrace.size(), 0ul); + EXPECT_THAT(symbolicStackTrace[0], testing::HasSubstr("GetStackTrace1")); +} + +TEST(StackTraceTest, StackTraceWithSkip) { + // TODO: Consider incorporating extra skipping to `GetCurrentStackTrace` on windows. +#if KONAN_WINDOWS + constexpr int kSkip = 2; +#else + constexpr int kSkip = 1; +#endif + auto stackTrace = GetStackTrace2(kSkip); + auto symbolicStackTrace = GetStackTraceStrings(stackTrace.data(), stackTrace.size()); + ASSERT_GT(symbolicStackTrace.size(), 0ul); + EXPECT_THAT(symbolicStackTrace[0], testing::HasSubstr("GetStackTrace2")); +} + TEST(StackTraceDeathTest, PrintStackTrace) { EXPECT_DEATH( - { kotlin::RunInNewThread(AbortWithStackTrace); }, -#if KONAN_WINDOWS - // TODO: Fix Windows to match other platforms. - testing::AllOf(testing::HasSubstr("AbortWithStackTrace"), testing::HasSubstr("PrintStackTraceStderr")) -#else - testing::AllOf(testing::HasSubstr("AbortWithStackTrace"), testing::Not(testing::HasSubstr("PrintStackTraceStderr"))) -#endif - ); + { AbortWithStackTrace(0); }, + testing::AllOf( + testing::HasSubstr("AbortWithStackTrace"), testing::HasSubstr("StackTraceDeathTest_PrintStackTrace_Test"), + testing::Not(testing::HasSubstr("PrintStackTraceStderr")))); +} + +TEST(StackTraceDeathTest, PrintStackTraceInSignalHandler) { + EXPECT_DEATH( + { + signal(SIGINT, &AbortWithStackTrace); + raise(SIGINT); + }, + testing::AllOf( + testing::HasSubstr("AbortWithStackTrace"), + testing::HasSubstr("StackTraceDeathTest_PrintStackTraceInSignalHandler_Test"), + testing::Not(testing::HasSubstr("PrintStackTraceStderr")))); }