From c660b4e7cf3b0b4ecb94f7e4d3f0a98b05af571e Mon Sep 17 00:00:00 2001 From: SingleAccretion Date: Wed, 4 Sep 2024 21:55:25 +0300 Subject: [PATCH 1/5] Initial runtime support for stack traces on Browser Missing features: 1) Safari/WebKit/JSC support (https://bugs.webkit.org/show_bug.cgi?id=278991). JSC will fall back to using the JS frames. 2) Proper hiding of funclets. 3) Method name display for native methods. We could easily do this if we were ok with relying on the implementation detail of how wasm-ld lays out functions, but that's (obviously) not something we can really do. --- .../Microsoft.NETCore.Native.targets | 19 +- src/coreclr/nativeaot/Directory.Build.props | 2 + .../System/Runtime/ExceptionHandling.wasm.cs | 116 +++++---- .../src/System/Runtime/RuntimeExports.cs | 5 - src/coreclr/nativeaot/Runtime/CMakeLists.txt | 7 + .../nativeaot/Runtime/RuntimeInstance.cpp | 2 + .../Runtime/wasm/StackTrace.Browser.cpp | 155 +++++++++++ .../Runtime/wasm/StackTraceIpCanary.cpp | 15 ++ .../Reflection/Augments/ReflectionAugments.cs | 2 + .../Core/Execution/ExecutionEnvironment.cs | 1 + .../Augments/RuntimeAugments.Browser.cs | 30 +++ .../Runtime/Augments/RuntimeAugments.cs | 11 +- .../src/System.Private.CoreLib.csproj | 16 +- .../src/System/Delegate.cs | 3 +- .../StackFrame.NativeAot.Browser.cs | 140 ++++++++++ .../Diagnostics/StackFrame.NativeAot.cs | 14 +- .../StackTrace.NativeAot.Browser.cs | 108 +++++--- .../Diagnostics/StackTrace.NativeAot.Wasi.cs | 14 +- .../Diagnostics/StackTrace.NativeAot.cs | 4 +- .../src/System/Exception.NativeAot.Browser.cs | 240 ++++++++++++++++++ .../src/System/Exception.NativeAot.LLVM.cs | 97 ------- .../src/System/Exception.NativeAot.Wasi.cs | 16 ++ .../src/System/Exception.NativeAot.cs | 10 +- .../ReflectionCoreCallbacksImplementation.cs | 5 + .../src/System/Runtime/RuntimeImports.Wasm.cs | 18 ++ .../ReflectionCoreCallbacksImplementation.cs | 1 + ...EnvironmentImplementation.MappingTables.cs | 58 +++++ .../Execution/ReflectionExecution.cs | 7 + .../StackTraceMetadata/StackTraceMetadata.cs | 14 + .../DependencyAnalysis/ObjectDataBuilder.cs | 2 +- .../Compiler/DependencyAnalysis/Relocation.cs | 8 +- .../StackTraceMethodMappingNode.cs | 2 +- .../CodeGen/WasmObjectWriter.EmitObject.cs | 32 +++ .../CodeGen/WasmObjectWriter.cs | 4 +- .../JitInterface/CorInfoImpl.Llvm.cs | 56 ++-- .../tests/StackFrameTests.cs | 5 + .../src/System/Diagnostics/StackFrame.cs | 5 +- .../src/System/Diagnostics/StackTrace.cs | 2 - .../System.Runtime.Tests.csproj | 6 - .../System/ExceptionTests.cs | 2 +- .../ExceptionDispatchInfoTests.cs | 2 +- src/tests/Common/dirs.proj | 1 - .../SmokeTests/HelloWasm/HelloWasm.cs | 9 - .../StackTraceMetadata/StackTraceMetadata.cs | 2 +- 44 files changed, 978 insertions(+), 290 deletions(-) create mode 100644 src/coreclr/nativeaot/Runtime/wasm/StackTrace.Browser.cpp create mode 100644 src/coreclr/nativeaot/Runtime/wasm/StackTraceIpCanary.cpp create mode 100644 src/coreclr/nativeaot/System.Private.CoreLib/src/Internal/Runtime/Augments/RuntimeAugments.Browser.cs create mode 100644 src/coreclr/nativeaot/System.Private.CoreLib/src/System/Diagnostics/StackFrame.NativeAot.Browser.cs create mode 100644 src/coreclr/nativeaot/System.Private.CoreLib/src/System/Exception.NativeAot.Browser.cs delete mode 100644 src/coreclr/nativeaot/System.Private.CoreLib/src/System/Exception.NativeAot.LLVM.cs create mode 100644 src/coreclr/nativeaot/System.Private.CoreLib/src/System/Exception.NativeAot.Wasi.cs diff --git a/src/coreclr/nativeaot/BuildIntegration/Microsoft.NETCore.Native.targets b/src/coreclr/nativeaot/BuildIntegration/Microsoft.NETCore.Native.targets index 0e8735e56dce..689f652c209c 100644 --- a/src/coreclr/nativeaot/BuildIntegration/Microsoft.NETCore.Native.targets +++ b/src/coreclr/nativeaot/BuildIntegration/Microsoft.NETCore.Native.targets @@ -546,21 +546,24 @@ The .NET Foundation licenses this file to you under the MIT license. + <_WasiComponentImports Include="$(IlcFrameworkNativePath)*.wit" /> + - - + + - - - - <_WasiComponentImports Include="$(IlcFrameworkNativePath)*.wit" /> + + + + + @@ -571,7 +574,7 @@ The .NET Foundation licenses this file to you under the MIT license. - + @@ -593,7 +596,7 @@ The .NET Foundation licenses this file to you under the MIT license. - + diff --git a/src/coreclr/nativeaot/Directory.Build.props b/src/coreclr/nativeaot/Directory.Build.props index b7a6ad0e6479..fff36a8f7edd 100644 --- a/src/coreclr/nativeaot/Directory.Build.props +++ b/src/coreclr/nativeaot/Directory.Build.props @@ -84,6 +84,8 @@ true AnyCPU TARGET_32BIT;TARGET_WASM;$(DefineConstants) + TARGET_BROWSER;$(DefineConstants) + TARGET_WASI;$(DefineConstants) TARGET_64BIT;TARGET_AMD64;$(DefineConstants) diff --git a/src/coreclr/nativeaot/Runtime.Base/src/System/Runtime/ExceptionHandling.wasm.cs b/src/coreclr/nativeaot/Runtime.Base/src/System/Runtime/ExceptionHandling.wasm.cs index 2ba393f6b329..8174958b948e 100644 --- a/src/coreclr/nativeaot/Runtime.Base/src/System/Runtime/ExceptionHandling.wasm.cs +++ b/src/coreclr/nativeaot/Runtime.Base/src/System/Runtime/ExceptionHandling.wasm.cs @@ -9,9 +9,12 @@ // Disable: Filter expression is a constant. We know. We just can't do an unfiltered catch. #pragma warning disable 7095 +// +// WASM exception handling. +// See: https://github.com/dotnet/runtimelab/blob/feature/NativeAOT-LLVM/docs/design/coreclr/botr/nativeaot-wasm-exception-handling.md. +// namespace System.Runtime { - // TODO-LLVM-EH: write and link a design document for this EH scheme. It is not terribly simple... internal static unsafe partial class EH { private const nuint UnwindIndexNotInTry = 0; @@ -21,6 +24,7 @@ internal static unsafe partial class EH private static ExceptionDispatchData? t_lastDispatchedException; [RuntimeExport("RhpThrowEx")] + [MethodImpl(MethodImplOptions.NoInlining)] private static void RhpThrowEx(object exception) { #if INPLACE_RUNTIME @@ -29,13 +33,14 @@ private static void RhpThrowEx(object exception) #else #error Implement "throw null" in non-INPLACE_RUNTIME builds #endif - DispatchException(exception, 0); + DispatchException(exception, RhEHFrameType.RH_EH_FIRST_FRAME); } [RuntimeExport("RhpRethrow")] - private static void RhpRethrow(object pException) + [MethodImpl(MethodImplOptions.NoInlining)] + private static void RhpRethrow(object exception) { - DispatchException(pException, RhEHFrameType.RH_EH_FIRST_RETHROW_FRAME); + DispatchException(exception, RhEHFrameType.RH_EH_FIRST_FRAME | RhEHFrameType.RH_EH_FIRST_RETHROW_FRAME); } // Note that this method cannot have any catch handlers as it manipulates the virtual unwind frames directly @@ -43,16 +48,11 @@ private static void RhpRethrow(object pException) // all user code via separate noinline methods. It also cannot throw any exceptions as that would lead to // infinite recursion. // + [MethodImpl(MethodImplOptions.NoInlining)] private static void DispatchException(object exception, RhEHFrameType flags) { - WasmEHLogFirstPassEnter(exception, flags); - + WasmEHLogFirstPassEnter(exception, (flags & RhEHFrameType.RH_EH_FIRST_RETHROW_FRAME) != 0); OnFirstChanceExceptionNoInline(exception); -#if INPLACE_RUNTIME - Exception.InitializeExceptionStackFrameLLVM(exception, (int)flags); -#else -#error Make InitializeExceptionStackFrameLLVM into a classlib export -#endif // Find the handler for this exception by virtually unwinding the stack of active protected regions. VirtualUnwindFrame** pLastFrameRef = (VirtualUnwindFrame**)InternalCalls.RhpGetRawLastVirtualUnwindFrameRef(); @@ -61,11 +61,14 @@ private static void DispatchException(object exception, RhEHFrameType flags) nuint unwindCount = 0; while (pFrame != null) { - EHClause clause; EHTable table = new EHTable(pFrame->UnwindTable); + GetAppendStackFrame(exception)(exception, table.GetStackTraceIP(), (int)flags); + flags = 0; + nuint index = pFrame->UnwindIndex; while (IsCatchUnwindIndex(index)) { + EHClause clause; nuint enclosingIndex = table.GetClauseInfo(index, &clause); WasmEHLogEHTableEntry(pFrame, index, &clause); @@ -310,6 +313,9 @@ private static void RhpPopUnwoundVirtualFrames() [RuntimeExport("RhpHandleUnhandledException")] private static void HandleUnhandledException(object exception) { + // Append JS frames to this unhandled exception for better diagnostics. + GetAppendStackFrame(exception)(exception, 0, 0); + OnUnhandledExceptionViaClassLib(exception); // We have to duplicate "UnhandledExceptionFailFastViaClasslib" because we cannot use code addresses to get helpers. @@ -334,6 +340,13 @@ private static void HandleUnhandledException(object exception) FallbackFailFast(RhFailFastReason.UnhandledException, exception); } + private static delegate* GetAppendStackFrame(object exception) + { + // We use this more direct way of invoking "AppendExceptionStackFrame" to preserve more user frames in truncated traces. + nint pAppendStackFrame = exception.GetMethodTable()->GetClasslibFunction(ClassLibFunctionId.AppendExceptionStackFrame); + return (delegate*)pAppendStackFrame; + } + // These are pushed by codegen on the shadow stack for frames that have at least one region protected by a catch. // private struct VirtualUnwindFrame @@ -360,71 +373,62 @@ private unsafe struct EHClause private unsafe struct EHTable { - private const nuint MetadataFilter = 1; - private const nuint MetadataClauseTypeFormat = 2; - private const int MetadataShift = 1; - private const nuint MetadataMask = ~(1u << MetadataShift); - - private const nuint FormatClauseType = 0; - private const nuint FormatSmall = 1; - private const nuint FormatLarge = 2; - private const nuint FormatMask = 3; + private const nuint MetadataLargeFormat = 1; + private const nuint MetadataFilter = 1 << 1; + private const int MetadataShift = 2; - private readonly void* _pEHTable; - private readonly nuint _format; + private readonly byte* _pEHTable; + private readonly bool _isLargeFormat; public EHTable(void* pUnwindTable) { - _pEHTable = (void*)((nuint)pUnwindTable & ~FormatMask); - _format = (nuint)pUnwindTable & FormatMask; + _pEHTable = (byte*)pUnwindTable; + _isLargeFormat = (*(byte*)pUnwindTable & MetadataLargeFormat) != 0; } public readonly nuint GetClauseInfo(nuint index, EHClause* pClause = null) { - nuint metadata; - nuint enclosingIndex = GetMetadata(index, &metadata); + Debug.Assert(IsCatchUnwindIndex(index)); + nuint largeFormatEntrySize = (nuint)(sizeof(uint) + sizeof(void*)); + nuint smallFormatEntrySize = (nuint)(sizeof(byte) + sizeof(void*)); + + nuint zeroBasedIndex = index - UnwindIndexBase; + nuint metadata = _isLargeFormat + ? Unsafe.ReadUnaligned(_pEHTable + zeroBasedIndex * largeFormatEntrySize) + : Unsafe.ReadUnaligned(_pEHTable + zeroBasedIndex * smallFormatEntrySize); + if (pClause != null) { - if (metadata == MetadataClauseTypeFormat) - { - pClause->Filter = null; - pClause->ClauseType = (MethodTable*)_pEHTable; - } - else if ((metadata & MetadataFilter) != 0) + nuint value = _isLargeFormat + ? Unsafe.ReadUnaligned(_pEHTable + zeroBasedIndex * largeFormatEntrySize + sizeof(uint)) + : Unsafe.ReadUnaligned(_pEHTable + zeroBasedIndex * smallFormatEntrySize + sizeof(byte)); + + if ((metadata & MetadataFilter) != 0) { - pClause->Filter = ((void**)_pEHTable)[index - UnwindIndexBase]; + pClause->Filter = (void*)value; pClause->ClauseType = null; } else { pClause->Filter = null; - pClause->ClauseType = ((MethodTable**)_pEHTable)[index - UnwindIndexBase]; + pClause->ClauseType = (MethodTable*)value; } } + nuint enclosingIndex = metadata >> MetadataShift; return enclosingIndex; } - private readonly nuint GetMetadata(nuint index, nuint* pMetadata) +#pragma warning disable CA1822 // Member 'GetStackTraceIP' does not access instance data and can be marked as static + public readonly nint GetStackTraceIP() { - Debug.Assert(IsCatchUnwindIndex(index)); - nuint metadata; - switch (_format) - { - case FormatClauseType: - *pMetadata = MetadataClauseTypeFormat; - return UnwindIndexNotInTry; - case FormatSmall: - metadata = ((byte*)_pEHTable)[-(nint)(index - UnwindIndexBase + 1)]; - break; - default: - Debug.Assert(_format == FormatLarge); - metadata = ((uint*)_pEHTable)[-(nint)(index - UnwindIndexBase + 1)]; - break; - } - - *pMetadata = metadata & MetadataMask; - return metadata >> MetadataShift; +#if TARGET_BROWSER + int wasmFunctionIndex = Unsafe.ReadUnaligned(_pEHTable - sizeof(int)); + int wasmFunctionIndexWithBias = Exception.GetBiasedWasmFunctionIndex(wasmFunctionIndex); + return wasmFunctionIndexWithBias; +#else + return 0; +#endif } } @@ -459,9 +463,9 @@ private static void WasmEHLog(string message, int pass, string prefix = "") } [Conditional("ENABLE_NOISY_WASM_EH_LOG")] - private static void WasmEHLogFirstPassEnter(object exception, RhEHFrameType flags) + private static void WasmEHLogFirstPassEnter(object exception, bool isFirstRethrowFrame) { - string kind = (flags & RhEHFrameType.RH_EH_FIRST_RETHROW_FRAME) != 0 ? "Rethrowing" : "Throwing"; + string kind = isFirstRethrowFrame ? "Rethrowing" : "Throwing"; WasmEHLog(kind + ": [" + exception.GetType() + "]", 1, "\n"); } diff --git a/src/coreclr/nativeaot/Runtime.Base/src/System/Runtime/RuntimeExports.cs b/src/coreclr/nativeaot/Runtime.Base/src/System/Runtime/RuntimeExports.cs index d8c4634406df..b7a1863d3727 100644 --- a/src/coreclr/nativeaot/Runtime.Base/src/System/Runtime/RuntimeExports.cs +++ b/src/coreclr/nativeaot/Runtime.Base/src/System/Runtime/RuntimeExports.cs @@ -292,13 +292,8 @@ public static unsafe void RhUnbox(object? obj, ref byte data, MethodTable* pUnbo [MethodImpl(MethodImplOptions.NoInlining)] // Ensures that the RhGetCurrentThreadStackTrace frame is always present public static unsafe int RhGetCurrentThreadStackTrace(IntPtr[] outputBuffer) { -#if TARGET_WASM - // TODO-LLVM: https://github.com/dotnet/runtimelab/issues/2404. - throw new NotImplementedException(); -#else fixed (IntPtr* pOutputBuffer = outputBuffer) return RhpGetCurrentThreadStackTrace(pOutputBuffer, (uint)((outputBuffer != null) ? outputBuffer.Length : 0), new UIntPtr(&pOutputBuffer)); -#endif } #pragma warning disable SYSLIB1054 // Use DllImport here instead of LibraryImport because this file is used by Test.CoreLib. diff --git a/src/coreclr/nativeaot/Runtime/CMakeLists.txt b/src/coreclr/nativeaot/Runtime/CMakeLists.txt index e841ed52f601..6e3a3496bfa7 100644 --- a/src/coreclr/nativeaot/Runtime/CMakeLists.txt +++ b/src/coreclr/nativeaot/Runtime/CMakeLists.txt @@ -224,6 +224,13 @@ if (CLR_CMAKE_TARGET_ARCH_WASM) ${ARCH_SOURCES_DIR}/StubDispatch.cpp ${ARCH_SOURCES_DIR}/WriteBarriers.cpp ) + if (CLR_CMAKE_TARGET_BROWSER) + list(APPEND COMMON_RUNTIME_SOURCES ${ARCH_SOURCES_DIR}/StackTrace.Browser.cpp) + + add_library(StackTraceIpCanary OBJECT ${ARCH_SOURCES_DIR}/StackTraceIpCanary.cpp) + add_dependencies(nativeaot StackTraceIpCanary) + install(FILES $ DESTINATION aotsdk COMPONENT nativeaot RENAME libStackTraceIpCanary.o) + endif() endif (CLR_CMAKE_TARGET_ARCH_WASM) list(APPEND RUNTIME_SOURCES_ARCH_ASM diff --git a/src/coreclr/nativeaot/Runtime/RuntimeInstance.cpp b/src/coreclr/nativeaot/Runtime/RuntimeInstance.cpp index 708356c2e3a9..2ff1cbd56450 100644 --- a/src/coreclr/nativeaot/Runtime/RuntimeInstance.cpp +++ b/src/coreclr/nativeaot/Runtime/RuntimeInstance.cpp @@ -66,6 +66,7 @@ FCIMPL1(uint8_t *, RhGetRuntimeVersion, int32_t* pcbLength) } FCIMPLEND +#ifndef TARGET_BROWSER FCIMPL1(uint8_t *, RhFindMethodStartAddress, void * codeAddr) { uint8_t *startAddress = dac_cast(GetRuntimeInstance()->FindMethodStartAddress(dac_cast(codeAddr))); @@ -76,6 +77,7 @@ FCIMPL1(uint8_t *, RhFindMethodStartAddress, void * codeAddr) #endif } FCIMPLEND +#endif // !TARGET_BROWSER PTR_uint8_t RuntimeInstance::FindMethodStartAddress(PTR_VOID ControlPC) { diff --git a/src/coreclr/nativeaot/Runtime/wasm/StackTrace.Browser.cpp b/src/coreclr/nativeaot/Runtime/wasm/StackTrace.Browser.cpp new file mode 100644 index 000000000000..cd9c33e6ca01 --- /dev/null +++ b/src/coreclr/nativeaot/Runtime/wasm/StackTrace.Browser.cpp @@ -0,0 +1,155 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#include +#include + +#include "CommonTypes.h" +#include "CommonMacros.h" + +// Recieve pointers as JS-native types to avoid BigInt overheads on 64 bit +// and the need to worry about normalizing large (> int.MaxValue) values. +using JSPointerType = double; + +EM_JS_DEPS(RhpBrowserStackTraceDependencies, "$wasmTable,$UTF8ArrayToString"); + +// It is a shortcut that this method is an FCall (i. e. called in cooperative mode) - computing the stack trace +// is a heavy operation that should ideally be done in preemptive mode. However, doing it this way allows us to +// avoid the complexity of skipping the PI stub managed frame that can be part of a QCall sequence in Debug code. +// +EM_JS(int32_t, RhpGetCurrentBrowserThreadStackTrace, (void* pShadowStack, JSPointerType pOutputBuffer, int allFramesAsJS), { +#ifdef TARGET_64BIT + const POINTER_SIZE = 8; + const POINTER_LOG2 = 3; +#else + const POINTER_SIZE = 4; + const POINTER_LOG2 = 2; +#endif + // Not ":wasm-function[" because of a WebKit Issue (WKI): https://bugs.webkit.org/show_bug.cgi?id=278991. + const WASM_FUNCTION_TEXT = "wasm-function["; + const WASM_OFFSET_TEXT = "]:0x"; + + // Our callers must employ the following pattern: + // int size = RhpGetCurrentBrowserThreadStackTrace(null) + // IntPtr[] data = new IntPtr[size]; + // RhpGetCurrentBrowserThreadStackTrace(ref data[0]) + // This way, we avoid fetching the same stack trace from the JS engine twice. + // + const isMeasurementPhase = pOutputBuffer === 0; + const jsStackTrace = isMeasurementPhase ? new Error().stack : Module.RhpGetCurrentBrowserThreadStackTraceValue; + + let actualBufferLength = 0; + let callerModuleId = ""; + for (let currentIndex = 0, currentFrameEndIndex; currentIndex < jsStackTrace.length; currentIndex = currentFrameEndIndex + 1) + { + currentFrameEndIndex = jsStackTrace.indexOf('\n', currentIndex); + if (currentFrameEndIndex < 0) + currentFrameEndIndex = jsStackTrace.length; + + // Unfortunately, we do have to rely here on the (undocumented) optimization that JS engines perform with string + // slicing, where the returned object is what in C# terms would be Memory, not a full string. + const currentFrame = jsStackTrace.slice(currentIndex, currentFrameEndIndex); + let wasmFuncStartIndex = currentFrame.indexOf(WASM_FUNCTION_TEXT); + + // Only start giving out frames once we've hit our managed WASM caller. All known engines implement enough of + // the stack trace handling for us to always find _some_ WASM frame (even if we end up reporting it as JS one). + if (wasmFuncStartIndex < 0 && actualBufferLength === 0) + continue; + + if (wasmFuncStartIndex >= 0 && currentFrame.endsWith(callerModuleId, wasmFuncStartIndex) && !allFramesAsJS) { + if (actualBufferLength === 0) { + // Unfortunately, there is no 100% reliable way to identify which part of the standard + // "WASM stack frame string" is the URL field, so we have to assume 'known' formats: + // V8 : at ForeignModuleFrame (wasm://wasm/ec89ddd2:wasm-function[1000]:0x1797) + // FF : ForeignModuleFrame@http://localhost:6931/HelloWasm.js line 1462 > WebAssembly.Module:wasm-function[13839]:0x14482 + // JSC : ForeignModule.wasm-function[ForeignModuleFrame]@[wasm code] + let idIndex = currentFrame.lastIndexOf('//', wasmFuncStartIndex); + if (idIndex < 0) + idIndex = 0; // The JSC case. + callerModuleId = currentFrame.slice(idIndex, wasmFuncStartIndex); + } + + // WASM or unknown frame. + if (!isMeasurementPhase) { + let wasmFunctionIndex = 0; + let wasmFunctionOffset = 0; + + wasmFuncStartIndex += WASM_FUNCTION_TEXT.length; + let wasmFuncEndIndex = currentFrame.indexOf(']', wasmFuncStartIndex); + if (wasmFuncEndIndex >= 0) { + // Add bias to make room for 'null' and the EDI separator. + wasmFunctionIndex = parseInt(currentFrame.slice(wasmFuncStartIndex, wasmFuncEndIndex), 10) + 2; + if (!isNaN(wasmFunctionIndex) && currentFrame.startsWith(WASM_OFFSET_TEXT, wasmFuncEndIndex)) { + wasmFunctionOffset = parseInt(currentFrame.slice(wasmFuncEndIndex + WASM_OFFSET_TEXT.length), 16); + } + } + + Module.HEAP32[pOutputBuffer >>> 2] = wasmFunctionIndex; // Note that NaNs will turn into zeroes here + pOutputBuffer += POINTER_SIZE; + Module.HEAP32[pOutputBuffer >>> 2] = wasmFunctionOffset; + pOutputBuffer += POINTER_SIZE; + } + + actualBufferLength += 2; + } else { // JS frame. + const lengthInChunks = (2 * currentFrame.length + (POINTER_SIZE - 1)) >> POINTER_LOG2; + + if (!isMeasurementPhase) { + Module.HEAP32[pOutputBuffer >>> 2] = -currentFrame.length; + pOutputBuffer += POINTER_SIZE; + + for (let i = 0; i < currentFrame.length; i++) // TODO-LLVM: is there a faster way to do this? + { + Module.HEAP16[(pOutputBuffer >>> 1) + i] = currentFrame.charCodeAt(i); + } + pOutputBuffer += lengthInChunks * POINTER_SIZE; + } + + actualBufferLength += 1 + lengthInChunks; + } + } + + Module.RhpGetCurrentBrowserThreadStackTraceValue = isMeasurementPhase ? jsStackTrace : null; + return actualBufferLength; +}); + +// Fill out the array of { indirect table index, biased wasm function index } tuples. +// We exploit the fact that by the WASM JS API spec, JS objects that represent funcrefs in +// the indirect function table must have the 'name' property set to their function's index. +// +// Note that this "bulk" version is just an optimization. We want to avoid WASM<->JS hops +// for this array. +// +EM_JS(void, RhpInitializeStackTraceIpMap, (JSPointerType pEntries, int count), { +#ifdef TARGET_64BIT + const POINTER_SIZE = 8; +#else + const POINTER_SIZE = 4; +#endif + for (let i = 0; i < count; i++) { + const fptr = Module.HEAP32[pEntries >>> 2]; + const func = wasmTable.get(fptr); + let wasmFuncIndex = parseInt(func.name) + 2; // Add bias. + if (isNaN(wasmFuncIndex)) + wasmFuncIndex = 0; // Be defensive against future extensions (e. g. JS Builtins) and WKI. + + pEntries += POINTER_SIZE; + Module.HEAP32[pEntries >>> 2] = wasmFuncIndex; + pEntries += POINTER_SIZE; + } +}); + +EM_JS(int32_t, RhpGetBiasedWasmFunctionIndexForFunctionPointer, (JSPointerType fptr), { + const func = wasmTable.get(fptr); + let wasmFuncIndex = parseInt(func.name) + 2; // Add bias. + if (isNaN(wasmFuncIndex)) + wasmFuncIndex = 0; // Be defensive against future extensions (e. g. JS Builtins) and WKI. + return wasmFuncIndex; +}); + +FCIMPL1(void*, RhFindMethodStartAddress, void* addr) +{ + // Our stack trace "IP"s are (biased) function indices, which do not require adjustment. + return addr; +} +FCIMPLEND diff --git a/src/coreclr/nativeaot/Runtime/wasm/StackTraceIpCanary.cpp b/src/coreclr/nativeaot/Runtime/wasm/StackTraceIpCanary.cpp new file mode 100644 index 000000000000..48ac3b2dcba8 --- /dev/null +++ b/src/coreclr/nativeaot/Runtime/wasm/StackTraceIpCanary.cpp @@ -0,0 +1,15 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#include "CommonTypes.h" +#include "CommonMacros.h" + +// TODO-LLVM: define "RhpStackTraceIpCanary" here via inline asm. Right now it has +// to be defined by ILC due to https://github.com/llvm/llvm-project/issues/100733. +extern "C" int RhpStackTraceIpCanary; + +FCIMPL0(int, RhpGetStackTraceIpCanary) +{ + return RhpStackTraceIpCanary; +} +FCIMPLEND diff --git a/src/coreclr/nativeaot/System.Private.CoreLib/src/Internal/Reflection/Augments/ReflectionAugments.cs b/src/coreclr/nativeaot/System.Private.CoreLib/src/Internal/Reflection/Augments/ReflectionAugments.cs index 4aac720d0114..f718401c265e 100644 --- a/src/coreclr/nativeaot/System.Private.CoreLib/src/Internal/Reflection/Augments/ReflectionAugments.cs +++ b/src/coreclr/nativeaot/System.Private.CoreLib/src/Internal/Reflection/Augments/ReflectionAugments.cs @@ -169,6 +169,8 @@ public abstract object ActivatorCreateInstance( public abstract MethodInfo GetDelegateMethod(Delegate del); + public abstract IntPtr ConvertStackTraceIpToFunctionPointer(IntPtr methodStartAddress); + public abstract MethodBase GetMethodBaseFromStartAddressIfAvailable(IntPtr methodStartAddress); public abstract Assembly GetAssemblyForHandle(RuntimeTypeHandle typeHandle); diff --git a/src/coreclr/nativeaot/System.Private.CoreLib/src/Internal/Reflection/Core/Execution/ExecutionEnvironment.cs b/src/coreclr/nativeaot/System.Private.CoreLib/src/Internal/Reflection/Core/Execution/ExecutionEnvironment.cs index b990dac4712e..d33d969bcdfb 100644 --- a/src/coreclr/nativeaot/System.Private.CoreLib/src/Internal/Reflection/Core/Execution/ExecutionEnvironment.cs +++ b/src/coreclr/nativeaot/System.Private.CoreLib/src/Internal/Reflection/Core/Execution/ExecutionEnvironment.cs @@ -79,6 +79,7 @@ public abstract class ExecutionEnvironment public abstract void GetEnumInfo(RuntimeTypeHandle typeHandle, out string[] names, out object[] values, out bool isFlags); public abstract IntPtr GetDynamicInvokeThunk(MethodBaseInvoker invoker); public abstract MethodInfo GetDelegateMethod(Delegate del); + public abstract IntPtr ConvertStackTraceIpToFunctionPointer(IntPtr methodStartAddress); public abstract MethodBase GetMethodBaseFromStartAddressIfAvailable(IntPtr methodStartAddress); public abstract IntPtr GetStaticClassConstructionContext(RuntimeTypeHandle typeHandle); diff --git a/src/coreclr/nativeaot/System.Private.CoreLib/src/Internal/Runtime/Augments/RuntimeAugments.Browser.cs b/src/coreclr/nativeaot/System.Private.CoreLib/src/Internal/Runtime/Augments/RuntimeAugments.Browser.cs new file mode 100644 index 000000000000..d5a1f660577a --- /dev/null +++ b/src/coreclr/nativeaot/System.Private.CoreLib/src/Internal/Runtime/Augments/RuntimeAugments.Browser.cs @@ -0,0 +1,30 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Runtime; + +namespace Internal.Runtime.Augments +{ + public partial class RuntimeAugments + { + public static int GetBiasedWasmFunctionIndex(int wasmFunctionIndex) + { + return Exception.GetBiasedWasmFunctionIndex(wasmFunctionIndex); + } + + public static unsafe void InitializeStackTraceIpMap(StackTraceIpAndFunctionPointer[] stackTraceIpMap) + { + fixed (void* pEntries = stackTraceIpMap) + RuntimeImports.RhpInitializeStackTraceIpMap((nint)pEntries, stackTraceIpMap.Length); + } + + public struct StackTraceIpAndFunctionPointer : IComparable + { + public IntPtr FunctionPointer; // Indirect table index. + public IntPtr StackTraceIp; // Biased WASM function index. + + public readonly int CompareTo(StackTraceIpAndFunctionPointer other) => StackTraceIp.CompareTo(other.StackTraceIp); + } + } +} diff --git a/src/coreclr/nativeaot/System.Private.CoreLib/src/Internal/Runtime/Augments/RuntimeAugments.cs b/src/coreclr/nativeaot/System.Private.CoreLib/src/Internal/Runtime/Augments/RuntimeAugments.cs index e19ed89bf7b0..6b5f341d26c3 100644 --- a/src/coreclr/nativeaot/System.Private.CoreLib/src/Internal/Runtime/Augments/RuntimeAugments.cs +++ b/src/coreclr/nativeaot/System.Private.CoreLib/src/Internal/Runtime/Augments/RuntimeAugments.cs @@ -33,7 +33,7 @@ namespace Internal.Runtime.Augments { - public static unsafe class RuntimeAugments + public static unsafe partial class RuntimeAugments { /// /// Callbacks used for metadata-based stack trace resolution. @@ -656,6 +656,15 @@ internal static StackTraceMetadataCallbacks StackTraceCallbacksIfAvailable } } + public static IntPtr ConvertFunctionPointerToStackTraceIp(nint functionPointer) + { +#if TARGET_BROWSER + return RuntimeImports.RhpGetBiasedWasmFunctionIndexForFunctionPointer((nuint)functionPointer); +#else + return functionPointer; +#endif + } + public static string TryGetMethodDisplayStringFromIp(IntPtr ip) { StackTraceMetadataCallbacks callbacks = StackTraceCallbacksIfAvailable; diff --git a/src/coreclr/nativeaot/System.Private.CoreLib/src/System.Private.CoreLib.csproj b/src/coreclr/nativeaot/System.Private.CoreLib/src/System.Private.CoreLib.csproj index 41dd74a58141..9ee1a0d994d2 100644 --- a/src/coreclr/nativeaot/System.Private.CoreLib/src/System.Private.CoreLib.csproj +++ b/src/coreclr/nativeaot/System.Private.CoreLib/src/System.Private.CoreLib.csproj @@ -191,8 +191,6 @@ - - @@ -234,7 +232,6 @@ - @@ -340,6 +337,19 @@ + + + + + + + + + + + + + $([MSBuild]::NormalizePath('$(MonoProjectRoot)', 'System.Private.CoreLib', 'src')) diff --git a/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Delegate.cs b/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Delegate.cs index 05e3859bee90..2f36d694e2eb 100644 --- a/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Delegate.cs +++ b/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Delegate.cs @@ -315,7 +315,8 @@ internal DiagnosticMethodInfo GetDiagnosticMethodInfo() { functionPointer = ldftnResult; } - return RuntimeAugments.StackTraceCallbacksIfAvailable?.TryGetDiagnosticMethodInfoFromStartAddress(functionPointer); + IntPtr methodStartAddress = RuntimeAugments.ConvertFunctionPointerToStackTraceIp(functionPointer); + return RuntimeAugments.StackTraceCallbacksIfAvailable?.TryGetDiagnosticMethodInfoFromStartAddress(methodStartAddress); } } diff --git a/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Diagnostics/StackFrame.NativeAot.Browser.cs b/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Diagnostics/StackFrame.NativeAot.Browser.cs new file mode 100644 index 000000000000..c259cbd5882b --- /dev/null +++ b/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Diagnostics/StackFrame.NativeAot.Browser.cs @@ -0,0 +1,140 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Runtime; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +using Internal.DeveloperExperience; + +namespace System.Diagnostics +{ + public partial class StackFrame + { + private IntPtr[] _eips; + private int _eipIndex; + + internal StackFrame(IntPtr[] eips, int eipIndex, bool needFileInfo) + { + InitializeForEip(eips, eipIndex, needFileInfo); + } + +#pragma warning disable CA1822 // Member 'GetNativeIPAddress' does not access instance data and can be marked as static + internal IntPtr GetNativeIPAddress() +#pragma warning restore CA1822 + { + // Return "null" - same rationale as below. + return 0; + } + + private static int GetNativeOffsetImpl() + { + // We could very well make this work - by parsing the WASM binary, for example. For now, however, + // we punt making the decision on what this should return, if anything - we probably will not + // be able to make WASI return the same value. + return OFFSET_UNKNOWN; + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private unsafe void BuildStackFrame(int frameIndex, bool needFileInfo) + { + const int SystemDiagnosticsStackDepth = 2; + + // We may have a value that has already overflown or will overflow. + frameIndex += SystemDiagnosticsStackDepth; + if (frameIndex < 0) + frameIndex = int.MaxValue; + + // We have to parse the entire stack to get just one frame... + int eipCount = RuntimeImports.RhpGetCurrentBrowserThreadStackTrace(0, Exception.ReportAllFramesAsJS()); + IntPtr[] eips = new IntPtr[eipCount]; + fixed (void* pEips = eips) + RuntimeImports.RhpGetCurrentBrowserThreadStackTrace((nuint)pEips, Exception.ReportAllFramesAsJS()); + + for (int eipIndex = 0, actualFrameIndex = 0; eipIndex < eipCount; actualFrameIndex++) + { + int length = Exception.GetBrowserFrameLengthInChunks(eips[eipIndex]); + if (frameIndex == actualFrameIndex) + { + // Trim the EIP array to avoid rooting the whole thing. + IntPtr[] justFrameEips = eips.AsSpan(eipIndex, length).ToArray(); + InitializeForEip(justFrameEips, 0, needFileInfo); + return; + } + + eipIndex += length; + } + + // Frame info not found, build a dummy instance. + InitializeForIpAddress(IntPtr.Zero, needFileInfo); + } + + private void InitializeForEip(IntPtr[] eips, int eipIndex, bool needFileInfo) + { + IntPtr ip; + IntPtr eip = eips[eipIndex]; + if (eip == Exception.EdiSeparator) + { + ip = Exception.EdiSeparator; + } + else + { + // We (have to) use the biased function index as IP because "0" is a valid function index. + if (Exception.GetBrowserFrameInfoWithBias(eip, out int wasmFunctionIndexWithBias) is not 0 || + Exception.IsValidBiasedWasmFunctionIndex(wasmFunctionIndexWithBias)) + { + _eips = eips; + _eipIndex = eipIndex; + } + ip = wasmFunctionIndexWithBias; + } + InitializeForIpAddress(ip, needFileInfo); + } + +#pragma warning disable IDE0060 // Remove unused parameter (includeFileInfo) + private string CreateStackTraceString(bool includeFileInfo, out bool isStackTraceHidden) +#pragma warning restore IDE0060 // Remove unused parameter + { + Debug.Assert(_ipAddress != Exception.EdiSeparator); + isStackTraceHidden = false; + + // An unknown frame? + if (_eips is null) + { + return ""; + } + + // A JS frame? + ReadOnlySpan jsFrame = Exception.GetJSFrame(_eips, _eipIndex); + if (!jsFrame.IsEmpty) + { + // Do a little munging here - our caller expects 'at'-less frames. + const string At = "at "; + int atIndex = jsFrame.IndexOf(At.AsSpan()); + if (atIndex >= 0) + { + jsFrame = jsFrame.Slice(atIndex + At.Length); + } + return jsFrame.ToString(); + } + + // A known WASM frame? + string methodName = DeveloperExperience.GetMethodName(_ipAddress, out _, out isStackTraceHidden); + if (methodName is null) + { + // A WASM frame not recorded in our stack trace data (e. g. a runtime helper). + Exception.GetBrowserFrameInfoWithoutBias(_eips[_eipIndex], out int wasmFunctionIndex); + methodName = $"wasm-function[{wasmFunctionIndex}]"; + } + + // Add in the "true" IP, the file-relative offset, if available. This allows easily mapping back to + // the file and line via tools like "emsymbolizer". + int wasmFunctionOffset = Exception.GetWasmFunctionOffset(_eips, _eipIndex); + if (wasmFunctionOffset != 0) + { + return $"{methodName}:0x{wasmFunctionOffset:x}"; + } + return methodName; + } + } +} diff --git a/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Diagnostics/StackFrame.NativeAot.cs b/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Diagnostics/StackFrame.NativeAot.cs index f4b862c17177..692af1f51e3d 100644 --- a/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Diagnostics/StackFrame.NativeAot.cs +++ b/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Diagnostics/StackFrame.NativeAot.cs @@ -69,6 +69,7 @@ private bool TryInitializeMethodBase() IntPtr methodStartAddress = _ipAddress - _nativeOffset; Debug.Assert(RuntimeImports.RhFindMethodStartAddress(_ipAddress) == methodStartAddress); + methodStartAddress = ReflectionAugments.ReflectionCoreCallbacks.ConvertStackTraceIpToFunctionPointer(methodStartAddress); _method = ReflectionAugments.ReflectionCoreCallbacks.GetMethodBaseFromStartAddressIfAvailable(methodStartAddress); if (_method == null) { @@ -117,6 +118,7 @@ private void InitializeForIpAddress(IntPtr ipAddress, bool needFileInfo) } } +#if !TARGET_BROWSER /// /// Internal stack frame initialization based on frame index within the stack of the current thread. /// @@ -141,6 +143,7 @@ internal IntPtr GetNativeIPAddress() { return _ipAddress; } +#endif /// /// Check whether method info is available. @@ -156,7 +159,7 @@ internal bool HasMethod() /// private bool AppendStackFrameWithoutMethodBase(StringBuilder builder) { - builder.Append(DeveloperExperience.Default.CreateStackTraceString(_ipAddress, includeFileInfo: false, out _)); + builder.Append(CreateStackTraceString(includeFileInfo: false, out _)); return true; } @@ -175,7 +178,7 @@ internal void AppendToStackTrace(StringBuilder builder) { if (_ipAddress != Exception.EdiSeparator) { - string s = DeveloperExperience.Default.CreateStackTraceString(_ipAddress, _needFileInfo, out bool isStackTraceHidden); + string s = CreateStackTraceString(_needFileInfo, out bool isStackTraceHidden); if (!isStackTraceHidden) { // Passing a default string for "at" in case SR.UsingResourceKeys() is true @@ -193,5 +196,12 @@ internal void AppendToStackTrace(StringBuilder builder) SR.Exception_EndStackTraceFromPreviousThrow); } } + +#if !TARGET_BROWSER + private string CreateStackTraceString(bool includeFileInfo, out bool isStackTraceHidden) + { + return DeveloperExperience.Default.CreateStackTraceString(_ipAddress, includeFileInfo, out isStackTraceHidden); + } +#endif } } diff --git a/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Diagnostics/StackTrace.NativeAot.Browser.cs b/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Diagnostics/StackTrace.NativeAot.Browser.cs index 4d9fd9d91577..1930282a9aee 100644 --- a/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Diagnostics/StackTrace.NativeAot.Browser.cs +++ b/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Diagnostics/StackTrace.NativeAot.Browser.cs @@ -1,65 +1,93 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System.Text; +using System.Runtime; +using System.Runtime.CompilerServices; using System.Runtime.InteropServices; namespace System.Diagnostics { public partial class StackTrace { - private readonly StringBuilder _builder = new StringBuilder(); + /// + /// Initialize the stack trace based on current thread and given initial frame index. + /// + [MethodImpl(MethodImplOptions.NoInlining)] + private unsafe void InitializeForCurrentThread(int skipFrames, bool needFileInfo) + { + const int SystemDiagnosticsStackDepth = 2; - [LibraryImport("*")] - private static unsafe partial int emscripten_get_callstack(int flags, byte* outBuf, int maxBytes); + int eipCount = RuntimeImports.RhpGetCurrentBrowserThreadStackTrace(0, Exception.ReportAllFramesAsJS()); + IntPtr[] eips = new IntPtr[eipCount]; + fixed (void* pEips = eips) + RuntimeImports.RhpGetCurrentBrowserThreadStackTrace((nuint)pEips, Exception.ReportAllFramesAsJS()); - private unsafe void InitializeForCurrentThread(int skipFrames, bool needFileInfo) + int skippedEipCount = Exception.SkipSystemFrames(eips, SystemDiagnosticsStackDepth); + InitializeForIpAddressArray(eips, skippedEipCount, skipFrames, needFileInfo); + } + + /// + /// Initialize the stack trace based on a given exception and initial frame index. + /// + private void InitializeForException(Exception exception, int skipFrames, bool needFileInfo) + { + InitializeForIpAddressArray(exception.GetStackIPs(), 0, skipFrames, needFileInfo); + } + + /// + /// Initialize the stack trace based on a given array of encoded IP addresses (EIPs). + /// + private void InitializeForIpAddressArray(IntPtr[] eips, int skippedEipCount, int skipFrames, bool needFileInfo) { - var backtraceBuffer = new byte[8192]; - int callstackLen; - // skip these 2: - // at S_P_CoreLib_System_Diagnostics_StackTrace__InitializeForCurrentThread (wasm-function[12314]:275) - // at S_P_CoreLib_System_Diagnostics_StackTrace___ctor_0(wasm-function[12724]:118) - skipFrames += 2; // METHODS_TO_SKIP is a constant so just change here + // Our callers may pass us values that have overflown. + if (skipFrames < 0) + skipFrames = int.MaxValue; - fixed (byte* curChar = backtraceBuffer) + // Calculate true frame count upfront - we need to skip EdiSeparators which get + // collapsed onto boolean flags on the preceding stack frame + int outputFrameCount = 0; + for (int eipIndex = skippedEipCount, actualFrameIndex = 0; eipIndex < eips.Length; actualFrameIndex++) { - callstackLen = emscripten_get_callstack(16 /* EM_LOG_JS_STACK */, curChar, backtraceBuffer.Length); + IntPtr eip = eips[eipIndex]; + if (actualFrameIndex >= skipFrames && eip != Exception.EdiSeparator) + { + outputFrameCount++; + } + + eipIndex += Exception.GetBrowserFrameLengthInChunks(eip); } - int _numOfFrames = 1; - int lineStartIx = 0; - int ix = 0; - for (; ix < callstackLen; ix++) + + if (outputFrameCount > 0) { - if (backtraceBuffer[ix] == '\n') + _stackFrames = new StackFrame[outputFrameCount]; + int outputFrameIndex = 0; + for (int eipIndex = skippedEipCount, actualFrameIndex = 0; eipIndex < eips.Length; actualFrameIndex++) { - if (_numOfFrames > skipFrames) + IntPtr eip = eips[eipIndex]; + if (actualFrameIndex >= skipFrames) { - _builder.Append(Encoding.Default.GetString(backtraceBuffer, lineStartIx, ix - lineStartIx + 1)); + if (outputFrameIndex >= outputFrameCount) + { + break; + } + + if (eip != Exception.EdiSeparator) + { + _stackFrames[outputFrameIndex++] = new StackFrame(eips, eipIndex, needFileInfo); + } + else if (outputFrameIndex > 0) + { + _stackFrames[outputFrameIndex - 1].SetIsLastFrameFromForeignExceptionStackTrace(); + } } - _numOfFrames++; - lineStartIx = ix + 1; + + eipIndex += Exception.GetBrowserFrameLengthInChunks(eip); } + Debug.Assert(outputFrameIndex == outputFrameCount); } - if (lineStartIx < ix) - { - _builder.AppendLine(Encoding.Default.GetString(backtraceBuffer, lineStartIx, ix - lineStartIx)); - } - _methodsToSkip = 0; - } - internal string ToString(TraceFormat traceFormat) - { - var stackTraceString = _builder.ToString(); - if (traceFormat == TraceFormat.Normal && stackTraceString.EndsWith(Environment.NewLine)) - return stackTraceString.Substring(0, stackTraceString.Length - Environment.NewLine.Length); - - return stackTraceString; - } - - internal void ToString(TraceFormat traceFormat, StringBuilder builder) - { - builder.Append(ToString(traceFormat)); + _numOfFrames = outputFrameCount; + _methodsToSkip = 0; } } } diff --git a/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Diagnostics/StackTrace.NativeAot.Wasi.cs b/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Diagnostics/StackTrace.NativeAot.Wasi.cs index f0854f320287..7c340d766756 100644 --- a/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Diagnostics/StackTrace.NativeAot.Wasi.cs +++ b/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Diagnostics/StackTrace.NativeAot.Wasi.cs @@ -4,24 +4,26 @@ #pragma warning disable CA1822 // Member does not access instance data and can be marked as static #pragma warning disable IDE0060 // Remove unused parameter -using System.Text; +using System; namespace System.Diagnostics { public partial class StackTrace { - private unsafe void InitializeForCurrentThread(int skipFrames, bool needFileInfo) + private void InitializeForCurrentThread(int skipFrames, bool needFileInfo) { - // There is now way, currently, to get the stack trace in WASI. + InitializeForIpAddressArray(null, 0, 0, needFileInfo); } - internal string ToString(TraceFormat traceFormat) + private void InitializeForException(Exception exception, int skipFrames, bool needFileInfo) { - return ""; + InitializeForIpAddressArray(null, 0, 0, needFileInfo); } - internal void ToString(TraceFormat traceFormat, StringBuilder builder) + private void InitializeForIpAddressArray(IntPtr[] ipAddresses, int skipFrames, int endFrameIndex, bool needFileInfo) { + _numOfFrames = 0; + _methodsToSkip = 0; } } } diff --git a/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Diagnostics/StackTrace.NativeAot.cs b/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Diagnostics/StackTrace.NativeAot.cs index f4c015efe34a..89539f2a1faa 100644 --- a/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Diagnostics/StackTrace.NativeAot.cs +++ b/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Diagnostics/StackTrace.NativeAot.cs @@ -25,7 +25,6 @@ private void InitializeForCurrentThread(int skipFrames, bool needFileInfo) Debug.Assert(trueFrameCount == frameCount); InitializeForIpAddressArray(stackTrace, skipFrames + SystemDiagnosticsStackDepth, frameCount, needFileInfo); } -#endif /// /// Initialize the stack trace based on a given exception and initial frame index. @@ -76,8 +75,8 @@ private void InitializeForIpAddressArray(IntPtr[] ipAddresses, int skipFrames, i _numOfFrames = outputFrameCount; _methodsToSkip = 0; } +#endif -#if !TARGET_WASM internal void ToString(TraceFormat traceFormat, StringBuilder builder) { if (_stackFrames != null) @@ -94,6 +93,5 @@ internal void ToString(TraceFormat traceFormat, StringBuilder builder) if (traceFormat == TraceFormat.TrailingNewLine && builder.Length == 0) builder.AppendLine(); } -#endif } } diff --git a/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Exception.NativeAot.Browser.cs b/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Exception.NativeAot.Browser.cs new file mode 100644 index 000000000000..b8a03def7e3b --- /dev/null +++ b/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Exception.NativeAot.Browser.cs @@ -0,0 +1,240 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics; +using System.Runtime; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace System +{ + public partial class Exception + { + private const int WasmFunctionIndexBias = 2; + private static int s_reportAllFramesAsJS; + + private IntPtr[] _eips; + private int _eipConsumedCount; + + [MethodImpl(MethodImplOptions.NoInlining)] + private unsafe void AppendStack(IntPtr ip, bool isFirstFrame, bool isFirstRethrowFrame) + { + Debug.Assert(!isFirstRethrowFrame || isFirstFrame); + + if (isFirstRethrowFrame) + { + // A rethrow means we should already have a valid stack trace data array on our hands. Just use it. + // The right 'catch' frame has already been appended by the previous dispatch - below should logically + // hold... but the stack trace could have been truncated, or is JS-only, so we can't assert it. + // Debug.Assert(IsWasmFrame(_eips[_eipConsumedCount - 2], out lastCatchIP) && ip == lastCatchIP); + // Q: perhaps we should re-initialize the array in the above case then, to supply more frames? + // A: "Resupplying" frames could be a valid strategy for all calls to this function, if we detect + // truncated input. But it could lead to confusing user experience: missing frames instead + // of the current 'small number of frames; disable truncation!'. So we leave this be for now. + Debug.Assert(_eips != null); + return; + } + + // On browser, we do stack trace handling for exceptions in two phases: + // 1. Initialize the whole stack trace - that's all the JS API can offer, and set the cursor to "zero" + // (no frames visible as yet). + // + if (isFirstFrame) + { + // Our implementation of "RhGetCurrentThreadStackTrace" caches the JS stack trace with this pattern, + // so, while we do have to parse the trace twice, we avoid a complex memory allocation contract. + int eipCount = RuntimeImports.RhpGetCurrentBrowserThreadStackTrace(0, ReportAllFramesAsJS()); + IntPtr[] allEips = new IntPtr[eipCount]; + fixed (void* pEips = allEips) + RuntimeImports.RhpGetCurrentBrowserThreadStackTrace((nuint)pEips, ReportAllFramesAsJS()); + + // Skip the following frames: + // AppendStack (this frame) + // AppendExceptionStackFrame + // DispatchException + // RhpThrowEx + int skipppedEipCount = SkipSystemFrames(allEips, skipCount: 4); + + // Q: could we use "_corDbgStackTrace" and "_idxFirstFreeStackTraceEntry" directly? + // A: yes, but it would require careful consideration of the interaction with all other mutation + // sources. Case to consider: filter calling "RestoreDispatchState". + _eips = allEips; + _eipConsumedCount = skipppedEipCount; + } + + // 2. Advance the cursor until it reaches the handling frame (or top of the stack, in case of an unhandled + // exception). + // + // A couple of things to consider here. + // 1. The stack trace data could be imprecise, due to, for example, engine-level inlining. Note, however, + // that in practice the JS engines are **very** good at preserving traces, even for inlined functions. + // This is fortunate for us because the various "skip N frames" places that exists can continue to + // function reliably even in the absense of WASM-level noinline annotation. + // 2. The stack trace data could be truncated (default NodeJS behavior). + // Both of these cases cause the frames to be missing from the trace, which means we will append "too much". + // That is mostly ok - our goal is to not append _too little_. + // + // The format of the Browser's "IP array" is a little complex. This is due to the requirements of storing + // native frames (strings copied verbatim from JS) "inline". Concretely, we can have the following values + // for "IP"s: + // 1. 0 - unknown/null frame. Takes two chunks to be the same size as a WASM frame. + // 2. 1 - 'EdiSeparator', as used elsewhere. + // 3. [2..int.MaxValue] - biased WASM function index. We have to bias the index because function index zero + // (and one) is a valid value, although it is probably impossible to construct a useful WASM binary that + // has it as a defined function (recall WASM indices start with imports). This index is followed by + // the function offset (file-relative, as reported by JS). + // 4. < 0 - encoded UTF16 strings that represent native frames. The encoding is as follows: + // [(1 << 31) | 'length' in UTF16 code points] + // [...] - the UTF16 code points themselves, tightly packed (the last 'chunk' may be padded with zeroes). + // + IntPtr[] eips = _eips; + int index = _eipConsumedCount; + while (index < eips.Length) + { + IntPtr eip = eips[index++]; + Debug.Assert(eip != EdiSeparator); // Should never appear in data from JS. + AppendStackIP(eip, false); + + int jsFrameLength = GetBrowserFrameInfoWithBias(eip, out int wasmFunctionIndexWithBias); + if (jsFrameLength is 0) + { + AppendStackIP(eips[index++], false); // Append the function offset. + + if (wasmFunctionIndexWithBias == ip) + { + // We have found the frame of interest. Note how function indices are not actually + // unique identifiers of frames due to recursion. However, it works out because we + // establish a new virtual unwind frame on entry to each function with EH, and then + // call this method for each said frame. + break; + } + } + else + { + int jsFrameLengthInChunks = GetJSFrameLengthInChunks(jsFrameLength); + for (int i = 0; i < jsFrameLengthInChunks; i++) + { + AppendStackIP(eips[index++], false); + } + } + } + + Debug.Assert(index <= eips.Length); + _eipConsumedCount = index; + } + + // The WASM binary may be modified post-link (e.g. by wasm-opt), which will invalidate our stack trace metadata. + // To handle this scenario gracefully, we fall back to using 'JS' frames for everything in that case. Obviously, + // this breaks all of managed stack trace features, but at least it allows ToString() to function and give back + // theoretically symbolicatable traces. + internal static unsafe int ReportAllFramesAsJS() + { + int reportAllFramesAsJS = s_reportAllFramesAsJS; + if (reportAllFramesAsJS is 0) + { + // To increase our chances of detecting post-link modification, the canary method is placed at the very + // end of the code section. Of course, this is not 100% reliable. E. g. it won't detect something like + // simple reordering of the functions (with the canary staying in place). That is acceptable. + delegate* pGetCanary = &RuntimeImports.RhpGetStackTraceIpCanary; + int expectedIp = GetBiasedWasmFunctionIndex(pGetCanary()); + int actualIp = RuntimeImports.RhpGetBiasedWasmFunctionIndexForFunctionPointer((nuint)pGetCanary); + s_reportAllFramesAsJS = reportAllFramesAsJS = (expectedIp == actualIp ? 0 : 1) + 1; + } + + return reportAllFramesAsJS - 1; + } + + internal static int SkipSystemFrames(IntPtr[] eips, int skipCount) + { + // Skip only if the stack trace is intact (we did not fall back to native frames). We could be + // even more precise here by comparing actual function indices, but the need for that has not + // yet been demonstrated. + int skipppedEipCount = 0; + for (int i = 0; i < skipCount; i++) + { + IntPtr eip = eips[skipppedEipCount]; + GetBrowserFrameInfoWithBias(eip, out int wasmFunctionIndexWithBias); + if (!IsValidBiasedWasmFunctionIndex(wasmFunctionIndexWithBias)) + { + skipppedEipCount = 0; + break; + } + + skipppedEipCount += GetBrowserFrameLengthInChunks(eip); + } + + return skipppedEipCount; + } + + // Keep the parsing code in sync with "RhpGetCurrentBrowserThreadStackTrace" in "StackTrace.Browser.cpp". + internal static int GetBrowserFrameInfoWithoutBias(nint eip, out int wasmFunctionIndex) + { + int jsFrameLength = GetBrowserFrameInfoWithBias(eip, out wasmFunctionIndex); + if (jsFrameLength is 0) + { + wasmFunctionIndex = GetUnbiasedWasmFunctionIndex(wasmFunctionIndex); + } + + return jsFrameLength; + } + + internal static int GetBrowserFrameInfoWithBias(nint eip, out int wasmFunctionIndexWithBias) + { + Debug.Assert(eip != EdiSeparator); + int actualEip = (int)eip; // Only the lower 32 bits are significant. + if (actualEip > 0) + { + wasmFunctionIndexWithBias = actualEip; + return 0; + } + + wasmFunctionIndexWithBias = 0; + return -actualEip; // Note that unknown frames turn into zero lengths here. + } + + internal static int GetBrowserFrameLengthInChunks(nint eip) + { + if (eip == EdiSeparator) + { + return 1; + } + int jsFrameLength = GetBrowserFrameInfoWithBias(eip, out _); + if (jsFrameLength != 0) + { + return 1 + GetJSFrameLengthInChunks(jsFrameLength); + } + return 2; + } + + internal static ReadOnlySpan GetJSFrame(IntPtr[] eips, int eipIndex) + { + int jsFrameLength = GetBrowserFrameInfoWithBias(eips[eipIndex], out _); + if (jsFrameLength is not 0) + { + ReadOnlySpan data = MemoryMarshal.Cast(eips.AsSpan(eipIndex + 1)); + ReadOnlySpan jsFrame = data.Slice(0, jsFrameLength); + return jsFrame; + } + return []; + } + + internal static int GetWasmFunctionOffset(nint[] eips, int eipIndex) + { + Debug.Assert(GetBrowserFrameInfoWithBias(eips[eipIndex], out int wasmFunctionIndexWithBias) is 0 && + IsValidBiasedWasmFunctionIndex(wasmFunctionIndexWithBias)); + return (int)eips[eipIndex + 1]; + } + + internal static bool IsValidBiasedWasmFunctionIndex(int wasmFunctionIndexWithBias) => wasmFunctionIndexWithBias >= WasmFunctionIndexBias; + + internal static int GetBiasedWasmFunctionIndex(int wasmFunctionIndex) => wasmFunctionIndex + WasmFunctionIndexBias; + + internal static int GetUnbiasedWasmFunctionIndex(int wasmFunctionIndexWithBias) + { + Debug.Assert(EdiSeparator + 1 == WasmFunctionIndexBias); + return wasmFunctionIndexWithBias - WasmFunctionIndexBias; + } + + private static int GetJSFrameLengthInChunks(int jsFrameLength) => (2 * jsFrameLength + (nint.Size - 1)) / nint.Size; + } +} diff --git a/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Exception.NativeAot.LLVM.cs b/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Exception.NativeAot.LLVM.cs deleted file mode 100644 index c9292f4e8715..000000000000 --- a/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Exception.NativeAot.LLVM.cs +++ /dev/null @@ -1,97 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.Diagnostics; -using System.Runtime.CompilerServices; -using System.Threading; - -namespace System -{ - public partial class Exception - { - // Since we track stack traces as strings, we need an equivalent to the EdiSeparator marker. - // When set, we should treat "throws" as rethrows that do not reset the stack trace. This is - // an imperfect emulation of the real thing, as we don't "append" frames, but rather capture - // the full trace upfront, still, it is better than nothing. - private bool _dispatchStateRestored; - - // TODO-LLVM: unify with "AppendExceptionStackFrame"; this is a partial copy. - [MethodImpl(MethodImplOptions.NoInlining)] - internal static unsafe void InitializeExceptionStackFrameLLVM(object exception, int flags) - { - // This method is called by the runtime's EH dispatch code and is not allowed to leak exceptions - // back into the dispatcher. - try - { - Exception? ex = exception as Exception; - if (ex == null) - Environment.FailFast("Exceptions must derive from the System.Exception class"); - - if (!RuntimeExceptionHelpers.SafeToPerformRichExceptionSupport) - return; - - bool isFirstRethrowFrame = (flags & (int)RhEHFrameType.RH_EH_FIRST_RETHROW_FRAME) != 0; - - // track count for metrics - if (!isFirstRethrowFrame) - Interlocked.Increment(ref s_exceptionCount); - - // When we're throwing an exception object, we reset its stacktrace with two exceptions: - // 1. Don't clear if we're rethrowing with `throw;`. - // 2. Don't clear if we're throwing through ExceptionDispatchInfo. - // This is done through invoking RestoreDispatchState which sets "_dispatchStateRestored" followed by throwing normally using `throw ex;`. - bool doSetTheStackTrace = !isFirstRethrowFrame && !ex._dispatchStateRestored; - - // If out of memory, avoid any calls that may allocate. Otherwise, they may fail - // with another OutOfMemoryException, which may lead to infinite recursion. - bool fatalOutOfMemory = ex == PreallocatedOutOfMemoryException.Instance; - - if (doSetTheStackTrace && !fatalOutOfMemory) - ex._stackTraceString = new StackTrace(1).ToString().Replace("__", ".").Replace("_", "."); - -#if FEATURE_PERFTRACING - string typeName = !fatalOutOfMemory ? ex.GetType().ToString() : "System.OutOfMemoryException"; - string message = !fatalOutOfMemory ? ex.Message : "Insufficient memory to continue the execution of the program."; - - fixed (char* exceptionTypeName = typeName, exceptionMessage = message) - Runtime.RuntimeImports.NativeRuntimeEventSource_LogExceptionThrown(exceptionTypeName, exceptionMessage, 0, ex.HResult); -#endif - } - catch - { - // We may end up with a confusing stack trace or a confusing ETW trace log, but at least we - // can continue to dispatch this exception. - } - } - - //================================================================================================================== - // Support for ExceptionDispatchInfo class - imports and exports the stack trace. - //================================================================================================================== - - internal DispatchState CaptureDispatchState() - { - return new DispatchState(_stackTraceString); - } - - internal void RestoreDispatchState(DispatchState DispatchState) - { - // Since EDI can be created at various points during exception dispatch (e.g. at various frames on the stack) for the same exception instance, - // they can have different data to be restored. Thus, to ensure atomicity of restoration from each EDI, perform the restore under a lock. - lock (s_DispatchStateLock) - { - _stackTraceString = DispatchState.StackTrace; - _dispatchStateRestored = true; - } - } - - internal readonly struct DispatchState - { - public readonly string StackTrace; - - public DispatchState(string stackTrace) - { - StackTrace = stackTrace; - } - } - } -} diff --git a/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Exception.NativeAot.Wasi.cs b/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Exception.NativeAot.Wasi.cs new file mode 100644 index 000000000000..bb88098bf2e4 --- /dev/null +++ b/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Exception.NativeAot.Wasi.cs @@ -0,0 +1,16 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#pragma warning disable CA1822 // Member does not access instance data and can be marked as static +#pragma warning disable IDE0060 // Remove unused parameter + +namespace System +{ + public partial class Exception + { + private void AppendStack(IntPtr ip, bool isFirstFrame, bool isFirstRethrowFrame) + { + // TODO-LLVM: implement managed stack traces on WASI. + } + } +} diff --git a/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Exception.NativeAot.cs b/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Exception.NativeAot.cs index b875712473f0..aef1e0c07fa7 100644 --- a/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Exception.NativeAot.cs +++ b/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Exception.NativeAot.cs @@ -23,7 +23,11 @@ public MethodBase? TargetSite if (!HasBeenThrown) return null; +#if TARGET_BROWSER + return new StackFrame(_corDbgStackTrace, 0, needFileInfo: false).GetMethod(); +#else return new StackFrame(_corDbgStackTrace[0], needFileInfo: false).GetMethod(); +#endif } } @@ -133,7 +137,11 @@ private static void AppendExceptionStackFrame(object exceptionObj, IntPtr IP, in bool fatalOutOfMemory = ex == PreallocatedOutOfMemoryException.Instance; if (!fatalOutOfMemory) +#if TARGET_WASM + ex.AppendStack(IP, isFirstFrame, isFirstRethrowFrame); +#else ex.AppendStackIP(IP, isFirstRethrowFrame); +#endif #if FEATURE_PERFTRACING if (isFirstFrame) @@ -157,7 +165,6 @@ private static void AppendExceptionStackFrame(object exceptionObj, IntPtr IP, in } } -#if !TARGET_WASM //================================================================================================================== // Support for ExceptionDispatchInfo class - imports and exports the stack trace. //================================================================================================================== @@ -206,7 +213,6 @@ public DispatchState(IntPtr[]? stackTrace) StackTrace = stackTrace; } } -#endif // This is the object against which a lock will be taken // when attempt to restore the EDI. Since its static, its possible diff --git a/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Reflection/Runtime/General/ReflectionCoreCallbacksImplementation.cs b/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Reflection/Runtime/General/ReflectionCoreCallbacksImplementation.cs index bcf6f6949034..a60d38a3596b 100644 --- a/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Reflection/Runtime/General/ReflectionCoreCallbacksImplementation.cs +++ b/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Reflection/Runtime/General/ReflectionCoreCallbacksImplementation.cs @@ -446,6 +446,11 @@ public sealed override MethodInfo GetDelegateMethod(Delegate del) return ReflectionCoreExecution.ExecutionEnvironment.GetDelegateMethod(del); } + public sealed override IntPtr ConvertStackTraceIpToFunctionPointer(IntPtr methodStartAddress) + { + return ReflectionCoreExecution.ExecutionEnvironment.ConvertStackTraceIpToFunctionPointer(methodStartAddress); + } + public sealed override MethodBase GetMethodBaseFromStartAddressIfAvailable(IntPtr methodStartAddress) { return ReflectionCoreExecution.ExecutionEnvironment.GetMethodBaseFromStartAddressIfAvailable(methodStartAddress); diff --git a/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Runtime/RuntimeImports.Wasm.cs b/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Runtime/RuntimeImports.Wasm.cs index f5fd9b8f3915..f4c27cede485 100644 --- a/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Runtime/RuntimeImports.Wasm.cs +++ b/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Runtime/RuntimeImports.Wasm.cs @@ -6,8 +6,12 @@ // assumes a managed calling convention (with a shadow stack), while those // functions are native. +using System.Runtime.CompilerServices; using System.Runtime.InteropServices; +#pragma warning disable SA1121 // Use built-in type alias +using JSPointerType = System.Double; + namespace System.Runtime { internal static partial class RuntimeImports @@ -211,5 +215,19 @@ internal static partial class RuntimeImports [LibraryImport(RuntimeLibrary)] [SuppressGCTransition] internal static unsafe partial float modff(float x, float* intptr); + + [MethodImpl(MethodImplOptions.InternalCall)] + [RuntimeImport(RuntimeLibrary, "RhpGetCurrentBrowserThreadStackTrace")] + internal static extern unsafe int RhpGetCurrentBrowserThreadStackTrace(JSPointerType pOutputBuffer, int allFramesAsJS); + + [MethodImpl(MethodImplOptions.InternalCall)] + [RuntimeImport(RuntimeLibrary, "RhpGetStackTraceIpCanary")] + internal static extern unsafe int RhpGetStackTraceIpCanary(); + + [LibraryImport(RuntimeLibrary, EntryPoint = "RhpGetBiasedWasmFunctionIndexForFunctionPointer")] + internal static unsafe partial int RhpGetBiasedWasmFunctionIndexForFunctionPointer(JSPointerType functionPointer); + + [LibraryImport(RuntimeLibrary, EntryPoint = "RhpInitializeStackTraceIpMap")] + internal static unsafe partial void RhpInitializeStackTraceIpMap(JSPointerType pEntries, int count); } } diff --git a/src/coreclr/nativeaot/System.Private.DisabledReflection/src/Internal/Reflection/ReflectionCoreCallbacksImplementation.cs b/src/coreclr/nativeaot/System.Private.DisabledReflection/src/Internal/Reflection/ReflectionCoreCallbacksImplementation.cs index c787fe99d2d6..ce29b29898ad 100644 --- a/src/coreclr/nativeaot/System.Private.DisabledReflection/src/Internal/Reflection/ReflectionCoreCallbacksImplementation.cs +++ b/src/coreclr/nativeaot/System.Private.DisabledReflection/src/Internal/Reflection/ReflectionCoreCallbacksImplementation.cs @@ -50,6 +50,7 @@ public override object ActivatorCreateInstance( public override void MakeTypedReference(object target, FieldInfo[] flds, out Type type, out int offset) => throw new NotSupportedException(SR.Reflection_Disabled); public override Assembly GetAssemblyForHandle(RuntimeTypeHandle typeHandle) => new RuntimeAssemblyInfo(typeHandle); public override void RunClassConstructor(RuntimeTypeHandle typeHandle) => throw new NotSupportedException(SR.Reflection_Disabled); + public override IntPtr ConvertStackTraceIpToFunctionPointer(IntPtr methodStartAddress) => 0; public override MethodBase GetMethodBaseFromStartAddressIfAvailable(IntPtr methodStartAddress) => null; } } diff --git a/src/coreclr/nativeaot/System.Private.Reflection.Execution/src/Internal/Reflection/Execution/ExecutionEnvironmentImplementation.MappingTables.cs b/src/coreclr/nativeaot/System.Private.Reflection.Execution/src/Internal/Reflection/Execution/ExecutionEnvironmentImplementation.MappingTables.cs index fd9fba5b7d12..027b11f21c8d 100644 --- a/src/coreclr/nativeaot/System.Private.Reflection.Execution/src/Internal/Reflection/Execution/ExecutionEnvironmentImplementation.MappingTables.cs +++ b/src/coreclr/nativeaot/System.Private.Reflection.Execution/src/Internal/Reflection/Execution/ExecutionEnvironmentImplementation.MappingTables.cs @@ -1,7 +1,12 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +#if TARGET_BROWSER +#define FEATURE_IP_TO_FUNCTION_POINTER_MAP +#endif + using System.Reflection.Runtime.General; +using System.Runtime.CompilerServices; using System.Threading; using global::Internal.Metadata.NativeFormat; @@ -445,6 +450,27 @@ public int CompareTo(FunctionPointerOffsetPair other) private struct FunctionPointersToOffsets { +#if FEATURE_IP_TO_FUNCTION_POINTER_MAP + public RuntimeAugments.StackTraceIpAndFunctionPointer[] StackTraceIpMap; + + public bool TryGetFunctionPointer(IntPtr methodStartAddress, out IntPtr functionPointer) + { + if (StackTraceIpMap != null) + { + var item = new RuntimeAugments.StackTraceIpAndFunctionPointer() { StackTraceIp = methodStartAddress }; + int index = Array.BinarySearch(StackTraceIpMap, item); + if (index > 0) + { + functionPointer = StackTraceIpMap[index].FunctionPointer; + Debug.Assert(functionPointer != 0); + return true; + } + } + + functionPointer = 0; + return false; + } +#endif public FunctionPointerOffsetPair[] Data; public bool TryGetOffsetsRange(IntPtr functionPointer, out int firstParserOffsetIndex, out int lastParserOffsetIndex) @@ -500,6 +526,25 @@ private KeyValuePair[] GetLdF return ldFtnReverseLookup; } + public sealed override IntPtr ConvertStackTraceIpToFunctionPointer(IntPtr methodStartAddress) + { +#if FEATURE_IP_TO_FUNCTION_POINTER_MAP + foreach ((_, FunctionPointersToOffsets data) in GetLdFtnReverseLookups_InvokeMap()) + { + if (data.TryGetFunctionPointer(methodStartAddress, out nint functionPointer)) + { + return functionPointer; + } + } + + // Not in the InvokeMap, return null. + return 0; +#else + // For other targets, IPs and function pointers are in the same address space. + return methodStartAddress; +#endif + } + internal unsafe void GetFunctionPointerAndInstantiationArgumentForOriginalLdFtnResult(IntPtr originalLdFtnResult, out IntPtr canonOriginalLdFtnResult, out IntPtr instantiationArgument) { if (FunctionPointerOps.IsGenericMethodPointer(originalLdFtnResult)) @@ -637,6 +682,19 @@ private static FunctionPointersToOffsets ComputeLdftnReverseLookup_InvokeMap(Nat functionPointerToOffsetInInvokeMap.Data = functionPointers.ToArray(); Array.Sort(functionPointerToOffsetInInvokeMap.Data); +#if FEATURE_IP_TO_FUNCTION_POINTER_MAP + FunctionPointerOffsetPair[] data = functionPointerToOffsetInInvokeMap.Data; + var stackTraceIpMap = new RuntimeAugments.StackTraceIpAndFunctionPointer[data.Length]; + for (int i = 0; i < data.Length; i++) + { + stackTraceIpMap[i].FunctionPointer = data[i].FunctionPointer; + } + + RuntimeAugments.InitializeStackTraceIpMap(stackTraceIpMap); + Array.Sort(stackTraceIpMap); + functionPointerToOffsetInInvokeMap.StackTraceIpMap = stackTraceIpMap; +#endif + return functionPointerToOffsetInInvokeMap; } diff --git a/src/coreclr/nativeaot/System.Private.Reflection.Execution/src/Internal/Reflection/Execution/ReflectionExecution.cs b/src/coreclr/nativeaot/System.Private.Reflection.Execution/src/Internal/Reflection/Execution/ReflectionExecution.cs index 749948d34036..71e470dd3836 100644 --- a/src/coreclr/nativeaot/System.Private.Reflection.Execution/src/Internal/Reflection/Execution/ReflectionExecution.cs +++ b/src/coreclr/nativeaot/System.Private.Reflection.Execution/src/Internal/Reflection/Execution/ReflectionExecution.cs @@ -51,6 +51,13 @@ internal static void Initialize() ExecutionEnvironment = executionEnvironment; } + public static IntPtr ConvertStackTraceIpToFunctionPointer(IntPtr methodStartAddress) + { + if (ExecutionEnvironment == null) + return 0; + return ExecutionEnvironment.ConvertStackTraceIpToFunctionPointer(methodStartAddress); + } + public static bool TryGetMethodMetadataFromStartAddress(IntPtr methodStartAddress, out MetadataReader reader, out TypeDefinitionHandle typeHandle, out MethodHandle methodHandle) { reader = null; diff --git a/src/coreclr/nativeaot/System.Private.StackTraceMetadata/src/Internal/StackTraceMetadata/StackTraceMetadata.cs b/src/coreclr/nativeaot/System.Private.StackTraceMetadata/src/Internal/StackTraceMetadata/StackTraceMetadata.cs index 15c65d5e9dd6..2c2b6c05ad35 100644 --- a/src/coreclr/nativeaot/System.Private.StackTraceMetadata/src/Internal/StackTraceMetadata/StackTraceMetadata.cs +++ b/src/coreclr/nativeaot/System.Private.StackTraceMetadata/src/Internal/StackTraceMetadata/StackTraceMetadata.cs @@ -14,6 +14,7 @@ using Internal.TypeSystem; using Debug = System.Diagnostics.Debug; +using Unsafe = System.Runtime.CompilerServices.Unsafe; using ReflectionExecution = Internal.Reflection.Execution.ReflectionExecution; namespace Internal.StackTraceMetadata @@ -70,6 +71,7 @@ public static unsafe string GetMethodNameFromStartAddressIfAvailable(IntPtr meth isStackTraceHidden = false; // We haven't found information in the stack trace metadata tables, but maybe reflection will have this + methodStartAddress = ReflectionExecution.ConvertStackTraceIpToFunctionPointer(methodStartAddress); if (IsReflectionExecutionAvailable() && ReflectionExecution.TryGetMethodMetadataFromStartAddress(methodStartAddress, out MetadataReader reader, out TypeDefinitionHandle typeHandle, @@ -125,6 +127,7 @@ public static unsafe string GetMethodNameFromStartAddressIfAvailable(IntPtr meth } // We haven't found information in the stack trace metadata tables, but maybe reflection will have this + methodStartAddress = ReflectionExecution.ConvertStackTraceIpToFunctionPointer(methodStartAddress); if (IsReflectionExecutionAvailable() && ReflectionExecution.TryGetMethodMetadataFromStartAddress(methodStartAddress, out MetadataReader reader, out TypeDefinitionHandle typeHandle, @@ -386,8 +389,13 @@ private unsafe void PopulateRvaToTokenMap(TypeManagerHandle handle, byte* pMap, GenericArguments = currentMethodInst, }; +#if TARGET_BROWSER + static void* ReadRelPtr32(byte* address) + => (void*)RuntimeAugments.GetBiasedWasmFunctionIndex(Unsafe.ReadUnaligned(address)); +#else static void* ReadRelPtr32(byte* address) => address + *(int*)address; +#endif } Debug.Assert(current == _stacktraceDatas.Length); @@ -421,7 +429,13 @@ public bool TryGetStackTraceData(int rva, out StackTraceData data) public struct StackTraceData : IComparable { +#if TARGET_BROWSER + // Browser/WASM uses function indices, which are not 'aligned', so we can't use 0x2. + // (We could bias them to be aligned, but it would complicate JS, hmm...). + private const int IsHiddenFlag = 1 << 31; +#else private const int IsHiddenFlag = 0x2; +#endif private readonly int _rvaAndIsHiddenBit; diff --git a/src/coreclr/tools/Common/Compiler/DependencyAnalysis/ObjectDataBuilder.cs b/src/coreclr/tools/Common/Compiler/DependencyAnalysis/ObjectDataBuilder.cs index 97c300af93a1..becf9cfda16c 100644 --- a/src/coreclr/tools/Common/Compiler/DependencyAnalysis/ObjectDataBuilder.cs +++ b/src/coreclr/tools/Common/Compiler/DependencyAnalysis/ObjectDataBuilder.cs @@ -289,7 +289,7 @@ public void EmitReloc(ISymbolNode symbol, RelocType relocType, int delta = 0) } } break; - case RelocType.R_WASM_FUNCTION_OFFSET_I32: + case RelocType.R_WASM_FUNCTION_INDEX_I32: case RelocType.IMAGE_REL_BASED_REL32: case RelocType.IMAGE_REL_BASED_RELPTR32: case RelocType.IMAGE_REL_BASED_ABSOLUTE: diff --git a/src/coreclr/tools/Common/Compiler/DependencyAnalysis/Relocation.cs b/src/coreclr/tools/Common/Compiler/DependencyAnalysis/Relocation.cs index 430f99031fd0..25dad4a3d533 100644 --- a/src/coreclr/tools/Common/Compiler/DependencyAnalysis/Relocation.cs +++ b/src/coreclr/tools/Common/Compiler/DependencyAnalysis/Relocation.cs @@ -70,8 +70,8 @@ public enum RelocType // // WASM relocations. // - R_WASM_FUNCTION_OFFSET_I32, // Offset of a function relative to the Code section. - R_WASM_FUNCTION_INDEX_LEB, // 32 bit function index, used by the call instruction. + R_WASM_FUNCTION_INDEX_I32, // 32 bit function index. + R_WASM_FUNCTION_INDEX_LEB, // 32 bit function index LEB, used by the call instruction. R_WASM_MEMORY_ADDR_SLEB, // 32 bit signed LEB for data references in code (i32.const). R_WASM_TABLE_INDEX_SLEB, // 32 bit signed LEB for function pointer references in code (i32.const). R_WASM_MEMORY_ADDR_SLEB64, // 64 bit signed LEB for data references in code (i64.const). @@ -572,7 +572,7 @@ public static unsafe void WriteValue(RelocType relocType, void* location, long v PutSLeb128((byte*)location, value, 10); break; #endif - case RelocType.R_WASM_FUNCTION_OFFSET_I32: + case RelocType.R_WASM_FUNCTION_INDEX_I32: case RelocType.IMAGE_REL_BASED_ABSOLUTE: case RelocType.IMAGE_REL_BASED_ADDR32NB: case RelocType.IMAGE_REL_BASED_HIGHLOW: @@ -653,7 +653,7 @@ public static unsafe long ReadValue(RelocType relocType, void* location) case RelocType.R_WASM_MEMORY_ADDR_SLEB64: return GetSLeb128((byte*)location); #endif - case RelocType.R_WASM_FUNCTION_OFFSET_I32: + case RelocType.R_WASM_FUNCTION_INDEX_I32: case RelocType.IMAGE_REL_BASED_ABSOLUTE: case RelocType.IMAGE_REL_BASED_ADDR32NB: case RelocType.IMAGE_REL_BASED_HIGHLOW: diff --git a/src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/DependencyAnalysis/StackTraceMethodMappingNode.cs b/src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/DependencyAnalysis/StackTraceMethodMappingNode.cs index e01dd403a215..16873c132bed 100644 --- a/src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/DependencyAnalysis/StackTraceMethodMappingNode.cs +++ b/src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/DependencyAnalysis/StackTraceMethodMappingNode.cs @@ -127,7 +127,7 @@ public override ObjectData GetData(NodeFactory factory, bool relocsOnly = false) command |= StackTraceDataCommand.IsStackTraceHidden; } - RelocType reloc = factory.Target.IsWasm ? RelocType.R_WASM_FUNCTION_OFFSET_I32 : RelocType.IMAGE_REL_BASED_RELPTR32; + RelocType reloc = factory.Target.IsWasm ? RelocType.R_WASM_FUNCTION_INDEX_I32 : RelocType.IMAGE_REL_BASED_RELPTR32; objData.EmitByte(commandReservation, command); objData.EmitReloc(factory.MethodEntrypoint(entry.Method), reloc); } diff --git a/src/coreclr/tools/aot/ILCompiler.LLVM/CodeGen/WasmObjectWriter.EmitObject.cs b/src/coreclr/tools/aot/ILCompiler.LLVM/CodeGen/WasmObjectWriter.EmitObject.cs index 13c4095e0c8c..56db30c15c93 100644 --- a/src/coreclr/tools/aot/ILCompiler.LLVM/CodeGen/WasmObjectWriter.EmitObject.cs +++ b/src/coreclr/tools/aot/ILCompiler.LLVM/CodeGen/WasmObjectWriter.EmitObject.cs @@ -2,12 +2,16 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Collections.Generic; +using System.Linq; using System.IO; using ILCompiler.DependencyAnalysis; using ILCompiler.DependencyAnalysisFramework; using ObjectData = ILCompiler.DependencyAnalysis.ObjectNode.ObjectData; +using Internal.TypeSystem; +using Internal.Text; + namespace ILCompiler.ObjectWriter { internal partial class WasmObjectWriter @@ -20,6 +24,13 @@ public static void EmitObject(string objectFilePath, IEnumerable WasmObjectWriter mainObjectWriter = new WasmObjectWriter(compilation); NodeFactory factory = compilation.NodeFactory; + // Add in the stack canary definition. TODO-LLVM: move this definition to the + // runtime once https://github.com/llvm/llvm-project/issues/100733 is fixed. + if (factory.Target.OperatingSystem == TargetOS.Browser) + { + nodes = nodes.Append(new StackTraceIpCanaryNode()); + } + foreach (DependencyNode depNode in nodes) { ObjectNode node = depNode as ObjectNode; @@ -68,5 +79,26 @@ public static void EmitObject(string objectFilePath, IEnumerable compilationResults.SerializeToFile(Path.ChangeExtension(objectFilePath, "results.txt")); } + + private sealed class StackTraceIpCanaryNode : ObjectNode, ISymbolDefinitionNode + { + public int Offset => 0; + public override bool IsShareable => false; + public override int ClassCode => 1933105605; + public override bool StaticDependenciesAreComputed => true; + + public override ObjectData GetData(NodeFactory factory, bool relocsOnly) + { + byte[] data = new byte[4]; + ExternSymbolNode canary = new ExternSymbolNode("RhpGetStackTraceIpCanary"); + Relocation reloc = new Relocation(RelocType.R_WASM_FUNCTION_INDEX_I32, 0, canary); + return new ObjectData(data, [reloc], data.Length, [this]); + } + + public void AppendMangledName(NameMangler nameMangler, Utf8StringBuilder sb) => sb.Append("RhpStackTraceIpCanary"u8); + public override ObjectNodeSection GetSection(NodeFactory factory) => ObjectNodeSection.DataSection; + + protected override string GetName(NodeFactory factory) => this.GetMangledName(factory.NameMangler); + } } } diff --git a/src/coreclr/tools/aot/ILCompiler.LLVM/CodeGen/WasmObjectWriter.cs b/src/coreclr/tools/aot/ILCompiler.LLVM/CodeGen/WasmObjectWriter.cs index c7fe40294f2b..f650637784f3 100644 --- a/src/coreclr/tools/aot/ILCompiler.LLVM/CodeGen/WasmObjectWriter.cs +++ b/src/coreclr/tools/aot/ILCompiler.LLVM/CodeGen/WasmObjectWriter.cs @@ -730,8 +730,8 @@ private WasmRelocationKind GetWasmRelocationKind(ref readonly Relocation relocat Debug.Assert(SymbolInfo.IsDataSymbol(relocation.Target)); return _is64Bit ? R_WASM_MEMORY_ADDR_I64 : R_WASM_MEMORY_ADDR_I32; - case RelocType.R_WASM_FUNCTION_OFFSET_I32: - return R_WASM_FUNCTION_OFFSET_I32; + case RelocType.R_WASM_FUNCTION_INDEX_I32: + return R_WASM_FUNCTION_INDEX_I32; case RelocType.R_WASM_FUNCTION_INDEX_LEB: return R_WASM_FUNCTION_INDEX_LEB; case RelocType.R_WASM_MEMORY_ADDR_SLEB: diff --git a/src/coreclr/tools/aot/ILCompiler.RyuJit/JitInterface/CorInfoImpl.Llvm.cs b/src/coreclr/tools/aot/ILCompiler.RyuJit/JitInterface/CorInfoImpl.Llvm.cs index 5dbdbd7305ec..81ddac5cced5 100644 --- a/src/coreclr/tools/aot/ILCompiler.RyuJit/JitInterface/CorInfoImpl.Llvm.cs +++ b/src/coreclr/tools/aot/ILCompiler.RyuJit/JitInterface/CorInfoImpl.Llvm.cs @@ -203,14 +203,7 @@ private static IntPtr getExceptionHandlingTable(IntPtr thisHandle, CORINFO_LLVM_ { CorInfoImpl _this = GetThis(thisHandle); RyuJitCompilation compilation = _this._compilation; - MethodIL methodIL = (MethodIL)_this.HandleToObject((void*)_this._methodScope); - if (count == 1 && pClauses[0].Flags == CORINFO_EH_CLAUSE_FLAGS.CORINFO_EH_CLAUSE_NONE && pClauses[0].EnclosingIndex == 0) - { - TypeDesc type = (TypeDesc)methodIL.GetObject((int)pClauses[0].ClauseTypeToken); - ISymbolNode symbol = compilation.NecessaryTypeSymbolIfPossible(type); - - return _this.ObjectToHandle(symbol); - } + MethodIL methodIL = (MethodIL)_this.HandleToObject((void*)_this._methodScope); // Assumes no inlining of EH. uint maxEclosingIndex = 0; for (int i = 0; i < count; i++) @@ -218,24 +211,19 @@ private static IntPtr getExceptionHandlingTable(IntPtr thisHandle, CORINFO_LLVM_ maxEclosingIndex = Math.Max(pClauses[i].EnclosingIndex, maxEclosingIndex); } - const int MetadataFilter = 1; - const int MetadataShift = 1; + const int MetadataLargeFormat = 1; + const int MetadataFilter = 1 << 1; + const int MetadataShift = 2; - int align = compilation.NodeFactory.Target.PointerSize; + Utf8StringBuilder sb = new(); ObjectDataBuilder builder = new(compilation.NodeFactory, relocsOnly: true); - builder.RequireInitialAlignment(align); + bool isLargeFormat = maxEclosingIndex > (byte.MaxValue >> MetadataShift); - bool isSmallFormat = maxEclosingIndex <= (byte.MaxValue >> MetadataShift); - if (isSmallFormat) - { - builder.EmitZeros(align - count % align); - } - else - { - builder.EmitZeros(align - 4 * count % align); - } + // EH info is prefixed by the stack trace IP. + builder.EmitReloc(_this._methodCodeNode, RelocType.R_WASM_FUNCTION_INDEX_I32); + int symbolDefOffset = builder.CountBytes; - for (int i = count - 1; i >= 0; i--) + for (int i = 0; i < count; i++) { CORINFO_LLVM_EH_CLAUSE* pClause = &pClauses[i]; uint metadata = pClause->EnclosingIndex << MetadataShift; @@ -244,25 +232,21 @@ private static IntPtr getExceptionHandlingTable(IntPtr thisHandle, CORINFO_LLVM_ metadata |= MetadataFilter; } - if (isSmallFormat) + if (isLargeFormat) { - Debug.Assert((byte)metadata == metadata); - builder.EmitByte((byte)metadata); + if (i == 0) + { + metadata |= MetadataLargeFormat; + } + + // Note how this is little endian, so the format metadata will always be in the first byte. + builder.EmitUInt(metadata); } else { - builder.EmitUInt(metadata); + Debug.Assert((byte)metadata == metadata); + builder.EmitByte((byte)metadata); } - } - - // This is the offset at which which the EH info symbol will be defined. - int symbolDefOffset = builder.CountBytes + (isSmallFormat ? 1 : 2); - Debug.Assert(builder.CountBytes % align == 0); - - Utf8StringBuilder sb = new(); - for (int i = 0; i < count; i++) - { - CORINFO_LLVM_EH_CLAUSE* pClause = &pClauses[i]; ISymbolNode symbol; if ((pClause->Flags & CORINFO_EH_CLAUSE_FLAGS.CORINFO_EH_CLAUSE_FILTER) != 0) diff --git a/src/libraries/System.Diagnostics.StackTrace/tests/StackFrameTests.cs b/src/libraries/System.Diagnostics.StackTrace/tests/StackFrameTests.cs index b8f225f33d8c..4ee22f45ac4e 100644 --- a/src/libraries/System.Diagnostics.StackTrace/tests/StackFrameTests.cs +++ b/src/libraries/System.Diagnostics.StackTrace/tests/StackFrameTests.cs @@ -197,6 +197,11 @@ private static void VerifyStackFrameSkipFrames(StackFrame stackFrame, bool isFil { Assert.Equal(StackFrame.OFFSET_UNKNOWN, stackFrame.GetNativeOffset()); } + else if (PlatformDetection.IsNativeAot && PlatformDetection.IsWasm) + { + // We have decided not to return anything here for now, for consistency between Browser and WASI. + Assert.Equal(StackFrame.OFFSET_UNKNOWN, stackFrame.GetNativeOffset()); + } else if (skipFrames <= 0) { Assert.True(stackFrame.GetNativeOffset() > 0, $"Expected GetNativeOffset() {stackFrame.GetNativeOffset()} for {stackFrame} to be greater than zero."); diff --git a/src/libraries/System.Private.CoreLib/src/System/Diagnostics/StackFrame.cs b/src/libraries/System.Private.CoreLib/src/System/Diagnostics/StackFrame.cs index 6041a2c88bf0..da09836b2524 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Diagnostics/StackFrame.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Diagnostics/StackFrame.cs @@ -151,10 +151,13 @@ public StackFrame(string? fileName, int lineNumber, int colNumber) /// public virtual int GetNativeOffset() { +#if NATIVEAOT && TARGET_BROWSER + return GetNativeOffsetImpl(); +#else return _nativeOffset; +#endif } - /// /// Returns the offset from the start of the IL code for the /// method being executed. This offset may be approximate depending diff --git a/src/libraries/System.Private.CoreLib/src/System/Diagnostics/StackTrace.cs b/src/libraries/System.Private.CoreLib/src/System/Diagnostics/StackTrace.cs index c980ab2b5c16..7fdfef8f6d34 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Diagnostics/StackTrace.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Diagnostics/StackTrace.cs @@ -198,7 +198,6 @@ internal enum TraceFormat TrailingNewLine, // include a trailing new line character } -#if !TARGET_WASM /// /// Builds a readable representation of the stack trace, specifying /// the format for backwards compatibility. @@ -209,7 +208,6 @@ internal string ToString(TraceFormat traceFormat) ToString(traceFormat, sb); return sb.ToString(); } -#endif #if !NATIVEAOT [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2026:RequiresUnreferencedCode", diff --git a/src/libraries/System.Runtime/tests/System.Runtime.Tests/System.Runtime.Tests.csproj b/src/libraries/System.Runtime/tests/System.Runtime.Tests/System.Runtime.Tests.csproj index 35f141d99693..8902ba0fbbcf 100644 --- a/src/libraries/System.Runtime/tests/System.Runtime.Tests/System.Runtime.Tests.csproj +++ b/src/libraries/System.Runtime/tests/System.Runtime.Tests/System.Runtime.Tests.csproj @@ -33,12 +33,6 @@ - - - - - - diff --git a/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/ExceptionTests.cs b/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/ExceptionTests.cs index 4115970310f6..5167baa2feb5 100644 --- a/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/ExceptionTests.cs +++ b/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/ExceptionTests.cs @@ -12,7 +12,7 @@ namespace System.Tests { - [ActiveIssue("https://github.com/dotnet/runtimelab/issues/2404", typeof(PlatformDetection), nameof(PlatformDetection.IsNativeAot), nameof(PlatformDetection.IsWasm))] + [ActiveIssue("https://github.com/dotnet/runtimelab/issues/2404", typeof(PlatformDetection), nameof(PlatformDetection.IsNativeAot), nameof(PlatformDetection.IsWasi))] public static class ExceptionTests { private const int COR_E_EXCEPTION = unchecked((int)0x80131500); diff --git a/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/Runtime/ExceptionServices/ExceptionDispatchInfoTests.cs b/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/Runtime/ExceptionServices/ExceptionDispatchInfoTests.cs index 20223a84a2fd..b5bb06c24ada 100644 --- a/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/Runtime/ExceptionServices/ExceptionDispatchInfoTests.cs +++ b/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/Runtime/ExceptionServices/ExceptionDispatchInfoTests.cs @@ -17,7 +17,7 @@ public static void StaticThrow_NullArgument_ThrowArgumentNullException() } [Fact] - [ActiveIssue("https://github.com/dotnet/runtimelab/issues/2404", typeof(PlatformDetection), nameof(PlatformDetection.IsNativeAot), nameof(PlatformDetection.IsWasm))] + [ActiveIssue("https://github.com/dotnet/runtimelab/issues/2404", typeof(PlatformDetection), nameof(PlatformDetection.IsNativeAot), nameof(PlatformDetection.IsWasi))] public static void StaticThrow_UpdatesStackTraceAppropriately() { const string RethrowMessageSubstring = "End of stack trace"; diff --git a/src/tests/Common/dirs.proj b/src/tests/Common/dirs.proj index 6f41911e03ed..bc02a0783d72 100644 --- a/src/tests/Common/dirs.proj +++ b/src/tests/Common/dirs.proj @@ -24,7 +24,6 @@ - diff --git a/src/tests/nativeaot/SmokeTests/HelloWasm/HelloWasm.cs b/src/tests/nativeaot/SmokeTests/HelloWasm/HelloWasm.cs index b255b8d8f421..ae26411026ce 100644 --- a/src/tests/nativeaot/SmokeTests/HelloWasm/HelloWasm.cs +++ b/src/tests/nativeaot/SmokeTests/HelloWasm/HelloWasm.cs @@ -4798,12 +4798,3 @@ public bool TestGetSet() return X == 17 && Y == 347 && S1 == "first string" && S2 == "a different string"; } } - -namespace System.Runtime.InteropServices -{ - - [AttributeUsage((System.AttributeTargets.Method | System.AttributeTargets.Class))] - internal class McgIntrinsicsAttribute : Attribute - { - } -} diff --git a/src/tests/nativeaot/SmokeTests/StackTraceMetadata/StackTraceMetadata.cs b/src/tests/nativeaot/SmokeTests/StackTraceMetadata/StackTraceMetadata.cs index 4bdc41945bc5..995159fa0a11 100644 --- a/src/tests/nativeaot/SmokeTests/StackTraceMetadata/StackTraceMetadata.cs +++ b/src/tests/nativeaot/SmokeTests/StackTraceMetadata/StackTraceMetadata.cs @@ -29,7 +29,7 @@ class DiagnosticMethodInfoTests { public static void Run() { -#if !CODEGEN_WASM // TODO-LLVM: https://github.com/dotnet/runtimelab/issues/2404. +#if !CODEGEN_WASI // TODO-LLVM: https://github.com/dotnet/runtimelab/issues/2404. #if STRIPPED DiagnosticMethodInfo dmi = DiagnosticMethodInfo.Create(new StackFrame()); if (dmi != null) From 6f17f6ce05f937b4e82b8b4c689d670153c93929 Mon Sep 17 00:00:00 2001 From: SingleAccretion Date: Mon, 9 Sep 2024 17:53:21 +0300 Subject: [PATCH 2/5] Forbid LLVM inlining of EH frames with virtual unwind frames --- src/coreclr/jit/llvmcodegen.cpp | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/coreclr/jit/llvmcodegen.cpp b/src/coreclr/jit/llvmcodegen.cpp index 3c9085e8021f..0f6b353071b3 100644 --- a/src/coreclr/jit/llvmcodegen.cpp +++ b/src/coreclr/jit/llvmcodegen.cpp @@ -137,6 +137,15 @@ void Llvm::annotateFunctions() { llvmFunc->addFnAttr(llvm::Attribute::NoInline); } + + // The runtime stack trace logic relies on a 1-to-1 correspondence between WASM (and therefore LLVM) + // frames and virtual unwind frames; it uses this to determine when to stop appending stack frames. + // We therefore cannot let LLVM inline methods with virtual unwind frames (~= with catch handlers). + // TODO-LLVM-StackTrace: do we need to apply this to (finally) funclets too? + if (m_unwindFrameLclNum != BAD_VAR_NUM) + { + llvmFunc->addFnAttr(llvm::Attribute::NoInline); + } } if (_compiler->opts.OptimizationEnabled()) From 1849f5a0931459831244e9da6646cb479a0545cd Mon Sep 17 00:00:00 2001 From: SingleAccretion Date: Mon, 9 Sep 2024 17:35:22 +0300 Subject: [PATCH 3/5] Respect AggressiveInlining --- src/coreclr/jit/llvmcodegen.cpp | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/coreclr/jit/llvmcodegen.cpp b/src/coreclr/jit/llvmcodegen.cpp index 0f6b353071b3..edcc61efa79a 100644 --- a/src/coreclr/jit/llvmcodegen.cpp +++ b/src/coreclr/jit/llvmcodegen.cpp @@ -156,6 +156,12 @@ void Llvm::annotateFunctions() llvmFunc->addFnAttr(llvm::Attribute::OptimizeForSize); } + if ((_compiler->info.compFlags & CORINFO_FLG_FORCEINLINE) != 0) + { + // LLVM's "alwaysinline" is stronger than the CLR's 'AggressiveInlining'; use "inlinehint" instead. + llvmFunc->addFnAttr(llvm::Attribute::InlineHint); + } + // Mark the shadow stack dereferenceable. if ((funcIdx != ROOT_FUNC_IDX) || _compiler->lvaGetDesc(_shadowStackLclNum)->lvIsParam) { From fa226d97a09122a5ca29f544d5eae20272f86c5a Mon Sep 17 00:00:00 2001 From: SingleAccretion Date: Wed, 4 Sep 2024 21:55:17 +0300 Subject: [PATCH 4/5] Add a test --- .../ExceptionHandlingTests.Common.cs | 9 +- .../SmokeTests/HelloWasm/HelloWasm.cs | 177 +++++++++++++++++- .../SmokeTests/HelloWasm/HelloWasm.csproj | 6 +- .../SmokeTests/HelloWasm/HelloWasm.js | 2 +- .../HelloWasm/Microsoft.JSInterop.js | 13 +- .../SmokeTests/HelloWasm/dotnet_support.js | 97 ++++++++-- .../nativeaot/SmokeTests/HelloWasm/shell.js | 2 +- 7 files changed, 266 insertions(+), 40 deletions(-) diff --git a/src/tests/nativeaot/SmokeTests/HelloWasm/ExceptionHandlingTests.Common.cs b/src/tests/nativeaot/SmokeTests/HelloWasm/ExceptionHandlingTests.Common.cs index c6d9d62a1e89..5944ae10044c 100644 --- a/src/tests/nativeaot/SmokeTests/HelloWasm/ExceptionHandlingTests.Common.cs +++ b/src/tests/nativeaot/SmokeTests/HelloWasm/ExceptionHandlingTests.Common.cs @@ -11,6 +11,7 @@ internal unsafe partial class Program { + internal static bool ExitOnFirstTestFailure = Environment.GetCommandLineArgs().AsSpan().Contains("-exit"); internal static bool Success = true; private static bool TestTryCatch() @@ -2051,7 +2052,13 @@ public static void FailTest(string failMessage = null) { Success = false; PrintLine("Failed."); - if (failMessage != null) PrintLine(failMessage + "-"); + if (failMessage != null) + PrintLine(failMessage + "-"); + + if (ExitOnFirstTestFailure) + { + Environment.Exit(-1); + } } public static void PrintString(string s) diff --git a/src/tests/nativeaot/SmokeTests/HelloWasm/HelloWasm.cs b/src/tests/nativeaot/SmokeTests/HelloWasm/HelloWasm.cs index ae26411026ce..7168429d9555 100644 --- a/src/tests/nativeaot/SmokeTests/HelloWasm/HelloWasm.cs +++ b/src/tests/nativeaot/SmokeTests/HelloWasm/HelloWasm.cs @@ -316,7 +316,6 @@ private static unsafe int Main(string[] args) TestStaticAbiCompatibleSignatures(); #if !CODEGEN_WASI // Easier to test with Javascript/Emscripten. - TestNamedModuleCall(); TestNamedModuleCallWithoutEntryPoint(); @@ -394,6 +393,8 @@ private static unsafe int Main(string[] args) if (OperatingSystem.IsBrowser()) { + TestForeignModuleInStackTrace(); + TestJavascriptCall(); } @@ -4136,14 +4137,170 @@ private static void TestSignedLongMulOvf() PassTest(); } + [UnconditionalSuppressMessage("Trimming", "IL2026")] + [MethodImpl(MethodImplOptions.NoInlining)] private static unsafe void TestStackTrace() { StartTest("Test StackTrace"); -#if DEBUG - EndTest(new StackTrace().ToString().Contains("TestStackTrace"), new StackTrace().ToString()); -#else - EndTest(new StackTrace().ToString().Contains("wasm-function")); -#endif + + StackTrace st = new StackTrace(); + if (!st.ToString().Contains(nameof(TestStackTrace)) || + !st.GetFrame(0).ToString().Contains(nameof(TestStackTrace)) || + !st.GetFrame(1).ToString().Contains("Main")) + { + FailTest($"Unexpected stack trace:\n{st}"); + return; + } + + StackFrame sf = new StackFrame(); + if (!sf.ToString().Contains(nameof(TestStackTrace))) + { + FailTest($"Unexpected stack frame: {sf}"); + return; + } + + Action del = new Action(TestStackTrace); + if (sf.GetMethod() != del.Method || sf.GetMethod() != st.GetFrame(0).GetMethod()) + { + FailTest($"Unexpected StackFrame.GetMethod(): {sf.GetMethod()}"); + return; + } + + DiagnosticMethodInfo delDmi = DiagnosticMethodInfo.Create(del); + if (delDmi is not { Name: nameof(TestStackTrace), DeclaringTypeName: nameof(Program) }) + { + FailTest($"Unexpected DiagnosticMethodInfo from a delegate: {delDmi?.DeclaringTypeName}::{delDmi?.Name}"); + return; + } + + bool result = true; + TestStackTracesInFilters(&result); + if (!result) + { + FailTest("Unexpected stack traces in filters"); + return; + } + + // Put AggressiveInliningMethodWithEH into the same LLVM module as TestStackTracesAndLlvmInlining. + JitUse((int*)(delegate*)&AggressiveInliningMethodWithEH); + if (!TestStackTracesAndLlvmInlining()) + { + FailTest("TestStackTracesAndLlvmInlining failed"); + return; + } + + PassTest(); + } + + private const int TestStackTracesInFiltersTotalDepth = 3; + + static void TestStackTracesInFilters(bool* pResult, int depth = TestStackTracesInFiltersTotalDepth) + { + bool Filter(Exception e) + { + StackTrace st = new StackTrace(e); + if (st.FrameCount != depth + 1) + { + Console.WriteLine($"Unexpected stack frame count in TestStackTracesInFilters(depth = {depth}): {st.FrameCount}"); + *pResult = false; + } + else if (!st.GetFrame(depth).ToString().Contains(nameof(TestStackTracesInFilters))) + { + Console.WriteLine($"Unexpected stack trace in TestStackTracesInFilters(depth = {depth}):\n{st}"); + *pResult = false; + } + return depth == TestStackTracesInFiltersTotalDepth; + } + + try + { + if (depth != 0) + { + TestStackTracesInFilters(pResult, depth - 1); + } + else + { + throw new Exception(); + } + } + catch (Exception e) when (Filter(e)) { } + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private static bool TestStackTracesAndLlvmInlining() + { + try + { + // Our runtime logic relies on the 1-1 correspondence between virtual unwind frames and WASM frames, + // otherwise it can't detect when to stop appending frames. So we can't let LLVM inline methods with EH. + AggressiveInliningMethodWithEH(); + } + catch (Exception e) + { + StackTrace st = new StackTrace(e); + if (st.FrameCount != 2) + { + PrintLine($"Unexpected stack trace in TestStackTracesAndLlvmInlining:\n{st}"); + return false; + } + return true; + } + return false; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void AggressiveInliningMethodWithEH() + { + try + { + throw new Exception(); + } + catch { throw; } + } + + private static void TestForeignModuleInStackTrace() + { + StartTest("Test foreign module in stack trace"); + string reason = null; + JSInterop.InternalCalls.TestForeignModuleInStackTraceJS(&reason, (delegate* unmanaged)&TestForeignModuleInStackTraceCallee); + if (reason != null) + { + FailTest(reason); + } + else + { + PassTest(); + } + } + + [UnmanagedCallersOnly] + private static unsafe void TestForeignModuleInStackTraceCallee(double pFailureReasonRaw) + { + string* pFailureReason = (string*)(nuint)pFailureReasonRaw; + + StackTrace st = new StackTrace(); + if (!st.GetFrame(0).ToString().Contains(nameof(TestForeignModuleInStackTraceCallee))) + { + *pFailureReason += $"\nFirst frame not {nameof(TestForeignModuleInStackTraceCallee)}"; + } + + const string ForeignFuncName = "ForeignModuleFrame"; + StackFrame ff = st.GetFrame(1); + if (!ff.ToString().Contains(ForeignFuncName)) // We generate a "names" section for this. + { + *pFailureReason += $"\n{ForeignFuncName} frame expected, got: {ff}"; + } + + var dmi = DiagnosticMethodInfo.Create(ff); + if (dmi != null) + { + *pFailureReason += $"\nExpected the foreign frame to not be managed, got: {dmi.DeclaringTypeName}::{dmi.Name}"; + } + + if (*pFailureReason != null) + { + *pFailureReason += $"\nFull stack trace:\n{st}"; + } } static void TestJavascriptCall() @@ -4399,6 +4556,14 @@ public static IntPtr InvokeJSUnmarshalled(out string exception, string js, IntPt { return InvokeJSUnmarshalledInternal(js, js.Length, p1, p2, p3, out exception); } + + [DllImport("js")] + private static extern int TestForeignModuleInStackTraceJS(double pReason, double pCallee); + + public static unsafe void TestForeignModuleInStackTraceJS(string* pReason, void* pCallee) + { + TestForeignModuleInStackTraceJS((nuint)pReason, (nuint)pCallee); + } } } diff --git a/src/tests/nativeaot/SmokeTests/HelloWasm/HelloWasm.csproj b/src/tests/nativeaot/SmokeTests/HelloWasm/HelloWasm.csproj index 6421623312f1..f27ce6a35e42 100644 --- a/src/tests/nativeaot/SmokeTests/HelloWasm/HelloWasm.csproj +++ b/src/tests/nativeaot/SmokeTests/HelloWasm/HelloWasm.csproj @@ -30,8 +30,12 @@ + + + + - --js-library $(MSBuildProjectDirectory)/dotnet_support.js --pre-js $(MSBuildProjectDirectory)/Microsoft.JSInterop.js --pre-js $(MSBuildProjectDirectory)/shell.js --post-js $(MSBuildProjectDirectory)/HelloWasm.js + -sENVIRONMENT=web,node,shell --js-library $(MSBuildProjectDirectory)/dotnet_support.js --pre-js $(MSBuildProjectDirectory)/Microsoft.JSInterop.js --post-js $(MSBuildProjectDirectory)/HelloWasm.js diff --git a/src/tests/nativeaot/SmokeTests/HelloWasm/HelloWasm.js b/src/tests/nativeaot/SmokeTests/HelloWasm/HelloWasm.js index 95459443d200..bb80ea2030ee 100644 --- a/src/tests/nativeaot/SmokeTests/HelloWasm/HelloWasm.js +++ b/src/tests/nativeaot/SmokeTests/HelloWasm/HelloWasm.js @@ -1 +1 @@ -window.Answer = function () { return 42; } +globalThis.Answer = function () { return 42; } diff --git a/src/tests/nativeaot/SmokeTests/HelloWasm/Microsoft.JSInterop.js b/src/tests/nativeaot/SmokeTests/HelloWasm/Microsoft.JSInterop.js index 993586cbc58a..badabe980bdc 100644 --- a/src/tests/nativeaot/SmokeTests/HelloWasm/Microsoft.JSInterop.js +++ b/src/tests/nativeaot/SmokeTests/HelloWasm/Microsoft.JSInterop.js @@ -1,15 +1,8 @@ -if(typeof window === 'undefined') { - window = global; - window.location = { - search: '' - }; -} // create window for node and set to global - // This is a single-file self-contained module to avoid the need for a Webpack build var DotNet; (function (DotNet) { - window.DotNet = DotNet; // Ensure reachable from anywhere + globalThis.DotNet = DotNet; // Ensure reachable from anywhere const jsonRevivers = []; const pendingAsyncCalls = {}; const cachedJSFunctions = {}; @@ -181,8 +174,8 @@ var DotNet; if (cachedJSFunctions.hasOwnProperty(identifier)) { return cachedJSFunctions[identifier]; } - let result = window; - let resultIdentifier = 'window'; + let result = globalThis; + let resultIdentifier = 'globalThis'; let lastSegmentValue; identifier.split('.').forEach(segment => { if (segment in result) { diff --git a/src/tests/nativeaot/SmokeTests/HelloWasm/dotnet_support.js b/src/tests/nativeaot/SmokeTests/HelloWasm/dotnet_support.js index 8fc1b0389a12..1b7b97108ea5 100644 --- a/src/tests/nativeaot/SmokeTests/HelloWasm/dotnet_support.js +++ b/src/tests/nativeaot/SmokeTests/HelloWasm/dotnet_support.js @@ -2,26 +2,7 @@ var DotNetSupportLib = { $DOTNET: { _dotnet_get_global: function () { - function testGlobal(obj) { - obj['___dotnet_global___'] = obj; - var success = typeof ___dotnet_global___ === 'object' && obj['___dotnet_global___'] === obj; - if (!success) { - delete obj['___dotnet_global___']; - } - return success; - } - if (typeof ___dotnet_global___ === 'object') { - return ___dotnet_global___; - } - if (typeof global === 'object' && testGlobal(global)) { - ___dotnet_global___ = global; - } else if (typeof window === 'object' && testGlobal(window)) { - ___dotnet_global___ = window; - } - if (typeof ___dotnet_global___ === 'object') { - return ___dotnet_global___; - } - throw Error('unable to get DotNet global object.'); + return globalThis; }, }, corert_wasm_invoke_js_unmarshalled: function (js, length, arg0, arg1, arg2, exception) { @@ -35,6 +16,82 @@ var DotNetSupportLib = { return funcInstance.call(null, arg0, arg1, arg2); }, + TestForeignModuleInStackTraceJS__deps: ['$wasmTable'], + TestForeignModuleInStackTraceJS: function (pReason, pCallee) { + // We force an index space collision with the managed 'pCallee' + // by padding the index space with empty functions. + const pCalleeFunc = wasmTable.get(pCallee); + const pCalleeFuncIndex = parseInt(pCalleeFunc.name); + + const encodeULeb128 = (bytes, val) => + { + do + { + let byteVal = val & 0x7F; + val >>>= 7; + if (val) byteVal |= 0x80; + bytes.push(byteVal); + } + while (val) + }; + const appendSection = (bytes, header, sectionBytes) => + { + bytes.push(header); + encodeULeb128(bytes, sectionBytes.length); + for (const elem of sectionBytes) bytes.push(elem); + }; + + let bytes = [ + 0x00, 0x61, 0x73, 0x6d, // magic ("\0asm") + 0x01, 0x00, 0x00, 0x00, // version: 1 + 0x01, 0x05, // Type section header + 0x01, 0x60, 0x01, 0x7C, 0x00, // [functype([f64], [])] + ]; + + // Import section: pad the function index space with dummy imports. + const exportedFuncIndex = pCalleeFuncIndex; + const importFuncCount = exportedFuncIndex; + const importSectionBytes = []; + encodeULeb128(importSectionBytes, importFuncCount); + for (let i = 0; i < importFuncCount; i++) + { + importSectionBytes.push(0x01, 0x6D, 0x01, 0x6E, 0x00, 0x00); // import + } + appendSection(bytes, 0x02, importSectionBytes); + + // Function section. + bytes.push( + 0x03, 0x02, // Function section header + 0x01, 0x00, // [typeidx: 0] + ); + + // Export section. + const exportSectionBytes = [0x01, 0x01, 0x65, 0x00]; // export + encodeULeb128(exportSectionBytes, exportedFuncIndex); + appendSection(bytes, 0x07, exportSectionBytes); + + bytes.push( + 0x0A, 0x08, // Code section header + 0x01, 0x06, 0x00, // [Function] + 0x20, 0x00, // local.get 0 + 0x10, 0x00, // call + 0x0B, // end + ); + + // Finally, the 'names' section s.t. we can check we have the right method in the trace. + const funcsNamesBytes = [0x01]; // [] + encodeULeb128(funcsNamesBytes, exportedFuncIndex); + funcsNamesBytes.push(0x12, 0x46, 0x6F, 0x72, 0x65, 0x69, 0x67, 0x6E, 0x4D, 0x6F, 0x64, 0x75, 0x6C, 0x65, 0x46, 0x72, 0x61, 0x6D, 0x65); + + const namesSectionBytes = [0x04, 0x6E, 0x61, 0x6D, 0x65]; // "names" + appendSection(namesSectionBytes, 0x01, funcsNamesBytes); // funcsnames subsection + + appendSection(bytes, 0x00, namesSectionBytes); + + const mod = new WebAssembly.Module(new Uint8Array(bytes)); + const inst = new WebAssembly.Instance(mod, { 'm': { 'n': wasmTable.get(pCallee) } }); + inst.exports['e'](pReason); + }, }; autoAddDeps(DotNetSupportLib, '$DOTNET'); diff --git a/src/tests/nativeaot/SmokeTests/HelloWasm/shell.js b/src/tests/nativeaot/SmokeTests/HelloWasm/shell.js index 12acc4ae8d5f..49fd188011d0 100644 --- a/src/tests/nativeaot/SmokeTests/HelloWasm/shell.js +++ b/src/tests/nativeaot/SmokeTests/HelloWasm/shell.js @@ -30,7 +30,7 @@ var Module = { throw 'DupImportTest was imported when it should have been removed as a duplicate.'; } - return successCallback(result['instance']); + return successCallback(result['instance'], result['module']); } instantiateAsync(wasmBinary, wasmBinaryFile, info, receiveInstantiationResult); From 309b95bef7575079a329e6243d588b52c031f591 Mon Sep 17 00:00:00 2001 From: SingleAccretion Date: Wed, 4 Sep 2024 22:14:03 +0300 Subject: [PATCH 5/5] Add System.Diagnostics.StackTrace.Tests to CI --- src/libraries/tests.proj | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/libraries/tests.proj b/src/libraries/tests.proj index 476eb825567f..29ca2bcc489c 100644 --- a/src/libraries/tests.proj +++ b/src/libraries/tests.proj @@ -617,6 +617,8 @@ + +