From fee66de56897e9bab810ba9f6849459744d03643 Mon Sep 17 00:00:00 2001 From: David Wrighton Date: Thu, 23 Apr 2026 15:03:55 -0700 Subject: [PATCH 01/43] Add READYTORUN_FIXUP_InjectStringThunks (0x39) for string-to-code thunk mappings This adds a new ReadyToRun fixup that enables mapping UTF-8 strings to pregenerated code thunks embedded in R2R images. The fixup is placed in the eager imports section and processed at module load time. Changes across all layers: Format definition: - Add READYTORUN_FIXUP_InjectStringThunks = 0x39 to readytorun.h and ReadyToRunConstants.cs - Bump R2R minor version from 5 to 6 in all three locations Runtime (CoreCLR VM): - Refactor StringThunkSHashTraits from wasm/helpers.cpp into shared stringthunkhash.h header, available to all platforms - Add pregeneratedstringthunks.cpp/.h with global hash table using copy-on-write CAS pattern for lock-free concurrent reads - InitializePregeneratedStringThunkHash() called at EE startup - LookupPregeneratedThunkByString() API returns PCODE or NULL - ProcessInjectStringThunksFixup() handles the fixup in LoadDynamicInfoEntry, merging new entries with existing ones Crossgen2 compiler: - Add abstract StringDiscoverableAssemblyStubNode (derives from AssemblyStubNode) with LookupString property; instances register themselves via OnMarked - Add InjectStringThunksSignature that collects all registered stubs at emission time and encodes them as (UTF8 string, RVA) pairs - Root the InjectStringThunks import eagerly in NodeFactory - Sort stubs by LookupString for deterministic compilation Tooling and documentation: - Add r2rdump parser case for InjectStringThunks signatures - Update readytorun-format.md with fixup table entry and format spec Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- docs/design/coreclr/botr/readytorun-format.md | 16 +++ src/coreclr/inc/readytorun.h | 5 +- .../nativeaot/Runtime/inc/ModuleHeaders.h | 2 +- .../Common/Internal/Runtime/ModuleHeaders.cs | 2 +- .../Internal/Runtime/ReadyToRunConstants.cs | 2 + .../ReadyToRun/InjectStringThunksSignature.cs | 71 ++++++++++ .../StringDiscoverableAssemblyStubNode.cs | 29 +++++ .../ReadyToRunCodegenNodeFactory.cs | 24 ++++ .../ILCompiler.ReadyToRun.csproj | 2 + .../ReadyToRunSignature.cs | 21 +++ src/coreclr/vm/CMakeLists.txt | 8 ++ src/coreclr/vm/ceemain.cpp | 3 + src/coreclr/vm/jitinterface.cpp | 11 ++ src/coreclr/vm/pregeneratedstringthunks.cpp | 121 ++++++++++++++++++ src/coreclr/vm/pregeneratedstringthunks.h | 19 +++ src/coreclr/vm/stringthunkhash.h | 19 +++ src/coreclr/vm/wasm/helpers.cpp | 11 +- 17 files changed, 354 insertions(+), 12 deletions(-) create mode 100644 src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/InjectStringThunksSignature.cs create mode 100644 src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/StringDiscoverableAssemblyStubNode.cs create mode 100644 src/coreclr/vm/pregeneratedstringthunks.cpp create mode 100644 src/coreclr/vm/pregeneratedstringthunks.h create mode 100644 src/coreclr/vm/stringthunkhash.h diff --git a/docs/design/coreclr/botr/readytorun-format.md b/docs/design/coreclr/botr/readytorun-format.md index 6ce4f1470da6a8..fee43d3b40bcdb 100644 --- a/docs/design/coreclr/botr/readytorun-format.md +++ b/docs/design/coreclr/botr/readytorun-format.md @@ -288,6 +288,7 @@ fixup kind, the rest of the signature varies based on the fixup kind. | READYTORUN_FIXUP_Verify_IL_Body | 0x36 | Verify an IL body is defined the same at compile time and runtime. A failed match will cause a hard runtime failure. See[IL Body signatures](il-body-signatures) for details. | READYTORUN_FIXUP_ContinuationLayout | 0x37 | Layout of an async method continuation type, followed by typespec signature | READYTORUN_FIXUP_ResumptionStubEntryPoint | 0x38 | Entry point of an async method resumption stub +| READYTORUN_FIXUP_InjectStringThunks | 0x39 | Inject pregenerated string-to-code thunk mappings. See [InjectStringThunks signatures](#injectstringthunks-signatures) for details. | READYTORUN_FIXUP_ModuleOverride | 0x80 | When or-ed to the fixup ID, the fixup byte in the signature is followed by an encoded uint with assemblyref index, either within the MSIL metadata of the master context module for the signature or within the manifest metadata R2R header table (used in cases inlining brings in references to assemblies not seen in the input MSIL). #### Method Signatures @@ -333,6 +334,21 @@ ECMA 335 does not have a natural encoding for describing an overridden method. T ECMA 335 does not define a format that can represent the exact implementation of a method by itself. This signature holds all of the IL of the method, the EH table, the locals table, and each token (other than type references) in those tables is replaced with an index into a local stream of signatures. Those signatures are simply verbatim copies of the needed metadata to describe MemberRefs, TypeSpecs, MethodSpecs, StandaloneSignatures and strings. All of that is bundled into a large byte array. In addition, a series of TypeSignatures follows which allow the type references to be resolved, as well as a methodreference to the uninstantiated method. Assuming all of this matches with the data that is present at runtime, the fixup is considered to be satisfied. See ReadyToRunStandaloneMetadata.cs for the exact details of the format. +#### InjectStringThunks signatures + +The `READYTORUN_FIXUP_InjectStringThunks` fixup is placed in an eager import section and is processed at R2R module load time. There is at most one such fixup per compilation. It encodes a mapping from UTF-8 strings to pregenerated code thunks embedded in the R2R image. + +The signature following the fixup kind byte is a series of elements: + +| Field | Size | Description +|:------|-----:|:----------- +| LookupString | variable | A null-terminated UTF-8 string (the lookup key) +| ThunkRVA | 4 bytes | An RVA into the module indicating the location of the thunk code. On WebAssembly platforms, this is an I32 function table index instead. + +The series terminates when the null-terminated string is the empty string (a single `0x00` byte). There is no trailing RVA after the terminal empty string. + +At runtime, the entries are merged into a global hash table. Strings already present in the table from previously loaded modules take precedence over new entries. The table can be queried via `LookupPregeneratedThunkByString`. + ### READYTORUN_IMPORT_SECTIONS::AuxiliaryData For slots resolved lazily via `READYTORUN_HELPER_DelayLoad_MethodCall` helper, auxiliary data are diff --git a/src/coreclr/inc/readytorun.h b/src/coreclr/inc/readytorun.h index b1998ad855888b..9fc10f6b29c409 100644 --- a/src/coreclr/inc/readytorun.h +++ b/src/coreclr/inc/readytorun.h @@ -20,7 +20,7 @@ // If you update this, ensure you run `git grep MINIMUM_READYTORUN_MAJOR_VERSION` // and handle pending work. #define READYTORUN_MAJOR_VERSION 18 -#define READYTORUN_MINOR_VERSION 0x0005 +#define READYTORUN_MINOR_VERSION 0x0006 #define MINIMUM_READYTORUN_MAJOR_VERSION 18 @@ -56,6 +56,7 @@ // R2R Version 18.3 adds the ExternalTypeMaps, ProxyTypeMaps, TypeMapAssemblyTargets sections // R2R Version 18.4 adds ThrowArgument, ThrowArgumentOutOfRange, ThrowPlatformNotSupported, and ThrowNotImplemented helpers // R2R Version 18.5 adds READYTORUN_FLAG_STRIPPED_IL_BODIES, READYTORUN_FLAG_STRIPPED_INLINING_INFO, and READYTORUN_FLAG_STRIPPED_DEBUG_INFO flags +// R2R Version 18.6 adds READYTORUN_FIXUP_InjectStringThunks for mapping strings to pregenerated code thunks struct READYTORUN_CORE_HEADER { @@ -310,6 +311,8 @@ enum ReadyToRunFixupKind READYTORUN_FIXUP_Continuation_Layout = 0x37, /* Layout of an async method continuation type */ READYTORUN_FIXUP_ResumptionStubEntryPoint = 0x38, /* Entry point of an async method resumption stub */ + READYTORUN_FIXUP_InjectStringThunks = 0x39, /* Inject pregenerated string-to-code thunk mappings into the global lookup table */ + READYTORUN_FIXUP_ModuleOverride = 0x80, /* followed by sig-encoded UInt with assemblyref index into either the assemblyref table of the MSIL metadata of the master context module for the signature or */ /* into the extra assemblyref table in the manifest metadata R2R header table (used in cases inlining brings in references to assemblies not seen in the MSIL). */ }; diff --git a/src/coreclr/nativeaot/Runtime/inc/ModuleHeaders.h b/src/coreclr/nativeaot/Runtime/inc/ModuleHeaders.h index bf8be4d276088c..bc1d51dffb34fa 100644 --- a/src/coreclr/nativeaot/Runtime/inc/ModuleHeaders.h +++ b/src/coreclr/nativeaot/Runtime/inc/ModuleHeaders.h @@ -12,7 +12,7 @@ struct ReadyToRunHeaderConstants static const uint32_t Signature = 0x00525452; // 'RTR' static const uint32_t CurrentMajorVersion = 18; - static const uint32_t CurrentMinorVersion = 5; + static const uint32_t CurrentMinorVersion = 6; }; struct ReadyToRunHeader diff --git a/src/coreclr/tools/Common/Internal/Runtime/ModuleHeaders.cs b/src/coreclr/tools/Common/Internal/Runtime/ModuleHeaders.cs index c2d18e209e65f4..dda2d9a8cec669 100644 --- a/src/coreclr/tools/Common/Internal/Runtime/ModuleHeaders.cs +++ b/src/coreclr/tools/Common/Internal/Runtime/ModuleHeaders.cs @@ -16,7 +16,7 @@ internal struct ReadyToRunHeaderConstants public const uint Signature = 0x00525452; // 'RTR' public const ushort CurrentMajorVersion = 18; - public const ushort CurrentMinorVersion = 5; + public const ushort CurrentMinorVersion = 6; } #if READYTORUN #pragma warning disable 0169 diff --git a/src/coreclr/tools/Common/Internal/Runtime/ReadyToRunConstants.cs b/src/coreclr/tools/Common/Internal/Runtime/ReadyToRunConstants.cs index b431b7b1ee800a..974a5e5889e6a4 100644 --- a/src/coreclr/tools/Common/Internal/Runtime/ReadyToRunConstants.cs +++ b/src/coreclr/tools/Common/Internal/Runtime/ReadyToRunConstants.cs @@ -193,6 +193,8 @@ public enum ReadyToRunFixupKind ContinuationLayout = 0x37, /* Layout of an async method continuation type */ ResumptionStubEntryPoint = 0x38, /* Entry point of an async method resumption stub */ + InjectStringThunks = 0x39, /* Inject pregenerated string-to-code thunk mappings into the global lookup table */ + ModuleOverride = 0x80, // followed by sig-encoded UInt with assemblyref index into either the assemblyref // table of the MSIL metadata of the master context module for the signature or diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/InjectStringThunksSignature.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/InjectStringThunksSignature.cs new file mode 100644 index 00000000000000..7a4a5f37dfd429 --- /dev/null +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/InjectStringThunksSignature.cs @@ -0,0 +1,71 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using System.Text; + +using Internal.ReadyToRunConstants; +using Internal.Text; +using Internal.TypeSystem; + +namespace ILCompiler.DependencyAnalysis.ReadyToRun +{ + /// + /// Signature node for the READYTORUN_FIXUP_InjectStringThunks fixup. + /// Encodes a series of (null-terminated UTF8 string, 4-byte RVA/table index) pairs, + /// terminated by an empty string (single 0x00 byte with no trailing RVA). + /// + internal class InjectStringThunksSignature : Signature + { + public InjectStringThunksSignature() + { + } + + public override int ClassCode => 1493287651; + + public override ObjectData GetData(NodeFactory factory, bool relocsOnly = false) + { + ObjectDataSignatureBuilder builder = new ObjectDataSignatureBuilder(factory, relocsOnly); + builder.AddSymbol(this); + builder.EmitByte((byte)ReadyToRunFixupKind.InjectStringThunks); + + if (!relocsOnly) + { + List stubs = factory.GetStringDiscoverableStubs(); + stubs.Sort((a, b) => string.CompareOrdinal(a.LookupString, b.LookupString)); + + foreach (StringDiscoverableAssemblyStubNode stub in stubs) + { + // Emit the null-terminated UTF8 string + byte[] stringBytes = Encoding.UTF8.GetBytes(stub.LookupString); + builder.EmitBytes(stringBytes); + builder.EmitByte(0); // null terminator + + // Emit a 4-byte relocation to the stub code. + // On WASM, this is a table index; on other platforms, an RVA. + RelocType relocType = factory.Target.Architecture == TargetArchitecture.Wasm32 + ? RelocType.WASM_TABLE_INDEX_I32 + : RelocType.IMAGE_REL_BASED_ADDR32NB; + builder.EmitReloc(stub, relocType, delta: factory.Target.CodeDelta); + } + } + + // Terminal empty string (no trailing RVA) + builder.EmitByte(0); + + return builder.ToObjectData(); + } + + public override void AppendMangledName(NameMangler nameMangler, Utf8StringBuilder sb) + { + sb.Append(nameMangler.CompilationUnitPrefix); + sb.Append("InjectStringThunks"u8); + } + + public override int CompareToImpl(ISortableNode other, CompilerComparer comparer) + { + // There should only be one instance of this signature per compilation + return 0; + } + } +} diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/StringDiscoverableAssemblyStubNode.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/StringDiscoverableAssemblyStubNode.cs new file mode 100644 index 00000000000000..35e97b9d91b17a --- /dev/null +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/StringDiscoverableAssemblyStubNode.cs @@ -0,0 +1,29 @@ +// 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; + +namespace ILCompiler.DependencyAnalysis +{ + /// + /// An abstract assembly stub node whose instances are discoverable by string key. + /// All instances present in the dependency graph are collected and emitted as a single + /// READYTORUN_FIXUP_InjectStringThunks eager fixup, mapping each LookupString to + /// the code address of the stub in the R2R image. + /// + public abstract class StringDiscoverableAssemblyStubNode : AssemblyStubNode + { + /// + /// The string key used to look up this stub at runtime via LookupPregeneratedThunkByString. + /// Must be non-empty and must not contain embedded null characters. + /// + public abstract string LookupString { get; } + + protected override void OnMarked(NodeFactory factory) + { + Debug.Assert(!string.IsNullOrEmpty(LookupString), "LookupString must be non-empty"); + Debug.Assert(!LookupString.Contains('\0'), "LookupString must not contain embedded null characters"); + factory.RegisterStringDiscoverableStub(this); + } + } +} diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRunCodegenNodeFactory.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRunCodegenNodeFactory.cs index 027ae0dbcc7140..8b5d547353155a 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRunCodegenNodeFactory.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRunCodegenNodeFactory.cs @@ -441,6 +441,25 @@ private void CreateNodeCaches() public ImportSectionNode ILBodyPrecodeImports; + private readonly ConcurrentBag _stringDiscoverableStubs = new ConcurrentBag(); + + /// + /// Register a StringDiscoverableAssemblyStubNode for inclusion in the InjectStringThunks fixup. + /// Called by StringDiscoverableAssemblyStubNode.OnMarked. + /// + public void RegisterStringDiscoverableStub(StringDiscoverableAssemblyStubNode stub) + { + _stringDiscoverableStubs.Add(stub); + } + + /// + /// Get all registered string-discoverable stubs. Should only be called after marking is complete. + /// + public List GetStringDiscoverableStubs() + { + return new List(_stringDiscoverableStubs); + } + private NodeCache _constructedHelpers; private LazyGenericsSupport.GenericCycleDetector _genericCycleDetector; @@ -943,6 +962,11 @@ public void AttachToDependencyGraph(DependencyAnalyzerBase graph, I ReadyToRunHelper.Module)); graph.AddRoot(ModuleImport, "Module import is required by the R2R format spec"); + // String-discoverable thunk injection fixup. The signature collects all + // StringDiscoverableAssemblyStubNode instances at emission time. + Import injectStringThunksImport = new Import(EagerImports, new InjectStringThunksSignature()); + graph.AddRoot(injectStringThunksImport, "InjectStringThunks fixup for string-discoverable stubs"); + if ((Target.Architecture != TargetArchitecture.X86) && (Target.Architecture != TargetArchitecture.Wasm32)) { Import personalityRoutineImport = new Import(EagerImports, new ReadyToRunHelperSignature( diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/ILCompiler.ReadyToRun.csproj b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/ILCompiler.ReadyToRun.csproj index 7c69dee450ee6d..67d7d95eb339f7 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/ILCompiler.ReadyToRun.csproj +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/ILCompiler.ReadyToRun.csproj @@ -281,6 +281,8 @@ + + diff --git a/src/coreclr/tools/aot/ILCompiler.Reflection.ReadyToRun/ReadyToRunSignature.cs b/src/coreclr/tools/aot/ILCompiler.Reflection.ReadyToRun/ReadyToRunSignature.cs index 406effbb5305aa..b0d94ce430fe2d 100644 --- a/src/coreclr/tools/aot/ILCompiler.Reflection.ReadyToRun/ReadyToRunSignature.cs +++ b/src/coreclr/tools/aot/ILCompiler.Reflection.ReadyToRun/ReadyToRunSignature.cs @@ -1372,6 +1372,27 @@ private ReadyToRunSignature ParseSignature(ReadyToRunFixupKind fixupType, String builder.Append($" (RESUMPTION_STUB RVA[0x{stubRVA:X}])"); break; + case ReadyToRunFixupKind.InjectStringThunks: + builder.Append(" (INJECT_STRING_THUNKS"); + while (true) + { + int strStart = Offset; + while (_image[Offset] != 0) + SkipBytes(1); + int strLen = Offset - strStart; + SkipBytes(1); // skip null terminator + + if (strLen == 0) + break; + + string lookupString = System.Text.Encoding.UTF8.GetString(_image, strStart, strLen); + uint thunkRVA = BitConverter.ToUInt32(_image, Offset); + SkipBytes(4); + builder.Append($" \"{lookupString}\"->RVA[0x{thunkRVA:X}]"); + } + builder.Append(')'); + break; + case ReadyToRunFixupKind.Check_VirtualFunctionOverride: case ReadyToRunFixupKind.Verify_VirtualFunctionOverride: ReadyToRunVirtualFunctionOverrideFlags flags = (ReadyToRunVirtualFunctionOverrideFlags)ReadUInt(); diff --git a/src/coreclr/vm/CMakeLists.txt b/src/coreclr/vm/CMakeLists.txt index a2dbcb4c89c429..d20b619fa3a97d 100644 --- a/src/coreclr/vm/CMakeLists.txt +++ b/src/coreclr/vm/CMakeLists.txt @@ -268,6 +268,14 @@ if(FEATURE_READYTORUN) ) endif(FEATURE_READYTORUN) +list(APPEND VM_SOURCES_DAC_AND_WKS_COMMON + pregeneratedstringthunks.cpp +) +list(APPEND VM_HEADERS_DAC_AND_WKS_COMMON + pregeneratedstringthunks.h + stringthunkhash.h +) + set(VM_SOURCES_DAC ${VM_SOURCES_DAC_AND_WKS_COMMON} conditionalweaktable.cpp # The usage of conditionalweaktable is only in the DAC, but we put the headers in the VM to enable validation. diff --git a/src/coreclr/vm/ceemain.cpp b/src/coreclr/vm/ceemain.cpp index 2d0680d41c6d19..b17a9a7510536b 100644 --- a/src/coreclr/vm/ceemain.cpp +++ b/src/coreclr/vm/ceemain.cpp @@ -176,6 +176,7 @@ #include "stringarraylist.h" #include "stubhelpers.h" +#include "pregeneratedstringthunks.h" #ifdef TARGET_WASM #include "wasm/helpers.hpp" #endif @@ -677,6 +678,8 @@ void EEStartupHelper() OnStackReplacementManager::StaticInitialize(); MethodTable::InitMethodDataCache(); + InitializePregeneratedStringThunkHash(); + #ifdef TARGET_WASM InitializeWasmThunkCaches(); #endif // TARGET_WASM diff --git a/src/coreclr/vm/jitinterface.cpp b/src/coreclr/vm/jitinterface.cpp index fda3b8af014b87..63c481d9f45de4 100644 --- a/src/coreclr/vm/jitinterface.cpp +++ b/src/coreclr/vm/jitinterface.cpp @@ -46,6 +46,7 @@ #include "sigbuilder.h" #include "openum.h" #include "fieldmarshaler.h" +#include "pregeneratedstringthunks.h" #ifdef HAVE_GCCOVER #include "gccover.h" #endif // HAVE_GCCOVER @@ -14740,6 +14741,16 @@ BOOL LoadDynamicInfoEntry(Module *currentModule, } break; } + + case READYTORUN_FIXUP_InjectStringThunks: + { + ReadyToRunInfo * pR2RInfo = currentModule->GetReadyToRunInfo(); + TADDR moduleBase = dac_cast(pR2RInfo->GetImage()->GetBase()); + ProcessInjectStringThunksFixup(moduleBase, pBlob); + result = 1; + } + break; + default: STRESS_LOG1(LF_ZAP, LL_WARNING, "Unknown ReadyToRunFixupKind %d\n", kind); _ASSERTE(!"Unknown ReadyToRunFixupKind"); diff --git a/src/coreclr/vm/pregeneratedstringthunks.cpp b/src/coreclr/vm/pregeneratedstringthunks.cpp new file mode 100644 index 00000000000000..78fb92e867c564 --- /dev/null +++ b/src/coreclr/vm/pregeneratedstringthunks.cpp @@ -0,0 +1,121 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// + +#include "common.h" +#include "pregeneratedstringthunks.h" +#include "stringthunkhash.h" + +#ifndef DACCESS_COMPILE + +static StringToThunkHash* s_pPregeneratedStringThunks = nullptr; + +void InitializePregeneratedStringThunkHash() +{ + WRAPPER_NO_CONTRACT; + + StringToThunkHash* newTable = new StringToThunkHash(); + s_pPregeneratedStringThunks = newTable; +} + +PCODE LookupPregeneratedThunkByString(const char* str) +{ + WRAPPER_NO_CONTRACT; + + StringToThunkHash* table = VolatileLoadWithoutBarrier(&s_pPregeneratedStringThunks); + if (table == nullptr) + return NULL; + + void* thunk; + if (table->Lookup(str, &thunk)) + return (PCODE)(size_t)thunk; + + return NULL; +} + +void ProcessInjectStringThunksFixup(TADDR moduleBase, PCCOR_SIGNATURE pBlob) +{ + STANDARD_VM_CONTRACT; + + // Retry loop: if another thread races us to update the global hash, we retry. + while (true) + { + StringToThunkHash* existingTable = VolatileLoadWithoutBarrier(&s_pPregeneratedStringThunks); + _ASSERTE(existingTable != nullptr); + + // First pass: check if there are any new strings not already in the table. + bool hasNewEntries = false; + { + PCCOR_SIGNATURE pCurrent = pBlob; + while (true) + { + const char* str = (const char*)pCurrent; + if (*str == '\0') + break; + + // Advance past the null-terminated string + pCurrent += strlen(str) + 1; + // Skip the 4-byte RVA/table index + pCurrent += 4; + + void* unused; + if (!existingTable->Lookup(str, &unused)) + { + hasNewEntries = true; + break; + } + } + } + + if (!hasNewEntries) + return; + + // Build a new table with all existing entries plus new ones. + StringToThunkHash* newTable = new StringToThunkHash(); + + // Copy existing entries + for (auto iter = existingTable->Begin(); iter != existingTable->End(); ++iter) + { + newTable->Add((*iter).Key(), (*iter).Value()); + } + + // Add new entries (existing entries take precedence) + { + PCCOR_SIGNATURE pCurrent = pBlob; + while (true) + { + const char* str = (const char*)pCurrent; + if (*str == '\0') + break; + + // Advance past the null-terminated string + pCurrent += strlen(str) + 1; + + // Read the 4-byte RVA (or WASM table index) + DWORD rva = GET_UNALIGNED_VAL32(pCurrent); + pCurrent += 4; + + void* unused; + if (!newTable->Lookup(str, &unused)) + { + void* codeAddr = (void*)(moduleBase + (TADDR)rva); + newTable->Add(str, codeAddr); + } + } + } + + // Try to swap in the new table. If CAS fails, another thread updated first - retry. + StringToThunkHash* previous = InterlockedCompareExchangeT(&s_pPregeneratedStringThunks, newTable, existingTable); + if (previous == existingTable) + { + // Success. The old table is intentionally leaked - it may still be in use + // by concurrent readers via VolatileLoadWithoutBarrier. + return; + } + + // CAS failed, another thread updated the table. Delete our new table and retry. + delete newTable; + } +} + +#endif // !DACCESS_COMPILE diff --git a/src/coreclr/vm/pregeneratedstringthunks.h b/src/coreclr/vm/pregeneratedstringthunks.h new file mode 100644 index 00000000000000..50fac9cad00f9b --- /dev/null +++ b/src/coreclr/vm/pregeneratedstringthunks.h @@ -0,0 +1,19 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// +#pragma once + +#include "daccess.h" + +// Initialize the global pregenerated string thunk hash table. +// Must be called during EE startup before any R2R module loading. +void InitializePregeneratedStringThunkHash(); + +// Look up a pregenerated thunk by its string key. +// Returns NULL if the string is not found in the table. +PCODE LookupPregeneratedThunkByString(const char* str); + +// Process a READYTORUN_FIXUP_InjectStringThunks fixup, adding new entries to the global hash. +// moduleBase is the base address of the R2R image. +// pBlob points to the first byte after the fixup kind byte in the signature. +void ProcessInjectStringThunksFixup(TADDR moduleBase, PCCOR_SIGNATURE pBlob); diff --git a/src/coreclr/vm/stringthunkhash.h b/src/coreclr/vm/stringthunkhash.h new file mode 100644 index 00000000000000..20f46afe8541b5 --- /dev/null +++ b/src/coreclr/vm/stringthunkhash.h @@ -0,0 +1,19 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// +#pragma once + +#include "shash.h" +#include "utilcode.h" + +// Hash traits for mapping C strings (const char*) to void pointers. +// Used for string-to-thunk lookup tables, both for WASM thunk caches +// and for ReadyToRun pregenerated string thunks. +class StringThunkSHashTraits : public MapSHashTraits +{ +public: + static BOOL Equals(const char* s1, const char* s2) { return strcmp(s1, s2) == 0; } + static count_t Hash(const char* key) { return HashStringA(key); } +}; + +typedef MapSHash> StringToThunkHash; diff --git a/src/coreclr/vm/wasm/helpers.cpp b/src/coreclr/vm/wasm/helpers.cpp index bcc938905914a4..a3340b4067c03c 100644 --- a/src/coreclr/vm/wasm/helpers.cpp +++ b/src/coreclr/vm/wasm/helpers.cpp @@ -5,7 +5,7 @@ #include #include #include "callhelpers.hpp" -#include "shash.h" +#include "stringthunkhash.h" #include "callingconvention.h" #include "cgensys.h" #include "readytorun.h" @@ -891,14 +891,7 @@ namespace return true; } - class StringThunkSHashTraits : public MapSHashTraits - { - public: - static BOOL Equals(const char* s1, const char* s2) { return strcmp(s1, s2) == 0; } - static count_t Hash(const char* key) { return HashStringA(key); } - }; - - typedef MapSHash> StringToWasmSigThunkHash; + typedef StringToThunkHash StringToWasmSigThunkHash; static StringToWasmSigThunkHash* thunkCache = nullptr; static StringToWasmSigThunkHash* portableEntrypointThunkCache = nullptr; From 88167910d49a8dcd7c02bf86383d1a0829523449 Mon Sep 17 00:00:00 2001 From: David Wrighton Date: Thu, 23 Apr 2026 15:10:21 -0700 Subject: [PATCH 02/43] Make InjectStringThunks import demand-driven via stub dependencies Instead of unconditionally rooting the InjectStringThunks import, store it on the NodeFactory and have each StringDiscoverableAssemblyStubNode declare a dependency on it via ComputeNonRelocationBasedDependencies. The import is only pulled into the graph when at least one stub is marked. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../StringDiscoverableAssemblyStubNode.cs | 11 +++++++++++ .../ReadyToRunCodegenNodeFactory.cs | 13 +++++++++---- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/StringDiscoverableAssemblyStubNode.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/StringDiscoverableAssemblyStubNode.cs index 35e97b9d91b17a..133ac674549d47 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/StringDiscoverableAssemblyStubNode.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/StringDiscoverableAssemblyStubNode.cs @@ -1,8 +1,11 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Collections.Generic; using System.Diagnostics; +using ILCompiler.DependencyAnalysisFramework; + namespace ILCompiler.DependencyAnalysis { /// @@ -19,6 +22,14 @@ public abstract class StringDiscoverableAssemblyStubNode : AssemblyStubNode /// public abstract string LookupString { get; } + protected override DependencyList ComputeNonRelocationBasedDependencies(NodeFactory factory) + { + DependencyList dependencies = new DependencyList(); + dependencies.Add(factory.InjectStringThunksImport, "StringDiscoverableAssemblyStubNode requires InjectStringThunks fixup"); + + return dependencies; + } + protected override void OnMarked(NodeFactory factory) { Debug.Assert(!string.IsNullOrEmpty(LookupString), "LookupString must be non-empty"); diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRunCodegenNodeFactory.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRunCodegenNodeFactory.cs index 8b5d547353155a..cb48e547534137 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRunCodegenNodeFactory.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRunCodegenNodeFactory.cs @@ -443,6 +443,12 @@ private void CreateNodeCaches() private readonly ConcurrentBag _stringDiscoverableStubs = new ConcurrentBag(); + /// + /// The eager import for the InjectStringThunks fixup. Created lazily when the first + /// StringDiscoverableAssemblyStubNode is registered. Each such stub depends on this import. + /// + public Import InjectStringThunksImport; + /// /// Register a StringDiscoverableAssemblyStubNode for inclusion in the InjectStringThunks fixup. /// Called by StringDiscoverableAssemblyStubNode.OnMarked. @@ -962,10 +968,9 @@ public void AttachToDependencyGraph(DependencyAnalyzerBase graph, I ReadyToRunHelper.Module)); graph.AddRoot(ModuleImport, "Module import is required by the R2R format spec"); - // String-discoverable thunk injection fixup. The signature collects all - // StringDiscoverableAssemblyStubNode instances at emission time. - Import injectStringThunksImport = new Import(EagerImports, new InjectStringThunksSignature()); - graph.AddRoot(injectStringThunksImport, "InjectStringThunks fixup for string-discoverable stubs"); + // Create the InjectStringThunks import but don't root it. It gets pulled in + // as a dependency of any StringDiscoverableAssemblyStubNode that gets marked. + InjectStringThunksImport = new Import(EagerImports, new InjectStringThunksSignature()); if ((Target.Architecture != TargetArchitecture.X86) && (Target.Architecture != TargetArchitecture.Wasm32)) { From 549b981d97c6dc59bfd574ca5fe82ee360b37996 Mon Sep 17 00:00:00 2001 From: David Wrighton Date: Thu, 23 Apr 2026 16:11:06 -0700 Subject: [PATCH 03/43] Return signature string from WasmLowering.GetSignature Change GetSignature to return (WasmFuncType, string) where the string is a compact serialization of the signature: Return type: 'v' (void), 'i'/'l'/'f'/'d'/'V' (primitives), 'S' (struct by ref with N bytes). Hidden params (this, retbuf, generic context, async continuation): 'i' or 'l' based on pointer size. Explicit params: 'i'/'l'/'f'/'d'/'V' (by value), 'S' (by ref), 'e' (empty struct, not emitted to WasmFuncType). Suffix 'p' indicates SP and PE params are generated (managed calls). Add IsEmptyStruct helper (stub returning false) for detecting empty structs by field count per the BasicCABI spec. Handle empty structs for both parameters ('e' encoding) and returns (treated as void). See https://github.com/dotnet/runtime/issues/127361. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Compiler/ObjectWriter/WasmObjectWriter.cs | 2 +- .../tools/Common/JitInterface/WasmLowering.cs | 88 +++++++++++++++++-- .../ReadyToRunCodegenNodeFactory.cs | 2 +- 3 files changed, 82 insertions(+), 10 deletions(-) diff --git a/src/coreclr/tools/Common/Compiler/ObjectWriter/WasmObjectWriter.cs b/src/coreclr/tools/Common/Compiler/ObjectWriter/WasmObjectWriter.cs index 7f1bb208f7b563..1f648cbaebcf2e 100644 --- a/src/coreclr/tools/Common/Compiler/ObjectWriter/WasmObjectWriter.cs +++ b/src/coreclr/tools/Common/Compiler/ObjectWriter/WasmObjectWriter.cs @@ -128,7 +128,7 @@ private void WriteSignatureIndexForFunction(MethodSignature managedSignature, Wa { SectionWriter writer = GetOrCreateSection(WasmObjectNodeSection.FunctionSection); - WasmFuncType signature = WasmLowering.GetSignature(managedSignature, flags); + (WasmFuncType signature, _) = WasmLowering.GetSignature(managedSignature, flags); Utf8String key = signature.GetMangledName(_nodeFactory.NameMangler); if (!_uniqueSignatures.TryGetValue(key, out int signatureIndex)) { diff --git a/src/coreclr/tools/Common/JitInterface/WasmLowering.cs b/src/coreclr/tools/Common/JitInterface/WasmLowering.cs index b49fa31d24888c..63e09327f3f042 100644 --- a/src/coreclr/tools/Common/JitInterface/WasmLowering.cs +++ b/src/coreclr/tools/Common/JitInterface/WasmLowering.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Diagnostics; +using System.Text; using ILCompiler; using ILCompiler.DependencyAnalysis.Wasm; @@ -121,6 +122,28 @@ public static WasmValueType LowerType(TypeDesc type) } } + /// + /// Determines whether a type is an empty struct (no instance fields) that should + /// be ignored in the WebAssembly calling convention per the BasicCABI spec. + /// + // WASM-TODO: This currently always returns false because .NET pads empty structs + // to size 1. A proper implementation should check for 0 non-static fields. + // See https://github.com/dotnet/runtime/issues/127361 + private static bool IsEmptyStruct(TypeDesc type) => false; + + /// + /// Maps a WasmValueType to its single-character signature encoding. + /// + private static char WasmValueTypeToSigChar(WasmValueType vt) => vt switch + { + WasmValueType.I32 => 'i', + WasmValueType.I64 => 'l', + WasmValueType.F32 => 'f', + WasmValueType.F64 => 'd', + WasmValueType.V128 => 'V', + _ => throw new NotSupportedException($"Unknown WasmValueType: {vt}") + }; + private static TypeDesc RaiseType(WasmValueType valueType, TypeSystemContext context) { return valueType switch @@ -156,7 +179,7 @@ public static MethodSignature RaiseSignature(WasmFuncType funcType, TypeSystemCo /// /// /// - public static WasmFuncType GetSignature(MethodDesc method) + public static (WasmFuncType FuncType, string SignatureString) GetSignature(MethodDesc method) { return GetSignature(method.Signature, GetLoweringFlags(method)); } @@ -188,10 +211,13 @@ public enum LoweringFlags IsUnmanagedCallersOnly = 0x4 } - public static WasmFuncType GetSignature(MethodSignature signature, LoweringFlags flags) + public static (WasmFuncType FuncType, string SignatureString) GetSignature(MethodSignature signature, LoweringFlags flags) { TypeDesc returnType = signature.ReturnType; WasmValueType pointerType = (signature.ReturnType.Context.Target.PointerSize == 4) ? WasmValueType.I32 : WasmValueType.I64; + char hiddenParamChar = WasmValueTypeToSigChar(pointerType); + + StringBuilder sigBuilder = new StringBuilder(); // Determine if the return value is via a return buffer // @@ -203,12 +229,29 @@ public static WasmFuncType GetSignature(MethodSignature signature, LoweringFlags if (loweredReturnType == null) { - hasReturnBuffer = true; - returnIsVoid = true; + if (IsEmptyStruct(returnType)) + { + // Empty struct return — treated as void with no return buffer + returnIsVoid = true; + sigBuilder.Append('v'); + } + else + { + hasReturnBuffer = true; + returnIsVoid = true; + int returnSize = returnType.GetElementSize().AsInt; + sigBuilder.Append('S'); + sigBuilder.Append(returnSize); + } } else if (loweredReturnType.IsVoid) { returnIsVoid = true; + sigBuilder.Append('v'); + } + else + { + sigBuilder.Append(WasmValueTypeToSigChar(LowerType(loweredReturnType))); } // Reserve space for potential implicit this, stack pointer parameter, portable entrypoint parameter, @@ -230,48 +273,77 @@ public static WasmFuncType GetSignature(MethodSignature signature, LoweringFlags if (hasReturnBuffer) { result.Add(pointerType); + sigBuilder.Append(hiddenParamChar); } } else // managed call { - result.Add(pointerType); // Stack pointer parameter + result.Add(pointerType); // Stack pointer parameter (encoded via 'p' suffix, not here) if (hasThis) { result.Add(pointerType); + sigBuilder.Append(hiddenParamChar); } if (hasReturnBuffer) { result.Add(pointerType); + sigBuilder.Append(hiddenParamChar); } } if (flags.HasFlag(LoweringFlags.HasGenericContextArg)) { result.Add(pointerType); // generic context + sigBuilder.Append(hiddenParamChar); } if (flags.HasFlag(LoweringFlags.IsAsyncCall)) { result.Add(pointerType); // async continuation + sigBuilder.Append(hiddenParamChar); } for (int i = explicitThis ? 1 : 0; i < signature.Length; i++) { - result.Add(LowerType(signature[i])); + TypeDesc paramType = signature[i]; + TypeDesc loweredParamType = LowerToAbiType(paramType); + + if (loweredParamType == null) + { + if (IsEmptyStruct(paramType)) + { + // Empty struct — not emitted as a WebAssembly argument + sigBuilder.Append('e'); + continue; + } + + // Struct that cannot be lowered to a single primitive — passed by reference + int paramSize = paramType.GetElementSize().AsInt; + sigBuilder.Append('S'); + sigBuilder.Append(paramSize); + result.Add(pointerType); + } + else + { + WasmValueType paramWasmType = LowerType(paramType); + sigBuilder.Append(WasmValueTypeToSigChar(paramWasmType)); + result.Add(paramWasmType); + } } if (!flags.HasFlag(LoweringFlags.IsUnmanagedCallersOnly)) { - result.Add(pointerType); // PE entrypoint parameter + result.Add(pointerType); // PE entrypoint parameter (encoded via 'p' suffix) + sigBuilder.Append('p'); } WasmResultType ps = new(result.ToArray()); WasmResultType ret = returnIsVoid ? new(Array.Empty()) : new([LowerType(loweredReturnType)]); - return new WasmFuncType(ps, ret); + return (new WasmFuncType(ps, ret), sigBuilder.ToString()); } } } diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRunCodegenNodeFactory.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRunCodegenNodeFactory.cs index cb48e547534137..60449676f91c13 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRunCodegenNodeFactory.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRunCodegenNodeFactory.cs @@ -1248,7 +1248,7 @@ public WasmTypeNode WasmTypeNode(CorInfoWasmType[] types) // memory efficiency on lookup public WasmTypeNode WasmTypeNode(MethodDesc method) { - WasmFuncType funcType = WasmLowering.GetSignature(method); + (WasmFuncType funcType, _) = WasmLowering.GetSignature(method); return _wasmTypeNodes.GetOrAdd(funcType); } } From 3ff2ebe0ad0599ebe7f8422360555d3dd4c1a7bf Mon Sep 17 00:00:00 2001 From: David Wrighton Date: Thu, 23 Apr 2026 16:21:11 -0700 Subject: [PATCH 04/43] Replace GetSignature tuple with WasmSignature struct Introduce WasmSignature readonly struct implementing IEquatable and IComparable. Equality and comparison are based on the signature string (with Debug.Assert that FuncType agrees when strings match). This enables sorting and deduplication of signatures by string alone. Update WasmLowering.GetSignature to return WasmSignature and update callers in WasmObjectWriter and ReadyToRunCodegenNodeFactory. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Target_Wasm/WasmTypes.cs | 36 +++++++++++++++++++ .../Compiler/ObjectWriter/WasmObjectWriter.cs | 2 +- .../tools/Common/JitInterface/WasmLowering.cs | 6 ++-- .../ReadyToRunCodegenNodeFactory.cs | 2 +- 4 files changed, 41 insertions(+), 5 deletions(-) diff --git a/src/coreclr/tools/Common/Compiler/DependencyAnalysis/Target_Wasm/WasmTypes.cs b/src/coreclr/tools/Common/Compiler/DependencyAnalysis/Target_Wasm/WasmTypes.cs index d2a560f9b98c36..d2814f9ff8ecdb 100644 --- a/src/coreclr/tools/Common/Compiler/DependencyAnalysis/Target_Wasm/WasmTypes.cs +++ b/src/coreclr/tools/Common/Compiler/DependencyAnalysis/Target_Wasm/WasmTypes.cs @@ -145,6 +145,42 @@ public static string ToTypeListString(this WasmResultType result) } } + public readonly struct WasmSignature : IEquatable, IComparable + { + public WasmFuncType FuncType { get; } + public string SignatureString { get; } + + public WasmSignature(WasmFuncType funcType, string signatureString) + { + FuncType = funcType; + SignatureString = signatureString; + } + + public bool Equals(WasmSignature other) + { + bool result = SignatureString.Equals(other.SignatureString, StringComparison.Ordinal); + Debug.Assert(!result || FuncType.Equals(other.FuncType), + "WasmSignature strings match but FuncTypes differ"); + + return result; + } + + public override bool Equals(object? obj) => obj is WasmSignature other && Equals(other); + + public override int GetHashCode() => StringComparer.Ordinal.GetHashCode(SignatureString); + + public int CompareTo(WasmSignature other) + { + int result = string.Compare(SignatureString, other.SignatureString, StringComparison.Ordinal); + Debug.Assert(result != 0 || FuncType.Equals(other.FuncType), + "WasmSignature strings match but FuncTypes differ"); + return result; + } + + public static bool operator ==(WasmSignature left, WasmSignature right) => left.Equals(right); + public static bool operator !=(WasmSignature left, WasmSignature right) => !left.Equals(right); + } + public struct WasmFuncType : IEquatable, IComparable { private readonly WasmResultType _params; diff --git a/src/coreclr/tools/Common/Compiler/ObjectWriter/WasmObjectWriter.cs b/src/coreclr/tools/Common/Compiler/ObjectWriter/WasmObjectWriter.cs index 1f648cbaebcf2e..07ecd1c268f0d8 100644 --- a/src/coreclr/tools/Common/Compiler/ObjectWriter/WasmObjectWriter.cs +++ b/src/coreclr/tools/Common/Compiler/ObjectWriter/WasmObjectWriter.cs @@ -128,7 +128,7 @@ private void WriteSignatureIndexForFunction(MethodSignature managedSignature, Wa { SectionWriter writer = GetOrCreateSection(WasmObjectNodeSection.FunctionSection); - (WasmFuncType signature, _) = WasmLowering.GetSignature(managedSignature, flags); + WasmFuncType signature = WasmLowering.GetSignature(managedSignature, flags).FuncType; Utf8String key = signature.GetMangledName(_nodeFactory.NameMangler); if (!_uniqueSignatures.TryGetValue(key, out int signatureIndex)) { diff --git a/src/coreclr/tools/Common/JitInterface/WasmLowering.cs b/src/coreclr/tools/Common/JitInterface/WasmLowering.cs index 63e09327f3f042..84387a178d6dd8 100644 --- a/src/coreclr/tools/Common/JitInterface/WasmLowering.cs +++ b/src/coreclr/tools/Common/JitInterface/WasmLowering.cs @@ -179,7 +179,7 @@ public static MethodSignature RaiseSignature(WasmFuncType funcType, TypeSystemCo /// /// /// - public static (WasmFuncType FuncType, string SignatureString) GetSignature(MethodDesc method) + public static WasmSignature GetSignature(MethodDesc method) { return GetSignature(method.Signature, GetLoweringFlags(method)); } @@ -211,7 +211,7 @@ public enum LoweringFlags IsUnmanagedCallersOnly = 0x4 } - public static (WasmFuncType FuncType, string SignatureString) GetSignature(MethodSignature signature, LoweringFlags flags) + public static WasmSignature GetSignature(MethodSignature signature, LoweringFlags flags) { TypeDesc returnType = signature.ReturnType; WasmValueType pointerType = (signature.ReturnType.Context.Target.PointerSize == 4) ? WasmValueType.I32 : WasmValueType.I64; @@ -343,7 +343,7 @@ public static (WasmFuncType FuncType, string SignatureString) GetSignature(Metho WasmResultType ret = returnIsVoid ? new(Array.Empty()) : new([LowerType(loweredReturnType)]); - return (new WasmFuncType(ps, ret), sigBuilder.ToString()); + return new WasmSignature(new WasmFuncType(ps, ret), sigBuilder.ToString()); } } } diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRunCodegenNodeFactory.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRunCodegenNodeFactory.cs index 60449676f91c13..7a8b209c8dc03f 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRunCodegenNodeFactory.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRunCodegenNodeFactory.cs @@ -1248,7 +1248,7 @@ public WasmTypeNode WasmTypeNode(CorInfoWasmType[] types) // memory efficiency on lookup public WasmTypeNode WasmTypeNode(MethodDesc method) { - (WasmFuncType funcType, _) = WasmLowering.GetSignature(method); + WasmFuncType funcType = WasmLowering.GetSignature(method).FuncType; return _wasmTypeNodes.GetOrAdd(funcType); } } From 1c79aab52326c23d82f514846a8d85400812a048 Mon Sep 17 00:00:00 2001 From: David Wrighton Date: Thu, 23 Apr 2026 17:05:54 -0700 Subject: [PATCH 05/43] Refactor WasmSignature, WasmImportThunk, and RaiseSignature - WasmImportThunk now takes WasmSignature and uses it for mangled name and comparison operations - WasmImportThunkPortableEntrypoint uses static WasmSignature values - RaiseSignature rewritten to parse signature string instead of WasmFuncType - Added CompilerTypeSystemContext.Wasm.cs with GetValueTupleStructOfSize cache using tree-based ValueTuple construction - Unmanaged calling convention flag set when 'p' suffix is absent - Roundtrip assert: raised signature re-lowered must equal original - Cache first empty struct found during lowering for 'e' roundtrip Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../CompilerTypeSystemContext.Wasm.cs | 89 ++++++++++++++++++ .../Target_Wasm/WasmTypes.cs | 2 +- .../tools/Common/JitInterface/WasmLowering.cs | 91 ++++++++++++++++--- .../ILCompiler.Compiler.csproj | 1 + .../ReadyToRun/WasmImportThunk.cs | 15 +-- .../WasmImportThunkPortableEntrypoint.cs | 21 +++-- .../ReadyToRunCodegenNodeFactory.cs | 21 +++-- .../ILCompiler.ReadyToRun.csproj | 1 + 8 files changed, 205 insertions(+), 36 deletions(-) create mode 100644 src/coreclr/tools/Common/Compiler/CompilerTypeSystemContext.Wasm.cs diff --git a/src/coreclr/tools/Common/Compiler/CompilerTypeSystemContext.Wasm.cs b/src/coreclr/tools/Common/Compiler/CompilerTypeSystemContext.Wasm.cs new file mode 100644 index 00000000000000..335eeb4e22de10 --- /dev/null +++ b/src/coreclr/tools/Common/Compiler/CompilerTypeSystemContext.Wasm.cs @@ -0,0 +1,89 @@ +// 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.Threading; + +using Internal.TypeSystem; + +namespace ILCompiler +{ + public partial class CompilerTypeSystemContext + { + private volatile TypeDesc[] _valueTupleStructsBySize = Array.Empty(); + private volatile TypeDesc _cachedEmptyStruct; + + /// + /// Gets the first empty struct type encountered during lowering, or null if none has been seen. + /// Used by RaiseSignature to produce a roundtrippable type for the 'e' encoding. + /// + public TypeDesc CachedEmptyStruct => _cachedEmptyStruct; + + /// + /// Caches an empty struct type discovered during lowering. Only the first one is retained. + /// + public void CacheEmptyStruct(TypeDesc type) + { + _cachedEmptyStruct ??= type; + } + + /// + /// Gets or creates a value type of the specified byte size, constructed from + /// nested ValueTuple<byte, ...> types. Used by WasmLowering to represent + /// struct parameters/returns in raised signatures. + /// + /// + /// Size 1 returns byte. + /// Size 2 returns ValueTuple<byte, byte>. + /// Size 5 returns ValueTuple<ValueTuple<byte, byte>, ValueTuple<byte, ValueTuple<byte, byte>>>. + /// Size N is split into halves: ValueTuple<(size N/2), (size N - N/2)>. + /// + public TypeDesc GetValueTupleStructOfSize(int size) + { + TypeDesc[] array = _valueTupleStructsBySize; + + if (size < array.Length && array[size] is not null) + { + return array[size]; + } + + return GetValueTupleStructOfSizeSlow(size); + } + + private TypeDesc GetValueTupleStructOfSizeSlow(int size) + { + TypeDesc[] array = _valueTupleStructsBySize; + + if (size >= array.Length) + { + TypeDesc[] newArray = new TypeDesc[size + 1]; + Array.Copy(array, newArray, array.Length); + _valueTupleStructsBySize = newArray; + array = newArray; + } + + TypeDesc result = BuildValueTupleStructOfSize(size); + array[size] = result; + + return result; + } + + private TypeDesc BuildValueTupleStructOfSize(int size) + { + TypeDesc byteType = GetWellKnownType(WellKnownType.Byte); + + if (size == 1) + { + return byteType; + } + + MetadataType valueTuple2 = SystemModule.GetType("System"u8, "ValueTuple`2"u8); + int leftSize = size / 2; + int rightSize = size - leftSize; + TypeDesc left = GetValueTupleStructOfSize(leftSize); + TypeDesc right = GetValueTupleStructOfSize(rightSize); + + return valueTuple2.MakeInstantiatedType(left, right); + } + } +} diff --git a/src/coreclr/tools/Common/Compiler/DependencyAnalysis/Target_Wasm/WasmTypes.cs b/src/coreclr/tools/Common/Compiler/DependencyAnalysis/Target_Wasm/WasmTypes.cs index d2814f9ff8ecdb..30944c04ce70cf 100644 --- a/src/coreclr/tools/Common/Compiler/DependencyAnalysis/Target_Wasm/WasmTypes.cs +++ b/src/coreclr/tools/Common/Compiler/DependencyAnalysis/Target_Wasm/WasmTypes.cs @@ -161,7 +161,7 @@ public bool Equals(WasmSignature other) bool result = SignatureString.Equals(other.SignatureString, StringComparison.Ordinal); Debug.Assert(!result || FuncType.Equals(other.FuncType), "WasmSignature strings match but FuncTypes differ"); - + return result; } diff --git a/src/coreclr/tools/Common/JitInterface/WasmLowering.cs b/src/coreclr/tools/Common/JitInterface/WasmLowering.cs index 84387a178d6dd8..11d8fac6667364 100644 --- a/src/coreclr/tools/Common/JitInterface/WasmLowering.cs +++ b/src/coreclr/tools/Common/JitInterface/WasmLowering.cs @@ -144,28 +144,90 @@ public static WasmValueType LowerType(TypeDesc type) _ => throw new NotSupportedException($"Unknown WasmValueType: {vt}") }; - private static TypeDesc RaiseType(WasmValueType valueType, TypeSystemContext context) + private static TypeDesc RaiseSigChar(char c, TypeSystemContext context) => c switch { - return valueType switch + 'i' => context.GetWellKnownType(WellKnownType.Int32), + 'l' => context.GetWellKnownType(WellKnownType.Int64), + 'f' => context.GetWellKnownType(WellKnownType.Single), + 'd' => context.GetWellKnownType(WellKnownType.Double), + 'V' => throw new NotSupportedException("SIMD types are not supported in this version of the compiler"), + _ => throw new InvalidOperationException($"Unknown signature char: {c}") + }; + + private static int ParseStructSize(string sig, ref int pos) + { + Debug.Assert(sig[pos] == 'S'); + pos++; // skip 'S' + int start = pos; + while (pos < sig.Length && char.IsDigit(sig[pos])) { - WasmValueType.I32 => context.GetWellKnownType(WellKnownType.Int32), - WasmValueType.I64 => context.GetWellKnownType(WellKnownType.Int64), - WasmValueType.F32 => context.GetWellKnownType(WellKnownType.Single), - WasmValueType.F64 => context.GetWellKnownType(WellKnownType.Double), - WasmValueType.V128 => throw new NotSupportedException("SIMD types are not supported in this version of the compiler"), - _ => throw new InvalidOperationException("Unknown WasmValueType: " + valueType), - }; + pos++; + } + return int.Parse(sig.AsSpan(start, pos - start)); } - public static MethodSignature RaiseSignature(WasmFuncType funcType, TypeSystemContext context) + public static MethodSignature RaiseSignature(WasmSignature wasmSignature, TypeSystemContext context) { + string sig = wasmSignature.SignatureString; + int pos = 0; + + // Parse return type + TypeDesc returnType; + if (sig[pos] == 'v') + { + returnType = context.GetWellKnownType(WellKnownType.Void); + pos++; + } + else if (sig[pos] == 'S') + { + int structSize = ParseStructSize(sig, ref pos); + returnType = ((CompilerTypeSystemContext)context).GetValueTupleStructOfSize(structSize); + } + else + { + returnType = RaiseSigChar(sig[pos], context); + pos++; + } + + // Parse parameters (everything until 'p' suffix or end of string) List parameters = new List(); - for (int i = 1; i < funcType.Params.Types.Length - 1; i++) + while (pos < sig.Length && sig[pos] != 'p') { - parameters.Add(RaiseType(funcType.Params.Types[i], context)); + char c = sig[pos]; + if (c == 'e') + { + // Empty struct — include the cached empty struct type for roundtrip fidelity + TypeDesc emptyStruct = ((CompilerTypeSystemContext)context).CachedEmptyStruct; + Debug.Assert(emptyStruct is not null, "Encountered 'e' in signature but no empty struct was cached during lowering"); + parameters.Add(emptyStruct); + pos++; + } + else if (c == 'S') + { + int structSize = ParseStructSize(sig, ref pos); + parameters.Add(((CompilerTypeSystemContext)context).GetValueTupleStructOfSize(structSize)); + } + else + { + parameters.Add(RaiseSigChar(c, context)); + pos++; + } } - TypeDesc returnType = funcType.Returns.Types.Length > 0 ? RaiseType(funcType.Returns.Types[0], context) : context.GetWellKnownType(WellKnownType.Void); - return new MethodSignature(MethodSignatureFlags.Static, 0, returnType, parameters.ToArray()); + + bool isManaged = pos < sig.Length && sig[pos] == 'p'; + MethodSignatureFlags flags = MethodSignatureFlags.Static; + if (!isManaged) + { + flags |= MethodSignatureFlags.UnmanagedCallingConvention; + } + + MethodSignature result = new MethodSignature(flags, 0, returnType, parameters.ToArray()); + + LoweringFlags relowerFlags = isManaged ? LoweringFlags.None : LoweringFlags.IsUnmanagedCallersOnly; + Debug.Assert(GetSignature(result, relowerFlags).Equals(wasmSignature), + "RaiseSignature produced a signature that does not roundtrip back to the same WasmSignature"); + + return result; } /// @@ -316,6 +378,7 @@ public static WasmSignature GetSignature(MethodSignature signature, LoweringFlag { // Empty struct — not emitted as a WebAssembly argument sigBuilder.Append('e'); + ((CompilerTypeSystemContext)signature.ReturnType.Context).CacheEmptyStruct(paramType); continue; } diff --git a/src/coreclr/tools/aot/ILCompiler.Compiler/ILCompiler.Compiler.csproj b/src/coreclr/tools/aot/ILCompiler.Compiler/ILCompiler.Compiler.csproj index a0e5f77c7c533b..90cebf4f9eb504 100644 --- a/src/coreclr/tools/aot/ILCompiler.Compiler/ILCompiler.Compiler.csproj +++ b/src/coreclr/tools/aot/ILCompiler.Compiler/ILCompiler.Compiler.csproj @@ -37,6 +37,7 @@ + diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/WasmImportThunk.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/WasmImportThunk.cs index 5e6e47fe771069..63020070a93009 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/WasmImportThunk.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/WasmImportThunk.cs @@ -19,6 +19,7 @@ public class WasmImportThunk : AssemblyStubNode, INodeWithTypeSignature, ISymbol private readonly TypeSystemContext _context; private readonly Import _helperCell; private readonly WasmTypeNode _typeNode; + private readonly WasmSignature _wasmSignature; private readonly ImportThunkKind _thunkKind; @@ -33,10 +34,11 @@ public class WasmImportThunk : AssemblyStubNode, INodeWithTypeSignature, ISymbol /// Import thunks are used to call a runtime-provided helper which fixes up an indirection cell in a particular /// import section. Optionally they may also contain a relocation for a specific indirection cell to fix up. /// - public WasmImportThunk(NodeFactory factory, WasmTypeNode typeNode, ReadyToRunHelper helperId, ImportSectionNode containingImportSection, bool useVirtualCall, bool useJumpableStub) + public WasmImportThunk(NodeFactory factory, WasmSignature wasmSignature, ReadyToRunHelper helperId, ImportSectionNode containingImportSection, bool useVirtualCall, bool useJumpableStub) { _context = factory.TypeSystemContext; - _typeNode = typeNode; + _wasmSignature = wasmSignature; + _typeNode = factory.WasmTypeNode(wasmSignature); _helperCell = factory.GetReadyToRunHelperCell(helperId); _containingImportSection = containingImportSection; @@ -72,8 +74,7 @@ public override void AppendMangledName(NameMangler nameMangler, Utf8StringBuilde { sb.Append("WasmDelayLoadHelper->"u8); _helperCell.AppendMangledName(nameMangler, sb); - sb.Append($"(ImportSection:{_containingImportSection.Name},Kind:{_thunkKind})"); - _typeNode.AppendMangledName(nameMangler, sb); + sb.Append($"(ImportSection:{_containingImportSection.Name},Kind:{_thunkKind},Sig:{_wasmSignature.SignatureString})"); } protected override string GetName(NodeFactory factory) @@ -85,7 +86,7 @@ protected override string GetName(NodeFactory factory) public override int ClassCode => 948271336; - MethodSignature INodeWithTypeSignature.Signature => WasmLowering.RaiseSignature(_typeNode.Type, _context); + MethodSignature INodeWithTypeSignature.Signature => WasmLowering.RaiseSignature(_wasmSignature, _context); bool INodeWithTypeSignature.IsUnmanagedCallersOnly => false; bool INodeWithTypeSignature.IsAsyncCall => false; @@ -98,7 +99,7 @@ public override int CompareToImpl(ISortableNode other, CompilerComparer comparer if (result != 0) return result; - result = _typeNode.CompareToImpl(otherNode._typeNode, comparer); + result = _wasmSignature.CompareTo(otherNode._wasmSignature); if (result != 0) return result; @@ -122,7 +123,7 @@ protected override void EmitCode(NodeFactory factory, ref Wasm.WasmEmitter instr ISymbolNode helperTypeIndex = factory.WasmTypeNode(_helperTypeParams); - MethodSignature methodSignature = WasmLowering.RaiseSignature(_typeNode.Type, _context); + MethodSignature methodSignature = WasmLowering.RaiseSignature(_wasmSignature, _context); (ArgIterator argit, TransitionBlock transitionBlock) = GCRefMapBuilder.BuildArgIterator(methodSignature, _context); int[] offsets = new int[methodSignature.Length]; diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/WasmImportThunkPortableEntrypoint.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/WasmImportThunkPortableEntrypoint.cs index 3a0faf084a0d38..a29a614e61cf13 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/WasmImportThunkPortableEntrypoint.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/WasmImportThunkPortableEntrypoint.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using ILCompiler.DependencyAnalysis.Wasm; using Internal.ReadyToRunConstants; using Internal.Text; using Internal.JitInterface; @@ -53,8 +54,16 @@ public override int CompareToImpl(ISortableNode other, CompilerComparer comparer return comparer.Compare(_import, otherNode._import); } - private static readonly CorInfoWasmType[] _genericLookupTypes32Bit = new CorInfoWasmType[] { CorInfoWasmType.CORINFO_WASM_TYPE_I32, CorInfoWasmType.CORINFO_WASM_TYPE_I32, CorInfoWasmType.CORINFO_WASM_TYPE_I32 }; - private static readonly CorInfoWasmType[] _genericLookupTypes64Bit = new CorInfoWasmType[] { CorInfoWasmType.CORINFO_WASM_TYPE_I64, CorInfoWasmType.CORINFO_WASM_TYPE_I64, CorInfoWasmType.CORINFO_WASM_TYPE_I64 }; + private static readonly WasmSignature _genericLookupSignature32Bit = new WasmSignature( + new WasmFuncType( + new WasmResultType(new[] { WasmValueType.I32, WasmValueType.I32 }), + new WasmResultType(new[] { WasmValueType.I32 })), + "iii"); + private static readonly WasmSignature _genericLookupSignature64Bit = new WasmSignature( + new WasmFuncType( + new WasmResultType(new[] { WasmValueType.I64, WasmValueType.I64 }), + new WasmResultType(new[] { WasmValueType.I64 })), + "lll"); public override ObjectData GetData(NodeFactory factory, System.Boolean relocsOnly = false) { @@ -62,19 +71,19 @@ public override ObjectData GetData(NodeFactory factory, System.Boolean relocsOnl ObjectDataBuilder builder = new ObjectDataBuilder(factory, relocsOnly); builder.AddSymbol(this); - WasmTypeNode typeNode; + WasmSignature wasmSignature; RelocType tableIndexPointerRelocType = factory.Target.PointerSize == 4 ? RelocType.WASM_TABLE_INDEX_I32 : RelocType.WASM_TABLE_INDEX_I64; if (_import.Signature is GenericLookupSignature) { - typeNode = factory.WasmTypeNode(factory.Target.PointerSize == 4 ? _genericLookupTypes32Bit : _genericLookupTypes64Bit); + wasmSignature = factory.Target.PointerSize == 4 ? _genericLookupSignature32Bit : _genericLookupSignature64Bit; } else { - typeNode = factory.WasmTypeNode(((MethodFixupSignature)(_import.Signature)).Method); + wasmSignature = WasmLowering.GetSignature(((MethodFixupSignature)(_import.Signature)).Method); } - builder.EmitReloc(factory.WasmImportThunk(typeNode, HelperId, _import.Table, UseVirtualCall, UseJumpableStub), tableIndexPointerRelocType); + builder.EmitReloc(factory.WasmImportThunk(wasmSignature, HelperId, _import.Table, UseVirtualCall, UseJumpableStub), tableIndexPointerRelocType); builder.EmitReloc(_import, RelocType.IMAGE_REL_BASED_ADDR32NB); if (factory.Target.PointerSize == 8) { diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRunCodegenNodeFactory.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRunCodegenNodeFactory.cs index 7a8b209c8dc03f..9bc229cce50974 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRunCodegenNodeFactory.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRunCodegenNodeFactory.cs @@ -297,7 +297,7 @@ private void CreateNodeCaches() _wasmImportThunks = new NodeCache(key => { - return new WasmImportThunk(this, key.TypeNode, key.Helper, key.ContainingImportSection, key.UseVirtualCall, key.UseJumpableStub); + return new WasmImportThunk(this, key.Signature, key.Helper, key.ContainingImportSection, key.UseVirtualCall, key.UseJumpableStub); }); _wasmImportThunkPortableEntrypoints = new NodeCache(key => @@ -755,15 +755,15 @@ public ISymbolDefinitionNode ImportThunk(ReadyToRunHelper helper, ImportSectionN } private struct WasmImportThunkKey : IEquatable { - public readonly WasmTypeNode TypeNode; + public readonly WasmSignature Signature; public readonly ReadyToRunHelper Helper; public readonly ImportSectionNode ContainingImportSection; public readonly bool UseVirtualCall; public readonly bool UseJumpableStub; - public WasmImportThunkKey(WasmTypeNode typeNode, ReadyToRunHelper helper, ImportSectionNode containingImportSection, bool useVirtualCall, bool useJumpableStub) + public WasmImportThunkKey(WasmSignature signature, ReadyToRunHelper helper, ImportSectionNode containingImportSection, bool useVirtualCall, bool useJumpableStub) { - TypeNode = typeNode; + Signature = signature; Helper = helper; ContainingImportSection = containingImportSection; UseVirtualCall = useVirtualCall; @@ -772,7 +772,7 @@ public WasmImportThunkKey(WasmTypeNode typeNode, ReadyToRunHelper helper, Import public bool Equals(WasmImportThunkKey other) { - return TypeNode == other.TypeNode && + return Signature.Equals(other.Signature) && Helper == other.Helper && ContainingImportSection == other.ContainingImportSection && UseVirtualCall == other.UseVirtualCall && @@ -787,7 +787,7 @@ public override bool Equals(object obj) public override int GetHashCode() { return HashCode.Combine(Helper.GetHashCode(), - TypeNode.GetHashCode(), + Signature.GetHashCode(), ContainingImportSection.GetHashCode(), UseVirtualCall.GetHashCode(), UseJumpableStub.GetHashCode()); @@ -796,9 +796,9 @@ public override int GetHashCode() private NodeCache _wasmImportThunks; - public ISymbolDefinitionNode WasmImportThunk(WasmTypeNode typeNode, ReadyToRunHelper helper, ImportSectionNode containingImportSection, bool useVirtualCall, bool useJumpableStub) + public ISymbolDefinitionNode WasmImportThunk(WasmSignature signature, ReadyToRunHelper helper, ImportSectionNode containingImportSection, bool useVirtualCall, bool useJumpableStub) { - WasmImportThunkKey thunkKey = new WasmImportThunkKey(typeNode, helper, containingImportSection, useVirtualCall, useJumpableStub); + WasmImportThunkKey thunkKey = new WasmImportThunkKey(signature, helper, containingImportSection, useVirtualCall, useJumpableStub); return _wasmImportThunks.GetOrAdd(thunkKey); } @@ -1244,6 +1244,11 @@ public WasmTypeNode WasmTypeNode(CorInfoWasmType[] types) return _wasmTypeNodes.GetOrAdd(funcType); } + public WasmTypeNode WasmTypeNode(WasmSignature signature) + { + return _wasmTypeNodes.GetOrAdd(signature.FuncType); + } + // TODO-Wasm: Do not use WasmFuncType directly as the key for better // memory efficiency on lookup public WasmTypeNode WasmTypeNode(MethodDesc method) diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/ILCompiler.ReadyToRun.csproj b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/ILCompiler.ReadyToRun.csproj index 67d7d95eb339f7..808bf4c138153a 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/ILCompiler.ReadyToRun.csproj +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/ILCompiler.ReadyToRun.csproj @@ -70,6 +70,7 @@ + From b6ada10155bbec38b2652439c01d0554da5719c3 Mon Sep 17 00:00:00 2001 From: David Wrighton Date: Thu, 23 Apr 2026 17:16:53 -0700 Subject: [PATCH 06/43] WasmImportThunk: iterate raised MethodSignature for arg handling Instead of iterating the wasm-level _typeNode params, iterate the raised MethodSignature. This enables: - Indirect struct args: zero-fill the transition block slot on store, and pass the original byref local directly on restore - Empty struct args: skip entirely (no wasm local exists) - Made WasmLowering.IsEmptyStruct public for cross-file access Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../tools/Common/JitInterface/WasmLowering.cs | 2 +- .../ReadyToRun/WasmImportThunk.cs | 148 ++++++++++++------ 2 files changed, 101 insertions(+), 49 deletions(-) diff --git a/src/coreclr/tools/Common/JitInterface/WasmLowering.cs b/src/coreclr/tools/Common/JitInterface/WasmLowering.cs index 11d8fac6667364..f107dd4b4d5854 100644 --- a/src/coreclr/tools/Common/JitInterface/WasmLowering.cs +++ b/src/coreclr/tools/Common/JitInterface/WasmLowering.cs @@ -129,7 +129,7 @@ public static WasmValueType LowerType(TypeDesc type) // WASM-TODO: This currently always returns false because .NET pads empty structs // to size 1. A proper implementation should check for 0 non-static fields. // See https://github.com/dotnet/runtime/issues/127361 - private static bool IsEmptyStruct(TypeDesc type) => false; + public static bool IsEmptyStruct(TypeDesc type) => false; /// /// Maps a WasmValueType to its single-character signature encoding. diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/WasmImportThunk.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/WasmImportThunk.cs index 63020070a93009..6adf972d9cf9c2 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/WasmImportThunk.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/WasmImportThunk.cs @@ -127,13 +127,14 @@ protected override void EmitCode(NodeFactory factory, ref Wasm.WasmEmitter instr (ArgIterator argit, TransitionBlock transitionBlock) = GCRefMapBuilder.BuildArgIterator(methodSignature, _context); int[] offsets = new int[methodSignature.Length]; - Debug.Assert(offsets.Length == _typeNode.Type.Params.Types.Length - 2); + bool[] isIndirectArg = new bool[methodSignature.Length]; int argIndex = 0; int argOffset; while ((argOffset = argit.GetNextOffset()) != TransitionBlock.InvalidOffset) { offsets[argIndex] = argOffset; + isIndirectArg[argIndex] = argit.IsArgPassedByRef(); argIndex++; } @@ -178,32 +179,63 @@ protected override void EmitCode(NodeFactory factory, ref Wasm.WasmEmitter instr // } // In the calling convention, the first arg is the sp arg, and the last is the portable entrypoint arg. Each of those are treated specially - for (int i = 1; i < _typeNode.Type.Params.Types.Length - 1; i++) + // Iterate over the raised MethodSignature params rather than wasm-level types. + // This allows us to: + // - Skip empty struct params (no wasm local exists) + // - Zero-fill indirect struct params instead of copying the byref pointer + int wasmLocalIndex = 1; // local 0 is $sp + for (int i = 0; i < methodSignature.Length; i++) { - expressions.Add(Local.Get(0)); - expressions.Add(Local.Get(i)); - WasmValueType type = _typeNode.Type.Params.Types[i]; - int currentOffset = offsets[i - 1]; - switch (type) + TypeDesc paramType = methodSignature[i]; + + if (WasmLowering.IsEmptyStruct(paramType)) + { + // Empty struct — no wasm local, nothing to store + continue; + } + + int currentOffset = offsets[i]; + + if (isIndirectArg[i]) + { + // Indirect struct — zero-fill the transition block slot instead of copying the byref pointer. + // The slot is pointer-aligned in the transition block, so we can safely zero in 4-byte chunks. + int structSize = paramType.GetElementSize().AsInt; + for (int zeroOffset = 0; zeroOffset < structSize; zeroOffset += 4) + { + expressions.Add(Local.Get(0)); + expressions.Add(I32.Const(0)); + expressions.Add(I32.Store((ulong)(currentOffset + zeroOffset))); + } + wasmLocalIndex++; + } + else { - case WasmValueType.I32: - expressions.Add(I32.Store((ulong)currentOffset)); - break; - case WasmValueType.F32: - expressions.Add(F32.Store((ulong)currentOffset)); - break; - case WasmValueType.I64: - expressions.Add(I64.Store((ulong)currentOffset)); - break; - case WasmValueType.F64: - expressions.Add(F64.Store((ulong)currentOffset)); - break; - case WasmValueType.V128: - expressions.Add(V128.Store((ulong)currentOffset)); - break; - - default: - throw new System.Exception("Unexpected wasm type arg"); + expressions.Add(Local.Get(0)); + expressions.Add(Local.Get(wasmLocalIndex)); + WasmValueType type = _typeNode.Type.Params.Types[wasmLocalIndex]; + switch (type) + { + case WasmValueType.I32: + expressions.Add(I32.Store((ulong)currentOffset)); + break; + case WasmValueType.F32: + expressions.Add(F32.Store((ulong)currentOffset)); + break; + case WasmValueType.I64: + expressions.Add(I64.Store((ulong)currentOffset)); + break; + case WasmValueType.F64: + expressions.Add(F64.Store((ulong)currentOffset)); + break; + case WasmValueType.V128: + expressions.Add(V128.Store((ulong)currentOffset)); + break; + + default: + throw new System.Exception("Unexpected wasm type arg"); + } + wasmLocalIndex++; } } // @@ -246,31 +278,51 @@ protected override void EmitCode(NodeFactory factory, ref Wasm.WasmEmitter instr // local.set (i+1) // } // In the calling convention, the first arg is the sp arg, and the last is the portable entrypoint arg. Each of those are treated specially - for (int i = 1; i < _typeNode.Type.Params.Types.Length - 1; i++) + // Iterate over the raised MethodSignature params to handle indirect/empty structs correctly + wasmLocalIndex = 1; + for (int i = 0; i < methodSignature.Length; i++) { - expressions.Add(Local.Get(0)); - WasmValueType type = _typeNode.Type.Params.Types[i]; - int currentOffset = offsets[i - 1]; - switch (type) + TypeDesc paramType = methodSignature[i]; + + if (WasmLowering.IsEmptyStruct(paramType)) + { + // Empty struct — no wasm local, nothing to restore + continue; + } + + if (isIndirectArg[i]) + { + // Indirect struct — pass the original byref pointer from the caller + expressions.Add(Local.Get(wasmLocalIndex)); + wasmLocalIndex++; + } + else { - case WasmValueType.I32: - expressions.Add(I32.Load((ulong)currentOffset)); - break; - case WasmValueType.F32: - expressions.Add(F32.Load((ulong)currentOffset)); - break; - case WasmValueType.I64: - expressions.Add(I64.Load((ulong)currentOffset)); - break; - case WasmValueType.F64: - expressions.Add(F64.Load((ulong)currentOffset)); - break; - case WasmValueType.V128: - expressions.Add(V128.Load((ulong)currentOffset)); - break; - - default: - throw new System.Exception("Unexpected wasm type arg"); + expressions.Add(Local.Get(0)); + WasmValueType type = _typeNode.Type.Params.Types[wasmLocalIndex]; + int currentOffset = offsets[i]; + switch (type) + { + case WasmValueType.I32: + expressions.Add(I32.Load((ulong)currentOffset)); + break; + case WasmValueType.F32: + expressions.Add(F32.Load((ulong)currentOffset)); + break; + case WasmValueType.I64: + expressions.Add(I64.Load((ulong)currentOffset)); + break; + case WasmValueType.F64: + expressions.Add(F64.Load((ulong)currentOffset)); + break; + case WasmValueType.V128: + expressions.Add(V128.Load((ulong)currentOffset)); + break; + + default: + throw new System.Exception("Unexpected wasm type arg"); + } + wasmLocalIndex++; } } // ; Add the portable entrypoint arg From 26927268a84356f888a7fca5bdb79001a2fc8ec6 Mon Sep 17 00:00:00 2001 From: David Wrighton Date: Fri, 24 Apr 2026 14:28:20 -0700 Subject: [PATCH 07/43] Flow through the jit call site recording into the existing recordCallSite api. Co-authored-by: Copilot --- src/coreclr/jit/codegenwasm.cpp | 5 +++- src/coreclr/jit/emit.cpp | 4 +-- src/coreclr/jit/emit.h | 8 +++++- src/coreclr/jit/emitwasm.cpp | 22 ++++++++++++++- src/coreclr/jit/gentree.cpp | 4 +-- src/coreclr/jit/gentree.h | 2 +- src/coreclr/jit/importercalls.cpp | 6 +++- src/coreclr/jit/jit.h | 6 ++++ src/coreclr/jit/lower.cpp | 8 ++++++ .../tools/Common/JitInterface/CorInfoImpl.cs | 6 ---- .../JitInterface/CorInfoImpl.ReadyToRun.cs | 28 +++++++++++++++++++ .../JitInterface/CorInfoImpl.RyuJit.cs | 6 ++++ 12 files changed, 90 insertions(+), 15 deletions(-) diff --git a/src/coreclr/jit/codegenwasm.cpp b/src/coreclr/jit/codegenwasm.cpp index a9d5308d63ee41..cc28164042272a 100644 --- a/src/coreclr/jit/codegenwasm.cpp +++ b/src/coreclr/jit/codegenwasm.cpp @@ -2477,14 +2477,17 @@ void CodeGen::genCallInstruction(GenTreeCall* call) params.debugInfo = di; } -#ifdef DEBUG +#if defined(DEBUG) || defined(TARGET_WASM) // Pass the call signature information down into the emitter so the emitter can associate // native call sites with the signatures they were generated from. if (!call->IsHelperCall()) { + _ASSERTE(params.hasAsyncRet == call->callSig->isAsyncCall()); params.sigInfo = call->callSig; + params.isUnmanagedCall = call->IsUnmanaged(); } #endif // DEBUG + GenTree* target = getCallTarget(call, ¶ms.methHnd); ArrayStack typeStack(m_compiler->getAllocator(CMK_Codegen)); diff --git a/src/coreclr/jit/emit.cpp b/src/coreclr/jit/emit.cpp index 98f74b755e1408..d1744150a629fa 100644 --- a/src/coreclr/jit/emit.cpp +++ b/src/coreclr/jit/emit.cpp @@ -745,7 +745,7 @@ void emitter::emitBegCG(Compiler* comp, COMP_HANDLE cmpHandle) m_compiler = comp; emitCmpHandle = cmpHandle; m_debugInfoSize = sizeof(instrDescDebugInfo*); -#ifndef DEBUG +#if !defined(DEBUG) && !defined(TARGET_WASM) if (!comp->opts.disAsm) m_debugInfoSize = 0; #endif @@ -10466,7 +10466,7 @@ void emitter::emitRecordCallSite(ULONG instrOffset, /* IN */ CORINFO_SIG_INFO* callSig, /* IN */ CORINFO_METHOD_HANDLE methodHandle) /* IN */ { -#if defined(DEBUG) +#if defined(DEBUG) || defined(TARGET_WASM) // Since CORINFO_SIG_INFO is a heavyweight structure, in most cases we can // lazily obtain it here using the given method handle (we only save the sig // info when we explicitly need it, i.e. for CALLI calls, vararg calls, and diff --git a/src/coreclr/jit/emit.h b/src/coreclr/jit/emit.h index 039e2ccaad169e..ec35a4dbf61c60 100644 --- a/src/coreclr/jit/emit.h +++ b/src/coreclr/jit/emit.h @@ -464,7 +464,7 @@ struct EmitCallParams { EmitCallType callType = EC_COUNT; CORINFO_METHOD_HANDLE methHnd = NO_METHOD_HANDLE; -#ifdef DEBUG +#if defined(DEBUG) || defined(TARGET_WASM) // Used to report call sites to the EE CORINFO_SIG_INFO* sigInfo = nullptr; #endif @@ -484,6 +484,9 @@ struct EmitCallParams ssize_t disp = 0; bool isJump = false; bool noSafePoint = false; +#ifdef TARGET_WASM + bool isUnmanagedCall = false; +#endif #ifdef TARGET_WASM CORINFO_WASM_TYPE_SYMBOL_HANDLE wasmSignature = nullptr; #endif @@ -639,6 +642,9 @@ class emitter GenTreeFlags idFlags = GTF_EMPTY; // for determining type of handle in idMemCookie bool idFinallyCall = false; // Branch instruction is a call to finally bool idCatchRet = false; // Instruction is for a catch 'return' +#ifdef TARGET_WASM + bool idIsUnmanagedCall = false; // Instruction is for an unmanaged call +#endif CORINFO_SIG_INFO* idCallSig = nullptr; // Used to report native call site signatures to the EE BasicBlock* idTargetBlock = nullptr; // Target block for branches diff --git a/src/coreclr/jit/emitwasm.cpp b/src/coreclr/jit/emitwasm.cpp index 3492406ccf2cbd..9c1132ae005cf2 100644 --- a/src/coreclr/jit/emitwasm.cpp +++ b/src/coreclr/jit/emitwasm.cpp @@ -206,9 +206,11 @@ void emitter::emitIns_Call(const EmitCallParams& params) unreached(); } + _ASSERTE(m_debugInfoSize > 0); // We always need the idCallSig so we can properly report the call sites to the R2R compiler if (m_debugInfoSize > 0) { - INDEBUG(id->idDebugOnlyInfo()->idCallSig = params.sigInfo); + id->idDebugOnlyInfo()->idCallSig = params.sigInfo; + id->idDebugOnlyInfo()->isUnmanagedCall = params.isUnmanagedCall; id->idDebugOnlyInfo()->idMemCookie = (size_t)params.methHnd; // method token id->idDebugOnlyInfo()->idFlags = GTF_ICON_METHOD_HDL; } @@ -854,6 +856,24 @@ size_t emitter::emitOutputInstr(insGroup* ig, instrDesc* id, BYTE** dp) } #endif + if ((ins == INS_call) || (ins == INS_return_call) || (ins == INS_call_indirect) || (ins == INS_return_call_indirect)) + { + CORINFO_SIG_INFO sigInfoLocal; + + CORINFO_SIG_INFO *sigInfoCall = id->idDebugOnlyInfo()->idCallSig; + if (id->idDebugOnlyInfo()->isUnmanagedCall) + { + _ASSERTE(sigInfoCall != NULL); + sigInfoLocal = *sigInfoCall; + // Unmanaged calls need to be reported with the unmanaged calling convention so that the R2R compiler can ignore this report + // for the purpose of determining if a call site needs to have a R2R to interpreter thunk generated + sigInfoLocal.callConv = CORINFO_CALLCONV_UNMGD; + sigInfoCall = &sigInfoLocal; + } + emitRecordCallSite(emitCurCodeOffs(*dp), sigInfoCall, + (CORINFO_METHOD_HANDLE)id->idDebugOnlyInfo()->idMemCookie); + } + *dp = dst; return sz; } diff --git a/src/coreclr/jit/gentree.cpp b/src/coreclr/jit/gentree.cpp index 826fba0d2ce604..80731fe62f3f0a 100644 --- a/src/coreclr/jit/gentree.cpp +++ b/src/coreclr/jit/gentree.cpp @@ -9758,7 +9758,7 @@ GenTreeCall* Compiler::gtNewCallNode(gtCallTypes callType, node->gtFlags |= GTF_CALL_POP_ARGS; #endif // UNIX_X86_ABI node->gtCallType = callType; - INDEBUG(node->callSig = nullptr;) + INDEBUG_OR_WASM(node->callSig = nullptr;) node->tailCallInfo = nullptr; node->gtRetClsHnd = nullptr; node->gtCallMoreFlags = GTF_CALL_M_EMPTY; @@ -11350,7 +11350,7 @@ GenTreeCall* Compiler::gtCloneExprCallHelper(GenTreeCall* tree) // we only really need one physical copy of it. Therefore a shallow pointer copy will suffice. // (Note that this still holds even if the tree we are cloning was created by an inlinee compiler, // because the inlinee still uses the inliner's memory allocator anyway.) - INDEBUG(copy->callSig = tree->callSig;) + INDEBUG_OR_WASM(copy->callSig = tree->callSig;) if (tree->IsUnmanaged()) { diff --git a/src/coreclr/jit/gentree.h b/src/coreclr/jit/gentree.h index 5d77e2218adfbe..390aee9a927ed4 100644 --- a/src/coreclr/jit/gentree.h +++ b/src/coreclr/jit/gentree.h @@ -5143,7 +5143,7 @@ struct GenTreeCall final : public GenTree { CallArgs gtArgs; -#ifdef DEBUG +#if defined(DEBUG) || defined(TARGET_WASM) // Used to register callsites with the EE CORINFO_SIG_INFO* callSig; #endif diff --git a/src/coreclr/jit/importercalls.cpp b/src/coreclr/jit/importercalls.cpp index 6ef07513ee4b10..aeb74a2b9d5836 100644 --- a/src/coreclr/jit/importercalls.cpp +++ b/src/coreclr/jit/importercalls.cpp @@ -1102,10 +1102,14 @@ var_types Compiler::impImportCall(OPCODE opcode, DONE: -#ifdef DEBUG +#if defined(DEBUG) || defined(TARGET_WASM) // In debug we want to be able to register callsites with the EE. assert(call->AsCall()->callSig == nullptr); +#ifdef TARGET_WASM + call->AsCall()->callSig = new (this, CMK_ASTNode) CORINFO_SIG_INFO; +#else call->AsCall()->callSig = new (this, CMK_DebugOnly) CORINFO_SIG_INFO; +#endif *call->AsCall()->callSig = *sig; #endif diff --git a/src/coreclr/jit/jit.h b/src/coreclr/jit/jit.h index 8b6aaa227ef84a..1f8903d22961d1 100644 --- a/src/coreclr/jit/jit.h +++ b/src/coreclr/jit/jit.h @@ -344,6 +344,12 @@ typedef ptrdiff_t ssize_t; #define DEBUGARG(x) #endif +#if defined (DEBUG) || defined(TARGET_WASM) +#define INDEBUG_OR_WASM(x) x +#else +#define INDEBUG_OR_WASM(x) +#endif + #if defined(DEBUG) || defined(LATE_DISASM) #define INDEBUG_LDISASM_COMMA(x) x, #else diff --git a/src/coreclr/jit/lower.cpp b/src/coreclr/jit/lower.cpp index e66cf2cbfebb20..8edf2b5ee5cf26 100644 --- a/src/coreclr/jit/lower.cpp +++ b/src/coreclr/jit/lower.cpp @@ -3016,6 +3016,14 @@ GenTree* Lowering::LowerCall(GenTree* node) { RequireOutgoingArgSpace(call, call->gtArgs.OutgoingArgsStackSize()); } + + // For non-helper, managed calls, if we have portable entry points enabled, we need to lower + // the call according to the portable entrypoint abi + if (!call->IsUnmanaged() && m_compiler->opts.jitFlags->IsSet(JitFlags::JIT_FLAG_PORTABLE_ENTRY_POINTS)) + { + // Inform the VM that we used are calling a portable entrypoint of a particular signature. + m_compiler->info.compCallPortableEntryPoint(call->gtCallMethHnd, call->gtCallSig); + } } if (varTypeIsStruct(call)) diff --git a/src/coreclr/tools/Common/JitInterface/CorInfoImpl.cs b/src/coreclr/tools/Common/JitInterface/CorInfoImpl.cs index ae543131333654..736ebbb5c59da2 100644 --- a/src/coreclr/tools/Common/JitInterface/CorInfoImpl.cs +++ b/src/coreclr/tools/Common/JitInterface/CorInfoImpl.cs @@ -4122,12 +4122,6 @@ private void reportFatalError(CorJitResult result) // CompileMethod is going to fail with this CorJitResult anyway. } -#pragma warning disable CA1822 // Mark members as static - private void recordCallSite(uint instrOffset, CORINFO_SIG_INFO* callSig, CORINFO_METHOD_STRUCT_* methodHandle) -#pragma warning restore CA1822 // Mark members as static - { - } - private ArrayBuilder _codeRelocs; private ArrayBuilder _roDataRelocs; private ArrayBuilder _rwDataRelocs; diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/JitInterface/CorInfoImpl.ReadyToRun.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/JitInterface/CorInfoImpl.ReadyToRun.cs index 0be908211636f7..bacb43f4bb2579 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/JitInterface/CorInfoImpl.ReadyToRun.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/JitInterface/CorInfoImpl.ReadyToRun.cs @@ -3515,5 +3515,33 @@ private void getThreadLocalStaticInfo_NativeAOT(CORINFO_THREAD_STATIC_INFO_NATIV // Implemented for NativeAOT only for now. } #pragma warning restore CA1822 // Mark members as static + +#pragma warning disable CA1822 // Mark members as static + private void recordCallSite(uint instrOffset, CORINFO_SIG_INFO* callSig, CORINFO_METHOD_STRUCT_* methodHandle) +#pragma warning restore CA1822 // Mark members as static + { + if ((callSig != null) && _compilation.NodeFactory.Target.IsWasm) + { + var sig = HandleToObject(callSig->methodSignature); + + LoweringFlags flags = 0; + if (callSig->hasTypeArg()) + { + flags |= LoweringFlags.HasGenericContextArg; + } + if (callSig->isAsyncCall()) + { + flags |= LoweringFlags.IsAsyncCall; + } + if ((callSig->getCallConv() & 0xF) != 0) + { + flags |= LoweringFlags.IsUnmanagedCallersOnly; + } + + WasmSignature wasmSig = WasmLowering.GetSignature(sig, flags); + + AddPrecodeFixup(null); // TODO! fix this to require the generation of a R2R to interp stub + } + } } } diff --git a/src/coreclr/tools/aot/ILCompiler.RyuJit/JitInterface/CorInfoImpl.RyuJit.cs b/src/coreclr/tools/aot/ILCompiler.RyuJit/JitInterface/CorInfoImpl.RyuJit.cs index 141ca0c60720b6..892a67cb2a4f15 100644 --- a/src/coreclr/tools/aot/ILCompiler.RyuJit/JitInterface/CorInfoImpl.RyuJit.cs +++ b/src/coreclr/tools/aot/ILCompiler.RyuJit/JitInterface/CorInfoImpl.RyuJit.cs @@ -2504,5 +2504,11 @@ private bool notifyMethodInfoUsage(CORINFO_METHOD_STRUCT_* ftn) { return true; } + +#pragma warning disable CA1822 // Mark members as static + private void recordCallSite(uint instrOffset, CORINFO_SIG_INFO* callSig, CORINFO_METHOD_STRUCT_* methodHandle) +#pragma warning restore CA1822 // Mark members as static + { + } } } From 7f6ef83a443bb0325a8feef921ada55a0ca50211 Mon Sep 17 00:00:00 2001 From: David Wrighton Date: Fri, 24 Apr 2026 14:59:07 -0700 Subject: [PATCH 08/43] Encode 'this' parameter as 'T' in WasmSignature strings The 'this' parameter is now encoded with a distinct 'T' character instead of 'i'/'l'. On raise, 'T' sets HasThis on the MethodSignature rather than adding an explicit parameter. This enables proper roundtripping and allows ArgIterator to correctly compute offsets (e.g. GetRetBuffArgOffset with hasThis). Also fix build errors in CorInfoImpl.ReadyToRun.cs: qualify LoweringFlags and cast getCallConv() to int. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../tools/Common/JitInterface/CorInfoTypes.cs | 4 ++-- .../tools/Common/JitInterface/WasmLowering.cs | 13 ++++++++++--- .../JitInterface/CorInfoImpl.ReadyToRun.cs | 10 +++++----- 3 files changed, 17 insertions(+), 10 deletions(-) diff --git a/src/coreclr/tools/Common/JitInterface/CorInfoTypes.cs b/src/coreclr/tools/Common/JitInterface/CorInfoTypes.cs index 2a098727ae5ab6..3c8b284c8a189f 100644 --- a/src/coreclr/tools/Common/JitInterface/CorInfoTypes.cs +++ b/src/coreclr/tools/Common/JitInterface/CorInfoTypes.cs @@ -116,14 +116,14 @@ public unsafe struct CORINFO_SIG_INFO public mdToken token; public CorInfoType retType { get { return (CorInfoType)_retType; } set { _retType = (byte)value; } } - private CorInfoCallConv getCallConv() { return (CorInfoCallConv)((callConv & CorInfoCallConv.CORINFO_CALLCONV_MASK)); } + internal CorInfoCallConv getCallConv() { return (CorInfoCallConv)((callConv & CorInfoCallConv.CORINFO_CALLCONV_MASK)); } private bool hasThis() { return ((callConv & CorInfoCallConv.CORINFO_CALLCONV_HASTHIS) != 0); } private bool hasExplicitThis() { return ((callConv & CorInfoCallConv.CORINFO_CALLCONV_EXPLICITTHIS) != 0); } private bool hasImplicitThis() { return ((callConv & (CorInfoCallConv.CORINFO_CALLCONV_HASTHIS | CorInfoCallConv.CORINFO_CALLCONV_EXPLICITTHIS)) == CorInfoCallConv.CORINFO_CALLCONV_HASTHIS); } private uint totalILArgs() { return (uint)(numArgs + (hasImplicitThis() ? 1 : 0)); } private bool isVarArg() { return ((getCallConv() == CorInfoCallConv.CORINFO_CALLCONV_VARARG) || (getCallConv() == CorInfoCallConv.CORINFO_CALLCONV_NATIVEVARARG)); } internal bool hasTypeArg() { return ((callConv & CorInfoCallConv.CORINFO_CALLCONV_PARAMTYPE) != 0); } - private bool isAsyncCall() { return ((callConv & CorInfoCallConv.CORINFO_CALLCONV_ASYNCCALL) != 0); } + internal bool isAsyncCall() { return ((callConv & CorInfoCallConv.CORINFO_CALLCONV_ASYNCCALL) != 0); } }; //---------------------------------------------------------------------------- diff --git a/src/coreclr/tools/Common/JitInterface/WasmLowering.cs b/src/coreclr/tools/Common/JitInterface/WasmLowering.cs index f107dd4b4d5854..5c2f455e6aacdf 100644 --- a/src/coreclr/tools/Common/JitInterface/WasmLowering.cs +++ b/src/coreclr/tools/Common/JitInterface/WasmLowering.cs @@ -191,10 +191,17 @@ public static MethodSignature RaiseSignature(WasmSignature wasmSignature, TypeSy // Parse parameters (everything until 'p' suffix or end of string) List parameters = new List(); + bool hasThis = false; while (pos < sig.Length && sig[pos] != 'p') { char c = sig[pos]; - if (c == 'e') + if (c == 'T') + { + // 'this' parameter — not added as explicit param, sets hasThis flag + hasThis = true; + pos++; + } + else if (c == 'e') { // Empty struct — include the cached empty struct type for roundtrip fidelity TypeDesc emptyStruct = ((CompilerTypeSystemContext)context).CachedEmptyStruct; @@ -215,7 +222,7 @@ public static MethodSignature RaiseSignature(WasmSignature wasmSignature, TypeSy } bool isManaged = pos < sig.Length && sig[pos] == 'p'; - MethodSignatureFlags flags = MethodSignatureFlags.Static; + MethodSignatureFlags flags = hasThis ? MethodSignatureFlags.None : MethodSignatureFlags.Static; if (!isManaged) { flags |= MethodSignatureFlags.UnmanagedCallingConvention; @@ -345,7 +352,7 @@ public static WasmSignature GetSignature(MethodSignature signature, LoweringFlag if (hasThis) { result.Add(pointerType); - sigBuilder.Append(hiddenParamChar); + sigBuilder.Append('T'); } if (hasReturnBuffer) diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/JitInterface/CorInfoImpl.ReadyToRun.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/JitInterface/CorInfoImpl.ReadyToRun.cs index bacb43f4bb2579..5b1c8046c14b27 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/JitInterface/CorInfoImpl.ReadyToRun.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/JitInterface/CorInfoImpl.ReadyToRun.cs @@ -3524,18 +3524,18 @@ private void recordCallSite(uint instrOffset, CORINFO_SIG_INFO* callSig, CORINFO { var sig = HandleToObject(callSig->methodSignature); - LoweringFlags flags = 0; + WasmLowering.LoweringFlags flags = 0; if (callSig->hasTypeArg()) { - flags |= LoweringFlags.HasGenericContextArg; + flags |= WasmLowering.LoweringFlags.HasGenericContextArg; } if (callSig->isAsyncCall()) { - flags |= LoweringFlags.IsAsyncCall; + flags |= WasmLowering.LoweringFlags.IsAsyncCall; } - if ((callSig->getCallConv() & 0xF) != 0) + if (((int)callSig->getCallConv() & 0xF) != 0) { - flags |= LoweringFlags.IsUnmanagedCallersOnly; + flags |= WasmLowering.LoweringFlags.IsUnmanagedCallersOnly; } WasmSignature wasmSig = WasmLowering.GetSignature(sig, flags); From 1ced59be636cd1ba579a7ba11c099e4bf891c586 Mon Sep 17 00:00:00 2001 From: David Wrighton Date: Fri, 24 Apr 2026 15:35:15 -0700 Subject: [PATCH 09/43] Add WasmR2RToInterpreterThunkNode and WASM memory instructions Add WasmR2RToInterpreterThunkNode, a StringDiscoverableAssemblyStubNode that captures arguments into a transition block and dispatches to the interpreter via READYTORUN_HELPER_InitInstClass. Key details: - Thunk keyed by WasmSignature, discoverable by 'I'-prefixed signature string - Arguments area is 16-byte aligned; TransitionBlock is 8-byte aligned - Indirect struct args copied with memory.copy + memory.fill padding - Stack pointer global saved/restored around helper call - V128 return uses 16-byte aligned buffer; others use 8-byte i64 store Also adds memory.copy, memory.fill, and i64.const WASM instructions, and updates WasmImportThunk to use memory.fill for indirect struct zero-filling. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Compiler/ObjectWriter/WasmInstructions.cs | 80 ++++- .../ReadyToRun/WasmImportThunk.cs | 16 +- .../WasmR2RToInterpreterThunkNode.cs | 337 ++++++++++++++++++ .../ReadyToRunCodegenNodeFactory.cs | 11 + .../ILCompiler.ReadyToRun.csproj | 1 + 5 files changed, 436 insertions(+), 9 deletions(-) create mode 100644 src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/WasmR2RToInterpreterThunkNode.cs diff --git a/src/coreclr/tools/Common/Compiler/ObjectWriter/WasmInstructions.cs b/src/coreclr/tools/Common/Compiler/ObjectWriter/WasmInstructions.cs index 813b439c2ecac3..e4c27619991e6f 100644 --- a/src/coreclr/tools/Common/Compiler/ObjectWriter/WasmInstructions.cs +++ b/src/coreclr/tools/Common/Compiler/ObjectWriter/WasmInstructions.cs @@ -135,6 +135,8 @@ public enum WasmExprKind RefNull = 0xD0, // Variable length instructions — not directly cast to a byte, instead the prefix byte is set in the upper 8 bits of the enum, and the lower 24 bits are the extended variable length opcode MemoryInit = unchecked((int)0xFC000008), + MemoryCopy = unchecked((int)0xFC00000A), + MemoryFill = unchecked((int)0xFC00000B), TableInit = unchecked((int)0xFC00000C), TableGrow = unchecked((int)0xFC00000F), V128Load = unchecked((int)0xFD00000A), @@ -183,7 +185,7 @@ public static bool IsGlobalVarExpr(this WasmExprKind kind) public static bool IsMemoryExpr(this WasmExprKind kind) { - return kind == WasmExprKind.MemoryInit; + return kind == WasmExprKind.MemoryInit || kind == WasmExprKind.MemoryCopy || kind == WasmExprKind.MemoryFill; } public static bool IsVariableLengthInstruction(this WasmExprKind kind) { @@ -569,8 +571,68 @@ public WasmUnaryExpr(WasmExprKind kind) : base(kind) // base class defaults are sufficient as the base class encodes just the opcode } + // Represents a memory.copy expression. + // Binary encoding: 0xFC prefix + u32(10) sub-opcode + u32(dstMemoryIndex) + u32(srcMemoryIndex) + // Stack operands: (dst: i32, src: i32, len: i32) -> () + class WasmMemoryCopyExpr : WasmExpr + { + public readonly int DstMemoryIndex; + public readonly int SrcMemoryIndex; + + public WasmMemoryCopyExpr(int dstMemoryIndex = 0, int srcMemoryIndex = 0) : base(WasmExprKind.MemoryCopy) + { + Debug.Assert(dstMemoryIndex >= 0); + Debug.Assert(srcMemoryIndex >= 0); + DstMemoryIndex = dstMemoryIndex; + SrcMemoryIndex = srcMemoryIndex; + } + + public override int Encode(Span buffer) + { + int pos = base.Encode(buffer); + pos += DwarfHelper.WriteULEB128(buffer.Slice(pos), (uint)DstMemoryIndex); + pos += DwarfHelper.WriteULEB128(buffer.Slice(pos), (uint)SrcMemoryIndex); + + return pos; + } + + public override int EncodeSize() + { + return base.EncodeSize() + + (int)DwarfHelper.SizeOfULEB128((uint)DstMemoryIndex) + + (int)DwarfHelper.SizeOfULEB128((uint)SrcMemoryIndex); + } + } + + // Represents a memory.fill expression. + // Binary encoding: 0xFC prefix + u32(11) sub-opcode + u32(memoryIndex) + // Stack operands: (dst: i32, val: i32, len: i32) -> () + class WasmMemoryFillExpr : WasmExpr + { + public readonly int MemoryIndex; + + public WasmMemoryFillExpr(int memoryIndex = 0) : base(WasmExprKind.MemoryFill) + { + Debug.Assert(memoryIndex >= 0); + MemoryIndex = memoryIndex; + } + + public override int Encode(Span buffer) + { + int pos = base.Encode(buffer); + pos += DwarfHelper.WriteULEB128(buffer.Slice(pos), (uint)MemoryIndex); + + return pos; + } + + public override int EncodeSize() + { + return base.EncodeSize() + + (int)DwarfHelper.SizeOfULEB128((uint)MemoryIndex); + } + } + // Represents a memory.init expression. - // Binary encoding: 0xFC prefix + u32(8) sub-opcode + u32(dataSegmentIndex) + u32(memoryIndex) class WasmMemoryInitExpr : WasmExpr { public readonly int DataSegmentIndex; @@ -760,6 +822,10 @@ public static WasmExpr ConstRVA(ISymbolNode symbolNode) static class I64 { + public static WasmExpr Const(long value) + { + return new WasmConstExpr(WasmExprKind.I64Const, value); + } public static WasmExpr Load(ulong offset) => new WasmMemoryArgInstruction(WasmExprKind.I64Load, 8, new WasmEncodableULong(offset)); public static WasmExpr Store(ulong offset) => new WasmMemoryArgInstruction(WasmExprKind.I64Store, 8, new WasmEncodableULong(offset)); } @@ -784,6 +850,16 @@ static class V128 static class Memory { + public static WasmExpr Copy(int dstMemoryIndex = 0, int srcMemoryIndex = 0) + { + return new WasmMemoryCopyExpr(dstMemoryIndex, srcMemoryIndex); + } + + public static WasmExpr Fill(int memoryIndex = 0) + { + return new WasmMemoryFillExpr(memoryIndex); + } + public static WasmExpr Init(int dataSegmentIndex, int memoryIndex = 0) { return new WasmMemoryInitExpr(dataSegmentIndex, memoryIndex); diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/WasmImportThunk.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/WasmImportThunk.cs index 6adf972d9cf9c2..273980c36fece6 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/WasmImportThunk.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/WasmImportThunk.cs @@ -199,14 +199,16 @@ protected override void EmitCode(NodeFactory factory, ref Wasm.WasmEmitter instr if (isIndirectArg[i]) { // Indirect struct — zero-fill the transition block slot instead of copying the byref pointer. - // The slot is pointer-aligned in the transition block, so we can safely zero in 4-byte chunks. int structSize = paramType.GetElementSize().AsInt; - for (int zeroOffset = 0; zeroOffset < structSize; zeroOffset += 4) - { - expressions.Add(Local.Get(0)); - expressions.Add(I32.Const(0)); - expressions.Add(I32.Store((ulong)(currentOffset + zeroOffset))); - } + int fillSize = AlignmentHelper.AlignUp(structSize, 8); + + // memory.fill: (dst, val, len) -> () + expressions.Add(Local.Get(0)); + expressions.Add(I32.Const(currentOffset)); + expressions.Add(I32.Add); + expressions.Add(I32.Const(0)); + expressions.Add(I32.Const(fillSize)); + expressions.Add(Memory.Fill()); wasmLocalIndex++; } else diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/WasmR2RToInterpreterThunkNode.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/WasmR2RToInterpreterThunkNode.cs new file mode 100644 index 00000000000000..bc48f9a63c17b4 --- /dev/null +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/WasmR2RToInterpreterThunkNode.cs @@ -0,0 +1,337 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using ILCompiler.DependencyAnalysis.Wasm; +using ILCompiler.ObjectWriter; +using ILCompiler.ObjectWriter.WasmInstructions; +using Internal.JitInterface; +using Internal.Text; +using Internal.TypeSystem; +using Internal.ReadyToRunConstants; +using System; +using System.Collections.Generic; +using System.Diagnostics; + +using ILCompiler.DependencyAnalysisFramework; + +namespace ILCompiler.DependencyAnalysis.ReadyToRun +{ + /// + /// A thunk that captures all arguments and dispatches to the interpreter via + /// READYTORUN_HELPER_InitInstClass. This node is string-discoverable so the + /// runtime can find it by WasmSignature string at execution time. + /// + public class WasmR2RToInterpreterThunkNode : StringDiscoverableAssemblyStubNode, INodeWithTypeSignature, ISymbolDefinitionNode, ISortableSymbolNode + { + private readonly TypeSystemContext _context; + private readonly WasmSignature _wasmSignature; + private readonly WasmTypeNode _typeNode; + private readonly Import _helperCell; + + // Helper signature: (I32, I32, I32, I32) -> () + private static readonly CorInfoWasmType[] s_helperTypeParams = new CorInfoWasmType[] + { + CorInfoWasmType.CORINFO_WASM_TYPE_VOID, + CorInfoWasmType.CORINFO_WASM_TYPE_I32, + CorInfoWasmType.CORINFO_WASM_TYPE_I32, + CorInfoWasmType.CORINFO_WASM_TYPE_I32, + CorInfoWasmType.CORINFO_WASM_TYPE_I32, + }; + + public override bool StaticDependenciesAreComputed => true; + public override bool IsShareable => false; + public override ObjectNodeSection GetSection(NodeFactory factory) => ObjectNodeSection.TextSection; + + public override string LookupString => "I" + _wasmSignature.SignatureString; + + MethodSignature INodeWithTypeSignature.Signature => WasmLowering.RaiseSignature(_wasmSignature, _context); + bool INodeWithTypeSignature.IsUnmanagedCallersOnly => false; + bool INodeWithTypeSignature.IsAsyncCall => false; + bool INodeWithTypeSignature.HasGenericContextArg => false; + + public WasmR2RToInterpreterThunkNode(NodeFactory factory, WasmSignature wasmSignature) + { + _context = factory.TypeSystemContext; + _wasmSignature = wasmSignature; + _typeNode = factory.WasmTypeNode(wasmSignature); + _helperCell = factory.GetReadyToRunHelperCell(ReadyToRunHelper.InitInstClass); + } + + public override void AppendMangledName(NameMangler nameMangler, Utf8StringBuilder sb) + { + sb.Append("WasmR2RToInterpreterThunk("u8); + sb.Append(_wasmSignature.SignatureString); + sb.Append(")"u8); + } + + protected override string GetName(NodeFactory factory) + { + Utf8StringBuilder sb = new Utf8StringBuilder(); + AppendMangledName(factory.NameMangler, sb); + return sb.ToString(); + } + + public override int ClassCode => 948271449; + + public override int CompareToImpl(ISortableNode other, CompilerComparer comparer) + { + WasmR2RToInterpreterThunkNode otherNode = (WasmR2RToInterpreterThunkNode)other; + return _wasmSignature.CompareTo(otherNode._wasmSignature); + } + + protected override DependencyList ComputeNonRelocationBasedDependencies(NodeFactory factory) + { + DependencyList dependencies = base.ComputeNonRelocationBasedDependencies(factory); + dependencies.Add(_typeNode, "Wasm R2R to interpreter thunk requires type node"); + return dependencies; + } + + protected override void EmitCode(NodeFactory factory, ref Wasm.WasmEmitter instructionEncoder, bool relocsOnly) + { + Debug.Assert(!instructionEncoder.Is64Bit); + + ISymbolNode helperTypeIndex = factory.WasmTypeNode(s_helperTypeParams); + + MethodSignature methodSignature = WasmLowering.RaiseSignature(_wasmSignature, _context); + (ArgIterator argit, TransitionBlock transitionBlock) = GCRefMapBuilder.BuildArgIterator(methodSignature, _context); + + bool hasRetBuffArg = argit.HasRetBuffArg(); + bool hasThis = !methodSignature.IsStatic; + + int[] offsets = new int[methodSignature.Length]; + bool[] isIndirectArg = new bool[methodSignature.Length]; + + int argIndex = 0; + int argOffset; + + // ArgIterator returns offsets relative to the TransitionBlock base, where arguments + // start at OffsetOfArgumentRegisters (== SizeOfTransitionBlock == 8 on Wasm32). + // We place args at argumentsOffset (16-byte aligned), so adjust each offset. + int argOffsetAdjustment = AlignmentHelper.AlignUp(transitionBlock.SizeOfTransitionBlock, 16) - transitionBlock.SizeOfTransitionBlock; + + while ((argOffset = argit.GetNextOffset()) != TransitionBlock.InvalidOffset) + { + offsets[argIndex] = argOffset + argOffsetAdjustment; + isIndirectArg[argIndex] = argit.IsArgPassedByRef(); + argIndex++; + } + + argit.Reset(); + + int sizeOfArgumentArray = argit.SizeOfFrameArgumentArray(); + int sizeOfTransitionBlock = transitionBlock.SizeOfTransitionBlock; + + // The arguments area must be 16-byte aligned. The TransitionBlock (8 bytes on Wasm32) + // sits before the arguments, so it is 8-byte aligned but not 16-byte aligned. + // Layout from base: [TransitionBlock (8)] [padding to 16-align args] [args...] + int argumentsOffset = AlignmentHelper.AlignUp(sizeOfTransitionBlock, 16); + int sizeOfStoredLocals = argumentsOffset + AlignmentHelper.AlignUp(sizeOfArgumentArray, 16); + + bool hasWasmReturn = _typeNode.Type.Returns.Types.Length > 0; + WasmValueType returnWasmType = hasWasmReturn ? _typeNode.Type.Returns.Types[0] : default; + + // Allocate a local return buffer. V128 needs 16 bytes and 16-byte alignment; + // other types need at most 8 bytes. localRetBufOffset is already 16-byte aligned + // (argumentsOffset is 16-aligned, and args size is rounded up to 16). + int retBufSize = (hasWasmReturn && returnWasmType == WasmValueType.V128) ? 16 : 8; + int localRetBufOffset = sizeOfStoredLocals; + int totalAlloc = AlignmentHelper.AlignUp(sizeOfStoredLocals + retBufSize, 16); + + List expressions = new List(); + + // Allocate stack space: local.get 0; i32.const totalAlloc; i32.sub; local.set 0 + expressions.Add(Local.Get(0)); + expressions.Add(I32.Const(totalAlloc)); + expressions.Add(I32.Sub); + expressions.Add(Local.Set(0)); + + // Initialize TransitionBlock: + // First 4 bytes (m_ReturnAddress) = 0 + expressions.Add(Local.Get(0)); + expressions.Add(I32.Const(0)); + expressions.Add(I32.Store(0)); + + // Second 4 bytes (m_StackPointer) = original SP (local 0 + totalAlloc) + expressions.Add(Local.Get(0)); + expressions.Add(Local.Get(0)); + expressions.Add(I32.Const(totalAlloc)); + expressions.Add(I32.Add); + expressions.Add(I32.Store(4)); + + // Store all arguments into the transition block area + int wasmLocalIndex = 1; // local 0 is $sp + for (int i = 0; i < methodSignature.Length; i++) + { + TypeDesc paramType = methodSignature[i]; + + if (WasmLowering.IsEmptyStruct(paramType)) + { + continue; + } + + int currentOffset = offsets[i]; + + if (isIndirectArg[i]) + { + // Indirect struct — copy the exact contents from the incoming pointer + int structSize = paramType.GetElementSize().AsInt; + + // memory.copy: (dst, src, len) -> () + // dst: base + currentOffset + expressions.Add(Local.Get(0)); + expressions.Add(I32.Const(currentOffset)); + expressions.Add(I32.Add); + // src: the byref pointer passed as the wasm local + expressions.Add(Local.Get(wasmLocalIndex)); + // len: struct size + expressions.Add(I32.Const(structSize)); + expressions.Add(Memory.Copy()); + + // Pad remaining bytes to alignment boundary with zeros + int alignment = structSize <= 4 ? 4 : 8; + int padding = AlignmentHelper.AlignUp(structSize, alignment) - structSize; + if (padding > 0) + { + // memory.fill: (dst, val, len) -> () + expressions.Add(Local.Get(0)); + expressions.Add(I32.Const(currentOffset + structSize)); + expressions.Add(I32.Add); + expressions.Add(I32.Const(0)); + expressions.Add(I32.Const(padding)); + expressions.Add(Memory.Fill()); + } + + wasmLocalIndex++; + } + else + { + expressions.Add(Local.Get(0)); + expressions.Add(Local.Get(wasmLocalIndex)); + WasmValueType type = _typeNode.Type.Params.Types[wasmLocalIndex]; + switch (type) + { + case WasmValueType.I32: + expressions.Add(I32.Store((ulong)currentOffset)); + break; + case WasmValueType.F32: + expressions.Add(F32.Store((ulong)currentOffset)); + break; + case WasmValueType.I64: + expressions.Add(I64.Store((ulong)currentOffset)); + break; + case WasmValueType.F64: + expressions.Add(F64.Store((ulong)currentOffset)); + break; + case WasmValueType.V128: + expressions.Add(V128.Store((ulong)currentOffset)); + break; + default: + throw new Exception("Unexpected wasm type arg"); + } + wasmLocalIndex++; + } + } + + // Zero the local return buffer + if (retBufSize <= 8) + { + expressions.Add(Local.Get(0)); + expressions.Add(I64.Const(0)); + expressions.Add(I64.Store((ulong)localRetBufOffset)); + } + else + { + expressions.Add(Local.Get(0)); + expressions.Add(I32.Const(localRetBufOffset)); + expressions.Add(I32.Add); + expressions.Add(I32.Const(0)); + expressions.Add(I32.Const(retBufSize)); + expressions.Add(Memory.Fill()); + } + + // Prepare helper call arguments: + // arg1: portable entrypoint (last wasm local) + int portableEntrypointLocalIndex = _typeNode.Type.Params.Types.Length - 1; + expressions.Add(Local.Get(portableEntrypointLocalIndex)); + + // arg2: pointer to the collected arguments (base + argumentsOffset) + expressions.Add(Local.Get(0)); + expressions.Add(I32.Const(argumentsOffset)); + expressions.Add(I32.Add); + + // arg3: size of arguments (excluding transition block) + expressions.Add(I32.Const(sizeOfArgumentArray)); + + // arg4: return buffer pointer + if (hasRetBuffArg) + { + // Load the return buffer pointer from the transition block where we stashed it + int retBuffArgOffset = transitionBlock.GetRetBuffArgOffset(hasThis) + argOffsetAdjustment; + expressions.Add(Local.Get(0)); + expressions.Add(I32.Load((ulong)retBuffArgOffset)); + } + else + { + // Pass pointer to local 8-byte return buffer + expressions.Add(Local.Get(0)); + expressions.Add(I32.Const(localRetBufOffset)); + expressions.Add(I32.Add); + } + + // Extra local to save/restore the stack pointer global across the helper call + int savedSpLocalIndex = _typeNode.Type.Params.Types.Length; + + // Save the current stack pointer global into a local, then set it to local 0 + // (16-byte aligned, <= all buffers allocated in this thunk). + expressions.Add(Global.Get(WasmObjectWriter.StackPointerGlobalIndex)); + expressions.Add(Local.Set(savedSpLocalIndex)); + expressions.Add(Local.Get(0)); + expressions.Add(Global.Set(WasmObjectWriter.StackPointerGlobalIndex)); + + // Load the helper function address and call + expressions.Add(Global.Get(WasmObjectWriter.ImageBaseGlobalIndex)); + expressions.Add(I32.LoadWithRVAOffset(_helperCell)); + expressions.Add(ControlFlow.CallIndirect(helperTypeIndex, 0)); + + // Restore the old stack pointer global + expressions.Add(Local.Get(savedSpLocalIndex)); + expressions.Add(Global.Set(WasmObjectWriter.StackPointerGlobalIndex)); + + // If the function has a wasm return value, load it from the local return buffer + if (hasWasmReturn) + { + Debug.Assert(_typeNode.Type.Returns.Types.Length == 1, "Expected exactly one wasm return type"); + expressions.Add(Local.Get(0)); + switch (returnWasmType) + { + case WasmValueType.I32: + expressions.Add(I32.Load((ulong)localRetBufOffset)); + break; + case WasmValueType.F32: + expressions.Add(F32.Load((ulong)localRetBufOffset)); + break; + case WasmValueType.I64: + expressions.Add(I64.Load((ulong)localRetBufOffset)); + break; + case WasmValueType.F64: + expressions.Add(F64.Load((ulong)localRetBufOffset)); + break; + case WasmValueType.V128: + expressions.Add(V128.Load((ulong)localRetBufOffset)); + break; + default: + throw new Exception("Unexpected wasm return type"); + } + } + + instructionEncoder.FunctionBody = new WasmFunctionBody(_typeNode.Type, new[] { WasmValueType.I32 }, expressions.ToArray()); + } + + protected override void EmitCode(NodeFactory factory, ref X64.X64Emitter instructionEncoder, bool relocsOnly) { throw new NotSupportedException(); } + protected override void EmitCode(NodeFactory factory, ref X86.X86Emitter instructionEncoder, bool relocsOnly) { throw new NotSupportedException(); } + protected override void EmitCode(NodeFactory factory, ref ARM.ARMEmitter instructionEncoder, bool relocsOnly) { throw new NotSupportedException(); } + protected override void EmitCode(NodeFactory factory, ref ARM64.ARM64Emitter instructionEncoder, bool relocsOnly) { throw new NotSupportedException(); } + protected override void EmitCode(NodeFactory factory, ref LoongArch64.LoongArch64Emitter instructionEncoder, bool relocsOnly) { throw new NotSupportedException(); } + protected override void EmitCode(NodeFactory factory, ref RiscV64.RiscV64Emitter instructionEncoder, bool relocsOnly) { throw new NotSupportedException(); } + } +} diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRunCodegenNodeFactory.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRunCodegenNodeFactory.cs index 9bc229cce50974..6de0c7b6f6cf0a 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRunCodegenNodeFactory.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRunCodegenNodeFactory.cs @@ -305,6 +305,11 @@ private void CreateNodeCaches() return new WasmImportThunkPortableEntrypoint(this, key.Import); }); + _wasmR2RToInterpreterThunks = new NodeCache(key => + { + return new WasmR2RToInterpreterThunkNode(this, key); + }); + _importMethods = new NodeCache(CreateMethodEntrypoint); _localMethodCache = new NodeCache(key => @@ -835,6 +840,12 @@ public ISymbolDefinitionNode WasmImportThunkPortableEntrypoint(DelayLoadHelperIm return _wasmImportThunkPortableEntrypoints.GetOrAdd(thunkKey); } + private NodeCache _wasmR2RToInterpreterThunks; + public WasmR2RToInterpreterThunkNode WasmR2RToInterpreterThunk(WasmSignature wasmSignature) + { + return _wasmR2RToInterpreterThunks.GetOrAdd(wasmSignature); + } + public void AttachToDependencyGraph(DependencyAnalyzerBase graph, ILProvider ilProvider) { graph.ComputingDependencyPhaseChange += Graph_ComputingDependencyPhaseChange; diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/ILCompiler.ReadyToRun.csproj b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/ILCompiler.ReadyToRun.csproj index 808bf4c138153a..5679cec33f1fa4 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/ILCompiler.ReadyToRun.csproj +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/ILCompiler.ReadyToRun.csproj @@ -306,6 +306,7 @@ + From 48562c34d422eb66f8430e45d4ef4fdeee343264 Mon Sep 17 00:00:00 2001 From: David Wrighton Date: Fri, 24 Apr 2026 15:59:29 -0700 Subject: [PATCH 10/43] Add WasmInterpreterToR2RThunkNode, fix retbuf handling, wire up R2R call site dependency - Add WasmInterpreterToR2RThunkNode: a StringDiscoverableAssemblyStubNode that bridges from interpreter calling convention to R2R compiled functions. Uses ArgIterator offsets (minus TransitionBlock size) to locate args in the interpreter buffer, sets up a TERMINATE_R2R_STACK_WALK frame, and dispatches via call_indirect. - Fix retbuf detection in both WasmR2RToInterpreterThunkNode and WasmInterpreterToR2RThunkNode to check SignatureString[0] == 'S' instead of using ArgIterator.HasRetBuffArg/GetRetBuffArgOffset. The R2R-to-interpreter thunk now passes the retbuf wasm local directly. - Add factory cache and accessor for WasmInterpreterToR2RThunk on ReadyToRunCodegenNodeFactory. - Fix recordCallSite TODO: wire up WasmR2RToInterpreterThunk dependency. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../WasmInterpreterToR2RThunkNode.cs | 291 ++++++++++++++++++ .../WasmR2RToInterpreterThunkNode.cs | 10 +- .../ReadyToRunCodegenNodeFactory.cs | 11 + .../ILCompiler.ReadyToRun.csproj | 1 + .../JitInterface/CorInfoImpl.ReadyToRun.cs | 2 +- 5 files changed, 309 insertions(+), 6 deletions(-) create mode 100644 src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/WasmInterpreterToR2RThunkNode.cs diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/WasmInterpreterToR2RThunkNode.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/WasmInterpreterToR2RThunkNode.cs new file mode 100644 index 00000000000000..e3e29b78955630 --- /dev/null +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/WasmInterpreterToR2RThunkNode.cs @@ -0,0 +1,291 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using ILCompiler.DependencyAnalysis.Wasm; +using ILCompiler.ObjectWriter; +using ILCompiler.ObjectWriter.WasmInstructions; +using Internal.JitInterface; +using Internal.Text; +using Internal.TypeSystem; +using System; +using System.Collections.Generic; +using System.Diagnostics; + +using ILCompiler.DependencyAnalysisFramework; + +namespace ILCompiler.DependencyAnalysis.ReadyToRun +{ + /// + /// A thunk that takes arguments in the interpreter calling convention + /// (pcode, pArgs, pRet, pPortableEntryPointContext) and calls a function + /// compiled via R2R with the appropriate wasm-level calling convention. + /// + public class WasmInterpreterToR2RThunkNode : StringDiscoverableAssemblyStubNode, INodeWithTypeSignature, ISymbolDefinitionNode, ISortableSymbolNode + { + private readonly TypeSystemContext _context; + private readonly WasmSignature _wasmSignature; + private readonly WasmTypeNode _targetTypeNode; + + private const int TerminateR2RStackWalk = 1; + + public override bool StaticDependenciesAreComputed => true; + public override bool IsShareable => false; + public override ObjectNodeSection GetSection(NodeFactory factory) => ObjectNodeSection.TextSection; + + public override string LookupString => "M" + _wasmSignature.SignatureString; + + MethodSignature INodeWithTypeSignature.Signature => WasmLowering.RaiseSignature(_wasmSignature, _context); + bool INodeWithTypeSignature.IsUnmanagedCallersOnly => false; + bool INodeWithTypeSignature.IsAsyncCall => false; + bool INodeWithTypeSignature.HasGenericContextArg => false; + + public WasmInterpreterToR2RThunkNode(NodeFactory factory, WasmSignature wasmSignature) + { + _context = factory.TypeSystemContext; + _wasmSignature = wasmSignature; + _targetTypeNode = factory.WasmTypeNode(wasmSignature); + } + + public override void AppendMangledName(NameMangler nameMangler, Utf8StringBuilder sb) + { + sb.Append("WasmInterpreterToR2RThunk("u8); + sb.Append(_wasmSignature.SignatureString); + sb.Append(")"u8); + } + + protected override string GetName(NodeFactory factory) + { + Utf8StringBuilder sb = new Utf8StringBuilder(); + AppendMangledName(factory.NameMangler, sb); + return sb.ToString(); + } + + public override int ClassCode => 948271450; + + public override int CompareToImpl(ISortableNode other, CompilerComparer comparer) + { + WasmInterpreterToR2RThunkNode otherNode = (WasmInterpreterToR2RThunkNode)other; + return _wasmSignature.CompareTo(otherNode._wasmSignature); + } + + protected override DependencyList ComputeNonRelocationBasedDependencies(NodeFactory factory) + { + DependencyList dependencies = base.ComputeNonRelocationBasedDependencies(factory); + dependencies.Add(_targetTypeNode, "Wasm interpreter-to-R2R thunk requires target type node"); + return dependencies; + } + + protected override void EmitCode(NodeFactory factory, ref Wasm.WasmEmitter instructionEncoder, bool relocsOnly) + { + Debug.Assert(!instructionEncoder.Is64Bit); + + ISymbolNode targetTypeIndex = _targetTypeNode; + + MethodSignature methodSignature = WasmLowering.RaiseSignature(_wasmSignature, _context); + (ArgIterator argit, TransitionBlock transitionBlock) = GCRefMapBuilder.BuildArgIterator(methodSignature, _context); + + bool hasRetBuffArg = _wasmSignature.SignatureString[0] == 'S'; + bool hasThis = !methodSignature.IsStatic; + + // Gather explicit-arg offsets and indirectness from ArgIterator. + // ArgIterator offsets are relative to the TransitionBlock base; the interpreter + // buffer has no TransitionBlock, so subtract SizeOfTransitionBlock (8) to get + // the byte offset into pArgs. + int sizeOfTransitionBlock = transitionBlock.SizeOfTransitionBlock; + int[] interpOffsets = new int[methodSignature.Length]; + bool[] isIndirectArg = new bool[methodSignature.Length]; + + int argIndex = 0; + int argOffset; + while ((argOffset = argit.GetNextOffset()) != TransitionBlock.InvalidOffset) + { + interpOffsets[argIndex] = argOffset - sizeOfTransitionBlock; + isIndirectArg[argIndex] = argit.IsArgPassedByRef(); + argIndex++; + } + + WasmFuncType targetFuncType = _targetTypeNode.Type; + bool hasWasmReturn = targetFuncType.Returns.Types.Length > 0; + + // Wasm locals for this thunk: + // local 0: pcode (I32) + // local 1: pArgs (I32) + // local 2: pRet (I32) + // local 3: pPortableEntryPointContext (I32) + // local 4: savedSp (I32) - save/restore SP global + const int LocalPcode = 0; + const int LocalPArgs = 1; + const int LocalPRet = 2; + const int LocalPortableEntrypoint = 3; + const int LocalSavedSp = 4; + + const int FrameSize = 16; // 16-byte aligned allocation for framePointer + + List expressions = new List(); + + // Save the current stack pointer global + expressions.Add(Global.Get(WasmObjectWriter.StackPointerGlobalIndex)); + expressions.Add(Local.Set(LocalSavedSp)); + + // Allocate frame space: sp -= FrameSize + expressions.Add(Local.Get(LocalSavedSp)); + expressions.Add(I32.Const(FrameSize)); + expressions.Add(I32.Sub); + expressions.Add(Global.Set(WasmObjectWriter.StackPointerGlobalIndex)); + + // Write TERMINATE_R2R_STACK_WALK (1) into the framePointer at new SP + expressions.Add(Global.Get(WasmObjectWriter.StackPointerGlobalIndex)); + expressions.Add(I32.Const(TerminateR2RStackWalk)); + expressions.Add(I32.Store(0)); + + // Build the arguments for the R2R call_indirect. + // Target R2R wasm params: ($sp, [retbuf], [this], explicit_params..., portableEntrypoint) + // We track targetParamIndex to look up the correct wasm type for each arg. + int targetParamIndex = 0; + + // If there is a wasm return value, push pRet underneath all the call args + // so that after call_indirect the stack is [pRet, return_value] for the store. + if (hasWasmReturn) + { + expressions.Add(Local.Get(LocalPRet)); + } + + // Param 0: $sp — pointer to the framePointer on the shadow stack + expressions.Add(Global.Get(WasmObjectWriter.StackPointerGlobalIndex)); + targetParamIndex++; + + // If the R2R function takes a return buffer, pass pRet directly as the retbuf arg + if (hasRetBuffArg) + { + expressions.Add(Local.Get(LocalPRet)); + targetParamIndex++; + } + + // If the method has a 'this' pointer, load it from pArgs at offset 0 + // (ArgIterator offset for this = OffsetOfArgumentRegisters = SizeOfTransitionBlock) + if (hasThis) + { + int thisInterpOffset = transitionBlock.OffsetOfArgumentRegisters - sizeOfTransitionBlock; + expressions.Add(Local.Get(LocalPArgs)); + expressions.Add(I32.Load((ulong)thisInterpOffset)); + targetParamIndex++; + } + + // Explicit parameters — load each from pArgs at the ArgIterator-derived offset + for (int i = 0; i < methodSignature.Length; i++) + { + TypeDesc paramType = methodSignature[i]; + + if (WasmLowering.IsEmptyStruct(paramType)) + { + continue; + } + + if (isIndirectArg[i]) + { + // Byreference struct — pass a pointer into the incoming pArgs buffer + expressions.Add(Local.Get(LocalPArgs)); + expressions.Add(I32.Const(interpOffsets[i])); + expressions.Add(I32.Add); + targetParamIndex++; + } + else + { + WasmValueType wasmType = targetFuncType.Params.Types[targetParamIndex]; + expressions.Add(Local.Get(LocalPArgs)); + switch (wasmType) + { + case WasmValueType.I32: + expressions.Add(I32.Load((ulong)interpOffsets[i])); + break; + case WasmValueType.I64: + expressions.Add(I64.Load((ulong)interpOffsets[i])); + break; + case WasmValueType.F32: + expressions.Add(F32.Load((ulong)interpOffsets[i])); + break; + case WasmValueType.F64: + expressions.Add(F64.Load((ulong)interpOffsets[i])); + break; + default: + throw new Exception("Unexpected wasm type for interpreter-to-R2R arg"); + } + targetParamIndex++; + } + } + + // Last R2R arg: portable entrypoint context + expressions.Add(Local.Get(LocalPortableEntrypoint)); + + // call_indirect with the target R2R function's type signature + expressions.Add(Local.Get(LocalPcode)); + expressions.Add(ControlFlow.CallIndirect(targetTypeIndex, 0)); + + // Handle wasm return value — pRet is already on the stack under the return value + if (hasWasmReturn) + { + Debug.Assert(targetFuncType.Returns.Types.Length == 1, "Expected exactly one wasm return type"); + WasmValueType returnWasmType = targetFuncType.Returns.Types[0]; + + // Stack is [pRet, return_value]. Store consumes [addr, value]. + switch (returnWasmType) + { + case WasmValueType.I32: + expressions.Add(I32.Store(0)); + break; + case WasmValueType.I64: + expressions.Add(I64.Store(0)); + break; + case WasmValueType.F32: + expressions.Add(F32.Store(0)); + break; + case WasmValueType.F64: + expressions.Add(F64.Store(0)); + break; + case WasmValueType.V128: + expressions.Add(V128.Store(0)); + break; + default: + throw new Exception("Unexpected wasm return type for interpreter-to-R2R"); + } + } + + // For struct returns via retbuf: the R2R function has already written the struct + // into pRet. Zero-pad to the appropriate alignment boundary. + if (hasRetBuffArg) + { + TypeDesc returnType = methodSignature.ReturnType; + int structSize = returnType.GetElementSize().AsInt; + int alignment = structSize <= 4 ? 4 : 8; + int padding = AlignmentHelper.AlignUp(structSize, alignment) - structSize; + if (padding > 0) + { + expressions.Add(Local.Get(LocalPRet)); + expressions.Add(I32.Const(structSize)); + expressions.Add(I32.Add); + expressions.Add(I32.Const(0)); + expressions.Add(I32.Const(padding)); + expressions.Add(Memory.Fill()); + } + } + + // Restore the stack pointer global + expressions.Add(Local.Get(LocalSavedSp)); + expressions.Add(Global.Set(WasmObjectWriter.StackPointerGlobalIndex)); + + instructionEncoder.FunctionBody = new WasmFunctionBody( + new WasmFuncType( + new WasmResultType(new[] { WasmValueType.I32, WasmValueType.I32, WasmValueType.I32, WasmValueType.I32 }), + new WasmResultType(Array.Empty())), + new[] { WasmValueType.I32 }, + expressions.ToArray()); + } + + protected override void EmitCode(NodeFactory factory, ref X64.X64Emitter instructionEncoder, bool relocsOnly) { throw new NotSupportedException(); } + protected override void EmitCode(NodeFactory factory, ref X86.X86Emitter instructionEncoder, bool relocsOnly) { throw new NotSupportedException(); } + protected override void EmitCode(NodeFactory factory, ref ARM.ARMEmitter instructionEncoder, bool relocsOnly) { throw new NotSupportedException(); } + protected override void EmitCode(NodeFactory factory, ref ARM64.ARM64Emitter instructionEncoder, bool relocsOnly) { throw new NotSupportedException(); } + protected override void EmitCode(NodeFactory factory, ref LoongArch64.LoongArch64Emitter instructionEncoder, bool relocsOnly) { throw new NotSupportedException(); } + protected override void EmitCode(NodeFactory factory, ref RiscV64.RiscV64Emitter instructionEncoder, bool relocsOnly) { throw new NotSupportedException(); } + } +} diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/WasmR2RToInterpreterThunkNode.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/WasmR2RToInterpreterThunkNode.cs index bc48f9a63c17b4..f57a597d2b738a 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/WasmR2RToInterpreterThunkNode.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/WasmR2RToInterpreterThunkNode.cs @@ -95,7 +95,7 @@ protected override void EmitCode(NodeFactory factory, ref Wasm.WasmEmitter instr MethodSignature methodSignature = WasmLowering.RaiseSignature(_wasmSignature, _context); (ArgIterator argit, TransitionBlock transitionBlock) = GCRefMapBuilder.BuildArgIterator(methodSignature, _context); - bool hasRetBuffArg = argit.HasRetBuffArg(); + bool hasRetBuffArg = _wasmSignature.SignatureString[0] == 'S'; bool hasThis = !methodSignature.IsStatic; int[] offsets = new int[methodSignature.Length]; @@ -265,10 +265,10 @@ protected override void EmitCode(NodeFactory factory, ref Wasm.WasmEmitter instr // arg4: return buffer pointer if (hasRetBuffArg) { - // Load the return buffer pointer from the transition block where we stashed it - int retBuffArgOffset = transitionBlock.GetRetBuffArgOffset(hasThis) + argOffsetAdjustment; - expressions.Add(Local.Get(0)); - expressions.Add(I32.Load((ulong)retBuffArgOffset)); + // The retbuf is a wasm parameter — pass it through directly. + // For managed calls: local 0 = $sp, local 1 = this (if present), then retbuf. + int retBufLocalIndex = 1 + (hasThis ? 1 : 0); + expressions.Add(Local.Get(retBufLocalIndex)); } else { diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRunCodegenNodeFactory.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRunCodegenNodeFactory.cs index 6de0c7b6f6cf0a..e3382742633fdf 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRunCodegenNodeFactory.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRunCodegenNodeFactory.cs @@ -310,6 +310,11 @@ private void CreateNodeCaches() return new WasmR2RToInterpreterThunkNode(this, key); }); + _wasmInterpreterToR2RThunks = new NodeCache(key => + { + return new WasmInterpreterToR2RThunkNode(this, key); + }); + _importMethods = new NodeCache(CreateMethodEntrypoint); _localMethodCache = new NodeCache(key => @@ -846,6 +851,12 @@ public WasmR2RToInterpreterThunkNode WasmR2RToInterpreterThunk(WasmSignature was return _wasmR2RToInterpreterThunks.GetOrAdd(wasmSignature); } + private NodeCache _wasmInterpreterToR2RThunks; + public WasmInterpreterToR2RThunkNode WasmInterpreterToR2RThunk(WasmSignature wasmSignature) + { + return _wasmInterpreterToR2RThunks.GetOrAdd(wasmSignature); + } + public void AttachToDependencyGraph(DependencyAnalyzerBase graph, ILProvider ilProvider) { graph.ComputingDependencyPhaseChange += Graph_ComputingDependencyPhaseChange; diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/ILCompiler.ReadyToRun.csproj b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/ILCompiler.ReadyToRun.csproj index 5679cec33f1fa4..b655ae1f029329 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/ILCompiler.ReadyToRun.csproj +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/ILCompiler.ReadyToRun.csproj @@ -306,6 +306,7 @@ + diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/JitInterface/CorInfoImpl.ReadyToRun.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/JitInterface/CorInfoImpl.ReadyToRun.cs index 5b1c8046c14b27..4c3b1a910bb1a8 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/JitInterface/CorInfoImpl.ReadyToRun.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/JitInterface/CorInfoImpl.ReadyToRun.cs @@ -3540,7 +3540,7 @@ private void recordCallSite(uint instrOffset, CORINFO_SIG_INFO* callSig, CORINFO WasmSignature wasmSig = WasmLowering.GetSignature(sig, flags); - AddPrecodeFixup(null); // TODO! fix this to require the generation of a R2R to interp stub + AddPrecodeFixup(_compilation.NodeFactory.WasmR2RToInterpreterThunk(wasmSig)); } } } From 71d86f0698e8e2454950c730dd3f2ca54d8a56c9 Mon Sep 17 00:00:00 2001 From: David Wrighton Date: Fri, 24 Apr 2026 16:11:54 -0700 Subject: [PATCH 11/43] Use _additionalDependencies for thunk wiring in CorInfoImpl - Add AddAdditionalDependency helper for lazily adding to _additionalDependencies - Move WasmR2RToInterpreterThunk from AddPrecodeFixup to AddAdditionalDependency in recordCallSite - Add WasmInterpreterToR2RThunk dependency for every compiled managed non-UnmanagedCallersOnly method on Wasm, using GetSignature(MethodDesc) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../JitInterface/CorInfoImpl.ReadyToRun.cs | 20 ++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/JitInterface/CorInfoImpl.ReadyToRun.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/JitInterface/CorInfoImpl.ReadyToRun.cs index 4c3b1a910bb1a8..4a6cc3ff13056a 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/JitInterface/CorInfoImpl.ReadyToRun.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/JitInterface/CorInfoImpl.ReadyToRun.cs @@ -29,6 +29,8 @@ using System.Runtime.CompilerServices; using ILCompiler.ReadyToRun.TypeSystem; +using DependencyList = ILCompiler.DependencyAnalysisFramework.DependencyNodeCore.DependencyList; + namespace Internal.JitInterface { class InfiniteCompileStress @@ -491,6 +493,12 @@ private void AddPrecodeFixup(ISymbolNode node) _precodeFixups.Add(node); } + private void AddAdditionalDependency(ISymbolNode node, string reason) + { + _additionalDependencies ??= new DependencyList(); + _additionalDependencies.Add(node, reason); + } + private void AddResumptionStubFixup(MethodWithGCInfo compiledStubNode) { _methodCodeNode.Fixups.Add(_compilation.SymbolNodeFactory.ResumptionStubEntryPoint(compiledStubNode)); @@ -855,6 +863,16 @@ public void CompileMethod(MethodWithGCInfo methodCodeNodeNeedingCode, Logger log var compilationResult = CompileMethodInternal(methodCodeNodeNeedingCode, methodIL); codeGotPublished = true; + // For managed methods on Wasm, add an interpreter-to-R2R thunk so the + // interpreter can call into this R2R-compiled function. + if (_compilation.NodeFactory.Target.IsWasm && !MethodBeingCompiled.IsUnmanagedCallersOnly) + { + WasmSignature wasmSig = WasmLowering.GetSignature(MethodBeingCompiled); + AddAdditionalDependency( + _compilation.NodeFactory.WasmInterpreterToR2RThunk(wasmSig), + "Interpreter-to-R2R thunk for compiled method"); + } + if (compilationResult == CompilationResult.CompilationRetryRequested && logger.IsVerbose) { logger.Writer.WriteLine($"Info: Method `{MethodBeingCompiled}` triggered recompilation to acquire stable tokens for cross module inline."); @@ -3540,7 +3558,7 @@ private void recordCallSite(uint instrOffset, CORINFO_SIG_INFO* callSig, CORINFO WasmSignature wasmSig = WasmLowering.GetSignature(sig, flags); - AddPrecodeFixup(_compilation.NodeFactory.WasmR2RToInterpreterThunk(wasmSig)); + AddAdditionalDependency(_compilation.NodeFactory.WasmR2RToInterpreterThunk(wasmSig), "R2R-to-interpreter thunk for call site"); } } } From b1cc073b766077923655ddd091cf89a14c416a55 Mon Sep 17 00:00:00 2001 From: David Wrighton Date: Fri, 24 Apr 2026 16:30:52 -0700 Subject: [PATCH 12/43] Fix the build. --- src/coreclr/jit/emitwasm.cpp | 6 +++--- src/coreclr/jit/lower.cpp | 8 -------- 2 files changed, 3 insertions(+), 11 deletions(-) diff --git a/src/coreclr/jit/emitwasm.cpp b/src/coreclr/jit/emitwasm.cpp index 9c1132ae005cf2..5c2595abd7bf93 100644 --- a/src/coreclr/jit/emitwasm.cpp +++ b/src/coreclr/jit/emitwasm.cpp @@ -210,7 +210,7 @@ void emitter::emitIns_Call(const EmitCallParams& params) if (m_debugInfoSize > 0) { id->idDebugOnlyInfo()->idCallSig = params.sigInfo; - id->idDebugOnlyInfo()->isUnmanagedCall = params.isUnmanagedCall; + id->idDebugOnlyInfo()->idIsUnmanagedCall = params.isUnmanagedCall; id->idDebugOnlyInfo()->idMemCookie = (size_t)params.methHnd; // method token id->idDebugOnlyInfo()->idFlags = GTF_ICON_METHOD_HDL; } @@ -861,13 +861,13 @@ size_t emitter::emitOutputInstr(insGroup* ig, instrDesc* id, BYTE** dp) CORINFO_SIG_INFO sigInfoLocal; CORINFO_SIG_INFO *sigInfoCall = id->idDebugOnlyInfo()->idCallSig; - if (id->idDebugOnlyInfo()->isUnmanagedCall) + if (id->idDebugOnlyInfo()->idIsUnmanagedCall) { _ASSERTE(sigInfoCall != NULL); sigInfoLocal = *sigInfoCall; // Unmanaged calls need to be reported with the unmanaged calling convention so that the R2R compiler can ignore this report // for the purpose of determining if a call site needs to have a R2R to interpreter thunk generated - sigInfoLocal.callConv = CORINFO_CALLCONV_UNMGD; + sigInfoLocal.callConv = CORINFO_CALLCONV_UNMANAGED; sigInfoCall = &sigInfoLocal; } emitRecordCallSite(emitCurCodeOffs(*dp), sigInfoCall, diff --git a/src/coreclr/jit/lower.cpp b/src/coreclr/jit/lower.cpp index 8edf2b5ee5cf26..e66cf2cbfebb20 100644 --- a/src/coreclr/jit/lower.cpp +++ b/src/coreclr/jit/lower.cpp @@ -3016,14 +3016,6 @@ GenTree* Lowering::LowerCall(GenTree* node) { RequireOutgoingArgSpace(call, call->gtArgs.OutgoingArgsStackSize()); } - - // For non-helper, managed calls, if we have portable entry points enabled, we need to lower - // the call according to the portable entrypoint abi - if (!call->IsUnmanaged() && m_compiler->opts.jitFlags->IsSet(JitFlags::JIT_FLAG_PORTABLE_ENTRY_POINTS)) - { - // Inform the VM that we used are calling a portable entrypoint of a particular signature. - m_compiler->info.compCallPortableEntryPoint(call->gtCallMethHnd, call->gtCallSig); - } } if (varTypeIsStruct(call)) From a4aa03abb0e662a13563c735eeaf886993565fda Mon Sep 17 00:00:00 2001 From: David Wrighton Date: Fri, 24 Apr 2026 16:59:45 -0700 Subject: [PATCH 13/43] Tweak around callsite recording issues Co-authored-by: Copilot --- src/coreclr/jit/codegenwasm.cpp | 2 +- src/coreclr/jit/emitwasm.cpp | 21 ++++++++++++++++++--- 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/src/coreclr/jit/codegenwasm.cpp b/src/coreclr/jit/codegenwasm.cpp index cc28164042272a..d87aeca5c50342 100644 --- a/src/coreclr/jit/codegenwasm.cpp +++ b/src/coreclr/jit/codegenwasm.cpp @@ -2482,7 +2482,7 @@ void CodeGen::genCallInstruction(GenTreeCall* call) // native call sites with the signatures they were generated from. if (!call->IsHelperCall()) { - _ASSERTE(params.hasAsyncRet == call->callSig->isAsyncCall()); + _ASSERTE(call->callSig == NULL || params.hasAsyncRet == call->callSig->isAsyncCall()); params.sigInfo = call->callSig; params.isUnmanagedCall = call->IsUnmanaged(); } diff --git a/src/coreclr/jit/emitwasm.cpp b/src/coreclr/jit/emitwasm.cpp index 5c2595abd7bf93..35d9143755bced 100644 --- a/src/coreclr/jit/emitwasm.cpp +++ b/src/coreclr/jit/emitwasm.cpp @@ -861,17 +861,32 @@ size_t emitter::emitOutputInstr(insGroup* ig, instrDesc* id, BYTE** dp) CORINFO_SIG_INFO sigInfoLocal; CORINFO_SIG_INFO *sigInfoCall = id->idDebugOnlyInfo()->idCallSig; + CORINFO_METHOD_HANDLE methodHandle = (CORINFO_METHOD_HANDLE)id->idDebugOnlyInfo()->idMemCookie; + + if (sigInfoCall == nullptr) + { + // For certain calls whose target is non-containable (e.g. tls access targets), `methodHandle` + // will be nullptr, because the target is present in a register. + if ((methodHandle != nullptr) && (Compiler::eeGetHelperNum(methodHandle) == CORINFO_HELP_UNDEF)) + { + m_compiler->eeGetMethodSig(methodHandle, &sigInfoLocal); + sigInfoCall = &sigInfoLocal; + } + } + if (id->idDebugOnlyInfo()->idIsUnmanagedCall) { _ASSERTE(sigInfoCall != NULL); - sigInfoLocal = *sigInfoCall; + if (sigInfoCall != &sigInfoLocal) + { + sigInfoLocal = *sigInfoCall; + } // Unmanaged calls need to be reported with the unmanaged calling convention so that the R2R compiler can ignore this report // for the purpose of determining if a call site needs to have a R2R to interpreter thunk generated sigInfoLocal.callConv = CORINFO_CALLCONV_UNMANAGED; sigInfoCall = &sigInfoLocal; } - emitRecordCallSite(emitCurCodeOffs(*dp), sigInfoCall, - (CORINFO_METHOD_HANDLE)id->idDebugOnlyInfo()->idMemCookie); + emitRecordCallSite(emitCurCodeOffs(*dp), sigInfoCall, methodHandle); } *dp = dst; From 895a6f6e37d6c0074ee5e6761e2833f962ef11f9 Mon Sep 17 00:00:00 2001 From: David Wrighton Date: Mon, 27 Apr 2026 09:42:43 -0700 Subject: [PATCH 14/43] Fix multiple crossgen2 WASM thunk generation bugs - Replace ValueTuple-based struct size construction with a cache of real struct types encountered during GetSignature. ValueTuples have auto layout which causes padding, making roundtrip size assertions fail. The cache uses a locked Dictionary for thread safety. - Fix RaiseSignature to skip the hidden retbuf pointer parameter when the return type is a struct (S encoding). Previously it was included in the raised MethodSignature parameters, causing GetSignature to emit a duplicate retbuf pointer on re-encoding. - Fix WasmImportThunk to handle 'this' pointer correctly: store/restore it separately before the explicit parameter loop, and start wasmLocalIndex past both 'this' and retbuf locals. - Fix WasmImportThunkPortableEntrypoint to strip IsUnmanagedCallersOnly flag when computing thunk signatures, since thunks always use managed calling convention. - Fix DelayLoadHelperImport to skip creating WasmImportThunk for GenericLookupSignature on WASM, as these are eager fixups that don't need import thunks. - Fix WasmR2RToInterpreterThunkNode to skip 'this' and retbuf wasm locals before iterating explicit parameters. - Skip creating R2R-to-interpreter thunks for unmanaged call sites, as they don't go through interpreter transitions. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../CompilerTypeSystemContext.Wasm.cs | 69 ++++++------------- .../tools/Common/JitInterface/WasmLowering.cs | 24 +++++-- .../ReadyToRun/DelayLoadHelperImport.cs | 38 ++++++++-- .../ReadyToRun/WasmImportThunk.cs | 20 ++++++ .../WasmImportThunkPortableEntrypoint.cs | 8 ++- .../WasmR2RToInterpreterThunkNode.cs | 17 +++++ .../JitInterface/CorInfoImpl.ReadyToRun.cs | 7 +- 7 files changed, 122 insertions(+), 61 deletions(-) diff --git a/src/coreclr/tools/Common/Compiler/CompilerTypeSystemContext.Wasm.cs b/src/coreclr/tools/Common/Compiler/CompilerTypeSystemContext.Wasm.cs index 335eeb4e22de10..f3a270d255af18 100644 --- a/src/coreclr/tools/Common/Compiler/CompilerTypeSystemContext.Wasm.cs +++ b/src/coreclr/tools/Common/Compiler/CompilerTypeSystemContext.Wasm.cs @@ -1,8 +1,7 @@ // 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.Threading; +using System.Collections.Generic; using Internal.TypeSystem; @@ -10,7 +9,8 @@ namespace ILCompiler { public partial class CompilerTypeSystemContext { - private volatile TypeDesc[] _valueTupleStructsBySize = Array.Empty(); + private readonly object _structCacheLock = new object(); + private readonly Dictionary _structsBySize = new Dictionary(); private volatile TypeDesc _cachedEmptyStruct; /// @@ -28,62 +28,35 @@ public void CacheEmptyStruct(TypeDesc type) } /// - /// Gets or creates a value type of the specified byte size, constructed from - /// nested ValueTuple<byte, ...> types. Used by WasmLowering to represent - /// struct parameters/returns in raised signatures. + /// Caches a struct type by its element size, so RaiseSignature can retrieve a real + /// type of that size. Only the first struct encountered for a given size is retained. /// - /// - /// Size 1 returns byte. - /// Size 2 returns ValueTuple<byte, byte>. - /// Size 5 returns ValueTuple<ValueTuple<byte, byte>, ValueTuple<byte, ValueTuple<byte, byte>>>. - /// Size N is split into halves: ValueTuple<(size N/2), (size N - N/2)>. - /// - public TypeDesc GetValueTupleStructOfSize(int size) + public void CacheStructBySize(TypeDesc type) { - TypeDesc[] array = _valueTupleStructsBySize; + int size = type.GetElementSize().AsInt; + if (size <= 0) + return; - if (size < array.Length && array[size] is not null) + lock (_structCacheLock) { - return array[size]; + _structsBySize.TryAdd(size, type); } - - return GetValueTupleStructOfSizeSlow(size); } - private TypeDesc GetValueTupleStructOfSizeSlow(int size) - { - TypeDesc[] array = _valueTupleStructsBySize; - - if (size >= array.Length) - { - TypeDesc[] newArray = new TypeDesc[size + 1]; - Array.Copy(array, newArray, array.Length); - _valueTupleStructsBySize = newArray; - array = newArray; - } - - TypeDesc result = BuildValueTupleStructOfSize(size); - array[size] = result; - - return result; - } - - private TypeDesc BuildValueTupleStructOfSize(int size) + /// + /// Gets a previously cached struct type of the specified byte size. + /// Returns null if no struct of that size has been cached. + /// Used by RaiseSignature to produce a roundtrippable type for the 'S<N>' encoding. + /// + public TypeDesc GetCachedStructOfSize(int size) { - TypeDesc byteType = GetWellKnownType(WellKnownType.Byte); - - if (size == 1) + lock (_structCacheLock) { - return byteType; + if (_structsBySize.TryGetValue(size, out TypeDesc result)) + return result; } - MetadataType valueTuple2 = SystemModule.GetType("System"u8, "ValueTuple`2"u8); - int leftSize = size / 2; - int rightSize = size - leftSize; - TypeDesc left = GetValueTupleStructOfSize(leftSize); - TypeDesc right = GetValueTupleStructOfSize(rightSize); - - return valueTuple2.MakeInstantiatedType(left, right); + return null; } } } diff --git a/src/coreclr/tools/Common/JitInterface/WasmLowering.cs b/src/coreclr/tools/Common/JitInterface/WasmLowering.cs index 5c2f455e6aacdf..4eec4d019a9ae6 100644 --- a/src/coreclr/tools/Common/JitInterface/WasmLowering.cs +++ b/src/coreclr/tools/Common/JitInterface/WasmLowering.cs @@ -173,6 +173,7 @@ public static MethodSignature RaiseSignature(WasmSignature wasmSignature, TypeSy // Parse return type TypeDesc returnType; + bool hasReturnBuffer = false; if (sig[pos] == 'v') { returnType = context.GetWellKnownType(WellKnownType.Void); @@ -181,7 +182,9 @@ public static MethodSignature RaiseSignature(WasmSignature wasmSignature, TypeSy else if (sig[pos] == 'S') { int structSize = ParseStructSize(sig, ref pos); - returnType = ((CompilerTypeSystemContext)context).GetValueTupleStructOfSize(structSize); + returnType = ((CompilerTypeSystemContext)context).GetCachedStructOfSize(structSize); + Debug.Assert(returnType is not null, $"No cached struct of size {structSize} for return type in signature '{sig}'"); + hasReturnBuffer = true; } else { @@ -192,6 +195,7 @@ public static MethodSignature RaiseSignature(WasmSignature wasmSignature, TypeSy // Parse parameters (everything until 'p' suffix or end of string) List parameters = new List(); bool hasThis = false; + while (pos < sig.Length && sig[pos] != 'p') { char c = sig[pos]; @@ -201,6 +205,13 @@ public static MethodSignature RaiseSignature(WasmSignature wasmSignature, TypeSy hasThis = true; pos++; } + else if (hasReturnBuffer) + { + // The hidden retbuf pointer follows 'T' (or comes first if no 'T'). + // Skip it — GetSignature will re-add it based on the struct return type. + hasReturnBuffer = false; + pos++; + } else if (c == 'e') { // Empty struct — include the cached empty struct type for roundtrip fidelity @@ -212,7 +223,9 @@ public static MethodSignature RaiseSignature(WasmSignature wasmSignature, TypeSy else if (c == 'S') { int structSize = ParseStructSize(sig, ref pos); - parameters.Add(((CompilerTypeSystemContext)context).GetValueTupleStructOfSize(structSize)); + TypeDesc cachedStruct = ((CompilerTypeSystemContext)context).GetCachedStructOfSize(structSize); + Debug.Assert(cachedStruct is not null, $"No cached struct of size {structSize} for parameter in signature '{sig}'"); + parameters.Add(cachedStruct); } else { @@ -231,8 +244,9 @@ public static MethodSignature RaiseSignature(WasmSignature wasmSignature, TypeSy MethodSignature result = new MethodSignature(flags, 0, returnType, parameters.ToArray()); LoweringFlags relowerFlags = isManaged ? LoweringFlags.None : LoweringFlags.IsUnmanagedCallersOnly; - Debug.Assert(GetSignature(result, relowerFlags).Equals(wasmSignature), - "RaiseSignature produced a signature that does not roundtrip back to the same WasmSignature"); + WasmSignature roundtripped = GetSignature(result, relowerFlags); + Debug.Assert(roundtripped.Equals(wasmSignature), + $"RaiseSignature roundtrip failed: input='{wasmSignature.SignatureString}', roundtripped='{roundtripped.SignatureString}'"); return result; } @@ -311,6 +325,7 @@ public static WasmSignature GetSignature(MethodSignature signature, LoweringFlag int returnSize = returnType.GetElementSize().AsInt; sigBuilder.Append('S'); sigBuilder.Append(returnSize); + ((CompilerTypeSystemContext)returnType.Context).CacheStructBySize(returnType); } } else if (loweredReturnType.IsVoid) @@ -394,6 +409,7 @@ public static WasmSignature GetSignature(MethodSignature signature, LoweringFlag sigBuilder.Append('S'); sigBuilder.Append(paramSize); result.Add(pointerType); + ((CompilerTypeSystemContext)paramType.Context).CacheStructBySize(paramType); } else { diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/DelayLoadHelperImport.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/DelayLoadHelperImport.cs index 19d4688723936d..78b6b3aa0f7f79 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/DelayLoadHelperImport.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/DelayLoadHelperImport.cs @@ -41,7 +41,15 @@ public DelayLoadHelperImport( _useJumpableStub = useJumpableStub; if (factory.Target.Architecture == TargetArchitecture.Wasm32) { - _delayLoadHelper = factory.WasmImportThunkPortableEntrypoint(this); + if (instanceSignature is GenericLookupSignature) + { + // Generic lookups are resolved via eager fixups and don't need import thunks + _delayLoadHelper = null; + } + else + { + _delayLoadHelper = factory.WasmImportThunkPortableEntrypoint(this); + } } else { @@ -74,10 +82,18 @@ public override void AppendMangledName(NameMangler nameMangler, Utf8StringBuilde public override void EncodeData(ref ObjectDataBuilder dataBuilder, NodeFactory factory, bool relocsOnly) { - // This needs to be an empty target pointer since it will be filled in with Module* - // when loaded by CoreCLR - dataBuilder.EmitReloc(_delayLoadHelper, - factory.Target.PointerSize == 4 ? RelocType.IMAGE_REL_BASED_HIGHLOW : RelocType.IMAGE_REL_BASED_DIR64, factory.Target.CodeDelta); + if (_delayLoadHelper is not null) + { + // This needs to be an empty target pointer since it will be filled in with Module* + // when loaded by CoreCLR + dataBuilder.EmitReloc(_delayLoadHelper, + factory.Target.PointerSize == 4 ? RelocType.IMAGE_REL_BASED_HIGHLOW : RelocType.IMAGE_REL_BASED_DIR64, factory.Target.CodeDelta); + } + else + { + // Eager fixups don't need a delay load helper thunk — emit a zero pointer + dataBuilder.EmitNaturalInt(0); + } if (Table.EntrySize == (factory.Target.PointerSize * 2)) { @@ -91,9 +107,17 @@ public override void EncodeData(ref ObjectDataBuilder dataBuilder, NodeFactory f public override IEnumerable GetStaticDependencies(NodeFactory factory) { - return new DependencyListEntry[] + if (_delayLoadHelper is not null) + { + return new DependencyListEntry[] + { + new DependencyListEntry(_delayLoadHelper, "Delay load helper thunk for ready-to-run fixup import"), + new DependencyListEntry(ImportSignature, "Signature for ready-to-run fixup import"), + }; + } + + return new DependencyListEntry[] { - new DependencyListEntry(_delayLoadHelper, "Delay load helper thunk for ready-to-run fixup import"), new DependencyListEntry(ImportSignature, "Signature for ready-to-run fixup import"), }; } diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/WasmImportThunk.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/WasmImportThunk.cs index 273980c36fece6..bdf1553e35ce84 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/WasmImportThunk.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/WasmImportThunk.cs @@ -183,7 +183,18 @@ protected override void EmitCode(NodeFactory factory, ref Wasm.WasmEmitter instr // This allows us to: // - Skip empty struct params (no wasm local exists) // - Zero-fill indirect struct params instead of copying the byref pointer + bool hasThis = !methodSignature.IsStatic; int wasmLocalIndex = 1; // local 0 is $sp + + // Store 'this' pointer if present — it occupies a wasm local but is not in the raised MethodSignature params + if (hasThis) + { + expressions.Add(Local.Get(0)); + expressions.Add(Local.Get(wasmLocalIndex)); + expressions.Add(I32.Store((ulong)transitionBlock.ThisOffset)); + wasmLocalIndex++; + } + for (int i = 0; i < methodSignature.Length; i++) { TypeDesc paramType = methodSignature[i]; @@ -282,6 +293,15 @@ protected override void EmitCode(NodeFactory factory, ref Wasm.WasmEmitter instr // In the calling convention, the first arg is the sp arg, and the last is the portable entrypoint arg. Each of those are treated specially // Iterate over the raised MethodSignature params to handle indirect/empty structs correctly wasmLocalIndex = 1; + + // Restore 'this' pointer if present + if (hasThis) + { + expressions.Add(Local.Get(0)); + expressions.Add(I32.Load((ulong)transitionBlock.ThisOffset)); + wasmLocalIndex++; + } + for (int i = 0; i < methodSignature.Length; i++) { TypeDesc paramType = methodSignature[i]; diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/WasmImportThunkPortableEntrypoint.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/WasmImportThunkPortableEntrypoint.cs index a29a614e61cf13..b11a7115897b6e 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/WasmImportThunkPortableEntrypoint.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/WasmImportThunkPortableEntrypoint.cs @@ -5,6 +5,7 @@ using Internal.ReadyToRunConstants; using Internal.Text; using Internal.JitInterface; +using Internal.TypeSystem; using System.Diagnostics; namespace ILCompiler.DependencyAnalysis.ReadyToRun @@ -81,7 +82,12 @@ public override ObjectData GetData(NodeFactory factory, System.Boolean relocsOnl } else { - wasmSignature = WasmLowering.GetSignature(((MethodFixupSignature)(_import.Signature)).Method); + MethodDesc method = ((MethodFixupSignature)(_import.Signature)).Method; + // The import thunk always uses managed calling convention ($sp + PE entrypoint) + // even if the underlying method is UnmanagedCallersOnly, because the thunk is + // called from R2R-generated managed code. + WasmLowering.LoweringFlags flags = WasmLowering.GetLoweringFlags(method) & ~WasmLowering.LoweringFlags.IsUnmanagedCallersOnly; + wasmSignature = WasmLowering.GetSignature(method.Signature, flags); } builder.EmitReloc(factory.WasmImportThunk(wasmSignature, HelperId, _import.Table, UseVirtualCall, UseJumpableStub), tableIndexPointerRelocType); builder.EmitReloc(_import, RelocType.IMAGE_REL_BASED_ADDR32NB); diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/WasmR2RToInterpreterThunkNode.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/WasmR2RToInterpreterThunkNode.cs index f57a597d2b738a..29dc47530b96d8 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/WasmR2RToInterpreterThunkNode.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/WasmR2RToInterpreterThunkNode.cs @@ -160,6 +160,23 @@ protected override void EmitCode(NodeFactory factory, ref Wasm.WasmEmitter instr // Store all arguments into the transition block area int wasmLocalIndex = 1; // local 0 is $sp + + // Handle 'this' pointer — it occupies a wasm local but is not in methodSignature.Length + if (hasThis) + { + int thisOffset = transitionBlock.ThisOffset + argOffsetAdjustment; + expressions.Add(Local.Get(0)); + expressions.Add(Local.Get(wasmLocalIndex)); + expressions.Add(I32.Store((ulong)thisOffset)); + wasmLocalIndex++; + } + + // Hidden retbuf pointer occupies a wasm local but is not in methodSignature params + if (hasRetBuffArg) + { + wasmLocalIndex++; + } + for (int i = 0; i < methodSignature.Length; i++) { TypeDesc paramType = methodSignature[i]; diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/JitInterface/CorInfoImpl.ReadyToRun.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/JitInterface/CorInfoImpl.ReadyToRun.cs index 4a6cc3ff13056a..9b20b7fe09b5a2 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/JitInterface/CorInfoImpl.ReadyToRun.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/JitInterface/CorInfoImpl.ReadyToRun.cs @@ -3558,7 +3558,12 @@ private void recordCallSite(uint instrOffset, CORINFO_SIG_INFO* callSig, CORINFO WasmSignature wasmSig = WasmLowering.GetSignature(sig, flags); - AddAdditionalDependency(_compilation.NodeFactory.WasmR2RToInterpreterThunk(wasmSig), "R2R-to-interpreter thunk for call site"); + // Only create R2R-to-interpreter thunks for managed calls. + // Unmanaged calls don't go through the interpreter transition. + if (!flags.HasFlag(WasmLowering.LoweringFlags.IsUnmanagedCallersOnly)) + { + AddAdditionalDependency(_compilation.NodeFactory.WasmR2RToInterpreterThunk(wasmSig), "R2R-to-interpreter thunk for call site"); + } } } } From 032dcd95270afe8419a415ba10f712666a97dfb3 Mon Sep 17 00:00:00 2001 From: David Wrighton Date: Mon, 27 Apr 2026 10:13:31 -0700 Subject: [PATCH 15/43] Fix Wasm ArgIterator SizeOfArgStack for methods with only unnamed args The ForceSigWalk method had two bugs in its Wasm-specific path for accounting unnamed arguments (this, retbuf, generic context, etc.) when no named arguments are present: 1. The check 'maxOffset == 0' could never be true because maxOffset is initialized to OffsetOfArgs (8 on Wasm32). Changed to compare against OffsetOfArgs. 2. The fallback 'maxOffset = _wasmOfsStack' was incorrect because _wasmOfsStack is relative to OffsetOfArgs, but maxOffset is an absolute offset. Changed to 'OffsetOfArgs + _wasmOfsStack'. These bugs caused GCRefMapBuilder to allocate a zero-length fake stack for methods with only unnamed arguments (e.g. parameterless instance methods), leading to IndexOutOfRangeException when writing the 'this' pointer GC ref at ThisOffset. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Compiler/DependencyAnalysis/ReadyToRun/ArgIterator.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/ArgIterator.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/ArgIterator.cs index f98f8990a65d98..c6b25756045c50 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/ArgIterator.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/ArgIterator.cs @@ -1721,10 +1721,10 @@ private void ForceSigWalk() } } - if (maxOffset == 0 && _transitionBlock.IsWasm32) + if (maxOffset == _transitionBlock.OffsetOfArgs && _transitionBlock.IsWasm32) { // Wasm puts all arguments on the stack, even the unnamed ones like the param registers, this pointer and async continuation. If we didn't see any named arguments, then we need to account for the unnamed ones here. - maxOffset = _wasmOfsStack; + maxOffset = _transitionBlock.OffsetOfArgs + _wasmOfsStack; } // Clear the iterator started flag From 2db92edecba28623a8f50173d8539efe2b5a4106 Mon Sep 17 00:00:00 2001 From: David Wrighton Date: Mon, 27 Apr 2026 13:04:33 -0700 Subject: [PATCH 16/43] Align SignatureMapper with WasmLowering S struct encoding - Replace 'n' encoding with 'S' for multi-field structs passed by ref - Add hardcoded struct sizes for QCallModule (8), QCallAssembly (8), GCHeapHardLimitInfo (64) so signatures produce S format - Add ParseSignatureTokens tokenizer to handle multi-char S tokens - Add Token-based API (TokenToNativeType/TokenToNameType/TokenToArgType) - Update InterpToNativeGenerator to use token-based parsing - Unknown struct types log a diagnostic at High importance Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../coreclr/InterpToNativeGenerator.cs | 53 ++++--- .../WasmAppBuilder/coreclr/SignatureMapper.cs | 139 ++++++++++++++---- 2 files changed, 137 insertions(+), 55 deletions(-) diff --git a/src/tasks/WasmAppBuilder/coreclr/InterpToNativeGenerator.cs b/src/tasks/WasmAppBuilder/coreclr/InterpToNativeGenerator.cs index 711b88198e1062..b42a11cb033dab 100644 --- a/src/tasks/WasmAppBuilder/coreclr/InterpToNativeGenerator.cs +++ b/src/tasks/WasmAppBuilder/coreclr/InterpToNativeGenerator.cs @@ -43,15 +43,16 @@ public void Generate(IEnumerable cookies, string outputPath) private static string SignatureToArguments(string signature) { - if (signature.Length <= 1) + var tokens = SignatureMapper.ParseSignatureTokens(signature); + if (tokens.Count <= 1) return "void"; - return string.Join(", ", signature.Skip(1).Select(static c => SignatureMapper.CharToNativeType(c))); + return string.Join(", ", tokens.Skip(1).Select(static t => SignatureMapper.TokenToNativeType(t))); } - private static string CallFuncName(IEnumerable args, string result, bool isPortableEntryPointCall) + private static string CallFuncName(IEnumerable args, string result, bool isPortableEntryPointCall) { - var paramTypes = args.Any() ? args.Join("_", (p, i) => SignatureMapper.CharToNameType(p)).ToString() : "Void"; + var paramTypes = args.Any() ? string.Join("_", args.Select(static t => SignatureMapper.TokenToNameType(t))) : "Void"; return $"CallFunc_{paramTypes}_Ret{result}{(isPortableEntryPointCall ? "_PE" : "")}"; } @@ -93,17 +94,19 @@ private static void Emit(StreamWriter w, IEnumerable cookies) string signature = signatureValue; try { - var result = Result(signature); - bool isPortableEntryPointCall = IsPortableEntryPointCall(signature); + var tokens = SignatureMapper.ParseSignatureTokens(signature); + string returnToken = tokens[0]; + var result = Result(returnToken); + bool isPortableEntryPointCall = IsPortableEntryPointCall(tokens); if (isPortableEntryPointCall) { // Portable entrypoints have an extra hidden parameter for the portable entrypoint context, so we need to adjust the signature and result accordingly for the call function generation - signature = signature.Substring(0, signature.Length - 1); + tokens.RemoveAt(tokens.Count - 1); } - var args = Args(signature); - var portabilityAssert = signature[0] == 'n' ? "PORTABILITY_ASSERT(\"Indirect struct return is not yet implemented.\");\n " : ""; + var args = Args(tokens); + var portabilityAssert = returnToken[0] == 'S' ? "PORTABILITY_ASSERT(\"Indirect struct return is not yet implemented.\");\n " : ""; - var portableEntryPointComma = signature.Length > 1 ? ", " : ""; + var portableEntryPointComma = args.Count > 0 ? ", " : ""; var portableEntrypointDeclaration = isPortableEntryPointCall ? portableEntryPointComma + "PCODE" : ""; var portableEntrypointParam = isPortableEntryPointCall ? portableEntryPointComma + "pPortableEntryPointContext" : ""; var portableEntrypointStackDeclaration = isPortableEntryPointCall ? "int*, " : ""; @@ -111,10 +114,10 @@ private static void Emit(StreamWriter w, IEnumerable cookies) w.Write( $$""" - {{(isPortableEntryPointCall ? "NOINLINE " : "")}}static void {{CallFuncName(args, SignatureMapper.CharToNameType(signature[0]), isPortableEntryPointCall)}}(PCODE pcode, int8_t* pArgs, int8_t* pRet{{(isPortableEntryPointCall ? ", PCODE pPortableEntryPointContext" : "")}}) + {{(isPortableEntryPointCall ? "NOINLINE " : "")}}static void {{CallFuncName(args, SignatureMapper.TokenToNameType(returnToken), isPortableEntryPointCall)}}(PCODE pcode, int8_t* pArgs, int8_t* pRet{{(isPortableEntryPointCall ? ", PCODE pPortableEntryPointContext" : "")}}) {{{(isPortableEntryPointCall ? "\n alignas(16) int framePointer = TERMINATE_R2R_STACK_WALK;" : "")}} - {{result.nativeType}} (*fptr)({{portableEntrypointStackDeclaration}}{{args.Join(", ", (p, i) => SignatureMapper.CharToNativeType(p))}}{{portableEntrypointDeclaration}}) = ({{result.nativeType}} (*)({{portableEntrypointStackDeclaration}}{{args.Join(", ", (p, i) => SignatureMapper.CharToNativeType(p))}}{{portableEntrypointDeclaration}}))pcode; - {{portabilityAssert}}{{(result.isVoid ? "" : "*" + "((" + result.nativeType + "*)pRet) = ")}}(*fptr)({{portableEntrypointStackParam}}{{args.Join(", ", (p, i) => $"{SignatureMapper.CharToArgType(p)}({i})")}}{{portableEntrypointParam}}); + {{result.nativeType}} (*fptr)({{portableEntrypointStackDeclaration}}{{string.Join(", ", args.Select(static t => SignatureMapper.TokenToNativeType(t)))}}{{portableEntrypointDeclaration}}) = ({{result.nativeType}} (*)({{portableEntrypointStackDeclaration}}{{string.Join(", ", args.Select(static t => SignatureMapper.TokenToNativeType(t)))}}{{portableEntrypointDeclaration}}))pcode; + {{portabilityAssert}}{{(result.isVoid ? "" : "*" + "((" + result.nativeType + "*)pRet) = ")}}(*fptr)({{portableEntrypointStackParam}}{{string.Join(", ", args.Select(static (t, i) => $"{SignatureMapper.TokenToArgType(t)}({i})"))}}{{portableEntrypointParam}}); } """); @@ -133,10 +136,11 @@ private static void Emit(StreamWriter w, IEnumerable cookies) {{signatures.Join($",{w.NewLine}", signature => { string initialSignature = signature; - bool isPortableEntryPointCall = IsPortableEntryPointCall(signature); + var tokens = SignatureMapper.ParseSignatureTokens(signature); + bool isPortableEntryPointCall = IsPortableEntryPointCall(tokens); if (isPortableEntryPointCall) - signature = signature.Substring(0, signature.Length - 1); - return $" {{ \"{initialSignature}\", (void*)&{CallFuncName(Args(signature), SignatureMapper.CharToNameType(signature[0]), isPortableEntryPointCall)} }}"; + tokens.RemoveAt(tokens.Count - 1); + return $" {{ \"{initialSignature}\", (void*)&{CallFuncName(Args(tokens), SignatureMapper.TokenToNameType(tokens[0]), isPortableEntryPointCall)} }}"; } )}} }; @@ -145,22 +149,17 @@ private static void Emit(StreamWriter w, IEnumerable cookies) """); - static IEnumerable Args(string signature) + static List Args(List tokens) { - for (int i = 1; i < signature.Length; ++i) - yield return signature[i]; + return tokens.Count > 1 ? tokens.GetRange(1, tokens.Count - 1) : new List(); } - static (bool isVoid, string nativeType) Result(string signature) - => new(SignatureMapper.IsVoidSignature(signature), SignatureMapper.CharToNativeType(signature[0])); + static (bool isVoid, string nativeType) Result(string returnToken) + => new(returnToken == "v", SignatureMapper.TokenToNativeType(returnToken)); - static bool IsPortableEntryPointCall(string signature) + static bool IsPortableEntryPointCall(List tokens) { -#if NETFRAMEWORK - return signature.EndsWith("p"); -#else - return signature.EndsWith('p'); -#endif + return tokens.Count > 0 && tokens[tokens.Count - 1] == "p"; } } } diff --git a/src/tasks/WasmAppBuilder/coreclr/SignatureMapper.cs b/src/tasks/WasmAppBuilder/coreclr/SignatureMapper.cs index b08d5a213c14da..cb9315a09ba764 100644 --- a/src/tasks/WasmAppBuilder/coreclr/SignatureMapper.cs +++ b/src/tasks/WasmAppBuilder/coreclr/SignatureMapper.cs @@ -6,14 +6,28 @@ using System.Linq; using System.Reflection; using System.Text; +using Microsoft.Build.Framework; namespace Microsoft.WebAssembly.Build.Tasks.CoreClr; internal static class SignatureMapper { - internal static char? TypeToChar(Type t, LogAdapter log, out bool isByRefStruct, int depth = 0) + // Hardcoded struct sizes for types that crossgen2 encodes as S. + // The fully general case is handled by crossgen2's type system; these + // cover the small set of multi-field structs that appear in InternalCall + // and PInvoke signatures. + private static readonly Dictionary s_knownStructSizes = new() + { + ["System.Runtime.CompilerServices.QCallModule"] = 8, + ["System.Runtime.CompilerServices.QCallAssembly"] = 8, + ["System.Runtime.CompilerServices.QCallTypeHandle"] = 8, + ["System.GC+GCHeapHardLimitInfo"] = 64, + }; + + internal static char? TypeToChar(Type t, LogAdapter log, out bool isByRefStruct, out int structSize, int depth = 0) { isByRefStruct = false; + structSize = 0; if (depth > 5) { log.Warning("WASM0064", $"Unbounded recursion detected through parameter type '{t.Name}'"); @@ -60,7 +74,7 @@ internal static class SignatureMapper else if (t.IsEnum) { Type underlyingType = t.GetEnumUnderlyingType(); - c = TypeToChar(underlyingType, log, out _, ++depth); + c = TypeToChar(underlyingType, log, out _, out structSize, ++depth); } else if (t.IsPointer) c = 'i'; @@ -72,10 +86,23 @@ internal static class SignatureMapper if (fields.Length == 1) { Type fieldType = fields[0].FieldType; - return TypeToChar(fieldType, log, out isByRefStruct, ++depth); + return TypeToChar(fieldType, log, out isByRefStruct, out structSize, ++depth); } else if (PInvokeTableGenerator.IsBlittable(t, log)) - c = 'n'; + { + string fullName = t.FullName ?? t.Name; + if (s_knownStructSizes.TryGetValue(fullName, out int size)) + { + structSize = size; + } + else + { + log.LogMessage(MessageImportance.High, + $"SignatureMapper: unknown multi-field struct '{fullName}' (fields: {fields.Length}) — size not hardcoded"); + } + + c = 'S'; + } isByRefStruct = true; } @@ -85,77 +112,133 @@ internal static class SignatureMapper return c; } + internal static char? TypeToChar(Type t, LogAdapter log, out bool isByRefStruct, int depth = 0) + => TypeToChar(t, log, out isByRefStruct, out _, depth); + + /// + /// Builds the multi-char token for a type in the signature string. + /// For most types this is a single character; for multi-field structs it is "S<N>". + /// + private static string? TypeToSignatureToken(Type t, LogAdapter log, out bool isByRefStruct) + { + char? c = TypeToChar(t, log, out isByRefStruct, out int structSize); + if (c is null) + return null; + + if (c == 'S' && structSize > 0) + return $"S{structSize}"; + + return c.Value.ToString(); + } + public static string? MethodToSignature(MethodInfo method, LogAdapter log, bool includeThis = false) { - string? result = TypeToChar(method.ReturnType, log, out bool resultIsByRef)?.ToString(); - if (result == null) - { + string? returnToken = TypeToSignatureToken(method.ReturnType, log, out bool resultIsByRef); + if (returnToken is null) return null; - } + + var sb = new StringBuilder(); if (resultIsByRef) { - result = "n"; + // Struct return — encode as S (the return type token already has the size) + sb.Append(returnToken); + } + else + { + sb.Append(returnToken); } if (includeThis && !method.IsStatic) { - result += 'i'; + sb.Append('i'); } foreach (var parameter in method.GetParameters()) { - char? parameterChar = TypeToChar(parameter.ParameterType, log, out _); - if (parameterChar == null) - { + string? paramToken = TypeToSignatureToken(parameter.ParameterType, log, out _); + if (paramToken is null) return null; - } - result += parameterChar; + sb.Append(paramToken); } - return result; + return sb.ToString(); } - public static string CharToNativeType(char c) => c switch + /// + /// Parses a signature string into individual tokens. + /// Single-char types produce one-char tokens; S<N> produces a multi-char token like "S8" or "S64". + /// The 'p' suffix is included as its own token. + /// + public static List ParseSignatureTokens(string signature) + { + var tokens = new List(); + int i = 0; + while (i < signature.Length) + { + if (signature[i] == 'S') + { + int start = i; + i++; // skip 'S' + while (i < signature.Length && char.IsDigit(signature[i])) + i++; + tokens.Add(signature.Substring(start, i - start)); + } + else + { + tokens.Add(signature[i].ToString()); + i++; + } + } + + return tokens; + } + + public static string TokenToNativeType(string token) => token[0] switch { 'v' => "void", 'i' => "int32_t", 'l' => "int64_t", 'f' => "float", 'd' => "double", - 'n' => "int32_t", - _ => throw new InvalidSignatureCharException(c) + 'S' => "int32_t", + 'p' => "PCODE", + _ => throw new InvalidSignatureCharException(token[0]) }; - public static string CharToNameType(char c) => c switch + public static string TokenToNameType(string token) => token[0] switch { 'v' => "Void", 'i' => "I32", 'l' => "I64", 'f' => "F32", 'd' => "F64", - 'n' => "IND", - _ => throw new InvalidSignatureCharException(c) + 'S' => "IND", + 'p' => "PE", + _ => throw new InvalidSignatureCharException(token[0]) }; - public static string CharToArgType(char c) => c switch + public static string TokenToArgType(string token) => token[0] switch { 'i' => "ARG_I32", 'l' => "ARG_I64", 'f' => "ARG_F32", 'd' => "ARG_F64", - 'n' => "ARG_IND", - _ => throw new InvalidSignatureCharException(c) + 'S' => "ARG_IND", + _ => throw new InvalidSignatureCharException(token[0]) }; + // Legacy single-char overloads — still used by consumers that don't encounter S tokens. + public static string CharToNativeType(char c) => TokenToNativeType(c.ToString()); + public static string CharToNameType(char c) => TokenToNameType(c.ToString()); + public static string CharToArgType(char c) => TokenToArgType(c.ToString()); + public static string TypeToNameType(Type t, LogAdapter log) { char? c = TypeToChar(t, log, out _); - if (c == null) - { + if (c is null) throw new InvalidSignatureCharException('?'); - } return CharToNameType(c.Value); } From 3e424ffa453c23980904701cad9bdc96a1d9bdfa Mon Sep 17 00:00:00 2001 From: David Wrighton Date: Mon, 27 Apr 2026 13:18:30 -0700 Subject: [PATCH 17/43] Fix struct size in thunk names and slot indexing, remove blittable gate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - TokenToNameType returns the full S token (e.g. S8, S64) so generated function names encode the struct size - ArgsWithSlotOffsets computes running slot indices: structs consume max(size/8, 1) slots instead of always 1 - Add TokenToSlotCount helper - Remove IsBlittable gate from TypeToChar — multi-field structs are always passed by pointer, matching crossgen2 WasmLowering behavior Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../coreclr/InterpToNativeGenerator.cs | 15 ++++++++++++++- .../WasmAppBuilder/coreclr/SignatureMapper.cs | 17 +++++++++++++++-- 2 files changed, 29 insertions(+), 3 deletions(-) diff --git a/src/tasks/WasmAppBuilder/coreclr/InterpToNativeGenerator.cs b/src/tasks/WasmAppBuilder/coreclr/InterpToNativeGenerator.cs index b42a11cb033dab..cf7216a1998eb6 100644 --- a/src/tasks/WasmAppBuilder/coreclr/InterpToNativeGenerator.cs +++ b/src/tasks/WasmAppBuilder/coreclr/InterpToNativeGenerator.cs @@ -117,7 +117,7 @@ private static void Emit(StreamWriter w, IEnumerable cookies) {{(isPortableEntryPointCall ? "NOINLINE " : "")}}static void {{CallFuncName(args, SignatureMapper.TokenToNameType(returnToken), isPortableEntryPointCall)}}(PCODE pcode, int8_t* pArgs, int8_t* pRet{{(isPortableEntryPointCall ? ", PCODE pPortableEntryPointContext" : "")}}) {{{(isPortableEntryPointCall ? "\n alignas(16) int framePointer = TERMINATE_R2R_STACK_WALK;" : "")}} {{result.nativeType}} (*fptr)({{portableEntrypointStackDeclaration}}{{string.Join(", ", args.Select(static t => SignatureMapper.TokenToNativeType(t)))}}{{portableEntrypointDeclaration}}) = ({{result.nativeType}} (*)({{portableEntrypointStackDeclaration}}{{string.Join(", ", args.Select(static t => SignatureMapper.TokenToNativeType(t)))}}{{portableEntrypointDeclaration}}))pcode; - {{portabilityAssert}}{{(result.isVoid ? "" : "*" + "((" + result.nativeType + "*)pRet) = ")}}(*fptr)({{portableEntrypointStackParam}}{{string.Join(", ", args.Select(static (t, i) => $"{SignatureMapper.TokenToArgType(t)}({i})"))}}{{portableEntrypointParam}}); + {{portabilityAssert}}{{(result.isVoid ? "" : "*" + "((" + result.nativeType + "*)pRet) = ")}}(*fptr)({{portableEntrypointStackParam}}{{string.Join(", ", ArgsWithSlotOffsets(args))}}{{portableEntrypointParam}}); } """); @@ -154,6 +154,19 @@ static List Args(List tokens) return tokens.Count > 1 ? tokens.GetRange(1, tokens.Count - 1) : new List(); } + static List ArgsWithSlotOffsets(List args) + { + var result = new List(); + int slot = 0; + foreach (var token in args) + { + result.Add($"{SignatureMapper.TokenToArgType(token)}({slot})"); + slot += SignatureMapper.TokenToSlotCount(token); + } + + return result; + } + static (bool isVoid, string nativeType) Result(string returnToken) => new(returnToken == "v", SignatureMapper.TokenToNativeType(returnToken)); diff --git a/src/tasks/WasmAppBuilder/coreclr/SignatureMapper.cs b/src/tasks/WasmAppBuilder/coreclr/SignatureMapper.cs index cb9315a09ba764..2e3808f9667773 100644 --- a/src/tasks/WasmAppBuilder/coreclr/SignatureMapper.cs +++ b/src/tasks/WasmAppBuilder/coreclr/SignatureMapper.cs @@ -88,7 +88,7 @@ internal static class SignatureMapper Type fieldType = fields[0].FieldType; return TypeToChar(fieldType, log, out isByRefStruct, out structSize, ++depth); } - else if (PInvokeTableGenerator.IsBlittable(t, log)) + else { string fullName = t.FullName ?? t.Name; if (s_knownStructSizes.TryGetValue(fullName, out int size)) @@ -214,7 +214,7 @@ public static List ParseSignatureTokens(string signature) 'l' => "I64", 'f' => "F32", 'd' => "F64", - 'S' => "IND", + 'S' => token, // e.g. "S8", "S64" — encodes size in the name 'p' => "PE", _ => throw new InvalidSignatureCharException(token[0]) }; @@ -229,6 +229,19 @@ public static List ParseSignatureTokens(string signature) _ => throw new InvalidSignatureCharException(token[0]) }; + /// + /// Returns the number of INTERP_STACK_SLOT_SIZE slots consumed by a token. + /// Struct tokens (S<N>) consume max(size / 8, 1) slots; all others consume 1. + /// + public static int TokenToSlotCount(string token) + { + if (token[0] != 'S' || token.Length < 2) + return 1; + + int size = int.Parse(token.Substring(1)); + return Math.Max(size / 8, 1); + } + // Legacy single-char overloads — still used by consumers that don't encounter S tokens. public static string CharToNativeType(char c) => TokenToNativeType(c.ToString()); public static string CharToNameType(char c) => TokenToNameType(c.ToString()); From 3573b5d2beed95c78118634a756f9cdf3c627bc2 Mon Sep 17 00:00:00 2001 From: David Wrighton Date: Mon, 27 Apr 2026 14:17:27 -0700 Subject: [PATCH 18/43] Align signature encoding across crossgen2, SignatureMapper, and runtime - helpers.cpp: Refactor GetSignatureKey to support S struct tokens, LowerTypeHandle for recursive single-field unwrapping, caller prefix parameter ('M' for calli, 'I' for PE-to-interpreter) - helpers.cpp: Use 'T' for this pointer encoding (was 'i') - WasmLowering.cs: Remove redundant hidden retbuf pointer from signature string (implied by S return type) - RaiseSignature: Remove hasReturnBuffer skip logic (no longer in string) - SignatureMapper.cs: Use 'T' for this pointer, add T to token maps - InterpToNativeGenerator.cs: Add 'M' prefix to g_wasmThunks entries - clr-abi.md: Document Type Lowering and Signature String Encoding spec Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- docs/design/coreclr/botr/clr-abi.md | 120 ++++ .../tools/Common/JitInterface/WasmLowering.cs | 11 - .../vm/wasm/callhelpers-interp-to-managed.cpp | 543 +++++++++--------- src/coreclr/vm/wasm/callhelpers-pinvoke.cpp | 4 +- src/coreclr/vm/wasm/helpers.cpp | 241 +++++--- .../coreclr/InterpToNativeGenerator.cs | 2 +- .../WasmAppBuilder/coreclr/SignatureMapper.cs | 5 +- 7 files changed, 574 insertions(+), 352 deletions(-) diff --git a/docs/design/coreclr/botr/clr-abi.md b/docs/design/coreclr/botr/clr-abi.md index 3f1cf8dd3fa090..a2a7754765634c 100644 --- a/docs/design/coreclr/botr/clr-abi.md +++ b/docs/design/coreclr/botr/clr-abi.md @@ -720,6 +720,126 @@ Structs are generally returned via hidden buffers, whose address is supplied by (TBD: ABI for vector types) +### Type Lowering + +Managed types are lowered to WebAssembly value types according to the following rules +(implemented in `WasmLowering.LowerToAbiType` and `WasmLowering.LowerType`): + +| Managed type | Wasm value type | +|---|---| +| `bool`, `char`, `sbyte`, `byte`, `short`, `ushort`, `int`, `uint` | `i32` | +| `long`, `ulong` | `i64` | +| `float` | `f32` | +| `double` | `f64` | +| `nint`, `nuint`, pointer, byref, function pointer | `i32` (pointer-sized) | +| Reference types (class, string, array, szarray, interface) | `i32` (pointer-sized) | +| Value type (struct) — single primitive field, no padding | Unwrap recursively to the field's wasm type | +| Value type (struct) — single field with padding, or multiple fields | Passed by reference (`i32` pointer) | +| Empty struct (zero instance fields) | Elided from the signature entirely | + +**Struct unwrapping** is recursive: a struct containing a single struct field, where the inner struct +has the same size as the outer, is unwrapped until a primitive is reached or the rule no longer applies. +For example, a struct `Wrapper { Inner value; }` where `Inner { int x; }` is unwrapped all the way +to `i32`. + +A struct is **not** unwrapped when: +- It has more than one instance field. +- It has exactly one instance field but the field's size differs from the struct's size (i.e., the + struct has padding due to explicit layout or alignment attributes). + +Structs that cannot be unwrapped are passed by reference. The caller allocates space on the linear +stack and passes a pointer. For return values, the caller provides a hidden return buffer pointer +(see Signature Encoding below). + +### Signature String Encoding + +Every managed method signature is encoded as a compact string that uniquely identifies its +lowered Wasm calling convention. This encoding is shared across three codebases: + +- **crossgen2** (`WasmLowering.GetSignature`): reference implementation, produces the string + during R2R compilation. +- **WasmAppBuilder** (`SignatureMapper`): MSBuild task that generates interpreter-to-native + thunk tables from reflection metadata. +- **CoreCLR runtime** (`helpers.cpp`, `GetSignatureKey`): runtime signature computation for + calli and portable entrypoint thunks. + +The string format is: + +``` + [] [...] ... [p] +``` + +**Return type** (first character): + +| Encoding | Meaning | +|---|---| +| `v` | void return, or empty struct return (no return buffer) | +| `i` | returns `i32` | +| `l` | returns `i64` | +| `f` | returns `f32` | +| `d` | returns `f64` | +| `S` | struct return via hidden buffer, `N` is the struct size in bytes | + +**This pointer** (if the method has a `this` parameter): + +| Encoding | Meaning | +|---|---| +| `T` | `this` pointer (managed instance methods) | + +**Hidden parameters** (inserted between `this` and explicit parameters, in order): + +1. **Generic context** (`i`): present when the method requires an inst method desc or + method table argument. +2. **Async continuation** (`i`): present for async calls. + +Note: the hidden return buffer pointer is **not** encoded in the signature string. Its +presence is implied by the return type being `S` — when the caller sees a struct return, +it knows a hidden retbuf pointer argument is present in the Wasm parameter list. + +**Explicit parameters** (one token per parameter, in declaration order): + +| Encoding | Meaning | +|---|---| +| `i` | `i32` parameter | +| `l` | `i64` parameter | +| `f` | `f32` parameter | +| `d` | `f64` parameter | +| `S` | struct parameter passed by reference, `N` is the struct size in bytes | +| `e` | empty struct parameter — elided from Wasm args but present in the string | + +**Suffix**: + +| Encoding | Meaning | +|---|---| +| `p` | managed call with portable entrypoint (the `&pe` argument is implicit) | +| *(absent)* | unmanaged callers only (reverse P/Invoke) | + +**Prefix** (applied by the caller, not part of the core encoding): + +When storing signature strings in thunk lookup tables, callers prepend a single-character +prefix to distinguish thunk categories: + +| Prefix | Meaning | +|---|---| +| `M` | Calli thunk or interpreter-to-native thunk | +| `I` | Portable entrypoint-to-interpreter thunk | + +**Examples**: + +| Method | Signature string (no prefix) | +|---|---| +| `static void F()` | `vp` | +| `static int F(int x)` | `iip` | +| `void F(int x)` (instance) | `vTip` | +| `static MyStruct F()` where `MyStruct` is 16 bytes | `S16p` | +| `static void F(MyStruct s)` where `MyStruct` is 8 bytes | `vS8p` | +| `static int F(float x, double y)` | `ifdp` | +| `[UnmanagedCallersOnly] static int F(int x)` | `ii` | + +**Slot sizing for structs**: When computing interpreter stack layout, struct parameters +(`S`) consume `max(N / 8, 1)` interpreter stack slots, while all other parameter types +consume exactly 1 slot. + ### Prolog The prolog will decrement the stack pointer by the fixed frame size, home any arguments that are stored on the linear stack, and zero initialize slots on the linear stack as appropriate. It will establish a frame pointer if one is needed. diff --git a/src/coreclr/tools/Common/JitInterface/WasmLowering.cs b/src/coreclr/tools/Common/JitInterface/WasmLowering.cs index 4eec4d019a9ae6..7f630458483acb 100644 --- a/src/coreclr/tools/Common/JitInterface/WasmLowering.cs +++ b/src/coreclr/tools/Common/JitInterface/WasmLowering.cs @@ -173,7 +173,6 @@ public static MethodSignature RaiseSignature(WasmSignature wasmSignature, TypeSy // Parse return type TypeDesc returnType; - bool hasReturnBuffer = false; if (sig[pos] == 'v') { returnType = context.GetWellKnownType(WellKnownType.Void); @@ -184,7 +183,6 @@ public static MethodSignature RaiseSignature(WasmSignature wasmSignature, TypeSy int structSize = ParseStructSize(sig, ref pos); returnType = ((CompilerTypeSystemContext)context).GetCachedStructOfSize(structSize); Debug.Assert(returnType is not null, $"No cached struct of size {structSize} for return type in signature '{sig}'"); - hasReturnBuffer = true; } else { @@ -205,13 +203,6 @@ public static MethodSignature RaiseSignature(WasmSignature wasmSignature, TypeSy hasThis = true; pos++; } - else if (hasReturnBuffer) - { - // The hidden retbuf pointer follows 'T' (or comes first if no 'T'). - // Skip it — GetSignature will re-add it based on the struct return type. - hasReturnBuffer = false; - pos++; - } else if (c == 'e') { // Empty struct — include the cached empty struct type for roundtrip fidelity @@ -357,7 +348,6 @@ public static WasmSignature GetSignature(MethodSignature signature, LoweringFlag if (hasReturnBuffer) { result.Add(pointerType); - sigBuilder.Append(hiddenParamChar); } } else // managed call @@ -373,7 +363,6 @@ public static WasmSignature GetSignature(MethodSignature signature, LoweringFlag if (hasReturnBuffer) { result.Add(pointerType); - sigBuilder.Append(hiddenParamChar); } } diff --git a/src/coreclr/vm/wasm/callhelpers-interp-to-managed.cpp b/src/coreclr/vm/wasm/callhelpers-interp-to-managed.cpp index c0265475c3447d..1dc9b441db12be 100644 --- a/src/coreclr/vm/wasm/callhelpers-interp-to-managed.cpp +++ b/src/coreclr/vm/wasm/callhelpers-interp-to-managed.cpp @@ -88,24 +88,132 @@ namespace *((int32_t*)pRet) = (*fptr)(); } + static void CallFunc_S64_RetI32(PCODE pcode, int8_t* pArgs, int8_t* pRet) + { + int32_t (*fptr)(int32_t) = (int32_t (*)(int32_t))pcode; + *((int32_t*)pRet) = (*fptr)(ARG_IND(0)); + } + + static void CallFunc_S8_RetI32(PCODE pcode, int8_t* pArgs, int8_t* pRet) + { + int32_t (*fptr)(int32_t) = (int32_t (*)(int32_t))pcode; + *((int32_t*)pRet) = (*fptr)(ARG_IND(0)); + } + + static void CallFunc_S8_S8_S8_RetI32(PCODE pcode, int8_t* pArgs, int8_t* pRet) + { + int32_t (*fptr)(int32_t, int32_t, int32_t) = (int32_t (*)(int32_t, int32_t, int32_t))pcode; + *((int32_t*)pRet) = (*fptr)(ARG_IND(0), ARG_IND(1), ARG_IND(2)); + } + + static void CallFunc_S8_S8_I32_RetI32(PCODE pcode, int8_t* pArgs, int8_t* pRet) + { + int32_t (*fptr)(int32_t, int32_t, int32_t) = (int32_t (*)(int32_t, int32_t, int32_t))pcode; + *((int32_t*)pRet) = (*fptr)(ARG_IND(0), ARG_IND(1), ARG_I32(2)); + } + + static void CallFunc_S8_S8_I32_S8_RetI32(PCODE pcode, int8_t* pArgs, int8_t* pRet) + { + int32_t (*fptr)(int32_t, int32_t, int32_t, int32_t) = (int32_t (*)(int32_t, int32_t, int32_t, int32_t))pcode; + *((int32_t*)pRet) = (*fptr)(ARG_IND(0), ARG_IND(1), ARG_I32(2), ARG_IND(3)); + } + + static void CallFunc_S8_S8_I32_I32_RetI32(PCODE pcode, int8_t* pArgs, int8_t* pRet) + { + int32_t (*fptr)(int32_t, int32_t, int32_t, int32_t) = (int32_t (*)(int32_t, int32_t, int32_t, int32_t))pcode; + *((int32_t*)pRet) = (*fptr)(ARG_IND(0), ARG_IND(1), ARG_I32(2), ARG_I32(3)); + } + + static void CallFunc_S8_I32_RetI32(PCODE pcode, int8_t* pArgs, int8_t* pRet) + { + int32_t (*fptr)(int32_t, int32_t) = (int32_t (*)(int32_t, int32_t))pcode; + *((int32_t*)pRet) = (*fptr)(ARG_IND(0), ARG_I32(1)); + } + + static void CallFunc_S8_I32_S8_S8_RetI32(PCODE pcode, int8_t* pArgs, int8_t* pRet) + { + int32_t (*fptr)(int32_t, int32_t, int32_t, int32_t) = (int32_t (*)(int32_t, int32_t, int32_t, int32_t))pcode; + *((int32_t*)pRet) = (*fptr)(ARG_IND(0), ARG_I32(1), ARG_IND(2), ARG_IND(3)); + } + + static void CallFunc_S8_I32_S8_I32_RetI32(PCODE pcode, int8_t* pArgs, int8_t* pRet) + { + int32_t (*fptr)(int32_t, int32_t, int32_t, int32_t) = (int32_t (*)(int32_t, int32_t, int32_t, int32_t))pcode; + *((int32_t*)pRet) = (*fptr)(ARG_IND(0), ARG_I32(1), ARG_IND(2), ARG_I32(3)); + } + + static void CallFunc_S8_I32_I32_RetI32(PCODE pcode, int8_t* pArgs, int8_t* pRet) + { + int32_t (*fptr)(int32_t, int32_t, int32_t) = (int32_t (*)(int32_t, int32_t, int32_t))pcode; + *((int32_t*)pRet) = (*fptr)(ARG_IND(0), ARG_I32(1), ARG_I32(2)); + } + + static void CallFunc_S8_I32_I32_I32_RetI32(PCODE pcode, int8_t* pArgs, int8_t* pRet) + { + int32_t (*fptr)(int32_t, int32_t, int32_t, int32_t) = (int32_t (*)(int32_t, int32_t, int32_t, int32_t))pcode; + *((int32_t*)pRet) = (*fptr)(ARG_IND(0), ARG_I32(1), ARG_I32(2), ARG_I32(3)); + } + + static void CallFunc_S8_I32_I32_I32_I32_RetI32(PCODE pcode, int8_t* pArgs, int8_t* pRet) + { + int32_t (*fptr)(int32_t, int32_t, int32_t, int32_t, int32_t) = (int32_t (*)(int32_t, int32_t, int32_t, int32_t, int32_t))pcode; + *((int32_t*)pRet) = (*fptr)(ARG_IND(0), ARG_I32(1), ARG_I32(2), ARG_I32(3), ARG_I32(4)); + } + + static void CallFunc_S8_I32_I32_I32_I32_I32_RetI32(PCODE pcode, int8_t* pArgs, int8_t* pRet) + { + int32_t (*fptr)(int32_t, int32_t, int32_t, int32_t, int32_t, int32_t) = (int32_t (*)(int32_t, int32_t, int32_t, int32_t, int32_t, int32_t))pcode; + *((int32_t*)pRet) = (*fptr)(ARG_IND(0), ARG_I32(1), ARG_I32(2), ARG_I32(3), ARG_I32(4), ARG_I32(5)); + } + static void CallFunc_I32_RetI32(PCODE pcode, int8_t* pArgs, int8_t* pRet) { int32_t (*fptr)(int32_t) = (int32_t (*)(int32_t))pcode; *((int32_t*)pRet) = (*fptr)(ARG_I32(0)); } + static void CallFunc_I32_S8_I32_RetI32(PCODE pcode, int8_t* pArgs, int8_t* pRet) + { + int32_t (*fptr)(int32_t, int32_t, int32_t) = (int32_t (*)(int32_t, int32_t, int32_t))pcode; + *((int32_t*)pRet) = (*fptr)(ARG_I32(0), ARG_IND(1), ARG_I32(2)); + } + + static void CallFunc_I32_S8_I32_I32_I32_RetI32(PCODE pcode, int8_t* pArgs, int8_t* pRet) + { + int32_t (*fptr)(int32_t, int32_t, int32_t, int32_t, int32_t) = (int32_t (*)(int32_t, int32_t, int32_t, int32_t, int32_t))pcode; + *((int32_t*)pRet) = (*fptr)(ARG_I32(0), ARG_IND(1), ARG_I32(2), ARG_I32(3), ARG_I32(4)); + } + static void CallFunc_I32_I32_RetI32(PCODE pcode, int8_t* pArgs, int8_t* pRet) { int32_t (*fptr)(int32_t, int32_t) = (int32_t (*)(int32_t, int32_t))pcode; *((int32_t*)pRet) = (*fptr)(ARG_I32(0), ARG_I32(1)); } + static void CallFunc_I32_I32_S8_I32_I32_RetI32(PCODE pcode, int8_t* pArgs, int8_t* pRet) + { + int32_t (*fptr)(int32_t, int32_t, int32_t, int32_t, int32_t) = (int32_t (*)(int32_t, int32_t, int32_t, int32_t, int32_t))pcode; + *((int32_t*)pRet) = (*fptr)(ARG_I32(0), ARG_I32(1), ARG_IND(2), ARG_I32(3), ARG_I32(4)); + } + + static void CallFunc_I32_I32_S8_I32_I32_S8_RetI32(PCODE pcode, int8_t* pArgs, int8_t* pRet) + { + int32_t (*fptr)(int32_t, int32_t, int32_t, int32_t, int32_t, int32_t) = (int32_t (*)(int32_t, int32_t, int32_t, int32_t, int32_t, int32_t))pcode; + *((int32_t*)pRet) = (*fptr)(ARG_I32(0), ARG_I32(1), ARG_IND(2), ARG_I32(3), ARG_I32(4), ARG_IND(5)); + } + static void CallFunc_I32_I32_I32_RetI32(PCODE pcode, int8_t* pArgs, int8_t* pRet) { int32_t (*fptr)(int32_t, int32_t, int32_t) = (int32_t (*)(int32_t, int32_t, int32_t))pcode; *((int32_t*)pRet) = (*fptr)(ARG_I32(0), ARG_I32(1), ARG_I32(2)); } + static void CallFunc_I32_I32_I32_S8_I32_RetI32(PCODE pcode, int8_t* pArgs, int8_t* pRet) + { + int32_t (*fptr)(int32_t, int32_t, int32_t, int32_t, int32_t) = (int32_t (*)(int32_t, int32_t, int32_t, int32_t, int32_t))pcode; + *((int32_t*)pRet) = (*fptr)(ARG_I32(0), ARG_I32(1), ARG_I32(2), ARG_IND(3), ARG_I32(4)); + } + static void CallFunc_I32_I32_I32_I32_RetI32(PCODE pcode, int8_t* pArgs, int8_t* pRet) { int32_t (*fptr)(int32_t, int32_t, int32_t, int32_t) = (int32_t (*)(int32_t, int32_t, int32_t, int32_t))pcode; @@ -163,12 +271,6 @@ namespace *((int32_t*)pRet) = (*fptr)(ARG_I32(0), ARG_I32(1), ARG_I32(2), ARG_I64(3)); } - static void CallFunc_I32_I32_I32_IND_I32_RetI32(PCODE pcode, int8_t* pArgs, int8_t* pRet) - { - int32_t (*fptr)(int32_t, int32_t, int32_t, int32_t, int32_t) = (int32_t (*)(int32_t, int32_t, int32_t, int32_t, int32_t))pcode; - *((int32_t*)pRet) = (*fptr)(ARG_I32(0), ARG_I32(1), ARG_I32(2), ARG_IND(3), ARG_I32(4)); - } - NOINLINE static void CallFunc_I32_I32_I32_RetI32_PE(PCODE pcode, int8_t* pArgs, int8_t* pRet, PCODE pPortableEntryPointContext) { alignas(16) int framePointer = TERMINATE_R2R_STACK_WALK; @@ -182,18 +284,6 @@ namespace *((int32_t*)pRet) = (*fptr)(ARG_I32(0), ARG_I32(1), ARG_I64(2)); } - static void CallFunc_I32_I32_IND_I32_I32_RetI32(PCODE pcode, int8_t* pArgs, int8_t* pRet) - { - int32_t (*fptr)(int32_t, int32_t, int32_t, int32_t, int32_t) = (int32_t (*)(int32_t, int32_t, int32_t, int32_t, int32_t))pcode; - *((int32_t*)pRet) = (*fptr)(ARG_I32(0), ARG_I32(1), ARG_IND(2), ARG_I32(3), ARG_I32(4)); - } - - static void CallFunc_I32_I32_IND_I32_I32_IND_RetI32(PCODE pcode, int8_t* pArgs, int8_t* pRet) - { - int32_t (*fptr)(int32_t, int32_t, int32_t, int32_t, int32_t, int32_t) = (int32_t (*)(int32_t, int32_t, int32_t, int32_t, int32_t, int32_t))pcode; - *((int32_t*)pRet) = (*fptr)(ARG_I32(0), ARG_I32(1), ARG_IND(2), ARG_I32(3), ARG_I32(4), ARG_IND(5)); - } - NOINLINE static void CallFunc_I32_I32_RetI32_PE(PCODE pcode, int8_t* pArgs, int8_t* pRet, PCODE pPortableEntryPointContext) { alignas(16) int framePointer = TERMINATE_R2R_STACK_WALK; @@ -231,18 +321,6 @@ namespace *((int32_t*)pRet) = (*fptr)(ARG_I32(0), ARG_I64(1), ARG_I64(2), ARG_I32(3)); } - static void CallFunc_I32_IND_I32_RetI32(PCODE pcode, int8_t* pArgs, int8_t* pRet) - { - int32_t (*fptr)(int32_t, int32_t, int32_t) = (int32_t (*)(int32_t, int32_t, int32_t))pcode; - *((int32_t*)pRet) = (*fptr)(ARG_I32(0), ARG_IND(1), ARG_I32(2)); - } - - static void CallFunc_I32_IND_I32_I32_I32_RetI32(PCODE pcode, int8_t* pArgs, int8_t* pRet) - { - int32_t (*fptr)(int32_t, int32_t, int32_t, int32_t, int32_t) = (int32_t (*)(int32_t, int32_t, int32_t, int32_t, int32_t))pcode; - *((int32_t*)pRet) = (*fptr)(ARG_I32(0), ARG_IND(1), ARG_I32(2), ARG_I32(3), ARG_I32(4)); - } - NOINLINE static void CallFunc_I32_RetI32_PE(PCODE pcode, int8_t* pArgs, int8_t* pRet, PCODE pPortableEntryPointContext) { alignas(16) int framePointer = TERMINATE_R2R_STACK_WALK; @@ -256,78 +334,6 @@ namespace *((int32_t*)pRet) = (*fptr)(ARG_I64(0), ARG_I32(1), ARG_I64(2), ARG_I32(3)); } - static void CallFunc_IND_RetI32(PCODE pcode, int8_t* pArgs, int8_t* pRet) - { - int32_t (*fptr)(int32_t) = (int32_t (*)(int32_t))pcode; - *((int32_t*)pRet) = (*fptr)(ARG_IND(0)); - } - - static void CallFunc_IND_I32_RetI32(PCODE pcode, int8_t* pArgs, int8_t* pRet) - { - int32_t (*fptr)(int32_t, int32_t) = (int32_t (*)(int32_t, int32_t))pcode; - *((int32_t*)pRet) = (*fptr)(ARG_IND(0), ARG_I32(1)); - } - - static void CallFunc_IND_I32_I32_RetI32(PCODE pcode, int8_t* pArgs, int8_t* pRet) - { - int32_t (*fptr)(int32_t, int32_t, int32_t) = (int32_t (*)(int32_t, int32_t, int32_t))pcode; - *((int32_t*)pRet) = (*fptr)(ARG_IND(0), ARG_I32(1), ARG_I32(2)); - } - - static void CallFunc_IND_I32_I32_I32_RetI32(PCODE pcode, int8_t* pArgs, int8_t* pRet) - { - int32_t (*fptr)(int32_t, int32_t, int32_t, int32_t) = (int32_t (*)(int32_t, int32_t, int32_t, int32_t))pcode; - *((int32_t*)pRet) = (*fptr)(ARG_IND(0), ARG_I32(1), ARG_I32(2), ARG_I32(3)); - } - - static void CallFunc_IND_I32_I32_I32_I32_RetI32(PCODE pcode, int8_t* pArgs, int8_t* pRet) - { - int32_t (*fptr)(int32_t, int32_t, int32_t, int32_t, int32_t) = (int32_t (*)(int32_t, int32_t, int32_t, int32_t, int32_t))pcode; - *((int32_t*)pRet) = (*fptr)(ARG_IND(0), ARG_I32(1), ARG_I32(2), ARG_I32(3), ARG_I32(4)); - } - - static void CallFunc_IND_I32_I32_I32_I32_I32_RetI32(PCODE pcode, int8_t* pArgs, int8_t* pRet) - { - int32_t (*fptr)(int32_t, int32_t, int32_t, int32_t, int32_t, int32_t) = (int32_t (*)(int32_t, int32_t, int32_t, int32_t, int32_t, int32_t))pcode; - *((int32_t*)pRet) = (*fptr)(ARG_IND(0), ARG_I32(1), ARG_I32(2), ARG_I32(3), ARG_I32(4), ARG_I32(5)); - } - - static void CallFunc_IND_I32_IND_I32_RetI32(PCODE pcode, int8_t* pArgs, int8_t* pRet) - { - int32_t (*fptr)(int32_t, int32_t, int32_t, int32_t) = (int32_t (*)(int32_t, int32_t, int32_t, int32_t))pcode; - *((int32_t*)pRet) = (*fptr)(ARG_IND(0), ARG_I32(1), ARG_IND(2), ARG_I32(3)); - } - - static void CallFunc_IND_I32_IND_IND_RetI32(PCODE pcode, int8_t* pArgs, int8_t* pRet) - { - int32_t (*fptr)(int32_t, int32_t, int32_t, int32_t) = (int32_t (*)(int32_t, int32_t, int32_t, int32_t))pcode; - *((int32_t*)pRet) = (*fptr)(ARG_IND(0), ARG_I32(1), ARG_IND(2), ARG_IND(3)); - } - - static void CallFunc_IND_IND_I32_RetI32(PCODE pcode, int8_t* pArgs, int8_t* pRet) - { - int32_t (*fptr)(int32_t, int32_t, int32_t) = (int32_t (*)(int32_t, int32_t, int32_t))pcode; - *((int32_t*)pRet) = (*fptr)(ARG_IND(0), ARG_IND(1), ARG_I32(2)); - } - - static void CallFunc_IND_IND_I32_I32_RetI32(PCODE pcode, int8_t* pArgs, int8_t* pRet) - { - int32_t (*fptr)(int32_t, int32_t, int32_t, int32_t) = (int32_t (*)(int32_t, int32_t, int32_t, int32_t))pcode; - *((int32_t*)pRet) = (*fptr)(ARG_IND(0), ARG_IND(1), ARG_I32(2), ARG_I32(3)); - } - - static void CallFunc_IND_IND_I32_IND_RetI32(PCODE pcode, int8_t* pArgs, int8_t* pRet) - { - int32_t (*fptr)(int32_t, int32_t, int32_t, int32_t) = (int32_t (*)(int32_t, int32_t, int32_t, int32_t))pcode; - *((int32_t*)pRet) = (*fptr)(ARG_IND(0), ARG_IND(1), ARG_I32(2), ARG_IND(3)); - } - - static void CallFunc_IND_IND_IND_RetI32(PCODE pcode, int8_t* pArgs, int8_t* pRet) - { - int32_t (*fptr)(int32_t, int32_t, int32_t) = (int32_t (*)(int32_t, int32_t, int32_t))pcode; - *((int32_t*)pRet) = (*fptr)(ARG_IND(0), ARG_IND(1), ARG_IND(2)); - } - NOINLINE static void CallFunc_Void_RetI32_PE(PCODE pcode, int8_t* pArgs, int8_t* pRet, PCODE pPortableEntryPointContext) { alignas(16) int framePointer = TERMINATE_R2R_STACK_WALK; @@ -406,171 +412,171 @@ namespace (*fptr)(); } - NOINLINE static void CallFunc_F64_I32_I32_RetVoid_PE(PCODE pcode, int8_t* pArgs, int8_t* pRet, PCODE pPortableEntryPointContext) + static void CallFunc_S8_RetVoid(PCODE pcode, int8_t* pArgs, int8_t* pRet) { - alignas(16) int framePointer = TERMINATE_R2R_STACK_WALK; - void (*fptr)(int*, double, int32_t, int32_t, PCODE) = (void (*)(int*, double, int32_t, int32_t, PCODE))pcode; - (*fptr)(&framePointer, ARG_F64(0), ARG_I32(1), ARG_I32(2), pPortableEntryPointContext); + void (*fptr)(int32_t) = (void (*)(int32_t))pcode; + (*fptr)(ARG_IND(0)); } - NOINLINE static void CallFunc_F32_I32_I32_RetVoid_PE(PCODE pcode, int8_t* pArgs, int8_t* pRet, PCODE pPortableEntryPointContext) + static void CallFunc_S8_S8_RetVoid(PCODE pcode, int8_t* pArgs, int8_t* pRet) { - alignas(16) int framePointer = TERMINATE_R2R_STACK_WALK; - void (*fptr)(int*, float, int32_t, int32_t, PCODE) = (void (*)(int*, float, int32_t, int32_t, PCODE))pcode; - (*fptr)(&framePointer, ARG_F32(0), ARG_I32(1), ARG_I32(2), pPortableEntryPointContext); + void (*fptr)(int32_t, int32_t) = (void (*)(int32_t, int32_t))pcode; + (*fptr)(ARG_IND(0), ARG_IND(1)); } - static void CallFunc_I32_RetVoid(PCODE pcode, int8_t* pArgs, int8_t* pRet) + static void CallFunc_S8_S8_I32_I32_I32_I32_I32_RetVoid(PCODE pcode, int8_t* pArgs, int8_t* pRet) { - void (*fptr)(int32_t) = (void (*)(int32_t))pcode; - (*fptr)(ARG_I32(0)); + void (*fptr)(int32_t, int32_t, int32_t, int32_t, int32_t, int32_t, int32_t) = (void (*)(int32_t, int32_t, int32_t, int32_t, int32_t, int32_t, int32_t))pcode; + (*fptr)(ARG_IND(0), ARG_IND(1), ARG_I32(2), ARG_I32(3), ARG_I32(4), ARG_I32(5), ARG_I32(6)); } - static void CallFunc_I32_I32_RetVoid(PCODE pcode, int8_t* pArgs, int8_t* pRet) + static void CallFunc_S8_I32_RetVoid(PCODE pcode, int8_t* pArgs, int8_t* pRet) { void (*fptr)(int32_t, int32_t) = (void (*)(int32_t, int32_t))pcode; - (*fptr)(ARG_I32(0), ARG_I32(1)); + (*fptr)(ARG_IND(0), ARG_I32(1)); } - static void CallFunc_I32_I32_I32_RetVoid(PCODE pcode, int8_t* pArgs, int8_t* pRet) + static void CallFunc_S8_I32_I32_RetVoid(PCODE pcode, int8_t* pArgs, int8_t* pRet) { void (*fptr)(int32_t, int32_t, int32_t) = (void (*)(int32_t, int32_t, int32_t))pcode; - (*fptr)(ARG_I32(0), ARG_I32(1), ARG_I32(2)); + (*fptr)(ARG_IND(0), ARG_I32(1), ARG_I32(2)); } - static void CallFunc_I32_I32_I32_I32_RetVoid(PCODE pcode, int8_t* pArgs, int8_t* pRet) + static void CallFunc_S8_I32_I32_I32_RetVoid(PCODE pcode, int8_t* pArgs, int8_t* pRet) { void (*fptr)(int32_t, int32_t, int32_t, int32_t) = (void (*)(int32_t, int32_t, int32_t, int32_t))pcode; - (*fptr)(ARG_I32(0), ARG_I32(1), ARG_I32(2), ARG_I32(3)); + (*fptr)(ARG_IND(0), ARG_I32(1), ARG_I32(2), ARG_I32(3)); } - static void CallFunc_I32_I32_I32_I32_I32_RetVoid(PCODE pcode, int8_t* pArgs, int8_t* pRet) + static void CallFunc_S8_I32_I32_I32_I32_RetVoid(PCODE pcode, int8_t* pArgs, int8_t* pRet) { void (*fptr)(int32_t, int32_t, int32_t, int32_t, int32_t) = (void (*)(int32_t, int32_t, int32_t, int32_t, int32_t))pcode; - (*fptr)(ARG_I32(0), ARG_I32(1), ARG_I32(2), ARG_I32(3), ARG_I32(4)); + (*fptr)(ARG_IND(0), ARG_I32(1), ARG_I32(2), ARG_I32(3), ARG_I32(4)); } - static void CallFunc_I32_I32_I32_I32_I32_I32_RetVoid(PCODE pcode, int8_t* pArgs, int8_t* pRet) + static void CallFunc_S8_I32_I32_I32_I32_I32_RetVoid(PCODE pcode, int8_t* pArgs, int8_t* pRet) { void (*fptr)(int32_t, int32_t, int32_t, int32_t, int32_t, int32_t) = (void (*)(int32_t, int32_t, int32_t, int32_t, int32_t, int32_t))pcode; - (*fptr)(ARG_I32(0), ARG_I32(1), ARG_I32(2), ARG_I32(3), ARG_I32(4), ARG_I32(5)); + (*fptr)(ARG_IND(0), ARG_I32(1), ARG_I32(2), ARG_I32(3), ARG_I32(4), ARG_I32(5)); } - static void CallFunc_I32_I32_I32_IND_IND_RetVoid(PCODE pcode, int8_t* pArgs, int8_t* pRet) + static void CallFunc_S8_I32_I32_I32_I32_I32_I32_RetVoid(PCODE pcode, int8_t* pArgs, int8_t* pRet) { - void (*fptr)(int32_t, int32_t, int32_t, int32_t, int32_t) = (void (*)(int32_t, int32_t, int32_t, int32_t, int32_t))pcode; - (*fptr)(ARG_I32(0), ARG_I32(1), ARG_I32(2), ARG_IND(3), ARG_IND(4)); + void (*fptr)(int32_t, int32_t, int32_t, int32_t, int32_t, int32_t, int32_t) = (void (*)(int32_t, int32_t, int32_t, int32_t, int32_t, int32_t, int32_t))pcode; + (*fptr)(ARG_IND(0), ARG_I32(1), ARG_I32(2), ARG_I32(3), ARG_I32(4), ARG_I32(5), ARG_I32(6)); } - static void CallFunc_I32_I32_I32_IND_IND_I32_RetVoid(PCODE pcode, int8_t* pArgs, int8_t* pRet) + static void CallFunc_S8_I32_I32_I32_I32_I32_I32_I32_I32_I32_I32_I32_RetVoid(PCODE pcode, int8_t* pArgs, int8_t* pRet) { - void (*fptr)(int32_t, int32_t, int32_t, int32_t, int32_t, int32_t) = (void (*)(int32_t, int32_t, int32_t, int32_t, int32_t, int32_t))pcode; - (*fptr)(ARG_I32(0), ARG_I32(1), ARG_I32(2), ARG_IND(3), ARG_IND(4), ARG_I32(5)); + void (*fptr)(int32_t, int32_t, int32_t, int32_t, int32_t, int32_t, int32_t, int32_t, int32_t, int32_t, int32_t, int32_t) = (void (*)(int32_t, int32_t, int32_t, int32_t, int32_t, int32_t, int32_t, int32_t, int32_t, int32_t, int32_t, int32_t))pcode; + (*fptr)(ARG_IND(0), ARG_I32(1), ARG_I32(2), ARG_I32(3), ARG_I32(4), ARG_I32(5), ARG_I32(6), ARG_I32(7), ARG_I32(8), ARG_I32(9), ARG_I32(10), ARG_I32(11)); } - NOINLINE static void CallFunc_I32_I32_I32_RetVoid_PE(PCODE pcode, int8_t* pArgs, int8_t* pRet, PCODE pPortableEntryPointContext) + NOINLINE static void CallFunc_F64_I32_I32_RetVoid_PE(PCODE pcode, int8_t* pArgs, int8_t* pRet, PCODE pPortableEntryPointContext) { alignas(16) int framePointer = TERMINATE_R2R_STACK_WALK; - void (*fptr)(int*, int32_t, int32_t, int32_t, PCODE) = (void (*)(int*, int32_t, int32_t, int32_t, PCODE))pcode; - (*fptr)(&framePointer, ARG_I32(0), ARG_I32(1), ARG_I32(2), pPortableEntryPointContext); + void (*fptr)(int*, double, int32_t, int32_t, PCODE) = (void (*)(int*, double, int32_t, int32_t, PCODE))pcode; + (*fptr)(&framePointer, ARG_F64(0), ARG_I32(1), ARG_I32(2), pPortableEntryPointContext); } - static void CallFunc_I32_I32_IND_IND_I32_RetVoid(PCODE pcode, int8_t* pArgs, int8_t* pRet) + NOINLINE static void CallFunc_F32_I32_I32_RetVoid_PE(PCODE pcode, int8_t* pArgs, int8_t* pRet, PCODE pPortableEntryPointContext) { - void (*fptr)(int32_t, int32_t, int32_t, int32_t, int32_t) = (void (*)(int32_t, int32_t, int32_t, int32_t, int32_t))pcode; - (*fptr)(ARG_I32(0), ARG_I32(1), ARG_IND(2), ARG_IND(3), ARG_I32(4)); + alignas(16) int framePointer = TERMINATE_R2R_STACK_WALK; + void (*fptr)(int*, float, int32_t, int32_t, PCODE) = (void (*)(int*, float, int32_t, int32_t, PCODE))pcode; + (*fptr)(&framePointer, ARG_F32(0), ARG_I32(1), ARG_I32(2), pPortableEntryPointContext); } - static void CallFunc_I32_I32_IND_IND_I32_I32_RetVoid(PCODE pcode, int8_t* pArgs, int8_t* pRet) + static void CallFunc_I32_RetVoid(PCODE pcode, int8_t* pArgs, int8_t* pRet) { - void (*fptr)(int32_t, int32_t, int32_t, int32_t, int32_t, int32_t) = (void (*)(int32_t, int32_t, int32_t, int32_t, int32_t, int32_t))pcode; - (*fptr)(ARG_I32(0), ARG_I32(1), ARG_IND(2), ARG_IND(3), ARG_I32(4), ARG_I32(5)); + void (*fptr)(int32_t) = (void (*)(int32_t))pcode; + (*fptr)(ARG_I32(0)); } - NOINLINE static void CallFunc_I32_I32_RetVoid_PE(PCODE pcode, int8_t* pArgs, int8_t* pRet, PCODE pPortableEntryPointContext) + static void CallFunc_I32_S8_I32_RetVoid(PCODE pcode, int8_t* pArgs, int8_t* pRet) { - alignas(16) int framePointer = TERMINATE_R2R_STACK_WALK; - void (*fptr)(int*, int32_t, int32_t, PCODE) = (void (*)(int*, int32_t, int32_t, PCODE))pcode; - (*fptr)(&framePointer, ARG_I32(0), ARG_I32(1), pPortableEntryPointContext); + void (*fptr)(int32_t, int32_t, int32_t) = (void (*)(int32_t, int32_t, int32_t))pcode; + (*fptr)(ARG_I32(0), ARG_IND(1), ARG_I32(2)); } - static void CallFunc_I32_IND_I32_RetVoid(PCODE pcode, int8_t* pArgs, int8_t* pRet) + static void CallFunc_I32_I32_RetVoid(PCODE pcode, int8_t* pArgs, int8_t* pRet) { - void (*fptr)(int32_t, int32_t, int32_t) = (void (*)(int32_t, int32_t, int32_t))pcode; - (*fptr)(ARG_I32(0), ARG_IND(1), ARG_I32(2)); + void (*fptr)(int32_t, int32_t) = (void (*)(int32_t, int32_t))pcode; + (*fptr)(ARG_I32(0), ARG_I32(1)); } - NOINLINE static void CallFunc_I32_RetVoid_PE(PCODE pcode, int8_t* pArgs, int8_t* pRet, PCODE pPortableEntryPointContext) + static void CallFunc_I32_I32_S8_S8_I32_RetVoid(PCODE pcode, int8_t* pArgs, int8_t* pRet) { - alignas(16) int framePointer = TERMINATE_R2R_STACK_WALK; - void (*fptr)(int*, int32_t, PCODE) = (void (*)(int*, int32_t, PCODE))pcode; - (*fptr)(&framePointer, ARG_I32(0), pPortableEntryPointContext); + void (*fptr)(int32_t, int32_t, int32_t, int32_t, int32_t) = (void (*)(int32_t, int32_t, int32_t, int32_t, int32_t))pcode; + (*fptr)(ARG_I32(0), ARG_I32(1), ARG_IND(2), ARG_IND(3), ARG_I32(4)); } - static void CallFunc_I64_RetVoid(PCODE pcode, int8_t* pArgs, int8_t* pRet) + static void CallFunc_I32_I32_S8_S8_I32_I32_RetVoid(PCODE pcode, int8_t* pArgs, int8_t* pRet) { - void (*fptr)(int64_t) = (void (*)(int64_t))pcode; - (*fptr)(ARG_I64(0)); + void (*fptr)(int32_t, int32_t, int32_t, int32_t, int32_t, int32_t) = (void (*)(int32_t, int32_t, int32_t, int32_t, int32_t, int32_t))pcode; + (*fptr)(ARG_I32(0), ARG_I32(1), ARG_IND(2), ARG_IND(3), ARG_I32(4), ARG_I32(5)); } - static void CallFunc_IND_RetVoid(PCODE pcode, int8_t* pArgs, int8_t* pRet) + static void CallFunc_I32_I32_I32_RetVoid(PCODE pcode, int8_t* pArgs, int8_t* pRet) { - void (*fptr)(int32_t) = (void (*)(int32_t))pcode; - (*fptr)(ARG_IND(0)); + void (*fptr)(int32_t, int32_t, int32_t) = (void (*)(int32_t, int32_t, int32_t))pcode; + (*fptr)(ARG_I32(0), ARG_I32(1), ARG_I32(2)); } - static void CallFunc_IND_I32_RetVoid(PCODE pcode, int8_t* pArgs, int8_t* pRet) + static void CallFunc_I32_I32_I32_S8_S8_RetVoid(PCODE pcode, int8_t* pArgs, int8_t* pRet) { - void (*fptr)(int32_t, int32_t) = (void (*)(int32_t, int32_t))pcode; - (*fptr)(ARG_IND(0), ARG_I32(1)); + void (*fptr)(int32_t, int32_t, int32_t, int32_t, int32_t) = (void (*)(int32_t, int32_t, int32_t, int32_t, int32_t))pcode; + (*fptr)(ARG_I32(0), ARG_I32(1), ARG_I32(2), ARG_IND(3), ARG_IND(4)); } - static void CallFunc_IND_I32_I32_RetVoid(PCODE pcode, int8_t* pArgs, int8_t* pRet) + static void CallFunc_I32_I32_I32_S8_S8_I32_RetVoid(PCODE pcode, int8_t* pArgs, int8_t* pRet) { - void (*fptr)(int32_t, int32_t, int32_t) = (void (*)(int32_t, int32_t, int32_t))pcode; - (*fptr)(ARG_IND(0), ARG_I32(1), ARG_I32(2)); + void (*fptr)(int32_t, int32_t, int32_t, int32_t, int32_t, int32_t) = (void (*)(int32_t, int32_t, int32_t, int32_t, int32_t, int32_t))pcode; + (*fptr)(ARG_I32(0), ARG_I32(1), ARG_I32(2), ARG_IND(3), ARG_IND(4), ARG_I32(5)); } - static void CallFunc_IND_I32_I32_I32_RetVoid(PCODE pcode, int8_t* pArgs, int8_t* pRet) + static void CallFunc_I32_I32_I32_I32_RetVoid(PCODE pcode, int8_t* pArgs, int8_t* pRet) { void (*fptr)(int32_t, int32_t, int32_t, int32_t) = (void (*)(int32_t, int32_t, int32_t, int32_t))pcode; - (*fptr)(ARG_IND(0), ARG_I32(1), ARG_I32(2), ARG_I32(3)); + (*fptr)(ARG_I32(0), ARG_I32(1), ARG_I32(2), ARG_I32(3)); } - static void CallFunc_IND_I32_I32_I32_I32_RetVoid(PCODE pcode, int8_t* pArgs, int8_t* pRet) + static void CallFunc_I32_I32_I32_I32_I32_RetVoid(PCODE pcode, int8_t* pArgs, int8_t* pRet) { void (*fptr)(int32_t, int32_t, int32_t, int32_t, int32_t) = (void (*)(int32_t, int32_t, int32_t, int32_t, int32_t))pcode; - (*fptr)(ARG_IND(0), ARG_I32(1), ARG_I32(2), ARG_I32(3), ARG_I32(4)); + (*fptr)(ARG_I32(0), ARG_I32(1), ARG_I32(2), ARG_I32(3), ARG_I32(4)); } - static void CallFunc_IND_I32_I32_I32_I32_I32_RetVoid(PCODE pcode, int8_t* pArgs, int8_t* pRet) + static void CallFunc_I32_I32_I32_I32_I32_I32_RetVoid(PCODE pcode, int8_t* pArgs, int8_t* pRet) { void (*fptr)(int32_t, int32_t, int32_t, int32_t, int32_t, int32_t) = (void (*)(int32_t, int32_t, int32_t, int32_t, int32_t, int32_t))pcode; - (*fptr)(ARG_IND(0), ARG_I32(1), ARG_I32(2), ARG_I32(3), ARG_I32(4), ARG_I32(5)); + (*fptr)(ARG_I32(0), ARG_I32(1), ARG_I32(2), ARG_I32(3), ARG_I32(4), ARG_I32(5)); } - static void CallFunc_IND_I32_I32_I32_I32_I32_I32_RetVoid(PCODE pcode, int8_t* pArgs, int8_t* pRet) + NOINLINE static void CallFunc_I32_I32_I32_RetVoid_PE(PCODE pcode, int8_t* pArgs, int8_t* pRet, PCODE pPortableEntryPointContext) { - void (*fptr)(int32_t, int32_t, int32_t, int32_t, int32_t, int32_t, int32_t) = (void (*)(int32_t, int32_t, int32_t, int32_t, int32_t, int32_t, int32_t))pcode; - (*fptr)(ARG_IND(0), ARG_I32(1), ARG_I32(2), ARG_I32(3), ARG_I32(4), ARG_I32(5), ARG_I32(6)); + alignas(16) int framePointer = TERMINATE_R2R_STACK_WALK; + void (*fptr)(int*, int32_t, int32_t, int32_t, PCODE) = (void (*)(int*, int32_t, int32_t, int32_t, PCODE))pcode; + (*fptr)(&framePointer, ARG_I32(0), ARG_I32(1), ARG_I32(2), pPortableEntryPointContext); } - static void CallFunc_IND_I32_I32_I32_I32_I32_I32_I32_I32_I32_I32_I32_RetVoid(PCODE pcode, int8_t* pArgs, int8_t* pRet) + NOINLINE static void CallFunc_I32_I32_RetVoid_PE(PCODE pcode, int8_t* pArgs, int8_t* pRet, PCODE pPortableEntryPointContext) { - void (*fptr)(int32_t, int32_t, int32_t, int32_t, int32_t, int32_t, int32_t, int32_t, int32_t, int32_t, int32_t, int32_t) = (void (*)(int32_t, int32_t, int32_t, int32_t, int32_t, int32_t, int32_t, int32_t, int32_t, int32_t, int32_t, int32_t))pcode; - (*fptr)(ARG_IND(0), ARG_I32(1), ARG_I32(2), ARG_I32(3), ARG_I32(4), ARG_I32(5), ARG_I32(6), ARG_I32(7), ARG_I32(8), ARG_I32(9), ARG_I32(10), ARG_I32(11)); + alignas(16) int framePointer = TERMINATE_R2R_STACK_WALK; + void (*fptr)(int*, int32_t, int32_t, PCODE) = (void (*)(int*, int32_t, int32_t, PCODE))pcode; + (*fptr)(&framePointer, ARG_I32(0), ARG_I32(1), pPortableEntryPointContext); } - static void CallFunc_IND_IND_RetVoid(PCODE pcode, int8_t* pArgs, int8_t* pRet) + NOINLINE static void CallFunc_I32_RetVoid_PE(PCODE pcode, int8_t* pArgs, int8_t* pRet, PCODE pPortableEntryPointContext) { - void (*fptr)(int32_t, int32_t) = (void (*)(int32_t, int32_t))pcode; - (*fptr)(ARG_IND(0), ARG_IND(1)); + alignas(16) int framePointer = TERMINATE_R2R_STACK_WALK; + void (*fptr)(int*, int32_t, PCODE) = (void (*)(int*, int32_t, PCODE))pcode; + (*fptr)(&framePointer, ARG_I32(0), pPortableEntryPointContext); } - static void CallFunc_IND_IND_I32_I32_I32_I32_I32_RetVoid(PCODE pcode, int8_t* pArgs, int8_t* pRet) + static void CallFunc_I64_RetVoid(PCODE pcode, int8_t* pArgs, int8_t* pRet) { - void (*fptr)(int32_t, int32_t, int32_t, int32_t, int32_t, int32_t, int32_t) = (void (*)(int32_t, int32_t, int32_t, int32_t, int32_t, int32_t, int32_t))pcode; - (*fptr)(ARG_IND(0), ARG_IND(1), ARG_I32(2), ARG_I32(3), ARG_I32(4), ARG_I32(5), ARG_I32(6)); + void (*fptr)(int64_t) = (void (*)(int64_t))pcode; + (*fptr)(ARG_I64(0)); } NOINLINE static void CallFunc_Void_RetVoid_PE(PCODE pcode, int8_t* pArgs, int8_t* pRet, PCODE pPortableEntryPointContext) @@ -582,95 +588,96 @@ namespace } const StringToWasmSigThunk g_wasmThunks[] = { - { "ddddp", (void*)&CallFunc_F64_F64_F64_RetF64_PE }, - { "dddp", (void*)&CallFunc_F64_F64_RetF64_PE }, - { "ddip", (void*)&CallFunc_F64_I32_RetF64_PE }, - { "ddp", (void*)&CallFunc_F64_RetF64_PE }, - { "di", (void*)&CallFunc_I32_RetF64 }, - { "ffffp", (void*)&CallFunc_F32_F32_F32_RetF32_PE }, - { "fffp", (void*)&CallFunc_F32_F32_RetF32_PE }, - { "ffip", (void*)&CallFunc_F32_I32_RetF32_PE }, - { "ffp", (void*)&CallFunc_F32_RetF32_PE }, - { "i", (void*)&CallFunc_Void_RetI32 }, - { "ii", (void*)&CallFunc_I32_RetI32 }, - { "iii", (void*)&CallFunc_I32_I32_RetI32 }, - { "iiii", (void*)&CallFunc_I32_I32_I32_RetI32 }, - { "iiiii", (void*)&CallFunc_I32_I32_I32_I32_RetI32 }, - { "iiiiii", (void*)&CallFunc_I32_I32_I32_I32_I32_RetI32 }, - { "iiiiiii", (void*)&CallFunc_I32_I32_I32_I32_I32_I32_RetI32 }, - { "iiiiiiii", (void*)&CallFunc_I32_I32_I32_I32_I32_I32_I32_RetI32 }, - { "iiiiiiiiiiiiiii", (void*)&CallFunc_I32_I32_I32_I32_I32_I32_I32_I32_I32_I32_I32_I32_I32_I32_RetI32 }, - { "iiiiiiip", (void*)&CallFunc_I32_I32_I32_I32_I32_I32_RetI32_PE }, - { "iiiiiip", (void*)&CallFunc_I32_I32_I32_I32_I32_RetI32_PE }, - { "iiiiip", (void*)&CallFunc_I32_I32_I32_I32_RetI32_PE }, - { "iiiil", (void*)&CallFunc_I32_I32_I32_I64_RetI32 }, - { "iiiini", (void*)&CallFunc_I32_I32_I32_IND_I32_RetI32 }, - { "iiiip", (void*)&CallFunc_I32_I32_I32_RetI32_PE }, - { "iiil", (void*)&CallFunc_I32_I32_I64_RetI32 }, - { "iiinii", (void*)&CallFunc_I32_I32_IND_I32_I32_RetI32 }, - { "iiiniin", (void*)&CallFunc_I32_I32_IND_I32_I32_IND_RetI32 }, - { "iiip", (void*)&CallFunc_I32_I32_RetI32_PE }, - { "iil", (void*)&CallFunc_I32_I64_RetI32 }, - { "iili", (void*)&CallFunc_I32_I64_I32_RetI32 }, - { "iiliiil", (void*)&CallFunc_I32_I64_I32_I32_I32_I64_RetI32 }, - { "iill", (void*)&CallFunc_I32_I64_I64_RetI32 }, - { "iilli", (void*)&CallFunc_I32_I64_I64_I32_RetI32 }, - { "iini", (void*)&CallFunc_I32_IND_I32_RetI32 }, - { "iiniii", (void*)&CallFunc_I32_IND_I32_I32_I32_RetI32 }, - { "iip", (void*)&CallFunc_I32_RetI32_PE }, - { "ilili", (void*)&CallFunc_I64_I32_I64_I32_RetI32 }, - { "in", (void*)&CallFunc_IND_RetI32 }, - { "ini", (void*)&CallFunc_IND_I32_RetI32 }, - { "inii", (void*)&CallFunc_IND_I32_I32_RetI32 }, - { "iniii", (void*)&CallFunc_IND_I32_I32_I32_RetI32 }, - { "iniiii", (void*)&CallFunc_IND_I32_I32_I32_I32_RetI32 }, - { "iniiiii", (void*)&CallFunc_IND_I32_I32_I32_I32_I32_RetI32 }, - { "inini", (void*)&CallFunc_IND_I32_IND_I32_RetI32 }, - { "ininn", (void*)&CallFunc_IND_I32_IND_IND_RetI32 }, - { "inni", (void*)&CallFunc_IND_IND_I32_RetI32 }, - { "innii", (void*)&CallFunc_IND_IND_I32_I32_RetI32 }, - { "innin", (void*)&CallFunc_IND_IND_I32_IND_RetI32 }, - { "innn", (void*)&CallFunc_IND_IND_IND_RetI32 }, - { "ip", (void*)&CallFunc_Void_RetI32_PE }, - { "l", (void*)&CallFunc_Void_RetI64 }, - { "li", (void*)&CallFunc_I32_RetI64 }, - { "liii", (void*)&CallFunc_I32_I32_I32_RetI64 }, - { "liiil", (void*)&CallFunc_I32_I32_I32_I64_RetI64 }, - { "lili", (void*)&CallFunc_I32_I64_I32_RetI64 }, - { "lillp", (void*)&CallFunc_I32_I64_I64_RetI64_PE }, - { "lilp", (void*)&CallFunc_I32_I64_RetI64_PE }, - { "lip", (void*)&CallFunc_I32_RetI64_PE }, - { "lllp", (void*)&CallFunc_I64_I64_RetI64_PE }, - { "lp", (void*)&CallFunc_Void_RetI64_PE }, - { "v", (void*)&CallFunc_Void_RetVoid }, - { "vdiip", (void*)&CallFunc_F64_I32_I32_RetVoid_PE }, - { "vfiip", (void*)&CallFunc_F32_I32_I32_RetVoid_PE }, - { "vi", (void*)&CallFunc_I32_RetVoid }, - { "vii", (void*)&CallFunc_I32_I32_RetVoid }, - { "viii", (void*)&CallFunc_I32_I32_I32_RetVoid }, - { "viiii", (void*)&CallFunc_I32_I32_I32_I32_RetVoid }, - { "viiiii", (void*)&CallFunc_I32_I32_I32_I32_I32_RetVoid }, - { "viiiiii", (void*)&CallFunc_I32_I32_I32_I32_I32_I32_RetVoid }, - { "viiinn", (void*)&CallFunc_I32_I32_I32_IND_IND_RetVoid }, - { "viiinni", (void*)&CallFunc_I32_I32_I32_IND_IND_I32_RetVoid }, - { "viiip", (void*)&CallFunc_I32_I32_I32_RetVoid_PE }, - { "viinni", (void*)&CallFunc_I32_I32_IND_IND_I32_RetVoid }, - { "viinnii", (void*)&CallFunc_I32_I32_IND_IND_I32_I32_RetVoid }, - { "viip", (void*)&CallFunc_I32_I32_RetVoid_PE }, - { "vini", (void*)&CallFunc_I32_IND_I32_RetVoid }, - { "vip", (void*)&CallFunc_I32_RetVoid_PE }, - { "vl", (void*)&CallFunc_I64_RetVoid }, - { "vn", (void*)&CallFunc_IND_RetVoid }, - { "vni", (void*)&CallFunc_IND_I32_RetVoid }, - { "vnii", (void*)&CallFunc_IND_I32_I32_RetVoid }, - { "vniii", (void*)&CallFunc_IND_I32_I32_I32_RetVoid }, - { "vniiii", (void*)&CallFunc_IND_I32_I32_I32_I32_RetVoid }, - { "vniiiii", (void*)&CallFunc_IND_I32_I32_I32_I32_I32_RetVoid }, - { "vniiiiii", (void*)&CallFunc_IND_I32_I32_I32_I32_I32_I32_RetVoid }, - { "vniiiiiiiiiii", (void*)&CallFunc_IND_I32_I32_I32_I32_I32_I32_I32_I32_I32_I32_I32_RetVoid }, - { "vnn", (void*)&CallFunc_IND_IND_RetVoid }, - { "vnniiiii", (void*)&CallFunc_IND_IND_I32_I32_I32_I32_I32_RetVoid }, - { "vp", (void*)&CallFunc_Void_RetVoid_PE } + { "Mddddp", (void*)&CallFunc_F64_F64_F64_RetF64_PE }, + { "Mdddp", (void*)&CallFunc_F64_F64_RetF64_PE }, + { "Mddip", (void*)&CallFunc_F64_I32_RetF64_PE }, + { "Mddp", (void*)&CallFunc_F64_RetF64_PE }, + { "Mdi", (void*)&CallFunc_I32_RetF64 }, + { "Mffffp", (void*)&CallFunc_F32_F32_F32_RetF32_PE }, + { "Mfffp", (void*)&CallFunc_F32_F32_RetF32_PE }, + { "Mffip", (void*)&CallFunc_F32_I32_RetF32_PE }, + { "Mffp", (void*)&CallFunc_F32_RetF32_PE }, + { "Mi", (void*)&CallFunc_Void_RetI32 }, + { "MiS64", (void*)&CallFunc_S64_RetI32 }, + { "MiS8", (void*)&CallFunc_S8_RetI32 }, + { "MiS8S8S8", (void*)&CallFunc_S8_S8_S8_RetI32 }, + { "MiS8S8i", (void*)&CallFunc_S8_S8_I32_RetI32 }, + { "MiS8S8iS8", (void*)&CallFunc_S8_S8_I32_S8_RetI32 }, + { "MiS8S8ii", (void*)&CallFunc_S8_S8_I32_I32_RetI32 }, + { "MiS8i", (void*)&CallFunc_S8_I32_RetI32 }, + { "MiS8iS8S8", (void*)&CallFunc_S8_I32_S8_S8_RetI32 }, + { "MiS8iS8i", (void*)&CallFunc_S8_I32_S8_I32_RetI32 }, + { "MiS8ii", (void*)&CallFunc_S8_I32_I32_RetI32 }, + { "MiS8iii", (void*)&CallFunc_S8_I32_I32_I32_RetI32 }, + { "MiS8iiii", (void*)&CallFunc_S8_I32_I32_I32_I32_RetI32 }, + { "MiS8iiiii", (void*)&CallFunc_S8_I32_I32_I32_I32_I32_RetI32 }, + { "Mii", (void*)&CallFunc_I32_RetI32 }, + { "MiiS8i", (void*)&CallFunc_I32_S8_I32_RetI32 }, + { "MiiS8iii", (void*)&CallFunc_I32_S8_I32_I32_I32_RetI32 }, + { "Miii", (void*)&CallFunc_I32_I32_RetI32 }, + { "MiiiS8ii", (void*)&CallFunc_I32_I32_S8_I32_I32_RetI32 }, + { "MiiiS8iiS8", (void*)&CallFunc_I32_I32_S8_I32_I32_S8_RetI32 }, + { "Miiii", (void*)&CallFunc_I32_I32_I32_RetI32 }, + { "MiiiiS8i", (void*)&CallFunc_I32_I32_I32_S8_I32_RetI32 }, + { "Miiiii", (void*)&CallFunc_I32_I32_I32_I32_RetI32 }, + { "Miiiiii", (void*)&CallFunc_I32_I32_I32_I32_I32_RetI32 }, + { "Miiiiiii", (void*)&CallFunc_I32_I32_I32_I32_I32_I32_RetI32 }, + { "Miiiiiiii", (void*)&CallFunc_I32_I32_I32_I32_I32_I32_I32_RetI32 }, + { "Miiiiiiiiiiiiiii", (void*)&CallFunc_I32_I32_I32_I32_I32_I32_I32_I32_I32_I32_I32_I32_I32_I32_RetI32 }, + { "Miiiiiiip", (void*)&CallFunc_I32_I32_I32_I32_I32_I32_RetI32_PE }, + { "Miiiiiip", (void*)&CallFunc_I32_I32_I32_I32_I32_RetI32_PE }, + { "Miiiiip", (void*)&CallFunc_I32_I32_I32_I32_RetI32_PE }, + { "Miiiil", (void*)&CallFunc_I32_I32_I32_I64_RetI32 }, + { "Miiiip", (void*)&CallFunc_I32_I32_I32_RetI32_PE }, + { "Miiil", (void*)&CallFunc_I32_I32_I64_RetI32 }, + { "Miiip", (void*)&CallFunc_I32_I32_RetI32_PE }, + { "Miil", (void*)&CallFunc_I32_I64_RetI32 }, + { "Miili", (void*)&CallFunc_I32_I64_I32_RetI32 }, + { "Miiliiil", (void*)&CallFunc_I32_I64_I32_I32_I32_I64_RetI32 }, + { "Miill", (void*)&CallFunc_I32_I64_I64_RetI32 }, + { "Miilli", (void*)&CallFunc_I32_I64_I64_I32_RetI32 }, + { "Miip", (void*)&CallFunc_I32_RetI32_PE }, + { "Milili", (void*)&CallFunc_I64_I32_I64_I32_RetI32 }, + { "Mip", (void*)&CallFunc_Void_RetI32_PE }, + { "Ml", (void*)&CallFunc_Void_RetI64 }, + { "Mli", (void*)&CallFunc_I32_RetI64 }, + { "Mliii", (void*)&CallFunc_I32_I32_I32_RetI64 }, + { "Mliiil", (void*)&CallFunc_I32_I32_I32_I64_RetI64 }, + { "Mlili", (void*)&CallFunc_I32_I64_I32_RetI64 }, + { "Mlillp", (void*)&CallFunc_I32_I64_I64_RetI64_PE }, + { "Mlilp", (void*)&CallFunc_I32_I64_RetI64_PE }, + { "Mlip", (void*)&CallFunc_I32_RetI64_PE }, + { "Mlllp", (void*)&CallFunc_I64_I64_RetI64_PE }, + { "Mlp", (void*)&CallFunc_Void_RetI64_PE }, + { "Mv", (void*)&CallFunc_Void_RetVoid }, + { "MvS8", (void*)&CallFunc_S8_RetVoid }, + { "MvS8S8", (void*)&CallFunc_S8_S8_RetVoid }, + { "MvS8S8iiiii", (void*)&CallFunc_S8_S8_I32_I32_I32_I32_I32_RetVoid }, + { "MvS8i", (void*)&CallFunc_S8_I32_RetVoid }, + { "MvS8ii", (void*)&CallFunc_S8_I32_I32_RetVoid }, + { "MvS8iii", (void*)&CallFunc_S8_I32_I32_I32_RetVoid }, + { "MvS8iiii", (void*)&CallFunc_S8_I32_I32_I32_I32_RetVoid }, + { "MvS8iiiii", (void*)&CallFunc_S8_I32_I32_I32_I32_I32_RetVoid }, + { "MvS8iiiiii", (void*)&CallFunc_S8_I32_I32_I32_I32_I32_I32_RetVoid }, + { "MvS8iiiiiiiiiii", (void*)&CallFunc_S8_I32_I32_I32_I32_I32_I32_I32_I32_I32_I32_I32_RetVoid }, + { "Mvdiip", (void*)&CallFunc_F64_I32_I32_RetVoid_PE }, + { "Mvfiip", (void*)&CallFunc_F32_I32_I32_RetVoid_PE }, + { "Mvi", (void*)&CallFunc_I32_RetVoid }, + { "MviS8i", (void*)&CallFunc_I32_S8_I32_RetVoid }, + { "Mvii", (void*)&CallFunc_I32_I32_RetVoid }, + { "MviiS8S8i", (void*)&CallFunc_I32_I32_S8_S8_I32_RetVoid }, + { "MviiS8S8ii", (void*)&CallFunc_I32_I32_S8_S8_I32_I32_RetVoid }, + { "Mviii", (void*)&CallFunc_I32_I32_I32_RetVoid }, + { "MviiiS8S8", (void*)&CallFunc_I32_I32_I32_S8_S8_RetVoid }, + { "MviiiS8S8i", (void*)&CallFunc_I32_I32_I32_S8_S8_I32_RetVoid }, + { "Mviiii", (void*)&CallFunc_I32_I32_I32_I32_RetVoid }, + { "Mviiiii", (void*)&CallFunc_I32_I32_I32_I32_I32_RetVoid }, + { "Mviiiiii", (void*)&CallFunc_I32_I32_I32_I32_I32_I32_RetVoid }, + { "Mviiip", (void*)&CallFunc_I32_I32_I32_RetVoid_PE }, + { "Mviip", (void*)&CallFunc_I32_I32_RetVoid_PE }, + { "Mvip", (void*)&CallFunc_I32_RetVoid_PE }, + { "Mvl", (void*)&CallFunc_I64_RetVoid }, + { "Mvp", (void*)&CallFunc_Void_RetVoid_PE } }; const size_t g_wasmThunksCount = sizeof(g_wasmThunks) / sizeof(g_wasmThunks[0]); diff --git a/src/coreclr/vm/wasm/callhelpers-pinvoke.cpp b/src/coreclr/vm/wasm/callhelpers-pinvoke.cpp index 2be5428681fe54..1c820b6d7b18ca 100644 --- a/src/coreclr/vm/wasm/callhelpers-pinvoke.cpp +++ b/src/coreclr/vm/wasm/callhelpers-pinvoke.cpp @@ -11,6 +11,7 @@ #include extern "C" { + uint32_t CompressionNative_CompressBound (uint32_t); uint32_t CompressionNative_Crc32 (uint32_t, void *, int32_t); int32_t CompressionNative_Deflate (void *, int32_t); int32_t CompressionNative_DeflateEnd (void *); @@ -192,6 +193,7 @@ static const Entry s_libSystem_Globalization_Native [] = { }; static const Entry s_libSystem_IO_Compression_Native [] = { + DllImportEntry(CompressionNative_CompressBound) // System.IO.Compression DllImportEntry(CompressionNative_Crc32) // System.IO.Compression DllImportEntry(CompressionNative_Deflate) // System.IO.Compression, System.Net.WebSockets DllImportEntry(CompressionNative_DeflateEnd) // System.IO.Compression, System.Net.WebSockets @@ -320,7 +322,7 @@ typedef struct PInvokeTable { static PInvokeTable s_PInvokeTables[] = { {"libSystem.Globalization.Native", s_libSystem_Globalization_Native, 33}, - {"libSystem.IO.Compression.Native", s_libSystem_IO_Compression_Native, 8}, + {"libSystem.IO.Compression.Native", s_libSystem_IO_Compression_Native, 9}, {"libSystem.Native", s_libSystem_Native, 94}, {"libSystem.Native.Browser", s_libSystem_Native_Browser, 1}, {"libSystem.Runtime.InteropServices.JavaScript.Native", s_libSystem_Runtime_InteropServices_JavaScript_Native, 6} diff --git a/src/coreclr/vm/wasm/helpers.cpp b/src/coreclr/vm/wasm/helpers.cpp index a3340b4067c03c..bbec1fa70dfc5f 100644 --- a/src/coreclr/vm/wasm/helpers.cpp +++ b/src/coreclr/vm/wasm/helpers.cpp @@ -286,20 +286,20 @@ namespace } const StringToWasmSigThunk g_wasmPortableEntryPointThunks[] = { - { "vp", (void*)&CallInterpreter_RetVoid }, - { "vip", (void*)&CallInterpreter_I32_RetVoid }, - { "viip", (void*)&CallInterpreter_I32_I32_RetVoid }, - { "viiip", (void*)&CallInterpreter_I32_I32_I32_RetVoid }, - { "viiiip", (void*)&CallInterpreter_I32_I32_I32_I32_RetVoid }, - { "ip", (void*)&CallInterpreter_RetI32 }, - { "iip", (void*)&CallInterpreter_I32_RetI32 }, - { "iiip", (void*)&CallInterpreter_I32_I32_RetI32 }, - { "iiiip", (void*)&CallInterpreter_I32_I32_I32_RetI32 }, - { "iiiiip", (void*)&CallInterpreter_I32_I32_I32_I32_RetI32 }, - { "iiiiiip", (void*)&CallInterpreter_I32_I32_I32_I32_I32_RetI32 }, - { "iiiiiiip", (void*)&CallInterpreter_I32_I32_I32_I32_I32_I32_RetI32 }, - { "iiiiiiiip", (void*)&CallInterpreter_I32_I32_I32_I32_I32_I32_I32_RetI32 }, - { "iiiiiiiiip", (void*)&CallInterpreter_I32_I32_I32_I32_I32_I32_I32_I32_RetI32 }, + { "Ivp", (void*)&CallInterpreter_RetVoid }, + { "Ivip", (void*)&CallInterpreter_I32_RetVoid }, + { "Iviip", (void*)&CallInterpreter_I32_I32_RetVoid }, + { "Iviiip", (void*)&CallInterpreter_I32_I32_I32_RetVoid }, + { "Iviiiip", (void*)&CallInterpreter_I32_I32_I32_I32_RetVoid }, + { "Iip", (void*)&CallInterpreter_RetI32 }, + { "Iiip", (void*)&CallInterpreter_I32_RetI32 }, + { "Iiiip", (void*)&CallInterpreter_I32_I32_RetI32 }, + { "Iiiiip", (void*)&CallInterpreter_I32_I32_I32_RetI32 }, + { "Iiiiiip", (void*)&CallInterpreter_I32_I32_I32_I32_RetI32 }, + { "Iiiiiiip", (void*)&CallInterpreter_I32_I32_I32_I32_I32_RetI32 }, + { "Iiiiiiiip", (void*)&CallInterpreter_I32_I32_I32_I32_I32_I32_RetI32 }, + { "Iiiiiiiiip", (void*)&CallInterpreter_I32_I32_I32_I32_I32_I32_I32_RetI32 }, + { "Iiiiiiiiiip", (void*)&CallInterpreter_I32_I32_I32_I32_I32_I32_I32_I32_RetI32 }, }; const size_t g_wasmPortableEntryPointThunksCount = sizeof(g_wasmPortableEntryPointThunks) / sizeof(g_wasmPortableEntryPointThunks[0]); @@ -759,12 +759,73 @@ namespace NotConvertible, ToI32, ToI64, - ToI32Indirect, ToF32, - ToF64 + ToF64, + ToStruct, // S — multi-field struct passed by pointer, structSize holds the size + ToEmpty, // e — empty struct, takes no wasm argument }; - ConvertType ConvertibleTo(CorElementType argType, MetaSig& sig, bool isReturn) + struct ConvertResult + { + ConvertType type; + uint32_t structSize; // only meaningful when type == ToStruct + }; + + // Lowers a TypeHandle to a ConvertResult, unwrapping single-field structs + // per the BasicCABI spec. + ConvertResult LowerTypeHandle(TypeHandle th) + { + uint32_t size = th.GetSize(); + + if (th.IsTypeDesc() || !th.AsMethodTable()->IsValueType()) + { + // Non-valuetype or TypeDesc — fall through to element type mapping + CorElementType elemType = th.GetSignatureCorElementType(); + switch (elemType) + { + case ELEMENT_TYPE_I4: case ELEMENT_TYPE_U4: + case ELEMENT_TYPE_I2: case ELEMENT_TYPE_U2: + case ELEMENT_TYPE_I1: case ELEMENT_TYPE_U1: + case ELEMENT_TYPE_BOOLEAN: case ELEMENT_TYPE_CHAR: + case ELEMENT_TYPE_I: case ELEMENT_TYPE_U: + case ELEMENT_TYPE_PTR: case ELEMENT_TYPE_BYREF: + case ELEMENT_TYPE_FNPTR: + case ELEMENT_TYPE_CLASS: case ELEMENT_TYPE_STRING: + case ELEMENT_TYPE_ARRAY: case ELEMENT_TYPE_SZARRAY: + return { ConvertType::ToI32, 0 }; + case ELEMENT_TYPE_I8: case ELEMENT_TYPE_U8: + return { ConvertType::ToI64, 0 }; + case ELEMENT_TYPE_R4: + return { ConvertType::ToF32, 0 }; + case ELEMENT_TYPE_R8: + return { ConvertType::ToF64, 0 }; + default: + return { ConvertType::NotConvertible, 0 }; + } + } + + MethodTable* pMT = th.AsMethodTable(); + uint32_t numInstanceFields = pMT->GetNumInstanceFields(); + + // WASM-TODO: Empty structs should return ToEmpty once .NET + // stops padding them to size 1. See runtime issue #127361. + + if (numInstanceFields == 1) + { + FieldDesc* pField = pMT->GetApproxFieldDescListRaw(); + TypeHandle fieldType = pField->GetApproxFieldTypeHandleThrowing(); + if (fieldType.GetSize() == size) + { + // Single field, no padding — unwrap recursively + return LowerTypeHandle(fieldType); + } + // One field with padding — treat as multi-field struct + } + + return { ConvertType::ToStruct, size }; + } + + ConvertResult ConvertibleTo(CorElementType argType, MetaSig& sig, bool isReturn) { // See https://github.com/WebAssembly/tool-conventions/blob/main/BasicCABI.md switch (argType) @@ -786,61 +847,69 @@ namespace case ELEMENT_TYPE_U: case ELEMENT_TYPE_FNPTR: case ELEMENT_TYPE_SZARRAY: - return ConvertType::ToI32; + return { ConvertType::ToI32, 0 }; case ELEMENT_TYPE_I8: case ELEMENT_TYPE_U8: - return ConvertType::ToI64; + return { ConvertType::ToI64, 0 }; case ELEMENT_TYPE_R4: - return ConvertType::ToF32; + return { ConvertType::ToF32, 0 }; case ELEMENT_TYPE_R8: - return ConvertType::ToF64; + return { ConvertType::ToF64, 0 }; case ELEMENT_TYPE_TYPEDBYREF: - // Typed references are passed indirectly in WASM since they are larger than pointer size. - return ConvertType::ToI32Indirect; case ELEMENT_TYPE_VALUETYPE: { - // In WASM, values types that are larger than pointer size or have multiple fields are passed indirectly. - // WASM-TODO: Single fields may not always be passed as i32. Floats and doubles are passed as f32 and f64 respectively. TypeHandle vt = isReturn ? sig.GetRetTypeHandleThrowing() : sig.GetLastTypeHandleThrowing(); - - if (!vt.IsTypeDesc() - && vt.AsMethodTable()->GetNumInstanceFields() >= 2) - { - return ConvertType::ToI32Indirect; - } - - return vt.GetSize() <= sizeof(uint32_t) - ? ConvertType::ToI32 - : ConvertType::ToI32Indirect; + return LowerTypeHandle(vt); } default: - return ConvertType::NotConvertible; + return { ConvertType::NotConvertible, 0 }; } } - char GetTypeCode(ConvertType type) + // Appends the encoding for a ConvertResult to keyBuffer. + // Returns the number of characters that would be written (even if pos >= maxSize). + // Only writes characters while pos < maxSize. + uint32_t AppendTypeCode(ConvertResult cr, char* keyBuffer, uint32_t pos, uint32_t maxSize) { - switch (type) + char c; + switch (cr.type) { - case ConvertType::ToI32: - return 'i'; - case ConvertType::ToI64: - return 'l'; - case ConvertType::ToF32: - return 'f'; - case ConvertType::ToF64: - return 'd'; - case ConvertType::ToI32Indirect: - return 'n'; + case ConvertType::ToI32: c = 'i'; break; + case ConvertType::ToI64: c = 'l'; break; + case ConvertType::ToF32: c = 'f'; break; + case ConvertType::ToF64: c = 'd'; break; + case ConvertType::ToEmpty: c = 'e'; break; + case ConvertType::ToStruct: + { + // Encode as S where N is the struct size in decimal + char sizeBuf[16]; + int len = sprintf_s(sizeBuf, sizeof(sizeBuf), "S%u", cr.structSize); + for (int j = 0; j < len; j++) + { + if (pos + (uint32_t)j < maxSize) + keyBuffer[pos + (uint32_t)j] = sizeBuf[j]; + } + return (uint32_t)len; + } default: PORTABILITY_ASSERT("Unknown type"); - return '?'; + c = '?'; + break; } + + if (pos < maxSize) + keyBuffer[pos] = c; + + return 1; } - bool GetSignatureKey(MetaSig& sig, char* keyBuffer, uint32_t maxSize) + // Computes the signature key string for a MetaSig. + // Returns the total number of characters needed (excluding null terminator). + // Only writes characters while pos < maxSize, so the buffer is never overflowed. + // Callers should check if the return value >= maxSize and retry with a larger buffer. + uint32_t GetSignatureKey(MetaSig& sig, char prefix, char* keyBuffer, uint32_t maxSize) { CONTRACTL { @@ -852,43 +921,53 @@ namespace uint32_t pos = 0; + if (pos < maxSize) + keyBuffer[pos] = prefix; + pos++; + if (sig.IsReturnTypeVoid()) { - keyBuffer[pos++] = 'v'; + if (pos < maxSize) + keyBuffer[pos] = 'v'; + pos++; } else { - keyBuffer[pos++] = GetTypeCode(ConvertibleTo(sig.GetReturnType(), sig, true /* isReturn */)); + ConvertResult cr = ConvertibleTo(sig.GetReturnType(), sig, true /* isReturn */); + if (cr.type == ConvertType::NotConvertible) + return UINT32_MAX; + pos += AppendTypeCode(cr, keyBuffer, pos, maxSize); } if (sig.HasThis()) - keyBuffer[pos++] = 'i'; + { + if (pos < maxSize) + keyBuffer[pos] = 'T'; + pos++; + } for (CorElementType argType = sig.NextArg(); argType != ELEMENT_TYPE_END; argType = sig.NextArg()) { - if (pos >= maxSize) - return false; - - keyBuffer[pos++] = GetTypeCode(ConvertibleTo(argType, sig, false /* isReturn */)); + ConvertResult cr = ConvertibleTo(argType, sig, false /* isReturn */); + if (cr.type == ConvertType::NotConvertible) + return UINT32_MAX; + pos += AppendTypeCode(cr, keyBuffer, pos, maxSize); } // Add the portable entrypoint parameter if (sig.GetCallingConvention() == IMAGE_CEE_CS_CALLCONV_DEFAULT) { - if (pos >= maxSize) - return false; - - keyBuffer[pos++] = 'p'; + if (pos < maxSize) + keyBuffer[pos] = 'p'; + pos++; } - if (pos >= maxSize) - return false; + if (pos < maxSize) + keyBuffer[pos] = 0; - keyBuffer[pos] = 0; - - return true; + return pos; } typedef StringToThunkHash StringToWasmSigThunkHash; @@ -932,10 +1011,21 @@ namespace return NULL; } - uint32_t keyBufferLen = sig.NumFixedArgs() + (sig.HasThis() ? 1 : 0) + 2 + ((callConv == IMAGE_CEE_CS_CALLCONV_DEFAULT) ? 1 : 0); + uint32_t keyBufferLen = sig.NumFixedArgs() + (sig.HasThis() ? 1 : 0) + 2 + ((callConv == IMAGE_CEE_CS_CALLCONV_DEFAULT) ? 1 : 0) + 1; // +1 for prefix char* keyBuffer = (char*)alloca(keyBufferLen); - if (!GetSignatureKey(sig, keyBuffer, keyBufferLen)) + uint32_t needed = GetSignatureKey(sig, 'M', keyBuffer, keyBufferLen); + if (needed == UINT32_MAX) return NULL; + if (needed >= keyBufferLen) + { + // S tokens made the key longer than the initial estimate — retry with exact size + keyBufferLen = needed + 1; + keyBuffer = (char*)alloca(keyBufferLen); + sig.Reset(); + needed = GetSignatureKey(sig, 'M', keyBuffer, keyBufferLen); + if (needed == UINT32_MAX || needed >= keyBufferLen) + return NULL; + } void* thunk = LookupThunk(keyBuffer); #ifdef _DEBUG @@ -966,10 +1056,21 @@ namespace default: return NULL; } - uint32_t keyBufferLen = sig.NumFixedArgs() + (sig.HasThis() ? 1 : 0) + 2 + 1; // +1 for the 'p' suffix to indicate portable entry point + uint32_t keyBufferLen = sig.NumFixedArgs() + (sig.HasThis() ? 1 : 0) + 2 + 1 + 1; // +1 for 'p' suffix, +1 for prefix char* keyBuffer = (char*)alloca(keyBufferLen); - if (!GetSignatureKey(sig, keyBuffer, keyBufferLen)) + uint32_t needed = GetSignatureKey(sig, 'I', keyBuffer, keyBufferLen); + if (needed == UINT32_MAX) return NULL; + if (needed >= keyBufferLen) + { + // S tokens made the key longer than the initial estimate — retry with exact size + keyBufferLen = needed + 1; + keyBuffer = (char*)alloca(keyBufferLen); + sig.Reset(); + needed = GetSignatureKey(sig, 'I', keyBuffer, keyBufferLen); + if (needed == UINT32_MAX || needed >= keyBufferLen) + return NULL; + } void* thunk = LookupPortableEntryPointThunk(keyBuffer); #ifdef _DEBUG diff --git a/src/tasks/WasmAppBuilder/coreclr/InterpToNativeGenerator.cs b/src/tasks/WasmAppBuilder/coreclr/InterpToNativeGenerator.cs index cf7216a1998eb6..38e768fed39be4 100644 --- a/src/tasks/WasmAppBuilder/coreclr/InterpToNativeGenerator.cs +++ b/src/tasks/WasmAppBuilder/coreclr/InterpToNativeGenerator.cs @@ -140,7 +140,7 @@ private static void Emit(StreamWriter w, IEnumerable cookies) bool isPortableEntryPointCall = IsPortableEntryPointCall(tokens); if (isPortableEntryPointCall) tokens.RemoveAt(tokens.Count - 1); - return $" {{ \"{initialSignature}\", (void*)&{CallFuncName(Args(tokens), SignatureMapper.TokenToNameType(tokens[0]), isPortableEntryPointCall)} }}"; + return $" {{ \"M{initialSignature}\", (void*)&{CallFuncName(Args(tokens), SignatureMapper.TokenToNameType(tokens[0]), isPortableEntryPointCall)} }}"; } )}} }; diff --git a/src/tasks/WasmAppBuilder/coreclr/SignatureMapper.cs b/src/tasks/WasmAppBuilder/coreclr/SignatureMapper.cs index 2e3808f9667773..9e98fa6925a32e 100644 --- a/src/tasks/WasmAppBuilder/coreclr/SignatureMapper.cs +++ b/src/tasks/WasmAppBuilder/coreclr/SignatureMapper.cs @@ -151,7 +151,7 @@ internal static class SignatureMapper if (includeThis && !method.IsStatic) { - sb.Append('i'); + sb.Append('T'); } foreach (var parameter in method.GetParameters()) @@ -203,6 +203,7 @@ public static List ParseSignatureTokens(string signature) 'f' => "float", 'd' => "double", 'S' => "int32_t", + 'T' => "int32_t", 'p' => "PCODE", _ => throw new InvalidSignatureCharException(token[0]) }; @@ -215,6 +216,7 @@ public static List ParseSignatureTokens(string signature) 'f' => "F32", 'd' => "F64", 'S' => token, // e.g. "S8", "S64" — encodes size in the name + 'T' => "I32", 'p' => "PE", _ => throw new InvalidSignatureCharException(token[0]) }; @@ -226,6 +228,7 @@ public static List ParseSignatureTokens(string signature) 'f' => "ARG_F32", 'd' => "ARG_F64", 'S' => "ARG_IND", + 'T' => "ARG_I32", _ => throw new InvalidSignatureCharException(token[0]) }; From e5479d6c40a375b8d8650d4e8ced5a2d3f84187c Mon Sep 17 00:00:00 2001 From: David Wrighton Date: Mon, 27 Apr 2026 14:26:44 -0700 Subject: [PATCH 19/43] Fix handling of This parameters in SignatureMapper --- .../vm/wasm/callhelpers-interp-to-managed.cpp | 24 +++++++++++++++++++ .../WasmAppBuilder/coreclr/SignatureMapper.cs | 2 +- 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/src/coreclr/vm/wasm/callhelpers-interp-to-managed.cpp b/src/coreclr/vm/wasm/callhelpers-interp-to-managed.cpp index 1dc9b441db12be..63f49736503fda 100644 --- a/src/coreclr/vm/wasm/callhelpers-interp-to-managed.cpp +++ b/src/coreclr/vm/wasm/callhelpers-interp-to-managed.cpp @@ -166,6 +166,20 @@ namespace *((int32_t*)pRet) = (*fptr)(ARG_IND(0), ARG_I32(1), ARG_I32(2), ARG_I32(3), ARG_I32(4), ARG_I32(5)); } + NOINLINE static void CallFunc_This_I32_RetI32_PE(PCODE pcode, int8_t* pArgs, int8_t* pRet, PCODE pPortableEntryPointContext) + { + alignas(16) int framePointer = TERMINATE_R2R_STACK_WALK; + int32_t (*fptr)(int*, int32_t, int32_t, PCODE) = (int32_t (*)(int*, int32_t, int32_t, PCODE))pcode; + *((int32_t*)pRet) = (*fptr)(&framePointer, ARG_I32(0), ARG_I32(1), pPortableEntryPointContext); + } + + NOINLINE static void CallFunc_This_RetI32_PE(PCODE pcode, int8_t* pArgs, int8_t* pRet, PCODE pPortableEntryPointContext) + { + alignas(16) int framePointer = TERMINATE_R2R_STACK_WALK; + int32_t (*fptr)(int*, int32_t, PCODE) = (int32_t (*)(int*, int32_t, PCODE))pcode; + *((int32_t*)pRet) = (*fptr)(&framePointer, ARG_I32(0), pPortableEntryPointContext); + } + static void CallFunc_I32_RetI32(PCODE pcode, int8_t* pArgs, int8_t* pRet) { int32_t (*fptr)(int32_t) = (int32_t (*)(int32_t))pcode; @@ -472,6 +486,13 @@ namespace (*fptr)(ARG_IND(0), ARG_I32(1), ARG_I32(2), ARG_I32(3), ARG_I32(4), ARG_I32(5), ARG_I32(6), ARG_I32(7), ARG_I32(8), ARG_I32(9), ARG_I32(10), ARG_I32(11)); } + NOINLINE static void CallFunc_This_RetVoid_PE(PCODE pcode, int8_t* pArgs, int8_t* pRet, PCODE pPortableEntryPointContext) + { + alignas(16) int framePointer = TERMINATE_R2R_STACK_WALK; + void (*fptr)(int*, int32_t, PCODE) = (void (*)(int*, int32_t, PCODE))pcode; + (*fptr)(&framePointer, ARG_I32(0), pPortableEntryPointContext); + } + NOINLINE static void CallFunc_F64_I32_I32_RetVoid_PE(PCODE pcode, int8_t* pArgs, int8_t* pRet, PCODE pPortableEntryPointContext) { alignas(16) int framePointer = TERMINATE_R2R_STACK_WALK; @@ -611,6 +632,8 @@ const StringToWasmSigThunk g_wasmThunks[] = { { "MiS8iii", (void*)&CallFunc_S8_I32_I32_I32_RetI32 }, { "MiS8iiii", (void*)&CallFunc_S8_I32_I32_I32_I32_RetI32 }, { "MiS8iiiii", (void*)&CallFunc_S8_I32_I32_I32_I32_I32_RetI32 }, + { "MiTip", (void*)&CallFunc_This_I32_RetI32_PE }, + { "MiTp", (void*)&CallFunc_This_RetI32_PE }, { "Mii", (void*)&CallFunc_I32_RetI32 }, { "MiiS8i", (void*)&CallFunc_I32_S8_I32_RetI32 }, { "MiiS8iii", (void*)&CallFunc_I32_S8_I32_I32_I32_RetI32 }, @@ -660,6 +683,7 @@ const StringToWasmSigThunk g_wasmThunks[] = { { "MvS8iiiii", (void*)&CallFunc_S8_I32_I32_I32_I32_I32_RetVoid }, { "MvS8iiiiii", (void*)&CallFunc_S8_I32_I32_I32_I32_I32_I32_RetVoid }, { "MvS8iiiiiiiiiii", (void*)&CallFunc_S8_I32_I32_I32_I32_I32_I32_I32_I32_I32_I32_I32_RetVoid }, + { "MvTp", (void*)&CallFunc_This_RetVoid_PE }, { "Mvdiip", (void*)&CallFunc_F64_I32_I32_RetVoid_PE }, { "Mvfiip", (void*)&CallFunc_F32_I32_I32_RetVoid_PE }, { "Mvi", (void*)&CallFunc_I32_RetVoid }, diff --git a/src/tasks/WasmAppBuilder/coreclr/SignatureMapper.cs b/src/tasks/WasmAppBuilder/coreclr/SignatureMapper.cs index 9e98fa6925a32e..d495a836c0dc4c 100644 --- a/src/tasks/WasmAppBuilder/coreclr/SignatureMapper.cs +++ b/src/tasks/WasmAppBuilder/coreclr/SignatureMapper.cs @@ -216,7 +216,7 @@ public static List ParseSignatureTokens(string signature) 'f' => "F32", 'd' => "F64", 'S' => token, // e.g. "S8", "S64" — encodes size in the name - 'T' => "I32", + 'T' => "This", 'p' => "PE", _ => throw new InvalidSignatureCharException(token[0]) }; From 2b240a0b50bd6c340afaa2b290fb6524b0231391 Mon Sep 17 00:00:00 2001 From: David Wrighton Date: Mon, 27 Apr 2026 14:41:05 -0700 Subject: [PATCH 20/43] Use fixed stack buffer with alloca fallback for signature key computation Replace dynamic alloca-based initial buffer sizing with a fixed 64-byte stack buffer. Fall back to alloca only when S tokens make the key exceed the initial buffer. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/coreclr/vm/wasm/helpers.cpp | 13 +++++++------ .../coreclr/ManagedToNativeGenerator.cs | 10 +--------- 2 files changed, 8 insertions(+), 15 deletions(-) diff --git a/src/coreclr/vm/wasm/helpers.cpp b/src/coreclr/vm/wasm/helpers.cpp index bbec1fa70dfc5f..befe2dacdcc649 100644 --- a/src/coreclr/vm/wasm/helpers.cpp +++ b/src/coreclr/vm/wasm/helpers.cpp @@ -1011,14 +1011,14 @@ namespace return NULL; } - uint32_t keyBufferLen = sig.NumFixedArgs() + (sig.HasThis() ? 1 : 0) + 2 + ((callConv == IMAGE_CEE_CS_CALLCONV_DEFAULT) ? 1 : 0) + 1; // +1 for prefix - char* keyBuffer = (char*)alloca(keyBufferLen); + char fixedBuffer[64]; + char* keyBuffer = fixedBuffer; + uint32_t keyBufferLen = sizeof(fixedBuffer); uint32_t needed = GetSignatureKey(sig, 'M', keyBuffer, keyBufferLen); if (needed == UINT32_MAX) return NULL; if (needed >= keyBufferLen) { - // S tokens made the key longer than the initial estimate — retry with exact size keyBufferLen = needed + 1; keyBuffer = (char*)alloca(keyBufferLen); sig.Reset(); @@ -1056,14 +1056,15 @@ namespace default: return NULL; } - uint32_t keyBufferLen = sig.NumFixedArgs() + (sig.HasThis() ? 1 : 0) + 2 + 1 + 1; // +1 for 'p' suffix, +1 for prefix - char* keyBuffer = (char*)alloca(keyBufferLen); + + char fixedBuffer[64]; + char* keyBuffer = fixedBuffer; + uint32_t keyBufferLen = sizeof(fixedBuffer); uint32_t needed = GetSignatureKey(sig, 'I', keyBuffer, keyBufferLen); if (needed == UINT32_MAX) return NULL; if (needed >= keyBufferLen) { - // S tokens made the key longer than the initial estimate — retry with exact size keyBufferLen = needed + 1; keyBuffer = (char*)alloca(keyBufferLen); sig.Reset(); diff --git a/src/tasks/WasmAppBuilder/coreclr/ManagedToNativeGenerator.cs b/src/tasks/WasmAppBuilder/coreclr/ManagedToNativeGenerator.cs index 8bc1c897bc9da5..368da23206f4a4 100644 --- a/src/tasks/WasmAppBuilder/coreclr/ManagedToNativeGenerator.cs +++ b/src/tasks/WasmAppBuilder/coreclr/ManagedToNativeGenerator.cs @@ -91,15 +91,7 @@ private void ExecuteInternal(LogAdapter log) // The signatures should be in the form of a string where the first character represents the return type and the // following characters represent the argument types. The type characters should match those used by the // SignatureMapper.CharToNativeType method. - string[] pregeneratedInterpreterToNativeSignatures = - { - "ip", - "iip", - "iiip", - "iiiip", - "vip", - "viip", - }; + string[] pregeneratedInterpreterToNativeSignatures = Array.Empty(); // Currently none, but can be added here as needed in the future. IEnumerable cookies = pinvoke.Generate(PInvokeModules, PInvokeOutputPath, ReversePInvokeOutputPath); cookies = cookies.Concat(internalCallCollector.GetSignatures()); From 01cb40e0e52e91335529ded03a7840c3e3acbb33 Mon Sep 17 00:00:00 2001 From: David Wrighton Date: Mon, 27 Apr 2026 14:50:32 -0700 Subject: [PATCH 21/43] Add R2R pregenerated thunk fallback to LookupThunk and LookupPortableEntryPointThunk Both functions now check the process-startup thunk cache first, then fall back to LookupPregeneratedThunkByString for thunks injected via READYTORUN_FIXUP_InjectStringThunks. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/coreclr/vm/wasm/helpers.cpp | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/src/coreclr/vm/wasm/helpers.cpp b/src/coreclr/vm/wasm/helpers.cpp index befe2dacdcc649..ad544344d4f1a5 100644 --- a/src/coreclr/vm/wasm/helpers.cpp +++ b/src/coreclr/vm/wasm/helpers.cpp @@ -6,6 +6,7 @@ #include #include "callhelpers.hpp" #include "stringthunkhash.h" +#include "pregeneratedstringthunks.h" #include "callingconvention.h" #include "cgensys.h" #include "readytorun.h" @@ -979,8 +980,14 @@ namespace StringToWasmSigThunkHash* table = thunkCache; _ASSERTE(table != nullptr && "Wasm thunk cache not initialized. Call InitializeWasmThunkCaches() at EEStartup."); void* thunk; - bool success = table->Lookup(key, &thunk); - return success ? thunk : nullptr; + if (table->Lookup(key, &thunk)) + return thunk; + + PCODE r2rThunk = LookupPregeneratedThunkByString(key); + if (r2rThunk != NULL) + return (void*)(size_t)r2rThunk; + + return nullptr; } void* LookupPortableEntryPointThunk(const char* key) @@ -988,8 +995,14 @@ namespace StringToWasmSigThunkHash* table = portableEntrypointThunkCache; _ASSERTE(table != nullptr && "Wasm portable entrypoint thunk cache not initialized. Call InitializeWasmThunkCaches() at EEStartup."); void* thunk; - bool success = table->Lookup(key, &thunk); - return success ? thunk : nullptr; + if (table->Lookup(key, &thunk)) + return thunk; + + PCODE r2rThunk = LookupPregeneratedThunkByString(key); + if (r2rThunk != NULL) + return (void*)(size_t)r2rThunk; + + return nullptr; } // This is a simple signature computation routine for signatures currently supported in the wasm environment. From fd87e4c6b73a348c5b831ec05c4ca5dcc37fac8c Mon Sep 17 00:00:00 2001 From: David Wrighton Date: Mon, 27 Apr 2026 16:51:36 -0700 Subject: [PATCH 22/43] Deferred R2R thunk resolution for PortableEntryPoints When a MethodDesc's PortableEntryPoint is initialized before the R2R module containing its thunk is loaded, the method is tracked on a per-LoaderAllocator SArray and resolved later when new R2R thunks are injected. - Add TrySetInterpreterThunk CAS-based thunk installation on PortableEntryPoint - Track pending methods per-LoaderAllocator using SArray with NULL-compaction on resolve - Single global lock (s_pendingThunkResolutionLock) protects both the LA registry and per-LA pending arrays, keeping LAs alive during scans - Registration flag on LoaderAllocator avoids duplicate list scans - Unregistration in LoaderAllocator::Destroy for correct collectible cleanup - LookupThunk/LookupPortableEntryPointThunk now also check R2R thunk hash - Remove stale WASM-TODO comments Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/coreclr/vm/ceemain.cpp | 4 + src/coreclr/vm/loaderallocator.cpp | 29 ++++ src/coreclr/vm/loaderallocator.hpp | 20 +++ src/coreclr/vm/method.cpp | 12 +- src/coreclr/vm/precode_portable.hpp | 10 ++ src/coreclr/vm/pregeneratedstringthunks.cpp | 149 ++++++++++++++++++++ src/coreclr/vm/pregeneratedstringthunks.h | 18 +++ src/coreclr/vm/wasm/helpers.cpp | 10 -- src/coreclr/vm/wasm/helpers.hpp | 6 + 9 files changed, 244 insertions(+), 14 deletions(-) diff --git a/src/coreclr/vm/ceemain.cpp b/src/coreclr/vm/ceemain.cpp index b17a9a7510536b..ccc52ae2c0970d 100644 --- a/src/coreclr/vm/ceemain.cpp +++ b/src/coreclr/vm/ceemain.cpp @@ -680,6 +680,10 @@ void EEStartupHelper() InitializePregeneratedStringThunkHash(); +#ifdef FEATURE_PORTABLE_ENTRYPOINTS + InitializePendingThunkResolutionLock(); +#endif // FEATURE_PORTABLE_ENTRYPOINTS + #ifdef TARGET_WASM InitializeWasmThunkCaches(); #endif // TARGET_WASM diff --git a/src/coreclr/vm/loaderallocator.cpp b/src/coreclr/vm/loaderallocator.cpp index 7cf751d3260593..4e48ffd14974e4 100644 --- a/src/coreclr/vm/loaderallocator.cpp +++ b/src/coreclr/vm/loaderallocator.cpp @@ -17,6 +17,10 @@ #include "interpexec.h" #endif +#ifdef FEATURE_PORTABLE_ENTRYPOINTS +#include "pregeneratedstringthunks.h" +#endif + //#define ENABLE_LOG_LOADER_ALLOCATOR_CLEANUP 1 #define STUBMANAGER_RANGELIST(stubManager) (stubManager::g_pManager->GetRangeList()) @@ -95,6 +99,10 @@ LoaderAllocator::LoaderAllocator(bool collectible) : m_pUMEntryThunkCache = NULL; #endif // !FEATURE_PORTABLE_ENTRYPOINTS +#ifdef FEATURE_PORTABLE_ENTRYPOINTS + m_registeredForPendingThunkResolution = false; +#endif // FEATURE_PORTABLE_ENTRYPOINTS + m_nLoaderAllocator = InterlockedIncrement64((LONGLONG *)&LoaderAllocator::cLoaderAllocatorsCreated); #ifdef FEATURE_PGO @@ -689,6 +697,10 @@ BOOL LoaderAllocator::Destroy(QCall::LoaderAllocatorHandle pLoaderAllocator) LoaderAllocator::RemoveMemoryToLoaderAllocatorAssociation(pLoaderAllocator); } +#ifdef FEATURE_PORTABLE_ENTRYPOINTS + UnregisterLoaderAllocatorForPendingThunkResolution(pLoaderAllocator); +#endif // FEATURE_PORTABLE_ENTRYPOINTS + // This will probably change for shared code unloading _ASSERTE(pID->GetType() == LAT_Assembly); @@ -2518,4 +2530,21 @@ bool LoaderAllocator::InsertObjectIntoFieldWithLifetimeOfCollectibleLoaderAlloca return result; } +#ifdef FEATURE_PORTABLE_ENTRYPOINTS + +void LoaderAllocator::AddPendingPortableEntryPointThunk(MethodDesc* pMD) +{ + CONTRACTL + { + THROWS; + GC_NOTRIGGER; + MODE_ANY; + } + CONTRACTL_END; + + AddPendingPortableEntryPointThunkUnderLock(this, pMD); +} + +#endif // FEATURE_PORTABLE_ENTRYPOINTS + #endif diff --git a/src/coreclr/vm/loaderallocator.hpp b/src/coreclr/vm/loaderallocator.hpp index 15ad9c726db0ef..2564d233ca0082 100644 --- a/src/coreclr/vm/loaderallocator.hpp +++ b/src/coreclr/vm/loaderallocator.hpp @@ -494,6 +494,15 @@ class LoaderAllocator PTR_AsyncContinuationsManager m_asyncContinuationsManager; +#ifdef FEATURE_PORTABLE_ENTRYPOINTS + // Methods whose PortableEntryPoint was initialized without an R2R-to-interpreter thunk + // because the thunk wasn't yet loaded. When a new R2R module injects string thunks, + // these methods are re-checked and resolved if a thunk is now available. + // Protected by s_pendingThunkResolutionLock (not m_crstLoaderAllocator). + SArray m_pendingPortableEntryPointThunks; + bool m_registeredForPendingThunkResolution; +#endif // FEATURE_PORTABLE_ENTRYPOINTS + #ifndef DACCESS_COMPILE public: @@ -906,6 +915,12 @@ class LoaderAllocator PTR_AsyncContinuationsManager GetAsyncContinuationsManager(); +#ifdef FEATURE_PORTABLE_ENTRYPOINTS + // Add a MethodDesc to the pending list of methods waiting for an R2R-to-interpreter thunk. + // Takes s_pendingThunkResolutionLock internally. + void AddPendingPortableEntryPointThunk(MethodDesc* pMD); +#endif // FEATURE_PORTABLE_ENTRYPOINTS + #ifndef DACCESS_COMPILE public: virtual void RegisterDependentHandleToNativeObjectForCleanup(LADependentHandleToNativeObject *dependentHandle) {}; @@ -914,6 +929,11 @@ class LoaderAllocator #endif friend struct ::cdac_data; +#ifdef FEATURE_PORTABLE_ENTRYPOINTS + friend void AddPendingPortableEntryPointThunkUnderLock(LoaderAllocator*, MethodDesc*); + friend void UnregisterLoaderAllocatorForPendingThunkResolution(LoaderAllocator*); + friend void ResolvePendingPortableEntryPointThunksGlobal(); +#endif // FEATURE_PORTABLE_ENTRYPOINTS }; // class LoaderAllocator template<> diff --git a/src/coreclr/vm/method.cpp b/src/coreclr/vm/method.cpp index 79040c69c9e08b..3a28611824762f 100644 --- a/src/coreclr/vm/method.cpp +++ b/src/coreclr/vm/method.cpp @@ -2949,10 +2949,6 @@ void MethodDesc::SetPortableEntrypointInitialStateForMethod(PortableEntryPoint * MODE_ANY; } CONTRACTL_END; - // WASM-TODO! This only handling the R2R to interpreter case for well known signatures. - // Eventually we will need to handle arbitrary signatures by looking in the loaded list of R2R modules - // as well as recording when we couldn't find something, in case another R2R module might be loaded - // later which has an R2R to interpreter stub for that given signature. void* pPortableEntryPointToInterpreter = GetPortableEntryPointToInterpreterThunk(this); if (pPortableEntryPointToInterpreter != nullptr) @@ -2962,6 +2958,14 @@ void MethodDesc::SetPortableEntrypointInitialStateForMethod(PortableEntryPoint * else { portableEntry->Init(this); + + // The R2R-to-interpreter thunk wasn't found yet. This can happen when an R2R module + // containing the thunk hasn't been loaded yet. Register this method for deferred + // resolution so it gets updated when new R2R thunks are injected. + if (!ContainsGenericVariables()) + { + GetLoaderAllocator()->AddPendingPortableEntryPointThunk(this); + } } // If we find actual code, we will remove this flag, but we want to prefer the interpreter entry point // until then to allow helpers to work for methods that haven't tried to get an entry point yet. diff --git a/src/coreclr/vm/precode_portable.hpp b/src/coreclr/vm/precode_portable.hpp index 6603fd907e3a2a..6f1714b2de86f2 100644 --- a/src/coreclr/vm/precode_portable.hpp +++ b/src/coreclr/vm/precode_portable.hpp @@ -114,6 +114,16 @@ class PortableEntryPoint final ClearFlagsInterlocked(kPrefersInterpreterEntryPoint); } + // Atomically install an interpreter thunk if _pActualCode is still NULL. + // Returns true if the thunk was installed, false if _pActualCode was already set. + bool TrySetInterpreterThunk(void* thunk) + { + LIMITED_METHOD_CONTRACT; + _ASSERTE(IsValid()); + _ASSERTE(thunk != nullptr); + return InterlockedCompareExchangeT(&_pActualCode, thunk, (void*)nullptr) == nullptr; + } + friend struct ::cdac_data; }; template<> diff --git a/src/coreclr/vm/pregeneratedstringthunks.cpp b/src/coreclr/vm/pregeneratedstringthunks.cpp index 78fb92e867c564..f23ba1ea7d2e91 100644 --- a/src/coreclr/vm/pregeneratedstringthunks.cpp +++ b/src/coreclr/vm/pregeneratedstringthunks.cpp @@ -5,6 +5,12 @@ #include "common.h" #include "pregeneratedstringthunks.h" #include "stringthunkhash.h" +#include "loaderallocator.hpp" + +#ifdef FEATURE_PORTABLE_ENTRYPOINTS +#include "wasm/helpers.hpp" +#include "precode_portable.hpp" +#endif #ifndef DACCESS_COMPILE @@ -110,6 +116,9 @@ void ProcessInjectStringThunksFixup(TADDR moduleBase, PCCOR_SIGNATURE pBlob) { // Success. The old table is intentionally leaked - it may still be in use // by concurrent readers via VolatileLoadWithoutBarrier. +#ifdef FEATURE_PORTABLE_ENTRYPOINTS + ResolvePendingPortableEntryPointThunksGlobal(); +#endif // FEATURE_PORTABLE_ENTRYPOINTS return; } @@ -118,4 +127,144 @@ void ProcessInjectStringThunksFixup(TADDR moduleBase, PCCOR_SIGNATURE pBlob) } } +// ===================================================================== +// Pending portable entrypoint thunk resolution +// ===================================================================== +#ifdef FEATURE_PORTABLE_ENTRYPOINTS + +// s_pendingThunkResolutionLock protects BOTH the global LA list AND the +// per-LoaderAllocator m_pendingPortableEntryPointThunks arrays. This avoids +// any lock-ordering issues and keeps LAs alive during the scan (Destroy +// takes the same lock to unregister). + +static CrstStatic s_pendingThunkResolutionLock; +static SArray s_pendingThunkLoaderAllocators; + +void InitializePendingThunkResolutionLock() +{ + WRAPPER_NO_CONTRACT; + s_pendingThunkResolutionLock.Init(CrstLeafLock, CRST_DEFAULT); +} + +void AddPendingPortableEntryPointThunkUnderLock(LoaderAllocator* pLoaderAllocator, MethodDesc* pMD) +{ + CONTRACTL + { + THROWS; + GC_NOTRIGGER; + MODE_ANY; + } + CONTRACTL_END; + + CrstHolder holder(&s_pendingThunkResolutionLock); + + pLoaderAllocator->m_pendingPortableEntryPointThunks.Append(pMD); + + if (!pLoaderAllocator->m_registeredForPendingThunkResolution) + { + s_pendingThunkLoaderAllocators.Append(pLoaderAllocator); + pLoaderAllocator->m_registeredForPendingThunkResolution = true; + } +} + +void UnregisterLoaderAllocatorForPendingThunkResolution(LoaderAllocator* pLoaderAllocator) +{ + CONTRACTL + { + NOTHROW; + GC_NOTRIGGER; + MODE_ANY; + } + CONTRACTL_END; + + CrstHolder holder(&s_pendingThunkResolutionLock); + + if (!pLoaderAllocator->m_registeredForPendingThunkResolution) + return; + + // Remove from the global array by finding and replacing with the last element. + COUNT_T count = s_pendingThunkLoaderAllocators.GetCount(); + for (COUNT_T i = 0; i < count; i++) + { + if (s_pendingThunkLoaderAllocators[i] == pLoaderAllocator) + { + if (i < count - 1) + { + s_pendingThunkLoaderAllocators[i] = s_pendingThunkLoaderAllocators[count - 1]; + } + s_pendingThunkLoaderAllocators.SetCount(count - 1); + break; + } + } + + pLoaderAllocator->m_registeredForPendingThunkResolution = false; + pLoaderAllocator->m_pendingPortableEntryPointThunks.Clear(); +} + +void ResolvePendingPortableEntryPointThunksGlobal() +{ + CONTRACTL + { + THROWS; + GC_NOTRIGGER; + MODE_ANY; + } + CONTRACTL_END; + + CrstHolder holder(&s_pendingThunkResolutionLock); + + COUNT_T laCount = s_pendingThunkLoaderAllocators.GetCount(); + for (COUNT_T laIdx = 0; laIdx < laCount; laIdx++) + { + LoaderAllocator* pLA = s_pendingThunkLoaderAllocators[laIdx]; + SArray& pending = pLA->m_pendingPortableEntryPointThunks; + COUNT_T count = pending.GetCount(); + COUNT_T nullCount = 0; + + for (COUNT_T i = 0; i < count; i++) + { + MethodDesc* pMD = pending[i]; + if (pMD == nullptr) + { + nullCount++; + continue; + } + + void* thunk = GetPortableEntryPointToInterpreterThunk(pMD); + if (thunk != nullptr) + { + PCODE portableEntry = pMD->GetPortableEntryPointIfExists(); + if (portableEntry != (PCODE)NULL) + { + PortableEntryPoint* pep = PortableEntryPoint::ToPortableEntryPoint(portableEntry); + pep->TrySetInterpreterThunk(thunk); + } + pending[i] = nullptr; + nullCount++; + } + } + + // Compact: move non-null entries to the front, then truncate. + if (nullCount > 0 && nullCount < count) + { + COUNT_T dest = 0; + for (COUNT_T src = 0; src < count; src++) + { + if (pending[src] != nullptr) + { + pending[dest] = pending[src]; + dest++; + } + } + pending.SetCount(dest); + } + else if (nullCount == count) + { + pending.Clear(); + } + } +} + +#endif // FEATURE_PORTABLE_ENTRYPOINTS + #endif // !DACCESS_COMPILE diff --git a/src/coreclr/vm/pregeneratedstringthunks.h b/src/coreclr/vm/pregeneratedstringthunks.h index 50fac9cad00f9b..a95d3fc1d25698 100644 --- a/src/coreclr/vm/pregeneratedstringthunks.h +++ b/src/coreclr/vm/pregeneratedstringthunks.h @@ -5,6 +5,8 @@ #include "daccess.h" +class LoaderAllocator; + // Initialize the global pregenerated string thunk hash table. // Must be called during EE startup before any R2R module loading. void InitializePregeneratedStringThunkHash(); @@ -17,3 +19,19 @@ PCODE LookupPregeneratedThunkByString(const char* str); // moduleBase is the base address of the R2R image. // pBlob points to the first byte after the fixup kind byte in the signature. void ProcessInjectStringThunksFixup(TADDR moduleBase, PCCOR_SIGNATURE pBlob); + +#ifdef FEATURE_PORTABLE_ENTRYPOINTS +// Initialize the lock used for pending thunk resolution tracking. +void InitializePendingThunkResolutionLock(); + +// Add a MethodDesc to its LoaderAllocator's pending list under the global lock. +// Registers the LoaderAllocator if not already registered. +void AddPendingPortableEntryPointThunkUnderLock(LoaderAllocator* pLoaderAllocator, MethodDesc* pMD); + +// Unregister a LoaderAllocator from the global pending thunk resolution list. +// Called during LoaderAllocator::Destroy. +void UnregisterLoaderAllocatorForPendingThunkResolution(LoaderAllocator* pLoaderAllocator); + +// After new thunks are injected, resolve pending methods across all registered LoaderAllocators. +void ResolvePendingPortableEntryPointThunksGlobal(); +#endif // FEATURE_PORTABLE_ENTRYPOINTS diff --git a/src/coreclr/vm/wasm/helpers.cpp b/src/coreclr/vm/wasm/helpers.cpp index ad544344d4f1a5..4524642e10403c 100644 --- a/src/coreclr/vm/wasm/helpers.cpp +++ b/src/coreclr/vm/wasm/helpers.cpp @@ -1090,16 +1090,6 @@ namespace #ifdef _DEBUG if (thunk == NULL) { - // WASM-TODO: The R2R compiler will be generating these for all necessary signatures, but the implementation - // will need to be clever here. Notably, R2R files can be dynamically loaded after the initial load, so we can't - // just drop a NULL here if the R2R to interpreter thunk isn't found. Instead, we'll need to keep a list of - // MethodDescs associated with a given signature and as we load R2R files, find the relevant thunks and update the - // PortableEntryPoint fields on them to be the right R2R to intepreter thunk. This list needs to be stored on the - // LoaderAllocator of the associated MethodDesc for proper lifetime management. - - // For debugging purposes, engineers working on R2R for WASM may wish to enable this log to see which signatures - // are missing R2R to interpreter thunks. Once the R2R to interpreter thunk generation and dynamic updating is implemented, - // we'll want to put registration of missing thunks somewhere around here. LOG((LF_STUBS, LL_INFO100000, "WASM R2R to interpreter call missing for key: %s\n", keyBuffer)); } #endif diff --git a/src/coreclr/vm/wasm/helpers.hpp b/src/coreclr/vm/wasm/helpers.hpp index 62b4299708db68..676b6970b40160 100644 --- a/src/coreclr/vm/wasm/helpers.hpp +++ b/src/coreclr/vm/wasm/helpers.hpp @@ -5,3 +5,9 @@ // Forward declaration for explicit initialization void InitializeWasmThunkCaches(); + +class MethodDesc; + +// Look up a pregenerated R2R-to-interpreter thunk for the given MethodDesc. +// Returns NULL if no thunk is available for the method's signature. +void* GetPortableEntryPointToInterpreterThunk(MethodDesc *pMD); From 3caf6dc5e078d5a6d06608c351c6c26169b0b3a9 Mon Sep 17 00:00:00 2001 From: David Wrighton Date: Tue, 28 Apr 2026 11:20:29 -0700 Subject: [PATCH 23/43] Fix size calculation in SignatureMapper --- src/tasks/WasmAppBuilder/coreclr/SignatureMapper.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tasks/WasmAppBuilder/coreclr/SignatureMapper.cs b/src/tasks/WasmAppBuilder/coreclr/SignatureMapper.cs index d495a836c0dc4c..aa93bcaf6112ed 100644 --- a/src/tasks/WasmAppBuilder/coreclr/SignatureMapper.cs +++ b/src/tasks/WasmAppBuilder/coreclr/SignatureMapper.cs @@ -242,7 +242,7 @@ public static int TokenToSlotCount(string token) return 1; int size = int.Parse(token.Substring(1)); - return Math.Max(size / 8, 1); + return Math.Max((size + 7) / 8, 1); } // Legacy single-char overloads — still used by consumers that don't encounter S tokens. From b1065f336b4210135b6f72278bbe8a504d9093b5 Mon Sep 17 00:00:00 2001 From: David Wrighton Date: Tue, 28 Apr 2026 11:36:04 -0700 Subject: [PATCH 24/43] Fix issue where function pointer we're using needs to be offset by TableBase, not by the image base. Co-authored-by: Copilot --- src/coreclr/inc/webcildecoder.h | 2 ++ src/coreclr/vm/jitinterface.cpp | 3 +-- src/coreclr/vm/pregeneratedstringthunks.cpp | 12 ++++++++++-- src/coreclr/vm/pregeneratedstringthunks.h | 2 +- 4 files changed, 14 insertions(+), 5 deletions(-) diff --git a/src/coreclr/inc/webcildecoder.h b/src/coreclr/inc/webcildecoder.h index f91bf891f2d2da..10333cc7c905d2 100644 --- a/src/coreclr/inc/webcildecoder.h +++ b/src/coreclr/inc/webcildecoder.h @@ -207,6 +207,8 @@ class WebcilDecoder CHECK CheckDirectory(IMAGE_DATA_DIRECTORY *pDir, int forbiddenFlags = 0, IsNullOK ok = NULL_NOT_OK) const; TADDR GetDirectoryData(IMAGE_DATA_DIRECTORY *pDir) const; + +public: SSIZE_T GetTableBaseOffset() const { if (m_pHeader == NULL) diff --git a/src/coreclr/vm/jitinterface.cpp b/src/coreclr/vm/jitinterface.cpp index 39c789f8c95b44..ed3c8ed8747c8a 100644 --- a/src/coreclr/vm/jitinterface.cpp +++ b/src/coreclr/vm/jitinterface.cpp @@ -14747,8 +14747,7 @@ BOOL LoadDynamicInfoEntry(Module *currentModule, case READYTORUN_FIXUP_InjectStringThunks: { ReadyToRunInfo * pR2RInfo = currentModule->GetReadyToRunInfo(); - TADDR moduleBase = dac_cast(pR2RInfo->GetImage()->GetBase()); - ProcessInjectStringThunksFixup(moduleBase, pBlob); + ProcessInjectStringThunksFixup(pR2RInfo, pBlob); result = 1; } break; diff --git a/src/coreclr/vm/pregeneratedstringthunks.cpp b/src/coreclr/vm/pregeneratedstringthunks.cpp index f23ba1ea7d2e91..9f13d91be3c784 100644 --- a/src/coreclr/vm/pregeneratedstringthunks.cpp +++ b/src/coreclr/vm/pregeneratedstringthunks.cpp @@ -39,7 +39,7 @@ PCODE LookupPregeneratedThunkByString(const char* str) return NULL; } -void ProcessInjectStringThunksFixup(TADDR moduleBase, PCCOR_SIGNATURE pBlob) +void ProcessInjectStringThunksFixup(ReadyToRunInfo * pR2RInfo, PCCOR_SIGNATURE pBlob) { STANDARD_VM_CONTRACT; @@ -76,6 +76,14 @@ void ProcessInjectStringThunksFixup(TADDR moduleBase, PCCOR_SIGNATURE pBlob) if (!hasNewEntries) return; +#ifdef TARGET_WASM + WebcilDecoder decoder; + decoder.Init(dac_cast(pR2RInfo->GetImage()->GetBase()), pR2RInfo->GetImage()->GetVirtualSize()); + TADDR rvaBase = decoder.GetTableBaseOffset(); +#else + TADDR rvaBase = dac_cast(pR2RInfo->GetImage()->GetBase()); +#endif + // Build a new table with all existing entries plus new ones. StringToThunkHash* newTable = new StringToThunkHash(); @@ -104,7 +112,7 @@ void ProcessInjectStringThunksFixup(TADDR moduleBase, PCCOR_SIGNATURE pBlob) void* unused; if (!newTable->Lookup(str, &unused)) { - void* codeAddr = (void*)(moduleBase + (TADDR)rva); + void* codeAddr = (void*)(rvaBase + (TADDR)rva); newTable->Add(str, codeAddr); } } diff --git a/src/coreclr/vm/pregeneratedstringthunks.h b/src/coreclr/vm/pregeneratedstringthunks.h index a95d3fc1d25698..5da1cb01eeb7cf 100644 --- a/src/coreclr/vm/pregeneratedstringthunks.h +++ b/src/coreclr/vm/pregeneratedstringthunks.h @@ -18,7 +18,7 @@ PCODE LookupPregeneratedThunkByString(const char* str); // Process a READYTORUN_FIXUP_InjectStringThunks fixup, adding new entries to the global hash. // moduleBase is the base address of the R2R image. // pBlob points to the first byte after the fixup kind byte in the signature. -void ProcessInjectStringThunksFixup(TADDR moduleBase, PCCOR_SIGNATURE pBlob); +void ProcessInjectStringThunksFixup(ReadyToRunInfo * pR2RInfo, PCCOR_SIGNATURE pBlob); #ifdef FEATURE_PORTABLE_ENTRYPOINTS // Initialize the lock used for pending thunk resolution tracking. From 3046e4fac42bc709535ee40e3de36aa3320bcc1a Mon Sep 17 00:00:00 2001 From: David Wrighton Date: Tue, 28 Apr 2026 11:55:46 -0700 Subject: [PATCH 25/43] Prevent duplicate pending thunk entries for reused DynamicMethodDescs Add FlagPendingThunkResolution on DynamicMethodDesc to track whether the method is already in the pending thunk resolution list. The flag is set/cleared using interlocked operations under s_pendingThunkResolutionLock, preventing unbounded growth from re-used LCG methods. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/coreclr/vm/method.cpp | 17 ++++++++++++++++- src/coreclr/vm/method.hpp | 12 +++++++++++- src/coreclr/vm/pregeneratedstringthunks.cpp | 10 ++++++++++ 3 files changed, 37 insertions(+), 2 deletions(-) diff --git a/src/coreclr/vm/method.cpp b/src/coreclr/vm/method.cpp index 8bf2ae3b0c3ea7..51bdb6598c1eea 100644 --- a/src/coreclr/vm/method.cpp +++ b/src/coreclr/vm/method.cpp @@ -2950,7 +2950,22 @@ void MethodDesc::SetPortableEntrypointInitialStateForMethod(PortableEntryPoint * // resolution so it gets updated when new R2R thunks are injected. if (!ContainsGenericVariables()) { - GetLoaderAllocator()->AddPendingPortableEntryPointThunk(this); + // DynamicMethodDescs (LCG) can be re-used, so guard against duplicate adds + // with a flag to avoid unbounded growth in the pending list. + bool shouldAdd = true; + if (IsDynamicMethod()) + { + DynamicMethodDesc* pDMD = AsDynamicMethodDesc(); + if (pDMD->HasFlags(DynamicMethodDesc::FlagPendingThunkResolution)) + { + shouldAdd = false; + } + } + + if (shouldAdd) + { + GetLoaderAllocator()->AddPendingPortableEntryPointThunk(this); + } } } // If we find actual code, we will remove this flag, but we want to prefer the interpreter entry point diff --git a/src/coreclr/vm/method.hpp b/src/coreclr/vm/method.hpp index 29b76fd0fede96..471210267a82db 100644 --- a/src/coreclr/vm/method.hpp +++ b/src/coreclr/vm/method.hpp @@ -2935,7 +2935,9 @@ class DynamicMethodDesc : public StoredSigMethodDesc FlagRequiresCOM = 0x00002000, FlagIsLCGMethod = 0x00004000, FlagIsILStub = 0x00008000, - // unused = 0x00010000, +#ifdef FEATURE_PORTABLE_ENTRYPOINTS + FlagPendingThunkResolution = 0x00010000, +#endif // unused = 0x00020000, FlagMask = 0x0003f800, StackArgSizeMask = 0xfffc0000, // native stack arg size for IL stubs @@ -2963,6 +2965,14 @@ class DynamicMethodDesc : public StoredSigMethodDesc { m_dwExtendedFlags = (m_dwExtendedFlags & ~flags); } + void InterlockedSetFlags(DWORD flags) + { + InterlockedOr((LONG*)&m_dwExtendedFlags, (LONG)flags); + } + void InterlockedClearFlags(DWORD flags) + { + InterlockedAnd((LONG*)&m_dwExtendedFlags, (LONG)~flags); + } ILStubType GetILStubType() const { diff --git a/src/coreclr/vm/pregeneratedstringthunks.cpp b/src/coreclr/vm/pregeneratedstringthunks.cpp index 9f13d91be3c784..f37d1c75ae6c9c 100644 --- a/src/coreclr/vm/pregeneratedstringthunks.cpp +++ b/src/coreclr/vm/pregeneratedstringthunks.cpp @@ -168,6 +168,11 @@ void AddPendingPortableEntryPointThunkUnderLock(LoaderAllocator* pLoaderAllocato pLoaderAllocator->m_pendingPortableEntryPointThunks.Append(pMD); + if (pMD->IsDynamicMethod()) + { + pMD->AsDynamicMethodDesc()->InterlockedSetFlags(DynamicMethodDesc::FlagPendingThunkResolution); + } + if (!pLoaderAllocator->m_registeredForPendingThunkResolution) { s_pendingThunkLoaderAllocators.Append(pLoaderAllocator); @@ -249,6 +254,11 @@ void ResolvePendingPortableEntryPointThunksGlobal() } pending[i] = nullptr; nullCount++; + + if (pMD->IsDynamicMethod()) + { + pMD->AsDynamicMethodDesc()->InterlockedClearFlags(DynamicMethodDesc::FlagPendingThunkResolution); + } } } From f7a9600c228e56eb04db195192aab14f41445f3c Mon Sep 17 00:00:00 2001 From: David Wrighton Date: Tue, 28 Apr 2026 13:14:15 -0700 Subject: [PATCH 26/43] Guard pregenerated thunks infrastructure with TARGET_WASM The pregenerated string thunk hash table, lookup, and pending resolution are only used on WASM. Guard them with TARGET_WASM, providing no-op stubs for InitializePregeneratedStringThunkHash and ProcessInjectStringThunksFixup on other platforms so callers remain unchanged. Also adds FlagPendingThunkResolution on DynamicMethodDesc with interlocked set/clear to prevent duplicate pending entries from reused LCG methods. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/coreclr/vm/pregeneratedstringthunks.cpp | 24 ++++++++++++++++----- src/coreclr/vm/pregeneratedstringthunks.h | 4 ++++ 2 files changed, 23 insertions(+), 5 deletions(-) diff --git a/src/coreclr/vm/pregeneratedstringthunks.cpp b/src/coreclr/vm/pregeneratedstringthunks.cpp index f37d1c75ae6c9c..09e7f924859f4e 100644 --- a/src/coreclr/vm/pregeneratedstringthunks.cpp +++ b/src/coreclr/vm/pregeneratedstringthunks.cpp @@ -4,15 +4,15 @@ #include "common.h" #include "pregeneratedstringthunks.h" + +#ifndef DACCESS_COMPILE + +#ifdef TARGET_WASM + #include "stringthunkhash.h" #include "loaderallocator.hpp" - -#ifdef FEATURE_PORTABLE_ENTRYPOINTS #include "wasm/helpers.hpp" #include "precode_portable.hpp" -#endif - -#ifndef DACCESS_COMPILE static StringToThunkHash* s_pPregeneratedStringThunks = nullptr; @@ -285,4 +285,18 @@ void ResolvePendingPortableEntryPointThunksGlobal() #endif // FEATURE_PORTABLE_ENTRYPOINTS +#else // !TARGET_WASM + +void InitializePregeneratedStringThunkHash() +{ + LIMITED_METHOD_CONTRACT; +} + +void ProcessInjectStringThunksFixup(ReadyToRunInfo * pR2RInfo, PCCOR_SIGNATURE pBlob) +{ + LIMITED_METHOD_CONTRACT; +} + +#endif // TARGET_WASM + #endif // !DACCESS_COMPILE diff --git a/src/coreclr/vm/pregeneratedstringthunks.h b/src/coreclr/vm/pregeneratedstringthunks.h index 5da1cb01eeb7cf..266867debbfff1 100644 --- a/src/coreclr/vm/pregeneratedstringthunks.h +++ b/src/coreclr/vm/pregeneratedstringthunks.h @@ -9,13 +9,17 @@ class LoaderAllocator; // Initialize the global pregenerated string thunk hash table. // Must be called during EE startup before any R2R module loading. +// No-op on non-WASM platforms. void InitializePregeneratedStringThunkHash(); +#ifdef TARGET_WASM // Look up a pregenerated thunk by its string key. // Returns NULL if the string is not found in the table. PCODE LookupPregeneratedThunkByString(const char* str); +#endif // TARGET_WASM // Process a READYTORUN_FIXUP_InjectStringThunks fixup, adding new entries to the global hash. +// On non-WASM platforms this is a no-op. // moduleBase is the base address of the R2R image. // pBlob points to the first byte after the fixup kind byte in the signature. void ProcessInjectStringThunksFixup(ReadyToRunInfo * pR2RInfo, PCCOR_SIGNATURE pBlob); From ce2f3f0ca35733f46d34ec31d27f2a8a41016955 Mon Sep 17 00:00:00 2001 From: David Wrighton Date: Tue, 28 Apr 2026 13:25:25 -0700 Subject: [PATCH 27/43] Fail build on unknown multi-field structs in SignatureMapper Instead of logging a message and producing an invalid signature, emit WASM0067 error and return null so the build fails with a clear diagnostic pointing at the missing entry in s_knownStructSizes. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/tasks/WasmAppBuilder/coreclr/SignatureMapper.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/tasks/WasmAppBuilder/coreclr/SignatureMapper.cs b/src/tasks/WasmAppBuilder/coreclr/SignatureMapper.cs index aa93bcaf6112ed..142e9b8c9dd5c5 100644 --- a/src/tasks/WasmAppBuilder/coreclr/SignatureMapper.cs +++ b/src/tasks/WasmAppBuilder/coreclr/SignatureMapper.cs @@ -6,7 +6,6 @@ using System.Linq; using System.Reflection; using System.Text; -using Microsoft.Build.Framework; namespace Microsoft.WebAssembly.Build.Tasks.CoreClr; @@ -97,8 +96,9 @@ internal static class SignatureMapper } else { - log.LogMessage(MessageImportance.High, - $"SignatureMapper: unknown multi-field struct '{fullName}' (fields: {fields.Length}) — size not hardcoded"); + log.Error("WASM0067", + $"SignatureMapper: unknown multi-field struct '{fullName}' (fields: {fields.Length}) — add its size to s_knownStructSizes in SignatureMapper.cs"); + return null; } c = 'S'; From 601ed471fe88f0a38e7b6b20b236c280fb818b3c Mon Sep 17 00:00:00 2001 From: David Wrighton Date: Tue, 28 Apr 2026 13:32:52 -0700 Subject: [PATCH 28/43] Apply formatting patch --- src/coreclr/jit/codegenwasm.cpp | 2 +- src/coreclr/jit/emit.h | 24 ++++++++++++------------ src/coreclr/jit/emitwasm.cpp | 23 +++++++++++++---------- src/coreclr/jit/importercalls.cpp | 4 ++-- src/coreclr/jit/jit.h | 2 +- 5 files changed, 29 insertions(+), 26 deletions(-) diff --git a/src/coreclr/jit/codegenwasm.cpp b/src/coreclr/jit/codegenwasm.cpp index d87aeca5c50342..6e8c5e8974ccf3 100644 --- a/src/coreclr/jit/codegenwasm.cpp +++ b/src/coreclr/jit/codegenwasm.cpp @@ -2483,7 +2483,7 @@ void CodeGen::genCallInstruction(GenTreeCall* call) if (!call->IsHelperCall()) { _ASSERTE(call->callSig == NULL || params.hasAsyncRet == call->callSig->isAsyncCall()); - params.sigInfo = call->callSig; + params.sigInfo = call->callSig; params.isUnmanagedCall = call->IsUnmanaged(); } #endif // DEBUG diff --git a/src/coreclr/jit/emit.h b/src/coreclr/jit/emit.h index ec35a4dbf61c60..1ab2cfb98d1fff 100644 --- a/src/coreclr/jit/emit.h +++ b/src/coreclr/jit/emit.h @@ -485,7 +485,7 @@ struct EmitCallParams bool isJump = false; bool noSafePoint = false; #ifdef TARGET_WASM - bool isUnmanagedCall = false; + bool isUnmanagedCall = false; #endif #ifdef TARGET_WASM CORINFO_WASM_TYPE_SYMBOL_HANDLE wasmSignature = nullptr; @@ -634,19 +634,19 @@ class emitter struct instrDescDebugInfo { - unsigned idNum = 0; - size_t idSize = 0; // size of the instruction descriptor - unsigned idVarRefOffs = 0; // IL offset for LclVar reference - unsigned idVarRefOffs2 = 0; // IL offset for 2nd LclVar reference (in case this is a pair) - size_t idMemCookie = 0; // compile time handle (check idFlags) - GenTreeFlags idFlags = GTF_EMPTY; // for determining type of handle in idMemCookie - bool idFinallyCall = false; // Branch instruction is a call to finally - bool idCatchRet = false; // Instruction is for a catch 'return' + unsigned idNum = 0; + size_t idSize = 0; // size of the instruction descriptor + unsigned idVarRefOffs = 0; // IL offset for LclVar reference + unsigned idVarRefOffs2 = 0; // IL offset for 2nd LclVar reference (in case this is a pair) + size_t idMemCookie = 0; // compile time handle (check idFlags) + GenTreeFlags idFlags = GTF_EMPTY; // for determining type of handle in idMemCookie + bool idFinallyCall = false; // Branch instruction is a call to finally + bool idCatchRet = false; // Instruction is for a catch 'return' #ifdef TARGET_WASM - bool idIsUnmanagedCall = false; // Instruction is for an unmanaged call + bool idIsUnmanagedCall = false; // Instruction is for an unmanaged call #endif - CORINFO_SIG_INFO* idCallSig = nullptr; // Used to report native call site signatures to the EE - BasicBlock* idTargetBlock = nullptr; // Target block for branches + CORINFO_SIG_INFO* idCallSig = nullptr; // Used to report native call site signatures to the EE + BasicBlock* idTargetBlock = nullptr; // Target block for branches #ifdef TARGET_WASM int lclBaseIndex = 0; // Base index of the WASM locals being declared diff --git a/src/coreclr/jit/emitwasm.cpp b/src/coreclr/jit/emitwasm.cpp index 35d9143755bced..3a91252617cafc 100644 --- a/src/coreclr/jit/emitwasm.cpp +++ b/src/coreclr/jit/emitwasm.cpp @@ -206,13 +206,14 @@ void emitter::emitIns_Call(const EmitCallParams& params) unreached(); } - _ASSERTE(m_debugInfoSize > 0); // We always need the idCallSig so we can properly report the call sites to the R2R compiler + _ASSERTE(m_debugInfoSize > 0); // We always need the idCallSig so we can properly report the call sites to the R2R + // compiler if (m_debugInfoSize > 0) { - id->idDebugOnlyInfo()->idCallSig = params.sigInfo; + id->idDebugOnlyInfo()->idCallSig = params.sigInfo; id->idDebugOnlyInfo()->idIsUnmanagedCall = params.isUnmanagedCall; - id->idDebugOnlyInfo()->idMemCookie = (size_t)params.methHnd; // method token - id->idDebugOnlyInfo()->idFlags = GTF_ICON_METHOD_HDL; + id->idDebugOnlyInfo()->idMemCookie = (size_t)params.methHnd; // method token + id->idDebugOnlyInfo()->idFlags = GTF_ICON_METHOD_HDL; } dispIns(id); @@ -856,11 +857,12 @@ size_t emitter::emitOutputInstr(insGroup* ig, instrDesc* id, BYTE** dp) } #endif - if ((ins == INS_call) || (ins == INS_return_call) || (ins == INS_call_indirect) || (ins == INS_return_call_indirect)) + if ((ins == INS_call) || (ins == INS_return_call) || (ins == INS_call_indirect) || + (ins == INS_return_call_indirect)) { CORINFO_SIG_INFO sigInfoLocal; - - CORINFO_SIG_INFO *sigInfoCall = id->idDebugOnlyInfo()->idCallSig; + + CORINFO_SIG_INFO* sigInfoCall = id->idDebugOnlyInfo()->idCallSig; CORINFO_METHOD_HANDLE methodHandle = (CORINFO_METHOD_HANDLE)id->idDebugOnlyInfo()->idMemCookie; if (sigInfoCall == nullptr) @@ -881,10 +883,11 @@ size_t emitter::emitOutputInstr(insGroup* ig, instrDesc* id, BYTE** dp) { sigInfoLocal = *sigInfoCall; } - // Unmanaged calls need to be reported with the unmanaged calling convention so that the R2R compiler can ignore this report - // for the purpose of determining if a call site needs to have a R2R to interpreter thunk generated + // Unmanaged calls need to be reported with the unmanaged calling convention so that the R2R compiler can + // ignore this report for the purpose of determining if a call site needs to have a R2R to interpreter thunk + // generated sigInfoLocal.callConv = CORINFO_CALLCONV_UNMANAGED; - sigInfoCall = &sigInfoLocal; + sigInfoCall = &sigInfoLocal; } emitRecordCallSite(emitCurCodeOffs(*dp), sigInfoCall, methodHandle); } diff --git a/src/coreclr/jit/importercalls.cpp b/src/coreclr/jit/importercalls.cpp index aeb74a2b9d5836..d110a448c78e97 100644 --- a/src/coreclr/jit/importercalls.cpp +++ b/src/coreclr/jit/importercalls.cpp @@ -1106,9 +1106,9 @@ var_types Compiler::impImportCall(OPCODE opcode, // In debug we want to be able to register callsites with the EE. assert(call->AsCall()->callSig == nullptr); #ifdef TARGET_WASM - call->AsCall()->callSig = new (this, CMK_ASTNode) CORINFO_SIG_INFO; + call->AsCall()->callSig = new (this, CMK_ASTNode) CORINFO_SIG_INFO; #else - call->AsCall()->callSig = new (this, CMK_DebugOnly) CORINFO_SIG_INFO; + call->AsCall()->callSig = new (this, CMK_DebugOnly) CORINFO_SIG_INFO; #endif *call->AsCall()->callSig = *sig; #endif diff --git a/src/coreclr/jit/jit.h b/src/coreclr/jit/jit.h index 1f8903d22961d1..5c7f32e258598d 100644 --- a/src/coreclr/jit/jit.h +++ b/src/coreclr/jit/jit.h @@ -344,7 +344,7 @@ typedef ptrdiff_t ssize_t; #define DEBUGARG(x) #endif -#if defined (DEBUG) || defined(TARGET_WASM) +#if defined(DEBUG) || defined(TARGET_WASM) #define INDEBUG_OR_WASM(x) x #else #define INDEBUG_OR_WASM(x) From 165325c23a013f955c358335796167bb2ad3315b Mon Sep 17 00:00:00 2001 From: David Wrighton Date: Fri, 1 May 2026 11:46:10 -0700 Subject: [PATCH 29/43] - Correct the implementation of LowerTypeHandle - Adjust when the interpreter thunks are attached, previously they triggered unsafe recursion in the type loader now they are attached in GetMultiCallableAddr, and when ExternalMethodFixupWorker finishes. - Adjust lock to respect the new type load dependency of the signature walk - This should cover the existing R2R usage, as R2R code does not directly dispatch on virtual functions - It also will cause more resolution of the interpreter thunks than necessary, as the interpreter codepath calls GetMultiCallableAddr often, but that could possibly be tweaked to go down a special path for scenarios where the acquired pointer is directly used to dispatch to more interpreted code. --- src/coreclr/inc/CrstTypes.def | 4 ++ src/coreclr/inc/crsttypes_generated.h | 65 +++++++++++---------- src/coreclr/vm/jitinterface.cpp | 5 +- src/coreclr/vm/method.cpp | 58 ++++++++++++++++-- src/coreclr/vm/method.hpp | 1 + src/coreclr/vm/precode_portable.cpp | 15 ++++- src/coreclr/vm/precode_portable.hpp | 16 ++--- src/coreclr/vm/pregeneratedstringthunks.cpp | 47 ++++++++++++++- src/coreclr/vm/pregeneratedstringthunks.h | 4 ++ src/coreclr/vm/prestub.cpp | 4 ++ src/coreclr/vm/runtimehandles.cpp | 5 ++ src/coreclr/vm/wasm/helpers.cpp | 5 +- 12 files changed, 179 insertions(+), 50 deletions(-) diff --git a/src/coreclr/inc/CrstTypes.def b/src/coreclr/inc/CrstTypes.def index 526bdd6e199037..5a73a52eb37a65 100644 --- a/src/coreclr/inc/CrstTypes.def +++ b/src/coreclr/inc/CrstTypes.def @@ -525,3 +525,7 @@ End Crst CallStubCache AcquiredBefore LoaderHeap End + +Crst PregeneratedStringThunks + AcquiredBefore UnresolvedClassLock +End \ No newline at end of file diff --git a/src/coreclr/inc/crsttypes_generated.h b/src/coreclr/inc/crsttypes_generated.h index c52fcd9eb65e2e..d6720d9ee42ac3 100644 --- a/src/coreclr/inc/crsttypes_generated.h +++ b/src/coreclr/inc/crsttypes_generated.h @@ -88,37 +88,38 @@ enum CrstType CrstPgoData = 70, CrstPinnedByrefValidation = 71, CrstPinnedHeapHandleTable = 72, - CrstProfilerGCRefDataFreeList = 73, - CrstProfilingAPIStatus = 74, - CrstRCWCache = 75, - CrstRCWCleanupList = 76, - CrstReadyToRunEntryPointToMethodDescMap = 77, - CrstReflection = 78, - CrstReJITGlobalRequest = 79, - CrstSigConvert = 80, - CrstSingleUseLock = 81, - CrstStressLog = 82, - CrstStubCache = 83, - CrstStubDispatchCache = 84, - CrstSyncBlockCache = 85, - CrstSyncHashLock = 86, - CrstSystemDomain = 87, - CrstSystemDomainDelayedUnloadList = 88, - CrstThreadIdDispenser = 89, - CrstThreadLocalStorageLock = 90, - CrstThreadStore = 91, - CrstTieredCompilation = 92, - CrstTypeEquivalenceMap = 93, - CrstTypeIDMap = 94, - CrstUMEntryThunkCache = 95, - CrstUMEntryThunkFreeListLock = 96, - CrstUniqueStack = 97, - CrstUnresolvedClassLock = 98, - CrstUnwindInfoTablePendingLock = 99, - CrstUnwindInfoTablePublishLock = 100, - CrstVSDIndirectionCellLock = 101, - CrstWrapperTemplate = 102, - kNumberOfCrstTypes = 103 + CrstPregeneratedStringThunks = 73, + CrstProfilerGCRefDataFreeList = 74, + CrstProfilingAPIStatus = 75, + CrstRCWCache = 76, + CrstRCWCleanupList = 77, + CrstReadyToRunEntryPointToMethodDescMap = 78, + CrstReflection = 79, + CrstReJITGlobalRequest = 80, + CrstSigConvert = 81, + CrstSingleUseLock = 82, + CrstStressLog = 83, + CrstStubCache = 84, + CrstStubDispatchCache = 85, + CrstSyncBlockCache = 86, + CrstSyncHashLock = 87, + CrstSystemDomain = 88, + CrstSystemDomainDelayedUnloadList = 89, + CrstThreadIdDispenser = 90, + CrstThreadLocalStorageLock = 91, + CrstThreadStore = 92, + CrstTieredCompilation = 93, + CrstTypeEquivalenceMap = 94, + CrstTypeIDMap = 95, + CrstUMEntryThunkCache = 96, + CrstUMEntryThunkFreeListLock = 97, + CrstUniqueStack = 98, + CrstUnresolvedClassLock = 99, + CrstUnwindInfoTablePendingLock = 100, + CrstUnwindInfoTablePublishLock = 101, + CrstVSDIndirectionCellLock = 102, + CrstWrapperTemplate = 103, + kNumberOfCrstTypes = 104 }; #endif // __CRST_TYPES_INCLUDED @@ -202,6 +203,7 @@ int g_rgCrstLevelMap[] = 3, // CrstPgoData 0, // CrstPinnedByrefValidation 15, // CrstPinnedHeapHandleTable + 7, // CrstPregeneratedStringThunks 0, // CrstProfilerGCRefDataFreeList 14, // CrstProfilingAPIStatus 3, // CrstRCWCache @@ -310,6 +312,7 @@ LPCSTR g_rgCrstNameMap[] = "CrstPgoData", "CrstPinnedByrefValidation", "CrstPinnedHeapHandleTable", + "CrstPregeneratedStringThunks", "CrstProfilerGCRefDataFreeList", "CrstProfilingAPIStatus", "CrstRCWCache", diff --git a/src/coreclr/vm/jitinterface.cpp b/src/coreclr/vm/jitinterface.cpp index ed3c8ed8747c8a..2fdc9d907bf087 100644 --- a/src/coreclr/vm/jitinterface.cpp +++ b/src/coreclr/vm/jitinterface.cpp @@ -11131,7 +11131,7 @@ PCODE CEECodeGenInfo::getHelperFtnStatic(CorInfoHelpFunc ftnNum) { // CoreLib calls newobj helpers via calli. Give these helpers a MethodDesc // so the interpreter can find the method signature for the call cookie. - portableEntryPoint->Init((void*)pfnHelper, CoreLibBinder::GetMethod(METHOD__RUNTIME_HELPERS__NEWOBJ_HELPER_DUMMY)); + portableEntryPoint->Init_WithNativeCode((void*)pfnHelper, CoreLibBinder::GetMethod(METHOD__RUNTIME_HELPERS__NEWOBJ_HELPER_DUMMY)); } else { @@ -11145,6 +11145,9 @@ PCODE CEECodeGenInfo::getHelperFtnStatic(CorInfoHelpFunc ftnNum) } else { +#ifdef FEATURE_PORTABLE_ENTRYPOINTS + MethodDesc::EnsurePortableEntryPointIsCallableFromR2R(pfnHelper); +#endif // FEATURE_PORTABLE_ENTRYPOINTS VolatileStore(&hlpFuncEntryPoints[ftnNum], pfnHelper); } } diff --git a/src/coreclr/vm/method.cpp b/src/coreclr/vm/method.cpp index 51bdb6598c1eea..69e9962fca4ce1 100644 --- a/src/coreclr/vm/method.cpp +++ b/src/coreclr/vm/method.cpp @@ -2129,6 +2129,9 @@ PCODE MethodDesc::GetMultiCallableAddrOfCode(CORINFO_ACCESS_FLAGS accessFlags /* // We have to allocate funcptr stub ret = GetLoaderAllocator()->GetFuncPtrStubs()->GetFuncPtrStub(this); } +#ifdef FEATURE_PORTABLE_ENTRYPOINTS + MethodDesc::EnsurePortableEntryPointIsCallableFromR2R(ret); +#endif // FEATURE_PORTABLE_ENTRYPOINTS return ret; } @@ -2926,6 +2929,37 @@ PCODE MethodDesc::GetPortableEntryPointIfExists() return GetTemporaryEntryPointIfExists(); } +// Prepare a portable entry point to be callable from R2R code. This doesn't necessarily +// fill in the native code slot, but if it is possible to do so it will. +// This must be called before any R2R code may call the target method. +// +// Currently this is implemented by calling this in GetMultiCallableAddrOfCode +// which works because current R2R codegen doesn't actually do direct vtable dispatch +// If/When we fix that, we'll have to figure out the best way to ensure this is called +// for virtual dispatches as well. +void MethodDesc::EnsurePortableEntryPointIsCallableFromR2R(PCODE entryPoint) +{ + WRAPPER_NO_CONTRACT; + + PortableEntryPoint *pep = PortableEntryPoint::ToPortableEntryPoint(entryPoint); + if (pep->HasNativeCode()) + { + return; + } + + MethodDesc* pMD = PortableEntryPoint::GetMethodDesc(entryPoint); + void* pPortableEntryPointToInterpreter = GetPortableEntryPointToInterpreterThunk(pMD); + if (pPortableEntryPointToInterpreter != nullptr) + { + pep->TrySetInterpreterThunk(pPortableEntryPointToInterpreter); + } + else + { + _ASSERTE(!pMD->ContainsGenericVariables()); + pMD->GetLoaderAllocator()->AddPendingPortableEntryPointThunk(pMD); + } +} + void MethodDesc::SetPortableEntrypointInitialStateForMethod(PortableEntryPoint *portableEntry) { CONTRACTL @@ -2935,11 +2969,28 @@ void MethodDesc::SetPortableEntrypointInitialStateForMethod(PortableEntryPoint * MODE_ANY; } CONTRACTL_END; + // Dynamic methods are often directly exposed as function pointers, so we want to intialize those early immediately. + if (!IsDynamicMethod()) + { + // Otherwise, we want to do lazy initialization of the portable entry point if it wasn't already initialized to some exact target + if (portableEntry->HasNativeCodeUnchecked()) + { + void* pPortableEntryPointToInterpreter = GetPortableEntryPointToInterpreterThunk(this); + _ASSERTE(pPortableEntryPointToInterpreter != nullptr); + portableEntry->Init_WithInterpreterThunk(pPortableEntryPointToInterpreter, this); + } + else + { + portableEntry->Init(this); + } + return; + } + void* pPortableEntryPointToInterpreter = GetPortableEntryPointToInterpreterThunk(this); if (pPortableEntryPointToInterpreter != nullptr) { - portableEntry->Init(pPortableEntryPointToInterpreter, this); + portableEntry->Init_WithInterpreterThunk(pPortableEntryPointToInterpreter, this); } else { @@ -2953,7 +3004,7 @@ void MethodDesc::SetPortableEntrypointInitialStateForMethod(PortableEntryPoint * // DynamicMethodDescs (LCG) can be re-used, so guard against duplicate adds // with a flag to avoid unbounded growth in the pending list. bool shouldAdd = true; - if (IsDynamicMethod()) + if (IsDynamicMethod()) // TODO this check is now redundant with the one above. { DynamicMethodDesc* pDMD = AsDynamicMethodDesc(); if (pDMD->HasFlags(DynamicMethodDesc::FlagPendingThunkResolution)) @@ -2968,9 +3019,6 @@ void MethodDesc::SetPortableEntrypointInitialStateForMethod(PortableEntryPoint * } } } - // If we find actual code, we will remove this flag, but we want to prefer the interpreter entry point - // until then to allow helpers to work for methods that haven't tried to get an entry point yet. - portableEntry->SetPrefersInterpreterEntryPoint(); } void MethodDesc::ResetPortableEntryPoint() diff --git a/src/coreclr/vm/method.hpp b/src/coreclr/vm/method.hpp index 1326241e919ec7..fe6f4f69fe8800 100644 --- a/src/coreclr/vm/method.hpp +++ b/src/coreclr/vm/method.hpp @@ -1621,6 +1621,7 @@ class MethodDesc void ResetPortableEntryPoint(); void SetPortableEntrypointInitialStateForMethod(PortableEntryPoint *portableEntry); + static void EnsurePortableEntryPointIsCallableFromR2R(PCODE entryPoint); #endif // FEATURE_PORTABLE_ENTRYPOINTS //******************************************************************************* diff --git a/src/coreclr/vm/precode_portable.cpp b/src/coreclr/vm/precode_portable.cpp index 934b5fb3c1e6ba..0ffdaa130a8591 100644 --- a/src/coreclr/vm/precode_portable.cpp +++ b/src/coreclr/vm/precode_portable.cpp @@ -117,7 +117,7 @@ void PortableEntryPoint::Init(MethodDesc* pMD) _pActualCode = NULL; _pMD = pMD; _pInterpreterData = NULL; - _flags = kNone; + _flags = kPrefersInterpreterEntryPoint; INDEBUG(_canary = CANARY_VALUE); } @@ -132,7 +132,18 @@ void PortableEntryPoint::Init(void* nativeEntryPoint) INDEBUG(_canary = CANARY_VALUE); } -void PortableEntryPoint::Init(void* nativeEntryPoint, MethodDesc* pMD) +void PortableEntryPoint::Init_WithInterpreterThunk(void* nativeEntryPoint, MethodDesc* pMD) +{ + LIMITED_METHOD_CONTRACT; + _ASSERTE(pMD != NULL); + _pActualCode = nativeEntryPoint; + _pMD = pMD; + _pInterpreterData = NULL; + _flags = kPrefersInterpreterEntryPoint; + INDEBUG(_canary = CANARY_VALUE); +} + +void PortableEntryPoint::Init_WithNativeCode(void* nativeEntryPoint, MethodDesc* pMD) { LIMITED_METHOD_CONTRACT; _ASSERTE(pMD != NULL); diff --git a/src/coreclr/vm/precode_portable.hpp b/src/coreclr/vm/precode_portable.hpp index 6f1714b2de86f2..d910342e851ae3 100644 --- a/src/coreclr/vm/precode_portable.hpp +++ b/src/coreclr/vm/precode_portable.hpp @@ -51,7 +51,8 @@ class PortableEntryPoint final public: void Init(MethodDesc* pMD); void Init(void* nativeEntryPoint); - void Init(void* nativeEntryPoint, MethodDesc* pMD); + void Init_WithInterpreterThunk(void* nativeEntryPoint, MethodDesc* pMD); + void Init_WithNativeCode(void* nativeEntryPoint, MethodDesc* pMD); // Check if the entry point represents a method with the UnmanagedCallersOnly attribute. // If it does, update the entry point to point to the UnmanagedCallersOnly thunk if not @@ -73,6 +74,13 @@ class PortableEntryPoint final return _pActualCode != nullptr; } + // This api can be used on a PortableEntryPoint which has not yet been initted + bool HasNativeCodeUnchecked() const + { + LIMITED_METHOD_CONTRACT; + return _pActualCode != nullptr; + } + bool IsPreparedForNativeCall() const { LIMITED_METHOD_CONTRACT; @@ -101,12 +109,6 @@ class PortableEntryPoint final InterlockedAnd(reinterpret_cast(&_flags), static_cast(~flags)); } public: - void SetPrefersInterpreterEntryPoint() - { - LIMITED_METHOD_CONTRACT; - _ASSERTE(IsValid()); - SetFlagsInterlocked(kPrefersInterpreterEntryPoint); - } void ClearPrefersInterpreterEntryPoint() { LIMITED_METHOD_CONTRACT; diff --git a/src/coreclr/vm/pregeneratedstringthunks.cpp b/src/coreclr/vm/pregeneratedstringthunks.cpp index 09e7f924859f4e..c3f905323a40e5 100644 --- a/src/coreclr/vm/pregeneratedstringthunks.cpp +++ b/src/coreclr/vm/pregeneratedstringthunks.cpp @@ -151,7 +151,41 @@ static SArray s_pendingThunkLoaderAllocators; void InitializePendingThunkResolutionLock() { WRAPPER_NO_CONTRACT; - s_pendingThunkResolutionLock.Init(CrstLeafLock, CRST_DEFAULT); + s_pendingThunkResolutionLock.Init(CrstPregeneratedStringThunks, CRST_DEFAULT); +} + +void ClearPendingThunkResolutionUnderLock(DynamicMethodDesc* pMD) +{ + CONTRACTL + { + THROWS; + GC_NOTRIGGER; + MODE_ANY; + } + CONTRACTL_END; + + CrstHolder holder(&s_pendingThunkResolutionLock); + + // Clear the pending thunk resolution flag for this method. + // This is necessary so that once a MethodDesc is no longer in use and gets recycled for a new method, it will not + // mistakenly be treated as needing a resolution. This must be done under the pendingThunkResolutionLock to avoid + // races with the ResolvePendingPortableEntryPointThunksGlobal loop. + pMD->InterlockedClearFlags(DynamicMethodDesc::FlagPendingThunkResolution); +} + +void PortableEntrypointThunkProcessingReady() +{ + CONTRACTL + { + THROWS; + GC_NOTRIGGER; + MODE_ANY; + } + CONTRACTL_END; + + // This is called once the EE is ready to process thunks (i.e. after the first R2R module is loaded and ProcessInjectStringThunksFixup can be called). + // At this point we can resolve any pending thunks that were added before we were ready. + ResolvePendingPortableEntryPointThunksGlobal(); } void AddPendingPortableEntryPointThunkUnderLock(LoaderAllocator* pLoaderAllocator, MethodDesc* pMD) @@ -243,6 +277,17 @@ void ResolvePendingPortableEntryPointThunksGlobal() continue; } + if (pMD->IsDynamicMethod()) + { + if (!pMD->AsDynamicMethodDesc()->HasFlags(DynamicMethodDesc::FlagPendingThunkResolution)) + { + // This can happen if the method was GC'd and its slot reused for a new method. Clear the entry so we don't repeatedly check it. + pending[i] = nullptr; + nullCount++; + continue; + } + } + void* thunk = GetPortableEntryPointToInterpreterThunk(pMD); if (thunk != nullptr) { diff --git a/src/coreclr/vm/pregeneratedstringthunks.h b/src/coreclr/vm/pregeneratedstringthunks.h index 266867debbfff1..0f7a2d3ee92e8b 100644 --- a/src/coreclr/vm/pregeneratedstringthunks.h +++ b/src/coreclr/vm/pregeneratedstringthunks.h @@ -6,6 +6,8 @@ #include "daccess.h" class LoaderAllocator; +class MethodDesc; +class DynamicMethodDesc; // Initialize the global pregenerated string thunk hash table. // Must be called during EE startup before any R2R module loading. @@ -32,6 +34,8 @@ void InitializePendingThunkResolutionLock(); // Registers the LoaderAllocator if not already registered. void AddPendingPortableEntryPointThunkUnderLock(LoaderAllocator* pLoaderAllocator, MethodDesc* pMD); +void ClearPendingThunkResolutionUnderLock(DynamicMethodDesc* pMD); + // Unregister a LoaderAllocator from the global pending thunk resolution list. // Called during LoaderAllocator::Destroy. void UnregisterLoaderAllocatorForPendingThunkResolution(LoaderAllocator* pLoaderAllocator); diff --git a/src/coreclr/vm/prestub.cpp b/src/coreclr/vm/prestub.cpp index c5b01c62128233..a26f2086ec9a98 100644 --- a/src/coreclr/vm/prestub.cpp +++ b/src/coreclr/vm/prestub.cpp @@ -2982,6 +2982,10 @@ EXTERN_C PCODE STDCALL ExternalMethodFixupWorker(TransitionBlock * pTransitionBl } } +#ifdef FEATURE_PORTABLE_ENTRYPOINTS + MethodDesc::EnsurePortableEntryPointIsCallableFromR2R(pCode); +#endif + // Force a GC on every jit if the stress level is high enough GCStress::MaybeTrigger(); if (g_externalMethodFixupTraceActiveCount > 0) diff --git a/src/coreclr/vm/runtimehandles.cpp b/src/coreclr/vm/runtimehandles.cpp index da8093d38779e7..1e1fb0c0222cae 100644 --- a/src/coreclr/vm/runtimehandles.cpp +++ b/src/coreclr/vm/runtimehandles.cpp @@ -31,6 +31,7 @@ #include "castcache.h" #include "encee.h" #include "finalizerthread.h" +#include "pregeneratedstringthunks.h" extern "C" BOOL QCALLTYPE MdUtf8String_EqualsCaseInsensitive(LPCUTF8 szLhs, LPCUTF8 szRhs, INT32 stringNumBytes) { @@ -1798,6 +1799,10 @@ extern "C" void QCALLTYPE RuntimeMethodHandle_Destroy(MethodDesc * pMethod) DynamicMethodDesc* pDynamicMethodDesc = pMethod->AsDynamicMethodDesc(); { +#if defined(FEATURE_PORTABLE_ENTRYPOINTS) + ClearPendingThunkResolutionUnderLock(pDynamicMethodDesc); +#endif + GCX_COOP(); // Destroy should be called only if the managed part is gone. diff --git a/src/coreclr/vm/wasm/helpers.cpp b/src/coreclr/vm/wasm/helpers.cpp index 4524642e10403c..bdd80e92185fc1 100644 --- a/src/coreclr/vm/wasm/helpers.cpp +++ b/src/coreclr/vm/wasm/helpers.cpp @@ -777,11 +777,10 @@ namespace ConvertResult LowerTypeHandle(TypeHandle th) { uint32_t size = th.GetSize(); + CorElementType elemType = th.GetSignatureCorElementType(); - if (th.IsTypeDesc() || !th.AsMethodTable()->IsValueType()) + if (elemType != ELEMENT_TYPE_VALUETYPE) { - // Non-valuetype or TypeDesc — fall through to element type mapping - CorElementType elemType = th.GetSignatureCorElementType(); switch (elemType) { case ELEMENT_TYPE_I4: case ELEMENT_TYPE_U4: From 81c32b15ac5d16bc0a8a7d3ee9502ded1a99ecf4 Mon Sep 17 00:00:00 2001 From: David Wrighton Date: Fri, 1 May 2026 16:34:11 -0700 Subject: [PATCH 30/43] - Fix WasmLowering to properly handle an unmanaged callers only - Fix dependency generation for InterpreterToR2R thunks - Both for the WasmTypeNode of the thunk - And for referencing the WasmInterpreterToR2R thunks - Fix InjectStringThunksSignature to use a table index relative to the tableBase. Add a new reloc to make that possible --- .../Compiler/DependencyAnalysis/ObjectDataBuilder.cs | 1 + .../Common/Compiler/DependencyAnalysis/Relocation.cs | 4 ++++ .../Common/Compiler/ObjectWriter/WasmObjectWriter.cs | 6 ++++-- src/coreclr/tools/Common/JitInterface/WasmLowering.cs | 8 ++++++-- .../ReadyToRun/InjectStringThunksSignature.cs | 2 +- .../ReadyToRun/WasmInterpreterToR2RThunkNode.cs | 4 +++- .../JitInterface/CorInfoImpl.ReadyToRun.cs | 5 +++-- 7 files changed, 22 insertions(+), 8 deletions(-) diff --git a/src/coreclr/tools/Common/Compiler/DependencyAnalysis/ObjectDataBuilder.cs b/src/coreclr/tools/Common/Compiler/DependencyAnalysis/ObjectDataBuilder.cs index 6018d69f5dda0e..91e57b3b29cca3 100644 --- a/src/coreclr/tools/Common/Compiler/DependencyAnalysis/ObjectDataBuilder.cs +++ b/src/coreclr/tools/Common/Compiler/DependencyAnalysis/ObjectDataBuilder.cs @@ -268,6 +268,7 @@ public void EmitReloc(ISymbolNode symbol, RelocType relocType, int delta = 0) switch (relocType) { case RelocType.WASM_TABLE_INDEX_I32: + case RelocType.WASM_TABLE_INDEX_REL_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 05f2e1213bf73f..3ed1f41e378e9d 100644 --- a/src/coreclr/tools/Common/Compiler/DependencyAnalysis/Relocation.cs +++ b/src/coreclr/tools/Common/Compiler/DependencyAnalysis/Relocation.cs @@ -61,6 +61,7 @@ public enum RelocType WASM_TABLE_INDEX_I64 = 0x208, // Wasm: a table index encoded as a 8-byte uint64, e.g. for storing the "address" of a function into linear memory WASM_MEMORY_ADDR_REL_LEB = 0x209, // Wasm: a relative linear memory index encoded as a 5-byte varuint32. Used as the immediate argument of a load or store instruction, // e.g. in R2R scenarios as an offset from $imageBase + WASM_TABLE_INDEX_REL_I32 = 0x20A, // Wasm: a table index encoded as a 4-byte uint32 relative to the tableBase of the R2R image // // Relocation operators related to TLS access @@ -668,6 +669,7 @@ public static unsafe void WriteValue(RelocType relocType, void* location, long v DwarfHelper.WritePaddedSLEB128(new Span((byte*)location, WASM_PADDED_RELOC_SIZE_32), value); return; case RelocType.WASM_TABLE_INDEX_I32: + case RelocType.WASM_TABLE_INDEX_REL_I32: *(uint*)location = checked((uint)value); return; case RelocType.WASM_TABLE_INDEX_I64: @@ -716,6 +718,7 @@ public static int GetSize(RelocType relocType) RelocType.WASM_MEMORY_ADDR_REL_LEB => WASM_PADDED_RELOC_SIZE_32, RelocType.WASM_MEMORY_ADDR_REL_SLEB => WASM_PADDED_RELOC_SIZE_32, RelocType.WASM_TABLE_INDEX_I32 => 4, + RelocType.WASM_TABLE_INDEX_REL_I32 => 4, RelocType.WASM_TABLE_INDEX_I64 => 8, _ => throw new NotSupportedException(), @@ -780,6 +783,7 @@ public static unsafe long ReadValue(RelocType relocType, void* location) case RelocType.WASM_FUNCTION_INDEX_LEB: case RelocType.WASM_TABLE_INDEX_SLEB: case RelocType.WASM_TABLE_INDEX_I32: + case RelocType.WASM_TABLE_INDEX_REL_I32: case RelocType.WASM_TABLE_INDEX_I64: case RelocType.WASM_TYPE_INDEX_LEB: case RelocType.WASM_GLOBAL_INDEX_LEB: diff --git a/src/coreclr/tools/Common/Compiler/ObjectWriter/WasmObjectWriter.cs b/src/coreclr/tools/Common/Compiler/ObjectWriter/WasmObjectWriter.cs index 07ecd1c268f0d8..0d7a2f283fdabc 100644 --- a/src/coreclr/tools/Common/Compiler/ObjectWriter/WasmObjectWriter.cs +++ b/src/coreclr/tools/Common/Compiler/ObjectWriter/WasmObjectWriter.cs @@ -980,12 +980,14 @@ private unsafe void ResolveRelocations(int sectionIndex, MemoryStream sectionStr case RelocType.WASM_TABLE_INDEX_I32: case RelocType.WASM_TABLE_INDEX_I64: case RelocType.WASM_TABLE_INDEX_SLEB: + case RelocType.WASM_TABLE_INDEX_REL_I32: { string symbolName = reloc.SymbolName.ToString(); int index = _uniqueSymbols[symbolName]; // Here, we are effectively writing a table offset relative to the table_base. - // These will need to be fixed up by the runtime after load by adding __image_function_pointer_base - // TODO-WASM: We need to emit these for fixup with an addend at runtime + // These will need to be fixed up by the runtime after load by adding tableBase + // except for WASM_TABLE_INDEX_REL_I32 and WASM_TABLE_INDEX_SLEB which are relative + // to the start of the table. Relocation.WriteValue(reloc.Type, pData, index); break; } diff --git a/src/coreclr/tools/Common/JitInterface/WasmLowering.cs b/src/coreclr/tools/Common/JitInterface/WasmLowering.cs index 7f630458483acb..6de7f7a80740c1 100644 --- a/src/coreclr/tools/Common/JitInterface/WasmLowering.cs +++ b/src/coreclr/tools/Common/JitInterface/WasmLowering.cs @@ -234,8 +234,7 @@ public static MethodSignature RaiseSignature(WasmSignature wasmSignature, TypeSy MethodSignature result = new MethodSignature(flags, 0, returnType, parameters.ToArray()); - LoweringFlags relowerFlags = isManaged ? LoweringFlags.None : LoweringFlags.IsUnmanagedCallersOnly; - WasmSignature roundtripped = GetSignature(result, relowerFlags); + WasmSignature roundtripped = GetSignature(result, LoweringFlags.None); Debug.Assert(roundtripped.Equals(wasmSignature), $"RaiseSignature roundtrip failed: input='{wasmSignature.SignatureString}', roundtripped='{roundtripped.SignatureString}'"); @@ -287,6 +286,11 @@ public enum LoweringFlags public static WasmSignature GetSignature(MethodSignature signature, LoweringFlags flags) { + if (!flags.HasFlag(LoweringFlags.IsUnmanagedCallersOnly) && signature.Flags.HasFlag(MethodSignatureFlags.UnmanagedCallingConvention)) + { + flags = flags | LoweringFlags.IsUnmanagedCallersOnly; + } + TypeDesc returnType = signature.ReturnType; WasmValueType pointerType = (signature.ReturnType.Context.Target.PointerSize == 4) ? WasmValueType.I32 : WasmValueType.I64; char hiddenParamChar = WasmValueTypeToSigChar(pointerType); diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/InjectStringThunksSignature.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/InjectStringThunksSignature.cs index 7a4a5f37dfd429..e5076a652ba18e 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/InjectStringThunksSignature.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/InjectStringThunksSignature.cs @@ -44,7 +44,7 @@ public override ObjectData GetData(NodeFactory factory, bool relocsOnly = false) // Emit a 4-byte relocation to the stub code. // On WASM, this is a table index; on other platforms, an RVA. RelocType relocType = factory.Target.Architecture == TargetArchitecture.Wasm32 - ? RelocType.WASM_TABLE_INDEX_I32 + ? RelocType.WASM_TABLE_INDEX_REL_I32 : RelocType.IMAGE_REL_BASED_ADDR32NB; builder.EmitReloc(stub, relocType, delta: factory.Target.CodeDelta); } diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/WasmInterpreterToR2RThunkNode.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/WasmInterpreterToR2RThunkNode.cs index e3e29b78955630..34f7218198757d 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/WasmInterpreterToR2RThunkNode.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/WasmInterpreterToR2RThunkNode.cs @@ -34,7 +34,8 @@ public class WasmInterpreterToR2RThunkNode : StringDiscoverableAssemblyStubNode, public override string LookupString => "M" + _wasmSignature.SignatureString; - MethodSignature INodeWithTypeSignature.Signature => WasmLowering.RaiseSignature(_wasmSignature, _context); + private static WasmSignature sigForInterpToR2RThunks = new WasmSignature(new WasmFuncType(new WasmResultType(new WasmValueType[]{WasmValueType.I32, WasmValueType.I32, WasmValueType.I32, WasmValueType.I32}), new WasmResultType(Array.Empty())), "viiii"); + MethodSignature INodeWithTypeSignature.Signature => WasmLowering.RaiseSignature(sigForInterpToR2RThunks, _context); bool INodeWithTypeSignature.IsUnmanagedCallersOnly => false; bool INodeWithTypeSignature.IsAsyncCall => false; bool INodeWithTypeSignature.HasGenericContextArg => false; @@ -72,6 +73,7 @@ protected override DependencyList ComputeNonRelocationBasedDependencies(NodeFact { DependencyList dependencies = base.ComputeNonRelocationBasedDependencies(factory); dependencies.Add(_targetTypeNode, "Wasm interpreter-to-R2R thunk requires target type node"); + dependencies.Add(factory.WasmTypeNode(sigForInterpToR2RThunks), "Wasm interpreter-to-R2R thunk requires type for the function entry point"); return dependencies; } diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/JitInterface/CorInfoImpl.ReadyToRun.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/JitInterface/CorInfoImpl.ReadyToRun.cs index 9b20b7fe09b5a2..d536af27796e2a 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/JitInterface/CorInfoImpl.ReadyToRun.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/JitInterface/CorInfoImpl.ReadyToRun.cs @@ -860,8 +860,6 @@ public void CompileMethod(MethodWithGCInfo methodCodeNodeNeedingCode, Logger log } } } - var compilationResult = CompileMethodInternal(methodCodeNodeNeedingCode, methodIL); - codeGotPublished = true; // For managed methods on Wasm, add an interpreter-to-R2R thunk so the // interpreter can call into this R2R-compiled function. @@ -873,6 +871,9 @@ public void CompileMethod(MethodWithGCInfo methodCodeNodeNeedingCode, Logger log "Interpreter-to-R2R thunk for compiled method"); } + var compilationResult = CompileMethodInternal(methodCodeNodeNeedingCode, methodIL); + codeGotPublished = true; + if (compilationResult == CompilationResult.CompilationRetryRequested && logger.IsVerbose) { logger.Writer.WriteLine($"Info: Method `{MethodBeingCompiled}` triggered recompilation to acquire stable tokens for cross module inline."); From fafce52f0ad30f2f0b5c18bf0165bc60ba943a51 Mon Sep 17 00:00:00 2001 From: David Wrighton Date: Fri, 1 May 2026 16:50:11 -0700 Subject: [PATCH 31/43] Add R2R helper for dispatch to interpreter for portable entrypoints Co-authored-by: Copilot --- src/coreclr/inc/readytorun.h | 4 +++- src/coreclr/nativeaot/Runtime/inc/ModuleHeaders.h | 2 +- .../tools/Common/Internal/Runtime/ModuleHeaders.cs | 2 +- .../Common/Internal/Runtime/ReadyToRunConstants.cs | 1 + .../ReadyToRun/WasmR2RToInterpreterThunkNode.cs | 4 ++-- .../ReadyToRunSignature.cs | 3 +++ src/coreclr/vm/jitinterface.cpp | 13 +++++++++++++ 7 files changed, 24 insertions(+), 5 deletions(-) diff --git a/src/coreclr/inc/readytorun.h b/src/coreclr/inc/readytorun.h index 9fc10f6b29c409..ebcec465ce4e2a 100644 --- a/src/coreclr/inc/readytorun.h +++ b/src/coreclr/inc/readytorun.h @@ -20,7 +20,7 @@ // If you update this, ensure you run `git grep MINIMUM_READYTORUN_MAJOR_VERSION` // and handle pending work. #define READYTORUN_MAJOR_VERSION 18 -#define READYTORUN_MINOR_VERSION 0x0006 +#define READYTORUN_MINOR_VERSION 0x0007 #define MINIMUM_READYTORUN_MAJOR_VERSION 18 @@ -57,6 +57,7 @@ // R2R Version 18.4 adds ThrowArgument, ThrowArgumentOutOfRange, ThrowPlatformNotSupported, and ThrowNotImplemented helpers // R2R Version 18.5 adds READYTORUN_FLAG_STRIPPED_IL_BODIES, READYTORUN_FLAG_STRIPPED_INLINING_INFO, and READYTORUN_FLAG_STRIPPED_DEBUG_INFO flags // R2R Version 18.6 adds READYTORUN_FIXUP_InjectStringThunks for mapping strings to pregenerated code thunks +// R2R Version 18.7 adds READYTORUN_HELPER_R2RToInterpreter struct READYTORUN_CORE_HEADER { @@ -493,6 +494,7 @@ enum ReadyToRunHelper READYTORUN_HELPER_InitClass = 0x116, READYTORUN_HELPER_InitInstClass = 0x117, + READYTORUN_HELPER_R2RToInterpreter = 0x118, }; #include "readytoruninstructionset.h" diff --git a/src/coreclr/nativeaot/Runtime/inc/ModuleHeaders.h b/src/coreclr/nativeaot/Runtime/inc/ModuleHeaders.h index bc1d51dffb34fa..27eac374c5893d 100644 --- a/src/coreclr/nativeaot/Runtime/inc/ModuleHeaders.h +++ b/src/coreclr/nativeaot/Runtime/inc/ModuleHeaders.h @@ -12,7 +12,7 @@ struct ReadyToRunHeaderConstants static const uint32_t Signature = 0x00525452; // 'RTR' static const uint32_t CurrentMajorVersion = 18; - static const uint32_t CurrentMinorVersion = 6; + static const uint32_t CurrentMinorVersion = 7; }; struct ReadyToRunHeader diff --git a/src/coreclr/tools/Common/Internal/Runtime/ModuleHeaders.cs b/src/coreclr/tools/Common/Internal/Runtime/ModuleHeaders.cs index dda2d9a8cec669..fc4acc9c25ca39 100644 --- a/src/coreclr/tools/Common/Internal/Runtime/ModuleHeaders.cs +++ b/src/coreclr/tools/Common/Internal/Runtime/ModuleHeaders.cs @@ -16,7 +16,7 @@ internal struct ReadyToRunHeaderConstants public const uint Signature = 0x00525452; // 'RTR' public const ushort CurrentMajorVersion = 18; - public const ushort CurrentMinorVersion = 6; + public const ushort CurrentMinorVersion = 7; } #if READYTORUN #pragma warning disable 0169 diff --git a/src/coreclr/tools/Common/Internal/Runtime/ReadyToRunConstants.cs b/src/coreclr/tools/Common/Internal/Runtime/ReadyToRunConstants.cs index 974a5e5889e6a4..a68b32c2b6c7d7 100644 --- a/src/coreclr/tools/Common/Internal/Runtime/ReadyToRunConstants.cs +++ b/src/coreclr/tools/Common/Internal/Runtime/ReadyToRunConstants.cs @@ -371,6 +371,7 @@ public enum ReadyToRunHelper InitClass = 0x116, InitInstClass = 0x117, + R2RToInterpreter = 0x118, // ********************************************************************************************** // diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/WasmR2RToInterpreterThunkNode.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/WasmR2RToInterpreterThunkNode.cs index 29dc47530b96d8..809338b05b00e9 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/WasmR2RToInterpreterThunkNode.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/WasmR2RToInterpreterThunkNode.cs @@ -18,7 +18,7 @@ namespace ILCompiler.DependencyAnalysis.ReadyToRun { /// /// A thunk that captures all arguments and dispatches to the interpreter via - /// READYTORUN_HELPER_InitInstClass. This node is string-discoverable so the + /// READYTORUN_HELPER_R2RToInterpreter. This node is string-discoverable so the /// runtime can find it by WasmSignature string at execution time. /// public class WasmR2RToInterpreterThunkNode : StringDiscoverableAssemblyStubNode, INodeWithTypeSignature, ISymbolDefinitionNode, ISortableSymbolNode @@ -54,7 +54,7 @@ public WasmR2RToInterpreterThunkNode(NodeFactory factory, WasmSignature wasmSign _context = factory.TypeSystemContext; _wasmSignature = wasmSignature; _typeNode = factory.WasmTypeNode(wasmSignature); - _helperCell = factory.GetReadyToRunHelperCell(ReadyToRunHelper.InitInstClass); + _helperCell = factory.GetReadyToRunHelperCell(ReadyToRunHelper.R2RToInterpreter); } public override void AppendMangledName(NameMangler nameMangler, Utf8StringBuilder sb) diff --git a/src/coreclr/tools/aot/ILCompiler.Reflection.ReadyToRun/ReadyToRunSignature.cs b/src/coreclr/tools/aot/ILCompiler.Reflection.ReadyToRun/ReadyToRunSignature.cs index b0d94ce430fe2d..2b54e3b9b3950c 100644 --- a/src/coreclr/tools/aot/ILCompiler.Reflection.ReadyToRun/ReadyToRunSignature.cs +++ b/src/coreclr/tools/aot/ILCompiler.Reflection.ReadyToRun/ReadyToRunSignature.cs @@ -2086,6 +2086,9 @@ private void ParseHelper(StringBuilder builder) case ReadyToRunHelper.InitInstClass: builder.Append("INIT_INST_CLASS"); break; + case ReadyToRunHelper.R2RToInterpreter: + builder.Append("R2R_TO_INTERPRETER"); + break; default: throw new BadImageFormatException(helperType.ToString()); diff --git a/src/coreclr/vm/jitinterface.cpp b/src/coreclr/vm/jitinterface.cpp index 2fdc9d907bf087..431aac188fe64b 100644 --- a/src/coreclr/vm/jitinterface.cpp +++ b/src/coreclr/vm/jitinterface.cpp @@ -63,6 +63,10 @@ #include "tailcallhelp.h" #include "patchpointinfo.h" +#if defined(FEATURE_INTERPRETER) && defined(FEATURE_PORTABLE_ENTRYPOINTS) +void ExecuteInterpretedMethodWithArgs_PortableEntryPoint(PCODE portableEntrypoint, TransitionBlock* block, size_t argsSize, int8_t* retBuff); +#endif + // The Stack Overflow probe takes place in the COOPERATIVE_TRANSITION_BEGIN() macro // @@ -14299,6 +14303,15 @@ BOOL LoadDynamicInfoEntry(Module *currentModule, result = (size_t)GetEEFuncEntryPoint(DelayLoad_Helper_ObjObj); break; + case READYTORUN_HELPER_R2RToInterpreter: +#if defined(FEATURE_INTERPRETER) && defined(FEATURE_PORTABLE_ENTRYPOINTS) + result = (size_t)GetEEFuncEntryPoint(ExecuteInterpretedMethodWithArgs_PortableEntryPoint); + break; +#else + STRESS_LOG1(LF_ZAP, LL_WARNING, "READYTORUN_HELPER_R2RToInterpreter unsupported for this target: %d\n", helperNum); + return FALSE; +#endif + default: STRESS_LOG1(LF_ZAP, LL_WARNING, "Unknown READYTORUN_HELPER %d\n", helperNum); _ASSERTE(!"Unknown READYTORUN_HELPER"); From 7675879e028156098574d576820d334953a13ed0 Mon Sep 17 00:00:00 2001 From: David Wrighton Date: Fri, 1 May 2026 17:20:55 -0700 Subject: [PATCH 32/43] Fix codegen for R2RToInterpreterThunk --- .../WasmR2RToInterpreterThunkNode.cs | 35 ++++++++++--------- 1 file changed, 18 insertions(+), 17 deletions(-) diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/WasmR2RToInterpreterThunkNode.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/WasmR2RToInterpreterThunkNode.cs index 809338b05b00e9..403f943de722a6 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/WasmR2RToInterpreterThunkNode.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/WasmR2RToInterpreterThunkNode.cs @@ -104,14 +104,9 @@ protected override void EmitCode(NodeFactory factory, ref Wasm.WasmEmitter instr int argIndex = 0; int argOffset; - // ArgIterator returns offsets relative to the TransitionBlock base, where arguments - // start at OffsetOfArgumentRegisters (== SizeOfTransitionBlock == 8 on Wasm32). - // We place args at argumentsOffset (16-byte aligned), so adjust each offset. - int argOffsetAdjustment = AlignmentHelper.AlignUp(transitionBlock.SizeOfTransitionBlock, 16) - transitionBlock.SizeOfTransitionBlock; - while ((argOffset = argit.GetNextOffset()) != TransitionBlock.InvalidOffset) { - offsets[argIndex] = argOffset + argOffsetAdjustment; + offsets[argIndex] = argOffset; isIndirectArg[argIndex] = argit.IsArgPassedByRef(); argIndex++; } @@ -123,8 +118,13 @@ protected override void EmitCode(NodeFactory factory, ref Wasm.WasmEmitter instr // The arguments area must be 16-byte aligned. The TransitionBlock (8 bytes on Wasm32) // sits before the arguments, so it is 8-byte aligned but not 16-byte aligned. - // Layout from base: [TransitionBlock (8)] [padding to 16-align args] [args...] + // Layout from base: [TransitionBlock (8)] [args...] int argumentsOffset = AlignmentHelper.AlignUp(sizeOfTransitionBlock, 16); + int transitionBlockOffset = argumentsOffset - sizeOfTransitionBlock; + for (int i = 0; i < offsets.Length; i++) + { + offsets[i] += transitionBlockOffset; + } int sizeOfStoredLocals = argumentsOffset + AlignmentHelper.AlignUp(sizeOfArgumentArray, 16); bool hasWasmReturn = _typeNode.Type.Returns.Types.Length > 0; @@ -149,14 +149,14 @@ protected override void EmitCode(NodeFactory factory, ref Wasm.WasmEmitter instr // First 4 bytes (m_ReturnAddress) = 0 expressions.Add(Local.Get(0)); expressions.Add(I32.Const(0)); - expressions.Add(I32.Store(0)); + expressions.Add(I32.Store((ulong)transitionBlockOffset)); // Second 4 bytes (m_StackPointer) = original SP (local 0 + totalAlloc) expressions.Add(Local.Get(0)); expressions.Add(Local.Get(0)); expressions.Add(I32.Const(totalAlloc)); expressions.Add(I32.Add); - expressions.Add(I32.Store(4)); + expressions.Add(I32.Store((ulong)(transitionBlockOffset + 4))); // Store all arguments into the transition block area int wasmLocalIndex = 1; // local 0 is $sp @@ -164,7 +164,7 @@ protected override void EmitCode(NodeFactory factory, ref Wasm.WasmEmitter instr // Handle 'this' pointer — it occupies a wasm local but is not in methodSignature.Length if (hasThis) { - int thisOffset = transitionBlock.ThisOffset + argOffsetAdjustment; + int thisOffset = transitionBlock.ThisOffset + transitionBlockOffset; expressions.Add(Local.Get(0)); expressions.Add(Local.Get(wasmLocalIndex)); expressions.Add(I32.Store((ulong)thisOffset)); @@ -181,14 +181,15 @@ protected override void EmitCode(NodeFactory factory, ref Wasm.WasmEmitter instr { TypeDesc paramType = methodSignature[i]; + int currentOffset = offsets[i]; + if (WasmLowering.IsEmptyStruct(paramType)) { - continue; + expressions.Add(Local.Get(0)); + expressions.Add(I32.Const(0)); + expressions.Add(I32.Store((ulong)currentOffset)); } - - int currentOffset = offsets[i]; - - if (isIndirectArg[i]) + else if (isIndirectArg[i]) { // Indirect struct — copy the exact contents from the incoming pointer int structSize = paramType.GetElementSize().AsInt; @@ -271,9 +272,9 @@ protected override void EmitCode(NodeFactory factory, ref Wasm.WasmEmitter instr int portableEntrypointLocalIndex = _typeNode.Type.Params.Types.Length - 1; expressions.Add(Local.Get(portableEntrypointLocalIndex)); - // arg2: pointer to the collected arguments (base + argumentsOffset) + // arg2: pointer to the collected arguments and transition block (base + transitionBlockOffset) expressions.Add(Local.Get(0)); - expressions.Add(I32.Const(argumentsOffset)); + expressions.Add(I32.Const(transitionBlockOffset)); expressions.Add(I32.Add); // arg3: size of arguments (excluding transition block) From 763c40669aed00bb168b661964d183af453c15ec Mon Sep 17 00:00:00 2001 From: David Wrighton Date: Mon, 4 May 2026 13:41:42 -0700 Subject: [PATCH 33/43] Tweak handling for when EnsurePortableEntryPointIsCallableFromR2R so that it doesn't trigger on unmanaged entrypoints --- src/coreclr/vm/method.cpp | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/coreclr/vm/method.cpp b/src/coreclr/vm/method.cpp index 69e9962fca4ce1..314bf56d106ddf 100644 --- a/src/coreclr/vm/method.cpp +++ b/src/coreclr/vm/method.cpp @@ -2122,6 +2122,9 @@ PCODE MethodDesc::GetMultiCallableAddrOfCode(CORINFO_ACCESS_FLAGS accessFlags /* PCODE ret = TryGetMultiCallableAddrOfCode(accessFlags); +#ifdef FEATURE_PORTABLE_ENTRYPOINTS + _ASSERTE((ret != (PCODE)NULL) && "PortableEntryPoint logic should always cause the TryGetMultiCallableAddrOfCode to return a value"); +#else if (ret == (PCODE)NULL) { GCX_COOP(); @@ -2129,9 +2132,7 @@ PCODE MethodDesc::GetMultiCallableAddrOfCode(CORINFO_ACCESS_FLAGS accessFlags /* // We have to allocate funcptr stub ret = GetLoaderAllocator()->GetFuncPtrStubs()->GetFuncPtrStub(this); } -#ifdef FEATURE_PORTABLE_ENTRYPOINTS - MethodDesc::EnsurePortableEntryPointIsCallableFromR2R(ret); -#endif // FEATURE_PORTABLE_ENTRYPOINTS +#endif return ret; } @@ -2212,6 +2213,12 @@ PCODE MethodDesc::TryGetMultiCallableAddrOfCode(CORINFO_ACCESS_FLAGS accessFlags { entryPoint = (PCODE)PortableEntryPoint::GetActualCode(entryPoint); } + else + { +#ifdef FEATURE_PORTABLE_ENTRYPOINTS + MethodDesc::EnsurePortableEntryPointIsCallableFromR2R(entryPoint); +#endif // FEATURE_PORTABLE_ENTRYPOINTS + } return entryPoint; From d5969b285b0c4f2f87f7ed04376da815e2786578 Mon Sep 17 00:00:00 2001 From: David Wrighton Date: Mon, 4 May 2026 14:49:44 -0700 Subject: [PATCH 34/43] - Update arg iterator usage in thunks for ByRef parameters - Adjust the Pending portable entrypoint thunk logic to be a MethodDesc property not a DynamicMethodDesc property - Handle TypedByReference in LowerTypeHandle Co-authored-by: Copilot --- .../ReadyToRun/TransitionBlock.cs | 2 +- .../ReadyToRun/WasmImportThunk.cs | 2 +- .../WasmInterpreterToR2RThunkNode.cs | 2 +- .../WasmR2RToInterpreterThunkNode.cs | 2 +- src/coreclr/vm/dllimport.cpp | 6 +++ src/coreclr/vm/method.cpp | 52 ++++--------------- src/coreclr/vm/method.hpp | 18 +++++-- src/coreclr/vm/pregeneratedstringthunks.cpp | 35 +++++++------ src/coreclr/vm/wasm/helpers.cpp | 2 +- 9 files changed, 52 insertions(+), 69 deletions(-) diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/TransitionBlock.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/TransitionBlock.cs index 87b7db9e1ebeab..b85e3fa2163b60 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/TransitionBlock.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/TransitionBlock.cs @@ -289,7 +289,7 @@ public bool IsArgPassedByRef(int size) /// Check whether an arg is automatically switched to passing by reference. /// Note that this overload does not handle varargs. This method only works for /// valuetypes - true value types, primitives, enums and TypedReference. - /// The method is only overridden to do something meaningful on X64 and ARM64. + /// The method is only overridden to do something meaningful on X64, ARM64 and WASM. /// /// Type to analyze public virtual bool IsArgPassedByRef(TypeHandle th) diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/WasmImportThunk.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/WasmImportThunk.cs index bdf1553e35ce84..ca66ab87016ac3 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/WasmImportThunk.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/WasmImportThunk.cs @@ -134,7 +134,7 @@ protected override void EmitCode(NodeFactory factory, ref Wasm.WasmEmitter instr while ((argOffset = argit.GetNextOffset()) != TransitionBlock.InvalidOffset) { offsets[argIndex] = argOffset; - isIndirectArg[argIndex] = argit.IsArgPassedByRef(); + isIndirectArg[argIndex] = argit.IsArgPassedByRef() && argit.IsValueType(); argIndex++; } diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/WasmInterpreterToR2RThunkNode.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/WasmInterpreterToR2RThunkNode.cs index 34f7218198757d..4b5e99fc637d20 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/WasmInterpreterToR2RThunkNode.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/WasmInterpreterToR2RThunkNode.cs @@ -102,7 +102,7 @@ protected override void EmitCode(NodeFactory factory, ref Wasm.WasmEmitter instr while ((argOffset = argit.GetNextOffset()) != TransitionBlock.InvalidOffset) { interpOffsets[argIndex] = argOffset - sizeOfTransitionBlock; - isIndirectArg[argIndex] = argit.IsArgPassedByRef(); + isIndirectArg[argIndex] = argit.IsArgPassedByRef() && argit.IsValueType(); argIndex++; } diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/WasmR2RToInterpreterThunkNode.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/WasmR2RToInterpreterThunkNode.cs index 403f943de722a6..b129fa441d64b8 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/WasmR2RToInterpreterThunkNode.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/WasmR2RToInterpreterThunkNode.cs @@ -107,7 +107,7 @@ protected override void EmitCode(NodeFactory factory, ref Wasm.WasmEmitter instr while ((argOffset = argit.GetNextOffset()) != TransitionBlock.InvalidOffset) { offsets[argIndex] = argOffset; - isIndirectArg[argIndex] = argit.IsArgPassedByRef(); + isIndirectArg[argIndex] = argit.IsArgPassedByRef() && argit.IsValueType(); argIndex++; } diff --git a/src/coreclr/vm/dllimport.cpp b/src/coreclr/vm/dllimport.cpp index ccca4a0178983c..8d9c6a6fb847e8 100644 --- a/src/coreclr/vm/dllimport.cpp +++ b/src/coreclr/vm/dllimport.cpp @@ -5913,6 +5913,12 @@ PCODE JitILStub(MethodDesc* pStubMD) // We need an entry point that can be called multiple times pCode = pStubMD->GetMultiCallableAddrOfCode(); } + else + { +#ifdef FEATURE_PORTABLE_ENTRYPOINTS + MethodDesc::EnsurePortableEntryPointIsCallableFromR2R(pStubMD->GetPortableEntryPoint()); +#endif // FEATURE_PORTABLE_ENTRYPOINTS + } return pCode; } diff --git a/src/coreclr/vm/method.cpp b/src/coreclr/vm/method.cpp index 314bf56d106ddf..1d8b6f3d400704 100644 --- a/src/coreclr/vm/method.cpp +++ b/src/coreclr/vm/method.cpp @@ -2976,56 +2976,17 @@ void MethodDesc::SetPortableEntrypointInitialStateForMethod(PortableEntryPoint * MODE_ANY; } CONTRACTL_END; - // Dynamic methods are often directly exposed as function pointers, so we want to intialize those early immediately. - if (!IsDynamicMethod()) - { - // Otherwise, we want to do lazy initialization of the portable entry point if it wasn't already initialized to some exact target - if (portableEntry->HasNativeCodeUnchecked()) - { - void* pPortableEntryPointToInterpreter = GetPortableEntryPointToInterpreterThunk(this); - _ASSERTE(pPortableEntryPointToInterpreter != nullptr); - portableEntry->Init_WithInterpreterThunk(pPortableEntryPointToInterpreter, this); - } - else - { - portableEntry->Init(this); - } - return; - } - - void* pPortableEntryPointToInterpreter = GetPortableEntryPointToInterpreterThunk(this); - - if (pPortableEntryPointToInterpreter != nullptr) + if (!IsDynamicMethod() && portableEntry->HasNativeCodeUnchecked()) { + void* pPortableEntryPointToInterpreter = GetPortableEntryPointToInterpreterThunk(this); + _ASSERTE(pPortableEntryPointToInterpreter != nullptr); portableEntry->Init_WithInterpreterThunk(pPortableEntryPointToInterpreter, this); } else { portableEntry->Init(this); - - // The R2R-to-interpreter thunk wasn't found yet. This can happen when an R2R module - // containing the thunk hasn't been loaded yet. Register this method for deferred - // resolution so it gets updated when new R2R thunks are injected. - if (!ContainsGenericVariables()) - { - // DynamicMethodDescs (LCG) can be re-used, so guard against duplicate adds - // with a flag to avoid unbounded growth in the pending list. - bool shouldAdd = true; - if (IsDynamicMethod()) // TODO this check is now redundant with the one above. - { - DynamicMethodDesc* pDMD = AsDynamicMethodDesc(); - if (pDMD->HasFlags(DynamicMethodDesc::FlagPendingThunkResolution)) - { - shouldAdd = false; - } - } - - if (shouldAdd) - { - GetLoaderAllocator()->AddPendingPortableEntryPointThunk(this); - } - } } + return; } void MethodDesc::ResetPortableEntryPoint() @@ -3486,7 +3447,12 @@ void MethodDesc::ResetCodeEntryPointForEnC() LOG((LF_ENC, LL_INFO100000, "MD::RCEPFENC: this:%p - %s::%s\n", this, m_pszDebugClassName, m_pszDebugMethodName)); #ifdef FEATURE_PORTABLE_ENTRYPOINTS + bool oldEntrypointHadNativeCode = GetPortableEntryPointIfExists() != (PCODE)NULL && PortableEntryPoint::ToPortableEntryPoint(GetPortableEntryPoint())->HasNativeCode(); ResetPortableEntryPoint(); + if (oldEntrypointHadNativeCode) + { + MethodDesc::EnsurePortableEntryPointIsCallableFromR2R(GetPortableEntryPoint()); + } #else // !FEATURE_PORTABLE_ENTRYPOINTS LOG((LF_ENC, LL_INFO100000, "MD::RCEPFENC: HasPrecode():%s, HasNativeCodeSlot():%s\n", (HasPrecode() ? "true" : "false"), (HasNativeCodeSlot() ? "true" : "false"))); diff --git a/src/coreclr/vm/method.hpp b/src/coreclr/vm/method.hpp index fe6f4f69fe8800..f3507ecf30a97e 100644 --- a/src/coreclr/vm/method.hpp +++ b/src/coreclr/vm/method.hpp @@ -1860,6 +1860,16 @@ class MethodDesc void PrepareForUseAsAFunctionPointer(); +#ifdef FEATURE_PORTABLE_ENTRYPOINTS + bool IsPendingThunkResolution() + { + return (VolatileLoad(&m_bFlags4) & enum_flag4_PendingThunkResolution) != 0; + } + void SetPendingThunkResolution(bool isPending) + { + InterlockedUpdateFlags4(enum_flag4_PendingThunkResolution, isPending ? TRUE : FALSE); + } +#endif private: void PrepareForUseAsADependencyOfANativeImageWorker(); @@ -1892,9 +1902,11 @@ class MethodDesc enum_flag4_RequiresStableEntryPoint = 0x02, enum_flag4_TemporaryEntryPointAssigned = 0x04, enum_flag4_EnCAddedMethod = 0x08, +#ifdef FEATURE_PORTABLE_ENTRYPOINTS + enum_flag4_PendingThunkResolution = 0x10, +#endif }; - void InterlockedSetFlags4(BYTE mask, BYTE newValue); BYTE m_bFlags4; // Used to hold more flags WORD m_wSlotNumber; // The slot number of this MethodDesc in the vtable array. @@ -2936,9 +2948,7 @@ class DynamicMethodDesc : public StoredSigMethodDesc FlagRequiresCOM = 0x00002000, FlagIsLCGMethod = 0x00004000, FlagIsILStub = 0x00008000, -#ifdef FEATURE_PORTABLE_ENTRYPOINTS - FlagPendingThunkResolution = 0x00010000, -#endif + // unused = 0x00010000, // unused = 0x00020000, FlagMask = 0x0003f800, StackArgSizeMask = 0xfffc0000, // native stack arg size for IL stubs diff --git a/src/coreclr/vm/pregeneratedstringthunks.cpp b/src/coreclr/vm/pregeneratedstringthunks.cpp index c3f905323a40e5..dbc84d6e28c094 100644 --- a/src/coreclr/vm/pregeneratedstringthunks.cpp +++ b/src/coreclr/vm/pregeneratedstringthunks.cpp @@ -170,7 +170,7 @@ void ClearPendingThunkResolutionUnderLock(DynamicMethodDesc* pMD) // This is necessary so that once a MethodDesc is no longer in use and gets recycled for a new method, it will not // mistakenly be treated as needing a resolution. This must be done under the pendingThunkResolutionLock to avoid // races with the ResolvePendingPortableEntryPointThunksGlobal loop. - pMD->InterlockedClearFlags(DynamicMethodDesc::FlagPendingThunkResolution); + pMD->SetPendingThunkResolution(false); } void PortableEntrypointThunkProcessingReady() @@ -200,11 +200,10 @@ void AddPendingPortableEntryPointThunkUnderLock(LoaderAllocator* pLoaderAllocato CrstHolder holder(&s_pendingThunkResolutionLock); - pLoaderAllocator->m_pendingPortableEntryPointThunks.Append(pMD); - - if (pMD->IsDynamicMethod()) + if (pMD->IsPendingThunkResolution()) { - pMD->AsDynamicMethodDesc()->InterlockedSetFlags(DynamicMethodDesc::FlagPendingThunkResolution); + // Already pending, nothing to do. + return; } if (!pLoaderAllocator->m_registeredForPendingThunkResolution) @@ -212,6 +211,10 @@ void AddPendingPortableEntryPointThunkUnderLock(LoaderAllocator* pLoaderAllocato s_pendingThunkLoaderAllocators.Append(pLoaderAllocator); pLoaderAllocator->m_registeredForPendingThunkResolution = true; } + + pLoaderAllocator->m_pendingPortableEntryPointThunks.Append(pMD); + + pMD->SetPendingThunkResolution(true); } void UnregisterLoaderAllocatorForPendingThunkResolution(LoaderAllocator* pLoaderAllocator) @@ -244,8 +247,8 @@ void UnregisterLoaderAllocatorForPendingThunkResolution(LoaderAllocator* pLoader } } - pLoaderAllocator->m_registeredForPendingThunkResolution = false; - pLoaderAllocator->m_pendingPortableEntryPointThunks.Clear(); + // Don't mark the loader allocator as unregistered, in case there is some degenerate path + // which attempts to register a thunk after this. } void ResolvePendingPortableEntryPointThunksGlobal() @@ -277,15 +280,13 @@ void ResolvePendingPortableEntryPointThunksGlobal() continue; } - if (pMD->IsDynamicMethod()) + if (!pMD->IsPendingThunkResolution()) { - if (!pMD->AsDynamicMethodDesc()->HasFlags(DynamicMethodDesc::FlagPendingThunkResolution)) - { - // This can happen if the method was GC'd and its slot reused for a new method. Clear the entry so we don't repeatedly check it. - pending[i] = nullptr; - nullCount++; - continue; - } + _ASSERTE(pMD->IsDynamicMethod()); + // This can happen if the method was GC'd and its slot reused for a new method. Clear the entry so we don't repeatedly check it. + pending[i] = nullptr; + nullCount++; + continue; } void* thunk = GetPortableEntryPointToInterpreterThunk(pMD); @@ -300,9 +301,9 @@ void ResolvePendingPortableEntryPointThunksGlobal() pending[i] = nullptr; nullCount++; - if (pMD->IsDynamicMethod()) + if (pMD->IsPendingThunkResolution()) { - pMD->AsDynamicMethodDesc()->InterlockedClearFlags(DynamicMethodDesc::FlagPendingThunkResolution); + pMD->SetPendingThunkResolution(false); } } } diff --git a/src/coreclr/vm/wasm/helpers.cpp b/src/coreclr/vm/wasm/helpers.cpp index bdd80e92185fc1..b2c27ac6e0cf96 100644 --- a/src/coreclr/vm/wasm/helpers.cpp +++ b/src/coreclr/vm/wasm/helpers.cpp @@ -779,7 +779,7 @@ namespace uint32_t size = th.GetSize(); CorElementType elemType = th.GetSignatureCorElementType(); - if (elemType != ELEMENT_TYPE_VALUETYPE) + if ((elemType != ELEMENT_TYPE_VALUETYPE) && (elemType != ELEMENT_TYPE_TYPEDBYREF)) { switch (elemType) { From 815b1070162c4ff94c86f5b7cf0346903530d161 Mon Sep 17 00:00:00 2001 From: David Wrighton Date: Mon, 4 May 2026 15:02:50 -0700 Subject: [PATCH 35/43] Feedback --- docs/design/coreclr/botr/clr-abi.md | 96 +------------------ docs/design/coreclr/botr/readytorun-format.md | 90 +++++++++++++++++ .../tools/Common/JitInterface/WasmLowering.cs | 2 + src/coreclr/vm/wasm/helpers.cpp | 2 + .../WasmAppBuilder/coreclr/SignatureMapper.cs | 3 + 5 files changed, 100 insertions(+), 93 deletions(-) diff --git a/docs/design/coreclr/botr/clr-abi.md b/docs/design/coreclr/botr/clr-abi.md index a2a7754765634c..16d9a08dd64914 100644 --- a/docs/design/coreclr/botr/clr-abi.md +++ b/docs/design/coreclr/botr/clr-abi.md @@ -714,9 +714,9 @@ The linear stack pointer `$sp` is the first argument to all methods. At a native A frame pointer, if used, points at the bottom of the "fixed" portion of the stack to facilitate use of Wasm addressing modes, which only allow positive offsets. -Structs are generally passed by-reference, unless they happen to exactly contain a single primitive field (or be a struct exactly containing such a struct). The linear stack provides the backing storage for the by-reference structs. +Arguments and return values are processed via the Type Lowering algorithm below. -Structs are generally returned via hidden buffers, whose address is supplied by the caller and passed just after the managed `this`, or after `$sp` argument when `this` is not present. In such cases the return value of the method is the address of the return value. But if the struct can be passed on the Wasm stack it is returned on the Wasm stack. +If a struct is returned via a hidden buffer, the address is supplied by the caller and passed just after the managed `this`, or after `$sp` argument when `this` is not present. In such cases the return value of the method is the address of the return value. But if the struct can be passed on the Wasm stack it is returned on the Wasm stack per the Type Lowering rules. (TBD: ABI for vector types) @@ -748,97 +748,7 @@ A struct is **not** unwrapped when: struct has padding due to explicit layout or alignment attributes). Structs that cannot be unwrapped are passed by reference. The caller allocates space on the linear -stack and passes a pointer. For return values, the caller provides a hidden return buffer pointer -(see Signature Encoding below). - -### Signature String Encoding - -Every managed method signature is encoded as a compact string that uniquely identifies its -lowered Wasm calling convention. This encoding is shared across three codebases: - -- **crossgen2** (`WasmLowering.GetSignature`): reference implementation, produces the string - during R2R compilation. -- **WasmAppBuilder** (`SignatureMapper`): MSBuild task that generates interpreter-to-native - thunk tables from reflection metadata. -- **CoreCLR runtime** (`helpers.cpp`, `GetSignatureKey`): runtime signature computation for - calli and portable entrypoint thunks. - -The string format is: - -``` - [] [...] ... [p] -``` - -**Return type** (first character): - -| Encoding | Meaning | -|---|---| -| `v` | void return, or empty struct return (no return buffer) | -| `i` | returns `i32` | -| `l` | returns `i64` | -| `f` | returns `f32` | -| `d` | returns `f64` | -| `S` | struct return via hidden buffer, `N` is the struct size in bytes | - -**This pointer** (if the method has a `this` parameter): - -| Encoding | Meaning | -|---|---| -| `T` | `this` pointer (managed instance methods) | - -**Hidden parameters** (inserted between `this` and explicit parameters, in order): - -1. **Generic context** (`i`): present when the method requires an inst method desc or - method table argument. -2. **Async continuation** (`i`): present for async calls. - -Note: the hidden return buffer pointer is **not** encoded in the signature string. Its -presence is implied by the return type being `S` — when the caller sees a struct return, -it knows a hidden retbuf pointer argument is present in the Wasm parameter list. - -**Explicit parameters** (one token per parameter, in declaration order): - -| Encoding | Meaning | -|---|---| -| `i` | `i32` parameter | -| `l` | `i64` parameter | -| `f` | `f32` parameter | -| `d` | `f64` parameter | -| `S` | struct parameter passed by reference, `N` is the struct size in bytes | -| `e` | empty struct parameter — elided from Wasm args but present in the string | - -**Suffix**: - -| Encoding | Meaning | -|---|---| -| `p` | managed call with portable entrypoint (the `&pe` argument is implicit) | -| *(absent)* | unmanaged callers only (reverse P/Invoke) | - -**Prefix** (applied by the caller, not part of the core encoding): - -When storing signature strings in thunk lookup tables, callers prepend a single-character -prefix to distinguish thunk categories: - -| Prefix | Meaning | -|---|---| -| `M` | Calli thunk or interpreter-to-native thunk | -| `I` | Portable entrypoint-to-interpreter thunk | - -**Examples**: - -| Method | Signature string (no prefix) | -|---|---| -| `static void F()` | `vp` | -| `static int F(int x)` | `iip` | -| `void F(int x)` (instance) | `vTip` | -| `static MyStruct F()` where `MyStruct` is 16 bytes | `S16p` | -| `static void F(MyStruct s)` where `MyStruct` is 8 bytes | `vS8p` | -| `static int F(float x, double y)` | `ifdp` | -| `[UnmanagedCallersOnly] static int F(int x)` | `ii` | - -**Slot sizing for structs**: When computing interpreter stack layout, struct parameters -(`S`) consume `max(N / 8, 1)` interpreter stack slots, while all other parameter types -consume exactly 1 slot. +stack and passes a pointer. For return values, the caller provides a hidden return buffer pointer. ### Prolog diff --git a/docs/design/coreclr/botr/readytorun-format.md b/docs/design/coreclr/botr/readytorun-format.md index fee43d3b40bcdb..a54253862324aa 100644 --- a/docs/design/coreclr/botr/readytorun-format.md +++ b/docs/design/coreclr/botr/readytorun-format.md @@ -1001,6 +1001,96 @@ enum ReadyToRunHelper }; ``` +# Wasm Signature String Encoding + +Every managed method signature is encoded as a compact string that uniquely identifies its +lowered Wasm calling convention. This encoding is used in R2R thunk lookup tables and is +shared across three codebases: + +- **crossgen2** (`WasmLowering.GetSignature`): reference implementation, produces the string + during R2R compilation. +- **WasmAppBuilder** (`SignatureMapper`): MSBuild task that generates interpreter-to-native + thunk tables from reflection metadata. +- **CoreCLR runtime** (`helpers.cpp`, `GetSignatureKey`): runtime signature computation for + calli and portable entrypoint thunks. + +The string format is: + +``` + [] [...] ... [p] +``` + +**Return type** (first character): + +| Encoding | Meaning | +|---|---| +| `v` | void return, or empty struct return (no return buffer) | +| `i` | returns `i32` | +| `l` | returns `i64` | +| `f` | returns `f32` | +| `d` | returns `f64` | +| `S` | struct return via hidden buffer, `N` is the struct size in bytes | + +**This pointer** (if the method has a `this` parameter): + +| Encoding | Meaning | +|---|---| +| `T` | `this` pointer (managed instance methods) | + +**Hidden parameters** (inserted between `this` and explicit parameters, in order): + +1. **Generic context** (`i`): present when the method requires an inst method desc or + method table argument. +2. **Async continuation** (`i`): present for async calls. + +Note: the hidden return buffer pointer is **not** encoded in the signature string. Its +presence is implied by the return type being `S` — when the caller sees a struct return, +it knows a hidden retbuf pointer argument is present in the Wasm parameter list. + +**Explicit parameters** (one token per parameter, in declaration order): + +| Encoding | Meaning | +|---|---| +| `i` | `i32` parameter | +| `l` | `i64` parameter | +| `f` | `f32` parameter | +| `d` | `f64` parameter | +| `S` | struct parameter passed by reference, `N` is the struct size in bytes | +| `e` | empty struct parameter — elided from Wasm args but present in the string | + +**Suffix**: + +| Encoding | Meaning | +|---|---| +| `p` | managed call with portable entrypoint (the `&pe` argument is implicit) | +| *(absent)* | unmanaged callers only (reverse P/Invoke) | + +**Prefix** (applied by the caller, not part of the core encoding): + +When storing signature strings in thunk lookup tables, callers prepend a single-character +prefix to distinguish thunk categories: + +| Prefix | Meaning | +|---|---| +| `M` | Calli thunk or interpreter-to-native thunk | +| `I` | Portable entrypoint-to-interpreter thunk | + +**Examples**: + +| Method | Signature string (no prefix) | +|---|---| +| `static void F()` | `vp` | +| `static int F(int x)` | `iip` | +| `void F(int x)` (instance) | `vTip` | +| `static MyStruct F()` where `MyStruct` is 16 bytes | `S16p` | +| `static void F(MyStruct s)` where `MyStruct` is 8 bytes | `vS8p` | +| `static int F(float x, double y)` | `ifdp` | +| `[UnmanagedCallersOnly] static int F(int x)` | `ii` | + +**Slot sizing for structs**: When computing interpreter stack layout, struct parameters +(`S`) consume `max(N / 8, 1)` interpreter stack slots, while all other parameter types +consume exactly 1 slot. + # References [ECMA-335](https://www.ecma-international.org/publications-and-standards/standards/ecma-335) diff --git a/src/coreclr/tools/Common/JitInterface/WasmLowering.cs b/src/coreclr/tools/Common/JitInterface/WasmLowering.cs index 6de7f7a80740c1..ab9857664c6116 100644 --- a/src/coreclr/tools/Common/JitInterface/WasmLowering.cs +++ b/src/coreclr/tools/Common/JitInterface/WasmLowering.cs @@ -243,6 +243,8 @@ public static MethodSignature RaiseSignature(WasmSignature wasmSignature, TypeSy /// /// Gets the Wasm-level signature for a given MethodDesc. + /// The signature string format is documented in docs/design/coreclr/botr/readytorun-format.md + /// (section "Wasm Signature String Encoding"). /// /// Parameters for managed Wasm calls have the following layout: /// i32 (SP), loweredParam0, ..., loweredParamN, i32 (PE entrypoint) diff --git a/src/coreclr/vm/wasm/helpers.cpp b/src/coreclr/vm/wasm/helpers.cpp index b2c27ac6e0cf96..1814c58a25e4d7 100644 --- a/src/coreclr/vm/wasm/helpers.cpp +++ b/src/coreclr/vm/wasm/helpers.cpp @@ -906,6 +906,8 @@ namespace } // Computes the signature key string for a MetaSig. + // The format is documented in docs/design/coreclr/botr/readytorun-format.md + // (section "Wasm Signature String Encoding"). // Returns the total number of characters needed (excluding null terminator). // Only writes characters while pos < maxSize, so the buffer is never overflowed. // Callers should check if the return value >= maxSize and retry with a larger buffer. diff --git a/src/tasks/WasmAppBuilder/coreclr/SignatureMapper.cs b/src/tasks/WasmAppBuilder/coreclr/SignatureMapper.cs index 142e9b8c9dd5c5..db7142945d1ba6 100644 --- a/src/tasks/WasmAppBuilder/coreclr/SignatureMapper.cs +++ b/src/tasks/WasmAppBuilder/coreclr/SignatureMapper.cs @@ -9,6 +9,9 @@ namespace Microsoft.WebAssembly.Build.Tasks.CoreClr; +// Computes Wasm signature strings from reflection metadata. +// The signature string format is documented in docs/design/coreclr/botr/readytorun-format.md +// (section "Wasm Signature String Encoding"). internal static class SignatureMapper { // Hardcoded struct sizes for types that crossgen2 encodes as S. From c233d728700f672efc7986e28a0c71d33ed9f498 Mon Sep 17 00:00:00 2001 From: David Wrighton Date: Tue, 5 May 2026 09:51:48 -0700 Subject: [PATCH 36/43] Follow up on re-running the tooling --- src/coreclr/vm/wasm/callhelpers-reverse.cpp | 56 ++++++--------------- 1 file changed, 14 insertions(+), 42 deletions(-) diff --git a/src/coreclr/vm/wasm/callhelpers-reverse.cpp b/src/coreclr/vm/wasm/callhelpers-reverse.cpp index b61aeb0d7e13fe..38340b5e5b135b 100644 --- a/src/coreclr/vm/wasm/callhelpers-reverse.cpp +++ b/src/coreclr/vm/wasm/callhelpers-reverse.cpp @@ -623,6 +623,19 @@ static void Call_System_Private_CoreLib_System_Exception_InternalPreserveStackTr ExecuteInterpretedMethodFromUnmanaged(MD_System_Private_CoreLib_System_Exception_InternalPreserveStackTrace_I32_I32_RetVoid, (int8_t*)args, sizeof(args), nullptr, (PCODE)&Call_System_Private_CoreLib_System_Exception_InternalPreserveStackTrace_I32_I32_RetVoid); } +static MethodDesc* MD_System_Private_CoreLib_System_StubHelpers_StubHelpers_InvokeArrayContentsConverter_I32_I32_I32_I32_I32_RetVoid = nullptr; +static void Call_System_Private_CoreLib_System_StubHelpers_StubHelpers_InvokeArrayContentsConverter_I32_I32_I32_I32_I32_RetVoid(void * arg0, void * arg1, int32_t arg2, void * arg3, void * arg4) +{ + int64_t args[5] = { (int64_t)arg0, (int64_t)arg1, (int64_t)arg2, (int64_t)arg3, (int64_t)arg4 }; + + // Lazy lookup of MethodDesc for the function export scenario. + if (!MD_System_Private_CoreLib_System_StubHelpers_StubHelpers_InvokeArrayContentsConverter_I32_I32_I32_I32_I32_RetVoid) + { + LookupUnmanagedCallersOnlyMethodByName("System.StubHelpers.StubHelpers, System.Private.CoreLib", "InvokeArrayContentsConverter", &MD_System_Private_CoreLib_System_StubHelpers_StubHelpers_InvokeArrayContentsConverter_I32_I32_I32_I32_I32_RetVoid); + } + ExecuteInterpretedMethodFromUnmanaged(MD_System_Private_CoreLib_System_StubHelpers_StubHelpers_InvokeArrayContentsConverter_I32_I32_I32_I32_I32_RetVoid, (int8_t*)args, sizeof(args), nullptr, (PCODE)&Call_System_Private_CoreLib_System_StubHelpers_StubHelpers_InvokeArrayContentsConverter_I32_I32_I32_I32_I32_RetVoid); +} + static MethodDesc* MD_System_Private_CoreLib_System_Runtime_InteropServices_DynamicInterfaceCastableHelpers_IsInterfaceImplemented_I32_I32_I32_I32_I32_RetVoid = nullptr; static void Call_System_Private_CoreLib_System_Runtime_InteropServices_DynamicInterfaceCastableHelpers_IsInterfaceImplemented_I32_I32_I32_I32_I32_RetVoid(void * arg0, void * arg1, int32_t arg2, void * arg3, void * arg4) { @@ -803,45 +816,6 @@ static int32_t Call_System_Private_CoreLib_System_Runtime_InteropServices_TypeMa return result; } -static MethodDesc* MD_System_Private_CoreLib_System_StubHelpers_StubHelpers_NonBlittableStructureArrayConvertToManaged_I32_I32_I32_I32_I32_RetVoid = nullptr; -static void Call_System_Private_CoreLib_System_StubHelpers_StubHelpers_NonBlittableStructureArrayConvertToManaged_I32_I32_I32_I32_I32_RetVoid(void * arg0, void * arg1, void * arg2, int32_t arg3, void * arg4) -{ - int64_t args[5] = { (int64_t)arg0, (int64_t)arg1, (int64_t)arg2, (int64_t)arg3, (int64_t)arg4 }; - - // Lazy lookup of MethodDesc for the function export scenario. - if (!MD_System_Private_CoreLib_System_StubHelpers_StubHelpers_NonBlittableStructureArrayConvertToManaged_I32_I32_I32_I32_I32_RetVoid) - { - LookupUnmanagedCallersOnlyMethodByName("System.StubHelpers.StubHelpers, System.Private.CoreLib", "NonBlittableStructureArrayConvertToManaged", &MD_System_Private_CoreLib_System_StubHelpers_StubHelpers_NonBlittableStructureArrayConvertToManaged_I32_I32_I32_I32_I32_RetVoid); - } - ExecuteInterpretedMethodFromUnmanaged(MD_System_Private_CoreLib_System_StubHelpers_StubHelpers_NonBlittableStructureArrayConvertToManaged_I32_I32_I32_I32_I32_RetVoid, (int8_t*)args, sizeof(args), nullptr, (PCODE)&Call_System_Private_CoreLib_System_StubHelpers_StubHelpers_NonBlittableStructureArrayConvertToManaged_I32_I32_I32_I32_I32_RetVoid); -} - -static MethodDesc* MD_System_Private_CoreLib_System_StubHelpers_StubHelpers_NonBlittableStructureArrayConvertToUnmanaged_I32_I32_I32_I32_I32_RetVoid = nullptr; -static void Call_System_Private_CoreLib_System_StubHelpers_StubHelpers_NonBlittableStructureArrayConvertToUnmanaged_I32_I32_I32_I32_I32_RetVoid(void * arg0, void * arg1, void * arg2, int32_t arg3, void * arg4) -{ - int64_t args[5] = { (int64_t)arg0, (int64_t)arg1, (int64_t)arg2, (int64_t)arg3, (int64_t)arg4 }; - - // Lazy lookup of MethodDesc for the function export scenario. - if (!MD_System_Private_CoreLib_System_StubHelpers_StubHelpers_NonBlittableStructureArrayConvertToUnmanaged_I32_I32_I32_I32_I32_RetVoid) - { - LookupUnmanagedCallersOnlyMethodByName("System.StubHelpers.StubHelpers, System.Private.CoreLib", "NonBlittableStructureArrayConvertToUnmanaged", &MD_System_Private_CoreLib_System_StubHelpers_StubHelpers_NonBlittableStructureArrayConvertToUnmanaged_I32_I32_I32_I32_I32_RetVoid); - } - ExecuteInterpretedMethodFromUnmanaged(MD_System_Private_CoreLib_System_StubHelpers_StubHelpers_NonBlittableStructureArrayConvertToUnmanaged_I32_I32_I32_I32_I32_RetVoid, (int8_t*)args, sizeof(args), nullptr, (PCODE)&Call_System_Private_CoreLib_System_StubHelpers_StubHelpers_NonBlittableStructureArrayConvertToUnmanaged_I32_I32_I32_I32_I32_RetVoid); -} - -static MethodDesc* MD_System_Private_CoreLib_System_StubHelpers_StubHelpers_NonBlittableStructureArrayFree_I32_I32_I32_I32_I32_RetVoid = nullptr; -static void Call_System_Private_CoreLib_System_StubHelpers_StubHelpers_NonBlittableStructureArrayFree_I32_I32_I32_I32_I32_RetVoid(void * arg0, void * arg1, void * arg2, int32_t arg3, void * arg4) -{ - int64_t args[5] = { (int64_t)arg0, (int64_t)arg1, (int64_t)arg2, (int64_t)arg3, (int64_t)arg4 }; - - // Lazy lookup of MethodDesc for the function export scenario. - if (!MD_System_Private_CoreLib_System_StubHelpers_StubHelpers_NonBlittableStructureArrayFree_I32_I32_I32_I32_I32_RetVoid) - { - LookupUnmanagedCallersOnlyMethodByName("System.StubHelpers.StubHelpers, System.Private.CoreLib", "NonBlittableStructureArrayFree", &MD_System_Private_CoreLib_System_StubHelpers_StubHelpers_NonBlittableStructureArrayFree_I32_I32_I32_I32_I32_RetVoid); - } - ExecuteInterpretedMethodFromUnmanaged(MD_System_Private_CoreLib_System_StubHelpers_StubHelpers_NonBlittableStructureArrayFree_I32_I32_I32_I32_I32_RetVoid, (int8_t*)args, sizeof(args), nullptr, (PCODE)&Call_System_Private_CoreLib_System_StubHelpers_StubHelpers_NonBlittableStructureArrayFree_I32_I32_I32_I32_I32_RetVoid); -} - static MethodDesc* MD_System_Private_CoreLib_System_Runtime_Loader_AssemblyLoadContext_OnAssemblyLoad_I32_I32_RetVoid = nullptr; static void Call_System_Private_CoreLib_System_Runtime_Loader_AssemblyLoadContext_OnAssemblyLoad_I32_I32_RetVoid(void * arg0, void * arg1) { @@ -1241,6 +1215,7 @@ const ReverseThunkMapEntry g_ReverseThunks[] = { 513042204, "InitializeDefaultEventSources#1:System.Private.CoreLib:System.Diagnostics.Tracing:EventSource", { &MD_System_Private_CoreLib_System_Diagnostics_Tracing_EventSource_InitializeDefaultEventSources_I32_RetVoid, (void*)&Call_System_Private_CoreLib_System_Diagnostics_Tracing_EventSource_InitializeDefaultEventSources_I32_RetVoid } }, { 266659693, "InitializeForMonitor#4:System.Private.CoreLib:System.Threading:Lock", { &MD_System_Private_CoreLib_System_Threading_Lock_InitializeForMonitor_I32_I32_I32_I32_RetVoid, (void*)&Call_System_Private_CoreLib_System_Threading_Lock_InitializeForMonitor_I32_I32_I32_I32_RetVoid } }, { 288803216, "InternalPreserveStackTrace#2:System.Private.CoreLib:System:Exception", { &MD_System_Private_CoreLib_System_Exception_InternalPreserveStackTrace_I32_I32_RetVoid, (void*)&Call_System_Private_CoreLib_System_Exception_InternalPreserveStackTrace_I32_I32_RetVoid } }, + { 2611291109, "InvokeArrayContentsConverter#5:System.Private.CoreLib:System.StubHelpers:StubHelpers", { &MD_System_Private_CoreLib_System_StubHelpers_StubHelpers_InvokeArrayContentsConverter_I32_I32_I32_I32_I32_RetVoid, (void*)&Call_System_Private_CoreLib_System_StubHelpers_StubHelpers_InvokeArrayContentsConverter_I32_I32_I32_I32_I32_RetVoid } }, { 3290644746, "IsInterfaceImplemented#5:System.Private.CoreLib:System.Runtime.InteropServices:DynamicInterfaceCastableHelpers", { &MD_System_Private_CoreLib_System_Runtime_InteropServices_DynamicInterfaceCastableHelpers_IsInterfaceImplemented_I32_I32_I32_I32_I32_RetVoid, (void*)&Call_System_Private_CoreLib_System_Runtime_InteropServices_DynamicInterfaceCastableHelpers_IsInterfaceImplemented_I32_I32_I32_I32_I32_RetVoid } }, { 1577711579, "LayoutTypeConvertToManaged#3:System.Private.CoreLib:System.StubHelpers:StubHelpers", { &MD_System_Private_CoreLib_System_StubHelpers_StubHelpers_LayoutTypeConvertToManaged_I32_I32_I32_RetVoid, (void*)&Call_System_Private_CoreLib_System_StubHelpers_StubHelpers_LayoutTypeConvertToManaged_I32_I32_I32_RetVoid } }, { 2780693056, "LayoutTypeConvertToUnmanaged#3:System.Private.CoreLib:System.StubHelpers:StubHelpers", { &MD_System_Private_CoreLib_System_StubHelpers_StubHelpers_LayoutTypeConvertToUnmanaged_I32_I32_I32_RetVoid, (void*)&Call_System_Private_CoreLib_System_StubHelpers_StubHelpers_LayoutTypeConvertToUnmanaged_I32_I32_I32_RetVoid } }, @@ -1253,9 +1228,6 @@ const ReverseThunkMapEntry g_ReverseThunks[] = { 3515006989, "NewPrecachedExternalTypeMap#1:System.Private.CoreLib:System.Runtime.InteropServices:TypeMapLazyDictionary", { &MD_System_Private_CoreLib_System_Runtime_InteropServices_TypeMapLazyDictionary_NewPrecachedExternalTypeMap_I32_RetI32, (void*)&Call_System_Private_CoreLib_System_Runtime_InteropServices_TypeMapLazyDictionary_NewPrecachedExternalTypeMap_I32_RetI32 } }, { 1731038108, "NewPrecachedProxyTypeMap#1:System.Private.CoreLib:System.Runtime.InteropServices:TypeMapLazyDictionary", { &MD_System_Private_CoreLib_System_Runtime_InteropServices_TypeMapLazyDictionary_NewPrecachedProxyTypeMap_I32_RetI32, (void*)&Call_System_Private_CoreLib_System_Runtime_InteropServices_TypeMapLazyDictionary_NewPrecachedProxyTypeMap_I32_RetI32 } }, { 3327247096, "NewProxyTypeEntry#2:System.Private.CoreLib:System.Runtime.InteropServices:TypeMapLazyDictionary", { &MD_System_Private_CoreLib_System_Runtime_InteropServices_TypeMapLazyDictionary_NewProxyTypeEntry_I32_I32_RetI32, (void*)&Call_System_Private_CoreLib_System_Runtime_InteropServices_TypeMapLazyDictionary_NewProxyTypeEntry_I32_I32_RetI32 } }, - { 3248038929, "NonBlittableStructureArrayConvertToManaged#5:System.Private.CoreLib:System.StubHelpers:StubHelpers", { &MD_System_Private_CoreLib_System_StubHelpers_StubHelpers_NonBlittableStructureArrayConvertToManaged_I32_I32_I32_I32_I32_RetVoid, (void*)&Call_System_Private_CoreLib_System_StubHelpers_StubHelpers_NonBlittableStructureArrayConvertToManaged_I32_I32_I32_I32_I32_RetVoid } }, - { 373411722, "NonBlittableStructureArrayConvertToUnmanaged#5:System.Private.CoreLib:System.StubHelpers:StubHelpers", { &MD_System_Private_CoreLib_System_StubHelpers_StubHelpers_NonBlittableStructureArrayConvertToUnmanaged_I32_I32_I32_I32_I32_RetVoid, (void*)&Call_System_Private_CoreLib_System_StubHelpers_StubHelpers_NonBlittableStructureArrayConvertToUnmanaged_I32_I32_I32_I32_I32_RetVoid } }, - { 2704509804, "NonBlittableStructureArrayFree#5:System.Private.CoreLib:System.StubHelpers:StubHelpers", { &MD_System_Private_CoreLib_System_StubHelpers_StubHelpers_NonBlittableStructureArrayFree_I32_I32_I32_I32_I32_RetVoid, (void*)&Call_System_Private_CoreLib_System_StubHelpers_StubHelpers_NonBlittableStructureArrayFree_I32_I32_I32_I32_I32_RetVoid } }, { 3837429452, "OnAssemblyLoad#2:System.Private.CoreLib:System.Runtime.Loader:AssemblyLoadContext", { &MD_System_Private_CoreLib_System_Runtime_Loader_AssemblyLoadContext_OnAssemblyLoad_I32_I32_RetVoid, (void*)&Call_System_Private_CoreLib_System_Runtime_Loader_AssemblyLoadContext_OnAssemblyLoad_I32_I32_RetVoid } }, { 1632250712, "OnAssemblyResolve#4:System.Private.CoreLib:System.Runtime.Loader:AssemblyLoadContext", { &MD_System_Private_CoreLib_System_Runtime_Loader_AssemblyLoadContext_OnAssemblyResolve_I32_I32_I32_I32_RetVoid, (void*)&Call_System_Private_CoreLib_System_Runtime_Loader_AssemblyLoadContext_OnAssemblyResolve_I32_I32_I32_I32_RetVoid } }, { 3308959471, "OnFirstChanceException#2:System.Private.CoreLib:System:AppContext", { &MD_System_Private_CoreLib_System_AppContext_OnFirstChanceException_I32_I32_RetVoid, (void*)&Call_System_Private_CoreLib_System_AppContext_OnFirstChanceException_I32_I32_RetVoid } }, From e43ee36477ccb08362354e17cab23487bd8b91b6 Mon Sep 17 00:00:00 2001 From: David Wrighton Date: Tue, 5 May 2026 10:51:59 -0700 Subject: [PATCH 37/43] Adjust thunk abi --- .../WasmInterpreterToR2RThunkNode.cs | 20 ++++++++----------- 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/WasmInterpreterToR2RThunkNode.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/WasmInterpreterToR2RThunkNode.cs index 4b5e99fc637d20..ed73571d05ef14 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/WasmInterpreterToR2RThunkNode.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/WasmInterpreterToR2RThunkNode.cs @@ -34,7 +34,7 @@ public class WasmInterpreterToR2RThunkNode : StringDiscoverableAssemblyStubNode, public override string LookupString => "M" + _wasmSignature.SignatureString; - private static WasmSignature sigForInterpToR2RThunks = new WasmSignature(new WasmFuncType(new WasmResultType(new WasmValueType[]{WasmValueType.I32, WasmValueType.I32, WasmValueType.I32, WasmValueType.I32}), new WasmResultType(Array.Empty())), "viiii"); + private static WasmSignature sigForInterpToR2RThunks = new WasmSignature(new WasmFuncType(new WasmResultType(new WasmValueType[]{WasmValueType.I32, WasmValueType.I32, WasmValueType.I32}), new WasmResultType(Array.Empty())), "viii"); MethodSignature INodeWithTypeSignature.Signature => WasmLowering.RaiseSignature(sigForInterpToR2RThunks, _context); bool INodeWithTypeSignature.IsUnmanagedCallersOnly => false; bool INodeWithTypeSignature.IsAsyncCall => false; @@ -110,16 +110,14 @@ protected override void EmitCode(NodeFactory factory, ref Wasm.WasmEmitter instr bool hasWasmReturn = targetFuncType.Returns.Types.Length > 0; // Wasm locals for this thunk: - // local 0: pcode (I32) + // local 0: portableEntryPoint (I32) // local 1: pArgs (I32) // local 2: pRet (I32) - // local 3: pPortableEntryPointContext (I32) - // local 4: savedSp (I32) - save/restore SP global - const int LocalPcode = 0; + // local 3: savedSp (I32) - save/restore SP global + const int LocalPortableEntrypoint = 0; const int LocalPArgs = 1; const int LocalPRet = 2; - const int LocalPortableEntrypoint = 3; - const int LocalSavedSp = 4; + const int LocalSavedSp = 3; const int FrameSize = 16; // 16-byte aligned allocation for framePointer @@ -220,7 +218,8 @@ protected override void EmitCode(NodeFactory factory, ref Wasm.WasmEmitter instr expressions.Add(Local.Get(LocalPortableEntrypoint)); // call_indirect with the target R2R function's type signature - expressions.Add(Local.Get(LocalPcode)); + expressions.Add(Local.Get(LocalPortableEntrypoint)); + expressions.Add(I32.Load(0)); // load the actual function index from the type node expressions.Add(ControlFlow.CallIndirect(targetTypeIndex, 0)); // Handle wasm return value — pRet is already on the stack under the return value @@ -275,10 +274,7 @@ protected override void EmitCode(NodeFactory factory, ref Wasm.WasmEmitter instr expressions.Add(Local.Get(LocalSavedSp)); expressions.Add(Global.Set(WasmObjectWriter.StackPointerGlobalIndex)); - instructionEncoder.FunctionBody = new WasmFunctionBody( - new WasmFuncType( - new WasmResultType(new[] { WasmValueType.I32, WasmValueType.I32, WasmValueType.I32, WasmValueType.I32 }), - new WasmResultType(Array.Empty())), + instructionEncoder.FunctionBody = new WasmFunctionBody(sigForInterpToR2RThunks.FuncType, new[] { WasmValueType.I32 }, expressions.ToArray()); } From d9090d87f35fc518f5b1509553d29cece3389943 Mon Sep 17 00:00:00 2001 From: David Wrighton Date: Tue, 5 May 2026 13:44:46 -0700 Subject: [PATCH 38/43] Add support for compiling R2R files as part of testing Wasm --- .../aot/crossgen2/Crossgen2RootCommand.cs | 5 + src/coreclr/tools/aot/crossgen2/Program.cs | 4 + .../tools/r2rtest/CommandLineOptions.cs | 8 ++ src/coreclr/tools/r2rtest/Crossgen2Runner.cs | 5 + src/tests/Common/CLRTest.Execute.Bash.targets | 101 +++++++++++++++++- .../Common/CLRTest.Execute.Batch.targets | 79 +++++++++++++- src/tests/Common/CoreRootArtifacts.targets | 14 ++- src/tests/build.cmd | 19 ++++ src/tests/build.proj | 4 +- src/tests/build.sh | 30 ++++++ 10 files changed, 259 insertions(+), 10 deletions(-) diff --git a/src/coreclr/tools/aot/crossgen2/Crossgen2RootCommand.cs b/src/coreclr/tools/aot/crossgen2/Crossgen2RootCommand.cs index 49127d3077831a..1540d9f143b10f 100644 --- a/src/coreclr/tools/aot/crossgen2/Crossgen2RootCommand.cs +++ b/src/coreclr/tools/aot/crossgen2/Crossgen2RootCommand.cs @@ -315,6 +315,11 @@ public static void PrintExtendedHelp(ParseResult _) Console.WriteLine(); Console.WriteLine(String.Format(SR.SwitchWithDefaultHelp, "--targetarch", String.Join("', '", ValidArchitectures), Helpers.GetTargetArchitecture(null).ToString().ToLowerInvariant())); Console.WriteLine(); + + string[] ValidObjFormats = ["pe", "macho", "wasm"]; + Console.WriteLine(String.Format(SR.SwitchWithDefaultHelp, "--obj-format", String.Join("', '", ValidObjFormats), "pe")); + Console.WriteLine(); + Console.WriteLine(String.Format(SR.SwitchWithDefaultHelp, "--type-validation", String.Join("', '", Enum.GetNames()), nameof(TypeValidationRule.Automatic))); Console.WriteLine(); diff --git a/src/coreclr/tools/aot/crossgen2/Program.cs b/src/coreclr/tools/aot/crossgen2/Program.cs index 592e6bc4300669..88b093bde7daed 100644 --- a/src/coreclr/tools/aot/crossgen2/Program.cs +++ b/src/coreclr/tools/aot/crossgen2/Program.cs @@ -414,6 +414,10 @@ private void RunSingleCompilation(Dictionary inFilePaths, Instru string rtrHeaderSymbolName = Get(_command.ReadyToRunHeaderSymbolName); ReadyToRunContainerFormat format = Get(_command.OutputFormat); + if (format == ReadyToRunContainerFormat.PE && typeSystemContext.Target.Architecture == TargetArchitecture.Wasm32) + { + format = ReadyToRunContainerFormat.Wasm; + } if (!composite && format != ReadyToRunContainerFormat.PE && format != ReadyToRunContainerFormat.Wasm) { throw new Exception(string.Format(SR.ErrorContainerFormatRequiresComposite, format)); diff --git a/src/coreclr/tools/r2rtest/CommandLineOptions.cs b/src/coreclr/tools/r2rtest/CommandLineOptions.cs index 82404653add802..c0b818a5922cf5 100644 --- a/src/coreclr/tools/r2rtest/CommandLineOptions.cs +++ b/src/coreclr/tools/r2rtest/CommandLineOptions.cs @@ -34,6 +34,7 @@ void CreateCommand(string name, string description, Option[] options, Func TargetArch { get; } = new("--target-arch") { Description = "Target architecture for crossgen2" }; + public Option TargetOs { get; } = + new("--target-os") { Description = "Target OS for crossgen2" }; + // // compile-nuget specific options // @@ -306,6 +312,7 @@ public BuildOptions(R2RTestRootCommand cmd, ParseResult res) Crossgen2Path = res.GetValue(cmd.Crossgen2Path); VerifyTypeAndFieldLayout = res.GetValue(cmd.VerifyTypeAndFieldLayout); TargetArch = res.GetValue(cmd.TargetArch); + TargetOs = res.GetValue(cmd.TargetOs); Exe = res.GetValue(cmd.Exe); NoJit = res.GetValue(cmd.NoJit); NoCrossgen2 = res.GetValue(cmd.NoCrossgen2); @@ -345,6 +352,7 @@ public BuildOptions(R2RTestRootCommand cmd, ParseResult res) public FileInfo Crossgen2Path { get; } public bool VerifyTypeAndFieldLayout { get; } public string TargetArch { get; } + public string TargetOs { get; } public bool Exe { get; } public bool NoJit { get; set; } public bool NoCrossgen2 { get; } diff --git a/src/coreclr/tools/r2rtest/Crossgen2Runner.cs b/src/coreclr/tools/r2rtest/Crossgen2Runner.cs index e9024d58da3a03..43fee7b771d150 100644 --- a/src/coreclr/tools/r2rtest/Crossgen2Runner.cs +++ b/src/coreclr/tools/r2rtest/Crossgen2Runner.cs @@ -122,6 +122,11 @@ protected override IEnumerable BuildCommandLineArguments(IEnumerable"$CORE_ROOT/corerun" $(CoreRunArgs) ${__DotEnvArg} "$CORE_ROOT/watchdog" $_WatcherTimeoutMins - - + + - + + /dev/null; then + kill -9 "$pid" 2>/dev/null + fi + fi + ) & + local watchdog=$! + + # Wait for the command to finish + wait "$pid" + local exit_code=$? + + # Kill watchdog and its children (sleep process) if still running + echo Kill watchdog with PID $watchdog + pkill -P "$watchdog" 2>/dev/null + kill "$watchdog" 2>/dev/null + wait "$watchdog" 2>/dev/null + + # If the process was killed by SIGKILL (128+9 = 137), map to 99 + if [ $exit_code -eq 137 ]; then + return 99 + else + return $exit_code + fi +} + +if [ ! -z ${RunCrossGen2+x} ]%3B then + TakeLock +fi + +if [ -z ${RunWithNodeJS+x} ] ; then + cd WasmApp + ./run-v8.sh +else + if [ -z ${__TestTimeout+x} ]; then + __TestTimeout=300000 + fi + + echo Running with node + echo CORE_ROOT: ${CORE_ROOT} + echo ExePath: ${PWD}/${ExePath} + echo CLRTestExecutionArguments: ${CLRTestExecutionArguments[@]} + echo node version: `node -v` + echo Timeout in ms: $__TestTimeout + cmd=( node --stack-size=8192 "${CORE_ROOT}/corerun.js" -c "${CORE_ROOT}" "${PWD}/${ExePath}" "${CLRTestExecutionArguments[@]}" ) + echo Running: "${cmd[@]}" + run_with_timeout $__TestTimeout "${cmd[@]}" +fi +CLRTestExitCode=$? + +if [ ! -z ${RunCrossGen2+x} ]%3B then + ReleaseLock +fi + ]]> + + - - + - + + + - + True @@ -128,11 +128,11 @@ - + <_Crossgen2Dir Condition="('$(TargetArchitecture)' != 'x64' and '$(BuildArchitecture)' == 'x64') or '$(EnableNativeSanitizers)' != ''">$(CoreCLRArtifactsPath)x64/crossgen2 - + <_CoreRootArtifactSource Include=" $(_Crossgen2Dir)/$(LibPrefix)clrjit_*$(LibSuffix); @@ -141,5 +141,13 @@ TargetDir="crossgen2/" /> + + + <_CoreRootArtifactSource + Include="$(CrossGen2HostArtifactsPath)/crossgen2-published/**/*" + TargetDir="crossgen2/" /> + + diff --git a/src/tests/build.cmd b/src/tests/build.cmd index b2d7e5d102a61c..4ae48b024d0299 100644 --- a/src/tests/build.cmd +++ b/src/tests/build.cmd @@ -54,6 +54,7 @@ set __Ninja=1 set __CMakeArgs= set __EnableNativeSanitizers= set __Priority=0 +set __CrossGen2HostArtifactsPath= set __BuildNeedTargetArg= @@ -106,6 +107,7 @@ if /i "%arg%" == "GenerateLayoutOnly" (set __GenerateLayoutOnly=1&set __SkipM if /i "%arg%" == "MSBuild" (set __Ninja=0&set processedArgs=!processedArgs! %1&shift&goto Arg_Loop) if /i "%arg%" == "crossgen2" (set __TestBuildMode=crossgen2&set processedArgs=!processedArgs! %1&shift&goto Arg_Loop) if /i "%arg%" == "composite" (set __CompositeBuildMode=1&set __TestBuildMode=crossgen2&set processedArgs=!processedArgs! %1&shift&goto Arg_Loop) +if /i "%arg%" == "crossgen2host" (set __CrossGen2HostArtifactsPath=%2&set processedArgs=!processedArgs! %1 %2&shift&shift&goto Arg_Loop) if /i "%arg%" == "pdb" (set __CreatePdb=1&set processedArgs=!processedArgs! %1&shift&goto Arg_Loop) if /i "%arg%" == "NativeAOT" (set __TestBuildMode=nativeaot&set processedArgs=!processedArgs! %1&shift&goto Arg_Loop) if /i "%arg%" == "Perfmap" (set __CreatePerfmap=1&set processedArgs=!processedArgs! %1&shift&goto Arg_Loop) @@ -227,6 +229,22 @@ if "%__Mono%"=="1" ( set __CommonMSBuildArgs=!__CommonMSBuildArgs! "/p:RuntimeFlavor=coreclr" ) +REM Auto-detect CrossGen2HostArtifactsPath for wasm targets (crossgen2 is never available in wasm builds) +if /i "%__BuildArch%" == "wasm" if "%__CrossGen2HostArtifactsPath%" == "" ( + REM Prefer PROCESSOR_ARCHITEW6432 for WOW64 compatibility, then fall back to PROCESSOR_ARCHITECTURE + set "__HostProcArch=%PROCESSOR_ARCHITEW6432%" + if "!__HostProcArch!" == "" set "__HostProcArch=%PROCESSOR_ARCHITECTURE%" + set "__HostArch=x64" + if /i "!__HostProcArch!" == "ARM64" set "__HostArch=arm64" + if /i "!__HostProcArch!" == "x86" set "__HostArch=x86" + set "__CrossGen2HostArtifactsPath=%__RootBinDir%\bin\coreclr\windows.!__HostArch!.%__BuildType%" + echo %__MsgPrefix%Auto-detected CrossGen2HostArtifactsPath: !__CrossGen2HostArtifactsPath! +) + +if not "%__CrossGen2HostArtifactsPath%" == "" ( + set __CommonMSBuildArgs=!__CommonMSBuildArgs! "/p:CrossGen2HostArtifactsPath=%__CrossGen2HostArtifactsPath%" +) + if %__Ninja% == 0 ( set __CommonMSBuildArgs=%__CommonMSBuildArgs% /p:UseVisualStudioNativeBinariesLayout=true ) @@ -368,6 +386,7 @@ echo -CopyNativeOnly: Only copy the native test binaries to the managed output. echo -GenerateLayoutOnly: Only generate the Core_Root layout without building managed or native test components. echo -MSBuild: Use MSBuild instead of Ninja. echo -Crossgen2: Precompiles the framework managed assemblies in coreroot using the Crossgen2 compiler. +echo -Crossgen2Host ^: Override the path to the host coreclr artifacts used for crossgen2 when targeting wasm. echo -Composite: Use Crossgen2 composite mode (all framework gets compiled into a single native R2R library). echo -PDB: Create PDB files when precompiling the framework managed assemblies. echo -NativeAOT: Builds the tests for Native AOT compilation. diff --git a/src/tests/build.proj b/src/tests/build.proj index e8455e5df74964..74e37a90e1025b 100644 --- a/src/tests/build.proj +++ b/src/tests/build.proj @@ -291,6 +291,7 @@ $(CrossgenCmd) --release $(CrossgenCmd) --nocleanup $(CrossgenCmd) --target-arch $(TargetArchitecture) + $(CrossgenCmd) --target-os $(TargetOS) $(CrossgenCmd) -dop $(NUMBER_OF_PROCESSORS) $(CrossgenCmd) -m "$(CORE_ROOT)\StandardOptimizationData.mibc" @@ -300,7 +301,8 @@ $(CrossgenCmd) --crossgen2-parallelism 1 $(CrossgenCmd) --verify-type-and-field-layout - $(CrossgenCmd) --crossgen2-path "$(__BinDir)\$(BuildArchitecture)\crossgen2\crossgen2$(ExeSuffix)" + $(CrossgenCmd) --crossgen2-path "$(__BinDir)\$(BuildArchitecture)\crossgen2\crossgen2$(ExeSuffix)" + $(CrossgenCmd) --crossgen2-path "$(CrossGen2HostArtifactsPath)\crossgen2-published\crossgen2$(ExeSuffix)" diff --git a/src/tests/build.sh b/src/tests/build.sh index a0709d7386517c..aec5f77b31a4e1 100755 --- a/src/tests/build.sh +++ b/src/tests/build.sh @@ -115,6 +115,9 @@ build_Tests() buildArgs+=("/maxcpucount") buildArgs+=("${__msbuildLog}" "${__msbuildWrn}" "${__msbuildErr}" "${__msbuildBinLog}") buildArgs+=("/p:NUMBER_OF_PROCESSORS=${__NumProc}") + if [[ -n "$__CrossGen2HostArtifactsPath" ]]; then + buildArgs+=("/p:CrossGen2HostArtifactsPath=$__CrossGen2HostArtifactsPath") + fi buildArgs+=("${__UnprocessedBuildArgs[@]}") # Disable warnAsError - https://github.com/dotnet/runtime/issues/11077 @@ -146,6 +149,7 @@ usage_list+=("-copynativeonly - Only copy the native test binaries to the manage usage_list+=("-generatelayoutonly - Only generate the Core_Root layout without building managed or native test components.") usage_list+=("") usage_list+=("-crossgen2 - Precompiles the framework managed assemblies in coreroot using the Crossgen2 compiler.") +usage_list+=("-crossgen2host: - Override the path to the host coreclr artifacts used for crossgen2 when targeting wasm.") usage_list+=("-composite - Use Crossgen2 composite mode (all framework gets compiled into a single native R2R library).") usage_list+=("-nativeaot - Builds the tests for Native AOT compilation.") usage_list+=("-priority1 - Include priority=1 tests in the build.") @@ -198,6 +202,17 @@ handle_arguments_local() { __TestBuildMode=crossgen2 ;; + crossgen2host*|-crossgen2host*) + local arg="$1" + local parts=(${arg//:/ }) + if [[ ${#parts[@]} -eq 1 ]]; then + __CrossGen2HostArtifactsPath="$2" + __ShiftArgs=1 + else + __CrossGen2HostArtifactsPath="${parts[1]}" + fi + ;; + nativeaot|-nativeaot) __TestBuildMode=nativeaot ;; @@ -352,6 +367,7 @@ __Mono=0 __MonoAot=0 __MonoFullAot=0 __BuildLogRootName="TestBuild" +__CrossGen2HostArtifactsPath= CORE_ROOT= EnableNativeSanitizers= @@ -402,6 +418,20 @@ __TestIntermediatesDir="$__RootBinDir/tests/coreclr/obj/$__OSPlatformConfig" __CrossCompIntermediatesDir="$__IntermediatesDir/crossgen" __MonoBinDir="$__RootBinDir/bin/mono/$__OSPlatformConfig" +# Auto-detect CrossGen2HostArtifactsPath for wasm targets (crossgen2 is never available in wasm builds) +if [[ "$__TargetArch" == "wasm" && -z "$__CrossGen2HostArtifactsPath" ]]; then + # Determine the host OS name as used in artifact paths + if [[ "$platform" == "darwin" ]]; then + __HostOSName="osx" + elif [[ "$platform" == "freebsd" ]]; then + __HostOSName="freebsd" + else + __HostOSName="linux" + fi + __CrossGen2HostArtifactsPath="$__RootBinDir/bin/coreclr/$__HostOSName.$__HostArch.$__BuildType" + echo "${__MsgPrefix}Auto-detected CrossGen2HostArtifactsPath: $__CrossGen2HostArtifactsPath" +fi + # CI_SPECIFIC - On CI machines, $HOME may not be set. In such a case, create a subfolder and set the variable to it. # This is needed by CLI to function. if [[ -z "$HOME" ]]; then From 102d90f9826903307f6fe382743e244cfa3d5a0e Mon Sep 17 00:00:00 2001 From: David Wrighton Date: Tue, 5 May 2026 14:34:19 -0700 Subject: [PATCH 39/43] Add Webcil support to R2RDump R2RDump previously could not read Webcil files (the format used for managed assemblies in WebAssembly environments). This adds a WebcilImageReader that implements IBinaryImageReader for the Webcil format, enabling R2RDump to dump headers, methods, and section contents from Webcil-format R2R images. Changes: - New WebcilImageReader.cs implementing IBinaryImageReader - ReadyToRunReader detects Webcil format (after MachO, before PE) - DumpModel handles Webcil in reference assembly loading - Program.cs maps OperatingSystem.Unknown to TargetOS.Linux for Webcil - ReadyToRunMethod gracefully handles null PEReader (Webcil has no PE) - ILCompiler.Reflection.ReadyToRun.csproj includes shared Webcil.cs Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../ILCompiler.Reflection.ReadyToRun.csproj | 1 + .../ReadyToRunMethod.cs | 3 +- .../ReadyToRunReader.cs | 4 + .../WebcilImageReader.cs | 384 ++++++++++++++++++ src/coreclr/tools/r2rdump/DumpModel.cs | 7 + src/coreclr/tools/r2rdump/Program.cs | 1 + 6 files changed, 398 insertions(+), 2 deletions(-) create mode 100644 src/coreclr/tools/aot/ILCompiler.Reflection.ReadyToRun/WebcilImageReader.cs diff --git a/src/coreclr/tools/aot/ILCompiler.Reflection.ReadyToRun/ILCompiler.Reflection.ReadyToRun.csproj b/src/coreclr/tools/aot/ILCompiler.Reflection.ReadyToRun/ILCompiler.Reflection.ReadyToRun.csproj index 68bba4009b6e65..f3390f66b22be7 100644 --- a/src/coreclr/tools/aot/ILCompiler.Reflection.ReadyToRun/ILCompiler.Reflection.ReadyToRun.csproj +++ b/src/coreclr/tools/aot/ILCompiler.Reflection.ReadyToRun/ILCompiler.Reflection.ReadyToRun.csproj @@ -28,6 +28,7 @@ + diff --git a/src/coreclr/tools/aot/ILCompiler.Reflection.ReadyToRun/ReadyToRunMethod.cs b/src/coreclr/tools/aot/ILCompiler.Reflection.ReadyToRun/ReadyToRunMethod.cs index 09487a29016796..0490fd8fe2a2cb 100644 --- a/src/coreclr/tools/aot/ILCompiler.Reflection.ReadyToRun/ReadyToRunMethod.cs +++ b/src/coreclr/tools/aot/ILCompiler.Reflection.ReadyToRun/ReadyToRunMethod.cs @@ -403,9 +403,8 @@ public ReadyToRunMethod( case HandleKind.MethodDefinition: { MethodDefinition methodDef = ComponentReader.MetadataReader.GetMethodDefinition((MethodDefinitionHandle)MethodHandle); - if (methodDef.RelativeVirtualAddress != 0) + if (methodDef.RelativeVirtualAddress != 0 && ComponentReader.ImageReader is not null) { - System.Diagnostics.Debug.Assert(ComponentReader.ImageReader != null, "Component should be a PE and have an associated PEReader"); MethodBodyBlock mbb = ComponentReader.ImageReader.GetMethodBody(methodDef.RelativeVirtualAddress); if (!mbb.LocalSignature.IsNil) { diff --git a/src/coreclr/tools/aot/ILCompiler.Reflection.ReadyToRun/ReadyToRunReader.cs b/src/coreclr/tools/aot/ILCompiler.Reflection.ReadyToRun/ReadyToRunReader.cs index 13dd20c4e979be..2b1f78c3259063 100644 --- a/src/coreclr/tools/aot/ILCompiler.Reflection.ReadyToRun/ReadyToRunReader.cs +++ b/src/coreclr/tools/aot/ILCompiler.Reflection.ReadyToRun/ReadyToRunReader.cs @@ -501,6 +501,10 @@ private unsafe void Initialize(IAssemblyMetadata metadata) { CompositeReader = new MachO.MachOImageReader(image); } + else if (WebcilImageReader.IsWebcilImage(image)) + { + CompositeReader = new WebcilImageReader(image); + } else { CompositeReader = new PEImageReader(new PEReader(Unsafe.As>(ref image))); diff --git a/src/coreclr/tools/aot/ILCompiler.Reflection.ReadyToRun/WebcilImageReader.cs b/src/coreclr/tools/aot/ILCompiler.Reflection.ReadyToRun/WebcilImageReader.cs new file mode 100644 index 00000000000000..72a8328b2b4055 --- /dev/null +++ b/src/coreclr/tools/aot/ILCompiler.Reflection.ReadyToRun/WebcilImageReader.cs @@ -0,0 +1,384 @@ +// 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.Buffers.Binary; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.IO; +using System.Reflection.Metadata; +using System.Reflection.PortableExecutable; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +using Microsoft.NET.WebAssembly.Webcil; + +namespace ILCompiler.Reflection.ReadyToRun +{ + /// + /// Wrapper around Webcil files that implements IBinaryImageReader. + /// Webcil is a stripped-down PE format used for managed assemblies in WebAssembly environments. + /// + public class WebcilImageReader : IBinaryImageReader + { + private readonly byte[] _image; + private readonly WebcilHeader _header; + private readonly ImmutableArray _sections; + private readonly long _webcilOffset; + private readonly DirectoryEntry _corHeaderMetadataDirectory; + private readonly CorFlags _corFlags; + private readonly DirectoryEntry _managedNativeHeaderDirectory; + + public Machine Machine => Machine.I386; // Webcil doesn't encode machine type; wasm targets use a placeholder + public OperatingSystem OperatingSystem => OperatingSystem.Unknown; + public ulong ImageBase => 0; + + public WebcilImageReader(byte[] image) + { + _image = image; + _webcilOffset = 0; + + // Check for WASM wrapper + if (IsWasmModule(image)) + { + if (!TryFindWebcilInWasm(image, out _webcilOffset)) + { + throw new BadImageFormatException("WASM module does not contain a Webcil payload"); + } + } + + // Read the Webcil header + if (!TryReadHeader(image, _webcilOffset, out _header)) + { + throw new BadImageFormatException("Not a valid Webcil file"); + } + + // Read section headers + _sections = ReadSections(image, _webcilOffset, _header); + + // Read the COR header to get metadata and R2R header locations + ReadCorHeader(image, out _corFlags, out _corHeaderMetadataDirectory, out _managedNativeHeaderDirectory); + } + + /// + /// Detects whether a byte array starts with the Webcil magic bytes (or is a WASM module containing Webcil). + /// + public static bool IsWebcilImage(byte[] image) + { + if (image.Length < 4) + return false; + + uint magic = BitConverter.ToUInt32(image, 0); + if (magic == WebcilConstants.WEBCIL_MAGIC) + return true; + + // Check if it's a WASM module that might contain Webcil + if (IsWasmModule(image)) + { + return TryFindWebcilInWasm(image, out _); + } + + return false; + } + + /// + /// Detects whether the file at the specified path is a Webcil image. + /// + public static bool IsWebcilImage(string filename) + { + try + { + byte[] header = new byte[4]; + using var stream = File.OpenRead(filename); + if (stream.Read(header, 0, 4) != 4) + return false; + + uint magic = BitConverter.ToUInt32(header, 0); + if (magic == WebcilConstants.WEBCIL_MAGIC) + return true; + + // Check for WASM magic (\0asm) + if (header[0] == 0x00 && header[1] == 0x61 && header[2] == 0x73 && header[3] == 0x6D) + { + // Read the full file to check for Webcil inside WASM + stream.Seek(0, SeekOrigin.Begin); + byte[] fullImage = new byte[stream.Length]; + stream.Read(fullImage, 0, fullImage.Length); + return TryFindWebcilInWasm(fullImage, out _); + } + + return false; + } + catch + { + return false; + } + } + + public ImmutableArray GetEntireImage() + => Unsafe.As>(ref Unsafe.AsRef(in _image)); + + public int GetOffset(int rva) + { + foreach (var section in _sections) + { + if ((uint)rva >= section.VirtualAddress && (uint)rva < section.VirtualAddress + section.VirtualSize) + { + uint offset = (uint)rva - section.VirtualAddress; + if (offset >= section.SizeOfRawData) + { + throw new BadImageFormatException($"RVA 0x{rva:X} maps beyond section raw data"); + } + return (int)(section.PointerToRawData + offset + _webcilOffset); + } + } + throw new BadImageFormatException($"RVA 0x{rva:X} not found in any Webcil section"); + } + + public bool TryGetReadyToRunHeader(out int rva, out bool isComposite) + { + // Check the ManagedNativeHeaderDirectory (same as PE's CorHeader.ManagedNativeHeaderDirectory) + if ((_corFlags & CorFlags.ILLibrary) != 0 && _managedNativeHeaderDirectory.Size != 0) + { + rva = _managedNativeHeaderDirectory.RelativeVirtualAddress; + isComposite = false; + return true; + } + + rva = 0; + isComposite = false; + return false; + } + + public IAssemblyMetadata GetStandaloneAssemblyMetadata() + { + if (_corHeaderMetadataDirectory.Size == 0) + return null; + + int metadataOffset = GetOffset(_corHeaderMetadataDirectory.RelativeVirtualAddress); + var metadataBytes = new byte[_corHeaderMetadataDirectory.Size]; + Array.Copy(_image, metadataOffset, metadataBytes, 0, _corHeaderMetadataDirectory.Size); + + return new WebcilAssemblyMetadata(metadataBytes); + } + + public IAssemblyMetadata GetManifestAssemblyMetadata(MetadataReader manifestReader) + => new ManifestAssemblyMetadata(manifestReader); + + public void DumpImageInformation(TextWriter writer) + { + writer.WriteLine($"Format: Webcil v{_header.VersionMajor}.{_header.VersionMinor}"); + writer.WriteLine($"Sections: {_header.CoffSections}"); + writer.WriteLine($"CliHeaderRVA: 0x{_header.PeCliHeaderRva:X}"); + writer.WriteLine($"CliHeaderSize: {_header.PeCliHeaderSize}"); + writer.WriteLine($"DebugRVA: 0x{_header.PeDebugRva:X}"); + writer.WriteLine($"DebugSize: {_header.PeDebugSize}"); + + writer.WriteLine("Sections:"); + for (int i = 0; i < _sections.Length; i++) + { + var section = _sections[i]; + writer.WriteLine($" [{i}] VA=0x{section.VirtualAddress:X} VSize=0x{section.VirtualSize:X} RawSize=0x{section.SizeOfRawData:X} RawPtr=0x{section.PointerToRawData:X}"); + } + } + + public Dictionary GetSections() + { + Dictionary sectionMap = []; + for (int i = 0; i < _sections.Length; i++) + { + sectionMap.Add($".webcil{i}", (int)_sections[i].SizeOfRawData); + } + return sectionMap; + } + + private void ReadCorHeader(byte[] image, out CorFlags flags, out DirectoryEntry metadataDirectory, out DirectoryEntry managedNativeHeaderDirectory) + { + int corHeaderOffset = GetOffset((int)_header.PeCliHeaderRva); + + // CorHeader layout: + // int32 cb (byte count) + // uint16 MajorRuntimeVersion + // uint16 MinorRuntimeVersion + // DirectoryEntry MetaData (RVA + Size) + // uint32 Flags + // int32 EntryPointTokenOrRelativeVirtualAddress + // DirectoryEntry Resources (RVA + Size) + // DirectoryEntry StrongNameSignature (RVA + Size) + // DirectoryEntry CodeManagerTable (RVA + Size) + // DirectoryEntry VTableFixups (RVA + Size) + // DirectoryEntry ExportAddressTableJumps (RVA + Size) + // DirectoryEntry ManagedNativeHeader (RVA + Size) + + int offset = corHeaderOffset; + offset += 4; // cb + offset += 2; // MajorRuntimeVersion + offset += 2; // MinorRuntimeVersion + + int metadataRva = BitConverter.ToInt32(image, offset); offset += 4; + int metadataSize = BitConverter.ToInt32(image, offset); offset += 4; + metadataDirectory = new DirectoryEntry(metadataRva, metadataSize); + + flags = (CorFlags)BitConverter.ToUInt32(image, offset); offset += 4; + + offset += 4; // EntryPointTokenOrRelativeVirtualAddress + offset += 8; // Resources + offset += 8; // StrongNameSignature + offset += 8; // CodeManagerTable + offset += 8; // VTableFixups + offset += 8; // ExportAddressTableJumps + + int managedNativeRva = BitConverter.ToInt32(image, offset); offset += 4; + int managedNativeSize = BitConverter.ToInt32(image, offset); + managedNativeHeaderDirectory = new DirectoryEntry(managedNativeRva, managedNativeSize); + } + + private static bool TryReadHeader(byte[] image, long offset, out WebcilHeader header) + { + header = default; + + // V0 header is 28 bytes, V1 is 32 bytes + const int V0HeaderSize = 28; + const int V1HeaderSize = 32; + + if (offset + V0HeaderSize > image.Length) + return false; + + unsafe + { + fixed (byte* p = &image[(int)offset]) + { + WebcilHeader temp; + Buffer.MemoryCopy(p, &temp, sizeof(WebcilHeader), V0HeaderSize); + header = temp; + } + } + + if (!BitConverter.IsLittleEndian) + { + header.Id = BinaryPrimitives.ReverseEndianness(header.Id); + header.VersionMajor = BinaryPrimitives.ReverseEndianness(header.VersionMajor); + header.VersionMinor = BinaryPrimitives.ReverseEndianness(header.VersionMinor); + header.CoffSections = BinaryPrimitives.ReverseEndianness(header.CoffSections); + header.PeCliHeaderRva = BinaryPrimitives.ReverseEndianness(header.PeCliHeaderRva); + header.PeCliHeaderSize = BinaryPrimitives.ReverseEndianness(header.PeCliHeaderSize); + header.PeDebugRva = BinaryPrimitives.ReverseEndianness(header.PeDebugRva); + header.PeDebugSize = BinaryPrimitives.ReverseEndianness(header.PeDebugSize); + } + + if (header.Id != WebcilConstants.WEBCIL_MAGIC) + return false; + + if (header.VersionMajor != 0 && header.VersionMajor != 1) + return false; + + if (header.VersionMinor != WebcilConstants.WC_VERSION_MINOR) + return false; + + if (header.VersionMajor >= 1) + { + if (offset + V1HeaderSize > image.Length) + return false; + + header.TableBase = BitConverter.ToUInt32(image, (int)offset + V0HeaderSize); + if (!BitConverter.IsLittleEndian) + { + header.TableBase = BinaryPrimitives.ReverseEndianness(header.TableBase); + } + } + else + { + header.TableBase = uint.MaxValue; + } + + return true; + } + + private static unsafe ImmutableArray ReadSections(byte[] image, long webcilOffset, WebcilHeader header) + { + int sectionSize = sizeof(WebcilSectionHeader); + long sectionDirectoryOffset = webcilOffset + (header.VersionMajor >= 1 ? 32 : 28); + var sections = ImmutableArray.CreateBuilder(header.CoffSections); + + for (int i = 0; i < header.CoffSections; i++) + { + long sectionOffset = sectionDirectoryOffset + (i * sectionSize); + WebcilSectionHeader sectionHeader; + fixed (byte* p = &image[(int)sectionOffset]) + { + sectionHeader = *(WebcilSectionHeader*)p; + } + + if (!BitConverter.IsLittleEndian) + { + sectionHeader = new WebcilSectionHeader( + virtualSize: BinaryPrimitives.ReverseEndianness(sectionHeader.VirtualSize), + virtualAddress: BinaryPrimitives.ReverseEndianness(sectionHeader.VirtualAddress), + sizeOfRawData: BinaryPrimitives.ReverseEndianness(sectionHeader.SizeOfRawData), + pointerToRawData: BinaryPrimitives.ReverseEndianness(sectionHeader.PointerToRawData) + ); + } + + sections.Add(sectionHeader); + } + + return sections.MoveToImmutable(); + } + + private static bool IsWasmModule(byte[] image) + { + // WASM magic: \0asm + return image.Length >= 4 + && image[0] == 0x00 + && image[1] == 0x61 + && image[2] == 0x73 + && image[3] == 0x6D; + } + + private static bool TryFindWebcilInWasm(byte[] image, out long webcilOffset) + { + webcilOffset = 0; + + // Simple scan: look for the Webcil magic in the WASM module + // The Webcil payload is embedded as a custom section in the WASM module + for (int i = 8; i <= image.Length - 4; i++) + { + uint candidate = BitConverter.ToUInt32(image, i); + if (candidate == WebcilConstants.WEBCIL_MAGIC) + { + // Verify this is a valid Webcil header + if (TryReadHeader(image, i, out _)) + { + webcilOffset = i; + return true; + } + } + } + + return false; + } + } + + /// + /// Assembly metadata implementation for Webcil images that don't have a PEReader. + /// + internal sealed unsafe class WebcilAssemblyMetadata : IAssemblyMetadata + { + private readonly byte[] _metadataBytes; + private readonly GCHandle _pinnedBytes; + private readonly MetadataReader _metadataReader; + + public WebcilAssemblyMetadata(byte[] metadataBytes) + { + _metadataBytes = metadataBytes; + _pinnedBytes = GCHandle.Alloc(_metadataBytes, GCHandleType.Pinned); + _metadataReader = new MetadataReader( + (byte*)_pinnedBytes.AddrOfPinnedObject(), + _metadataBytes.Length); + } + + public PEReader ImageReader => null; + + public MetadataReader MetadataReader => _metadataReader; + } +} diff --git a/src/coreclr/tools/r2rdump/DumpModel.cs b/src/coreclr/tools/r2rdump/DumpModel.cs index fb920f2e5308be..509286020bfa07 100644 --- a/src/coreclr/tools/r2rdump/DumpModel.cs +++ b/src/coreclr/tools/r2rdump/DumpModel.cs @@ -95,6 +95,13 @@ static IAssemblyMetadata Open(string filename) { byte[] image = File.ReadAllBytes(filename); + if (WebcilImageReader.IsWebcilImage(image)) + { + var webcilReader = new WebcilImageReader(image); + return webcilReader.GetStandaloneAssemblyMetadata() + ?? throw new BadImageFormatException($"ECMA metadata not found in Webcil file '{filename}'"); + } + PEReader peReader = new PEReader(Unsafe.As>(ref image)); if (!peReader.HasMetadata) diff --git a/src/coreclr/tools/r2rdump/Program.cs b/src/coreclr/tools/r2rdump/Program.cs index 7dd5f49c5a8d85..b21bd1b77d7067 100644 --- a/src/coreclr/tools/r2rdump/Program.cs +++ b/src/coreclr/tools/r2rdump/Program.cs @@ -221,6 +221,7 @@ public void Dump(ReadyToRunReader r2r) OperatingSystem.Apple => TargetOS.OSX, OperatingSystem.FreeBSD => TargetOS.FreeBSD, OperatingSystem.NetBSD => TargetOS.FreeBSD, + OperatingSystem.Unknown => TargetOS.Linux, // Webcil/WASM images don't encode OS; use Linux as fallback _ => throw new NotImplementedException(r2r.OperatingSystem.ToString()), }; TargetDetails details = new(architecture, os, TargetAbi.NativeAot); From bde557b1823f8520cd5f47137df4dcf492eb8d18 Mon Sep 17 00:00:00 2001 From: David Wrighton Date: Tue, 5 May 2026 15:13:08 -0700 Subject: [PATCH 40/43] Add GetSectionData to IAssemblyMetadata and implement in all types Replace the PEReader ImageReader property with a GetSectionData(int rva) method that returns a BlobReader. This decouples the interface from PEReader, enabling non-PE formats (Webcil) to provide section data. Implementations: - StandaloneAssemblyMetadata: delegates to PEReader.GetSectionData - ManifestAssemblyMetadata: same with null-guard - WebcilAssemblyMetadata: resolves RVA via WebcilImageReader sections - SimpleAssemblyMetadata (tests): delegates to PEReader.GetSectionData Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../TestCasesRunner/R2RResultChecker.cs | 2 +- .../IAssemblyMetadata.cs | 3 +- .../ManifestAssemblyMetadata.cs | 7 +++- .../ReadyToRunMethod.cs | 14 ++++--- .../StandaloneAssemblyMetadata.cs | 2 +- .../WebcilImageReader.cs | 38 ++++++++++++------- 6 files changed, 43 insertions(+), 23 deletions(-) diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCasesRunner/R2RResultChecker.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCasesRunner/R2RResultChecker.cs index c2e5e651b32cd6..082bd903a3551f 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCasesRunner/R2RResultChecker.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCasesRunner/R2RResultChecker.cs @@ -483,7 +483,7 @@ public SimpleAssemblyMetadata(string path) _peReader = new PEReader(new MemoryStream(imageBytes)); } - public PEReader ImageReader => _peReader; + public BlobReader GetSectionData(int relativeVirtualAddress) => _peReader.GetSectionData(relativeVirtualAddress).GetReader(); public MetadataReader MetadataReader => _peReader.GetMetadataReader(); } diff --git a/src/coreclr/tools/aot/ILCompiler.Reflection.ReadyToRun/IAssemblyMetadata.cs b/src/coreclr/tools/aot/ILCompiler.Reflection.ReadyToRun/IAssemblyMetadata.cs index 5a49d9a06b82fb..a296d0b10387b8 100644 --- a/src/coreclr/tools/aot/ILCompiler.Reflection.ReadyToRun/IAssemblyMetadata.cs +++ b/src/coreclr/tools/aot/ILCompiler.Reflection.ReadyToRun/IAssemblyMetadata.cs @@ -11,8 +11,7 @@ namespace ILCompiler.Reflection.ReadyToRun /// public interface IAssemblyMetadata { - PEReader ImageReader { get; } - + BlobReader GetSectionData(int relativeVirtualAddress); MetadataReader MetadataReader { get; } } } diff --git a/src/coreclr/tools/aot/ILCompiler.Reflection.ReadyToRun/ManifestAssemblyMetadata.cs b/src/coreclr/tools/aot/ILCompiler.Reflection.ReadyToRun/ManifestAssemblyMetadata.cs index e6c0032a9109cc..bd7cba6adcc638 100644 --- a/src/coreclr/tools/aot/ILCompiler.Reflection.ReadyToRun/ManifestAssemblyMetadata.cs +++ b/src/coreclr/tools/aot/ILCompiler.Reflection.ReadyToRun/ManifestAssemblyMetadata.cs @@ -36,7 +36,12 @@ public ManifestAssemblyMetadata(PEReader peReader, MetadataReader metadataReader _peReader = peReader; } - public PEReader ImageReader => _peReader; + public BlobReader GetSectionData(int relativeVirtualAddress) + { + if (_peReader is null) + return default; + return _peReader.GetSectionData(relativeVirtualAddress).GetReader(); + } public MetadataReader MetadataReader => _metadataReader; diff --git a/src/coreclr/tools/aot/ILCompiler.Reflection.ReadyToRun/ReadyToRunMethod.cs b/src/coreclr/tools/aot/ILCompiler.Reflection.ReadyToRun/ReadyToRunMethod.cs index 0490fd8fe2a2cb..ffc9ce174c5061 100644 --- a/src/coreclr/tools/aot/ILCompiler.Reflection.ReadyToRun/ReadyToRunMethod.cs +++ b/src/coreclr/tools/aot/ILCompiler.Reflection.ReadyToRun/ReadyToRunMethod.cs @@ -403,13 +403,17 @@ public ReadyToRunMethod( case HandleKind.MethodDefinition: { MethodDefinition methodDef = ComponentReader.MetadataReader.GetMethodDefinition((MethodDefinitionHandle)MethodHandle); - if (methodDef.RelativeVirtualAddress != 0 && ComponentReader.ImageReader is not null) + if (methodDef.RelativeVirtualAddress != 0) { - MethodBodyBlock mbb = ComponentReader.ImageReader.GetMethodBody(methodDef.RelativeVirtualAddress); - if (!mbb.LocalSignature.IsNil) + BlobReader sectionData = ComponentReader.GetSectionData(methodDef.RelativeVirtualAddress); + if (sectionData.Length > 0) { - StandaloneSignature ss = ComponentReader.MetadataReader.GetStandaloneSignature(mbb.LocalSignature); - LocalSignature = ss.DecodeLocalSignature(typeProvider, genericContext); + MethodBodyBlock mbb = MethodBodyBlock.Create(sectionData); + if (!mbb.LocalSignature.IsNil) + { + StandaloneSignature ss = ComponentReader.MetadataReader.GetStandaloneSignature(mbb.LocalSignature); + LocalSignature = ss.DecodeLocalSignature(typeProvider, genericContext); + } } } Name = ComponentReader.MetadataReader.GetString(methodDef.Name); diff --git a/src/coreclr/tools/aot/ILCompiler.Reflection.ReadyToRun/StandaloneAssemblyMetadata.cs b/src/coreclr/tools/aot/ILCompiler.Reflection.ReadyToRun/StandaloneAssemblyMetadata.cs index a5ccea15f3d869..23b28e28a8e814 100644 --- a/src/coreclr/tools/aot/ILCompiler.Reflection.ReadyToRun/StandaloneAssemblyMetadata.cs +++ b/src/coreclr/tools/aot/ILCompiler.Reflection.ReadyToRun/StandaloneAssemblyMetadata.cs @@ -28,7 +28,7 @@ public StandaloneAssemblyMetadata(PEReader peReader) _metadataReader = _peReader.GetMetadataReader(); } - public PEReader ImageReader => _peReader; + public BlobReader GetSectionData(int relativeVirtualAddress) => _peReader.GetSectionData(relativeVirtualAddress).GetReader(); public MetadataReader MetadataReader => _metadataReader; } diff --git a/src/coreclr/tools/aot/ILCompiler.Reflection.ReadyToRun/WebcilImageReader.cs b/src/coreclr/tools/aot/ILCompiler.Reflection.ReadyToRun/WebcilImageReader.cs index 72a8328b2b4055..29ef96bde4a401 100644 --- a/src/coreclr/tools/aot/ILCompiler.Reflection.ReadyToRun/WebcilImageReader.cs +++ b/src/coreclr/tools/aot/ILCompiler.Reflection.ReadyToRun/WebcilImageReader.cs @@ -9,7 +9,6 @@ using System.Reflection.Metadata; using System.Reflection.PortableExecutable; using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; using Microsoft.NET.WebAssembly.Webcil; @@ -102,7 +101,7 @@ public static bool IsWebcilImage(string filename) { // Read the full file to check for Webcil inside WASM stream.Seek(0, SeekOrigin.Begin); - byte[] fullImage = new byte[stream.Length]; + byte[] fullImage = GC.AllocateArray((int)stream.Length, pinned: true); stream.Read(fullImage, 0, fullImage.Length); return TryFindWebcilInWasm(fullImage, out _); } @@ -118,6 +117,8 @@ public static bool IsWebcilImage(string filename) public ImmutableArray GetEntireImage() => Unsafe.As>(ref Unsafe.AsRef(in _image)); + internal byte[] GetImage() => _image; + public int GetOffset(int rva) { foreach (var section in _sections) @@ -156,10 +157,10 @@ public IAssemblyMetadata GetStandaloneAssemblyMetadata() return null; int metadataOffset = GetOffset(_corHeaderMetadataDirectory.RelativeVirtualAddress); - var metadataBytes = new byte[_corHeaderMetadataDirectory.Size]; + var metadataBytes = GC.AllocateArray(_corHeaderMetadataDirectory.Size, pinned: true); Array.Copy(_image, metadataOffset, metadataBytes, 0, _corHeaderMetadataDirectory.Size); - return new WebcilAssemblyMetadata(metadataBytes); + return new WebcilAssemblyMetadata(metadataBytes, this); } public IAssemblyMetadata GetManifestAssemblyMetadata(MetadataReader manifestReader) @@ -364,20 +365,31 @@ private static bool TryFindWebcilInWasm(byte[] image, out long webcilOffset) /// internal sealed unsafe class WebcilAssemblyMetadata : IAssemblyMetadata { - private readonly byte[] _metadataBytes; - private readonly GCHandle _pinnedBytes; private readonly MetadataReader _metadataReader; + private readonly WebcilImageReader _webcilReader; - public WebcilAssemblyMetadata(byte[] metadataBytes) + public WebcilAssemblyMetadata(byte[] metadataBytes, WebcilImageReader webcilReader) { - _metadataBytes = metadataBytes; - _pinnedBytes = GCHandle.Alloc(_metadataBytes, GCHandleType.Pinned); - _metadataReader = new MetadataReader( - (byte*)_pinnedBytes.AddrOfPinnedObject(), - _metadataBytes.Length); + _webcilReader = webcilReader; + fixed (byte* p = metadataBytes) + { + _metadataReader = new MetadataReader(p, metadataBytes.Length); + } } - public PEReader ImageReader => null; + public BlobReader GetSectionData(int relativeVirtualAddress) + { + if (_webcilReader is null) + return default; + + int offset = _webcilReader.GetOffset(relativeVirtualAddress); + byte[] image = _webcilReader.GetImage(); + int remaining = image.Length - offset; + fixed (byte* p = image) + { + return new BlobReader(p + offset, remaining); + } + } public MetadataReader MetadataReader => _metadataReader; } From 43dca47d2df393647de8e85c1662f9f722b96eab Mon Sep 17 00:00:00 2001 From: David Wrighton Date: Tue, 5 May 2026 16:36:25 -0700 Subject: [PATCH 41/43] Add WASM bytecode disassembler for R2RDump Implement a full WASM instruction disassembler that decodes WebAssembly binary format into WAT-style text output. This enables the --disasm flag in R2RDump to work with Webcil/WASM R2R images. - Add WasmDisassembler.cs with complete opcode tables for all standard WASM instructions (control, parametric, variable, table, memory, numeric, conversion, sign-extension, reference types) plus 0xFC (bulk memory/saturating truncation), 0xFB (GC), and 0xFD (SIMD) prefixed opcodes - Add WebcilImageReader.GetWasmFunctionBody() to parse the WASM module's type, function, and code sections to extract function info including type signature and local declarations - Integrate into TextDumper.DumpWasmDisasm() to print parameters and locals with their local indices, result types, and disassembled instructions Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../WebcilImageReader.cs | 166 +++++ src/coreclr/tools/r2rdump/TextDumper.cs | 97 ++- src/coreclr/tools/r2rdump/WasmDisassembler.cs | 680 ++++++++++++++++++ 3 files changed, 939 insertions(+), 4 deletions(-) create mode 100644 src/coreclr/tools/r2rdump/WasmDisassembler.cs diff --git a/src/coreclr/tools/aot/ILCompiler.Reflection.ReadyToRun/WebcilImageReader.cs b/src/coreclr/tools/aot/ILCompiler.Reflection.ReadyToRun/WebcilImageReader.cs index 29ef96bde4a401..501e3f5e06f35a 100644 --- a/src/coreclr/tools/aot/ILCompiler.Reflection.ReadyToRun/WebcilImageReader.cs +++ b/src/coreclr/tools/aot/ILCompiler.Reflection.ReadyToRun/WebcilImageReader.cs @@ -119,6 +119,172 @@ public ImmutableArray GetEntireImage() internal byte[] GetImage() => _image; + /// + /// Returns true if this Webcil image is wrapped inside a WASM module. + /// + public bool IsWasmWrapped => _webcilOffset > 0; + + /// + /// Represents a decoded WASM function body with its locals and type signature. + /// + public readonly struct WasmFunctionInfo + { + public byte[] Image { get; init; } + public int InstructionOffset { get; init; } + public int InstructionLength { get; init; } + /// Local variable declarations: (count, valtype byte) pairs. + public IReadOnlyList<(uint Count, byte ValType)> Locals { get; init; } + /// Parameter types from the function's type signature. + public IReadOnlyList ParamTypes { get; init; } + /// Result types from the function's type signature. + public IReadOnlyList ResultTypes { get; init; } + } + + /// + /// Gets the full function info for a WASM function by its index in the code section. + /// Returns null if the image is not WASM-wrapped or the function index is out of range. + /// + public WasmFunctionInfo? GetWasmFunctionBody(int functionIndex) + { + if (!IsWasmWrapped) + return null; + + // First pass: collect type section (section 1) and function section (section 3) + List<(byte[] ParamTypes, byte[] ResultTypes)> types = null; + List funcTypeIndices = null; + int codeOffset = -1; + + int offset = 8; // Skip WASM magic + version + while (offset < _image.Length) + { + byte sectionId = _image[offset++]; + uint sectionSize = ReadLebU32(_image, ref offset); + int sectionEnd = offset + (int)sectionSize; + + switch (sectionId) + { + case 1: // Type section + types = ParseTypeSection(_image, ref offset, sectionEnd); + break; + case 3: // Function section + funcTypeIndices = ParseFunctionSection(_image, ref offset, sectionEnd); + break; + case 10: // Code section + codeOffset = offset; + break; + } + + offset = sectionEnd; + } + + if (codeOffset < 0) + return null; + + // Parse the code section to find the target function body + offset = codeOffset; + uint funcCount = ReadLebU32(_image, ref offset); + for (uint i = 0; i < funcCount; i++) + { + uint bodySize = ReadLebU32(_image, ref offset); + int bodyEnd = offset + (int)bodySize; + + if (i == (uint)functionIndex) + { + // Read local declarations + var locals = new List<(uint Count, byte ValType)>(); + uint localDeclCount = ReadLebU32(_image, ref offset); + for (uint j = 0; j < localDeclCount; j++) + { + uint count = ReadLebU32(_image, ref offset); + byte valType = _image[offset++]; + locals.Add((count, valType)); + } + + int instrLength = bodyEnd - offset; + + // Resolve type signature + byte[] paramTypes = Array.Empty(); + byte[] resultTypes = Array.Empty(); + if (funcTypeIndices is not null && (uint)functionIndex < funcTypeIndices.Count) + { + uint typeIdx = funcTypeIndices[functionIndex]; + if (types is not null && typeIdx < types.Count) + { + paramTypes = types[(int)typeIdx].ParamTypes; + resultTypes = types[(int)typeIdx].ResultTypes; + } + } + + return new WasmFunctionInfo + { + Image = _image, + InstructionOffset = offset, + InstructionLength = instrLength, + Locals = locals, + ParamTypes = paramTypes, + ResultTypes = resultTypes + }; + } + + offset = bodyEnd; + } + + return null; + } + + private static List<(byte[] ParamTypes, byte[] ResultTypes)> ParseTypeSection(byte[] data, ref int offset, int end) + { + var types = new List<(byte[], byte[])>(); + uint count = ReadLebU32(data, ref offset); + for (uint i = 0; i < count && offset < end; i++) + { + byte form = data[offset++]; + // 0x60 = func type + if (form != 0x60) + { + // Skip unknown type forms + break; + } + uint paramCount = ReadLebU32(data, ref offset); + byte[] paramTypes = new byte[paramCount]; + for (uint j = 0; j < paramCount; j++) + paramTypes[j] = data[offset++]; + uint resultCount = ReadLebU32(data, ref offset); + byte[] resultTypes = new byte[resultCount]; + for (uint j = 0; j < resultCount; j++) + resultTypes[j] = data[offset++]; + types.Add((paramTypes, resultTypes)); + } + return types; + } + + private static List ParseFunctionSection(byte[] data, ref int offset, int end) + { + var indices = new List(); + uint count = ReadLebU32(data, ref offset); + for (uint i = 0; i < count && offset < end; i++) + { + indices.Add(ReadLebU32(data, ref offset)); + } + return indices; + } + + private static uint ReadLebU32(byte[] data, ref int offset) + { + uint result = 0; + int shift = 0; + byte b; + do + { + if (offset >= data.Length) + return result; + b = data[offset++]; + result |= (uint)(b & 0x7F) << shift; + shift += 7; + } while ((b & 0x80) != 0); + return result; + } + public int GetOffset(int rva) { foreach (var section in _sections) diff --git a/src/coreclr/tools/r2rdump/TextDumper.cs b/src/coreclr/tools/r2rdump/TextDumper.cs index d9fd852366421f..13d30dd36f337a 100644 --- a/src/coreclr/tools/r2rdump/TextDumper.cs +++ b/src/coreclr/tools/r2rdump/TextDumper.cs @@ -230,21 +230,37 @@ public override void DumpRuntimeFunction(RuntimeFunction rtf) _writer.WriteLine(rtf.Method.SignatureString); rtf.WriteTo(_writer, _model); + bool isWasm = _r2r.CompositeReader is WebcilImageReader; + if (_model.Disasm) { - DumpDisasm(rtf, _r2r.GetOffset(rtf.StartAddress)); + if (isWasm) + { + DumpWasmDisasm(rtf); + } + else + { + DumpDisasm(rtf, _r2r.GetOffset(rtf.StartAddress)); + } } if (_model.Raw) { - _writer.WriteLine("Raw Bytes:"); - DumpBytes(rtf.StartAddress, (uint)rtf.Size); + if (isWasm) + { + _writer.WriteLine("Raw Bytes: (not available for WASM function indices)"); + } + else + { + _writer.WriteLine("Raw Bytes:"); + DumpBytes(rtf.StartAddress, (uint)rtf.Size); + } } if (_model.Unwind) { _writer.WriteLine("UnwindInfo:"); _writer.Write(rtf.UnwindInfo); - if (_model.Raw) + if (_model.Raw && !isWasm) { DumpBytes(rtf.UnwindRVA, (uint)rtf.UnwindInfo.Size); } @@ -252,6 +268,79 @@ public override void DumpRuntimeFunction(RuntimeFunction rtf) SkipLine(); } + private void DumpWasmDisasm(RuntimeFunction rtf) + { + var webcilReader = (WebcilImageReader)_r2r.CompositeReader; + var body = webcilReader.GetWasmFunctionBody(rtf.StartAddress); + if (body is null) + { + _writer.WriteLine($" ; WASM function index: {rtf.StartAddress} (function body not found)"); + return; + } + + var info = body.Value; + _writer.WriteLine($" ; WASM function index: {rtf.StartAddress}"); + + // Print parameters with their local indices + int localIdx = 0; + if (info.ParamTypes.Count > 0) + { + _writer.WriteLine(" ; Parameters:"); + foreach (byte paramType in info.ParamTypes) + { + _writer.WriteLine($" ; [{localIdx}] {WasmValTypeName(paramType)}"); + localIdx++; + } + } + + // Print locals with their local indices + if (info.Locals.Count > 0) + { + _writer.WriteLine(" ; Locals:"); + foreach (var (count, valType) in info.Locals) + { + string typeName = WasmValTypeName(valType); + for (uint k = 0; k < count; k++) + { + _writer.WriteLine($" ; [{localIdx}] {typeName}"); + localIdx++; + } + } + } + + // Print result types + if (info.ResultTypes.Count > 0) + { + string resultStr = string.Join(", ", FormatValTypes(info.ResultTypes)); + _writer.WriteLine($" ; Results: {resultStr}"); + } + + _writer.WriteLine(); + var disasm = new WasmDisassembler(info.Image, info.InstructionOffset, info.InstructionLength); + _writer.Write(disasm.Disassemble()); + } + + private static IEnumerable FormatValTypes(IReadOnlyList types) + { + foreach (byte t in types) + yield return WasmValTypeName(t); + } + + private static string WasmValTypeName(byte b) + { + return b switch + { + 0x7F => "i32", + 0x7E => "i64", + 0x7D => "f32", + 0x7C => "f64", + 0x7B => "v128", + 0x70 => "funcref", + 0x6F => "externref", + _ => $"0x{b:X2}" + }; + } + /// /// Dumps disassembly and register liveness /// diff --git a/src/coreclr/tools/r2rdump/WasmDisassembler.cs b/src/coreclr/tools/r2rdump/WasmDisassembler.cs new file mode 100644 index 00000000000000..9d11f5e14b8dbf --- /dev/null +++ b/src/coreclr/tools/r2rdump/WasmDisassembler.cs @@ -0,0 +1,680 @@ +// 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.Collections.Generic; +using System.Text; + +namespace R2RDump +{ + /// + /// Disassembler for WebAssembly bytecode. + /// Decodes WASM binary instructions into WAT (WebAssembly Text) format. + /// Based on the WebAssembly specification: https://webassembly.github.io/spec/core/ + /// + internal sealed class WasmDisassembler + { + private readonly byte[] _code; + private int _offset; + private readonly int _baseOffset; + private readonly int _endOffset; + + public WasmDisassembler(byte[] code, int offset, int length) + { + _code = code; + _baseOffset = offset; + _offset = offset; + _endOffset = offset + length; + } + + /// + /// Disassemble all instructions in the function body and return the textual representation. + /// + public string Disassemble() + { + var sb = new StringBuilder(); + int indent = 0; + + while (_offset < _endOffset) + { + int instrOffset = _offset - _baseOffset; + string instr = DecodeInstruction(ref indent); + + sb.Append($" {instrOffset:X4}: "); + if (indent > 0) + { + sb.Append(' ', indent * 2); + } + sb.AppendLine(instr); + } + + return sb.ToString(); + } + + private string DecodeInstruction(ref int indent) + { + byte opcode = ReadByte(); + + switch (opcode) + { + // Control instructions + case 0x00: return "unreachable"; + case 0x01: return "nop"; + case 0x02: + { + string bt = ReadBlockType(); + indent++; + return $"block{bt}"; + } + case 0x03: + { + string bt = ReadBlockType(); + indent++; + return $"loop{bt}"; + } + case 0x04: + { + string bt = ReadBlockType(); + indent++; + return $"if{bt}"; + } + case 0x05: + return "else"; + case 0x08: + return $"throw {ReadU32()}"; + case 0x0A: + return "throw_ref"; + case 0x0B: + if (indent > 0) indent--; + return "end"; + case 0x0C: return $"br {ReadU32()}"; + case 0x0D: return $"br_if {ReadU32()}"; + case 0x0E: + { + uint count = ReadU32(); + var sb = new StringBuilder("br_table"); + for (uint i = 0; i <= count; i++) + { + sb.Append($" {ReadU32()}"); + } + return sb.ToString(); + } + case 0x0F: return "return"; + case 0x10: return $"call {ReadU32()}"; + case 0x11: + { + uint typeIdx = ReadU32(); + uint tableIdx = ReadU32(); + return $"call_indirect {tableIdx} (type {typeIdx})"; + } + case 0x12: return $"return_call {ReadU32()}"; + case 0x13: + { + uint typeIdx = ReadU32(); + uint tableIdx = ReadU32(); + return $"return_call_indirect {tableIdx} (type {typeIdx})"; + } + case 0x14: return $"call_ref {ReadU32()}"; + case 0x15: return $"return_call_ref {ReadU32()}"; + + // Parametric instructions + case 0x1A: return "drop"; + case 0x1B: return "select"; + case 0x1C: + { + uint count = ReadU32(); + var sb = new StringBuilder("select"); + for (uint i = 0; i < count; i++) + { + sb.Append($" {ValTypeName(ReadByte())}"); + } + return sb.ToString(); + } + + // Variable instructions + case 0x20: return $"local.get {ReadU32()}"; + case 0x21: return $"local.set {ReadU32()}"; + case 0x22: return $"local.tee {ReadU32()}"; + case 0x23: return $"global.get {ReadU32()}"; + case 0x24: return $"global.set {ReadU32()}"; + + // Table instructions + case 0x25: return $"table.get {ReadU32()}"; + case 0x26: return $"table.set {ReadU32()}"; + + // Memory instructions + case 0x28: return $"i32.load {ReadMemArg()}"; + case 0x29: return $"i64.load {ReadMemArg()}"; + case 0x2A: return $"f32.load {ReadMemArg()}"; + case 0x2B: return $"f64.load {ReadMemArg()}"; + case 0x2C: return $"i32.load8_s {ReadMemArg()}"; + case 0x2D: return $"i32.load8_u {ReadMemArg()}"; + case 0x2E: return $"i32.load16_s {ReadMemArg()}"; + case 0x2F: return $"i32.load16_u {ReadMemArg()}"; + case 0x30: return $"i64.load8_s {ReadMemArg()}"; + case 0x31: return $"i64.load8_u {ReadMemArg()}"; + case 0x32: return $"i64.load16_s {ReadMemArg()}"; + case 0x33: return $"i64.load16_u {ReadMemArg()}"; + case 0x34: return $"i64.load32_s {ReadMemArg()}"; + case 0x35: return $"i64.load32_u {ReadMemArg()}"; + case 0x36: return $"i32.store {ReadMemArg()}"; + case 0x37: return $"i64.store {ReadMemArg()}"; + case 0x38: return $"f32.store {ReadMemArg()}"; + case 0x39: return $"f64.store {ReadMemArg()}"; + case 0x3A: return $"i32.store8 {ReadMemArg()}"; + case 0x3B: return $"i32.store16 {ReadMemArg()}"; + case 0x3C: return $"i64.store8 {ReadMemArg()}"; + case 0x3D: return $"i64.store16 {ReadMemArg()}"; + case 0x3E: return $"i64.store32 {ReadMemArg()}"; + case 0x3F: + { + uint memIdx = ReadU32(); + return $"memory.size {memIdx}"; + } + case 0x40: + { + uint memIdx = ReadU32(); + return $"memory.grow {memIdx}"; + } + + // Numeric instructions - constants + case 0x41: return $"i32.const {ReadI32()}"; + case 0x42: return $"i64.const {ReadI64()}"; + case 0x43: return $"f32.const {ReadF32()}"; + case 0x44: return $"f64.const {ReadF64()}"; + + // Numeric instructions - comparison (i32) + case 0x45: return "i32.eqz"; + case 0x46: return "i32.eq"; + case 0x47: return "i32.ne"; + case 0x48: return "i32.lt_s"; + case 0x49: return "i32.lt_u"; + case 0x4A: return "i32.gt_s"; + case 0x4B: return "i32.gt_u"; + case 0x4C: return "i32.le_s"; + case 0x4D: return "i32.le_u"; + case 0x4E: return "i32.ge_s"; + case 0x4F: return "i32.ge_u"; + + // Numeric instructions - comparison (i64) + case 0x50: return "i64.eqz"; + case 0x51: return "i64.eq"; + case 0x52: return "i64.ne"; + case 0x53: return "i64.lt_s"; + case 0x54: return "i64.lt_u"; + case 0x55: return "i64.gt_s"; + case 0x56: return "i64.gt_u"; + case 0x57: return "i64.le_s"; + case 0x58: return "i64.le_u"; + case 0x59: return "i64.ge_s"; + case 0x5A: return "i64.ge_u"; + + // Numeric instructions - comparison (f32) + case 0x5B: return "f32.eq"; + case 0x5C: return "f32.ne"; + case 0x5D: return "f32.lt"; + case 0x5E: return "f32.gt"; + case 0x5F: return "f32.le"; + case 0x60: return "f32.ge"; + + // Numeric instructions - comparison (f64) + case 0x61: return "f64.eq"; + case 0x62: return "f64.ne"; + case 0x63: return "f64.lt"; + case 0x64: return "f64.gt"; + case 0x65: return "f64.le"; + case 0x66: return "f64.ge"; + + // Numeric instructions - arithmetic (i32) + case 0x67: return "i32.clz"; + case 0x68: return "i32.ctz"; + case 0x69: return "i32.popcnt"; + case 0x6A: return "i32.add"; + case 0x6B: return "i32.sub"; + case 0x6C: return "i32.mul"; + case 0x6D: return "i32.div_s"; + case 0x6E: return "i32.div_u"; + case 0x6F: return "i32.rem_s"; + case 0x70: return "i32.rem_u"; + case 0x71: return "i32.and"; + case 0x72: return "i32.or"; + case 0x73: return "i32.xor"; + case 0x74: return "i32.shl"; + case 0x75: return "i32.shr_s"; + case 0x76: return "i32.shr_u"; + case 0x77: return "i32.rotl"; + case 0x78: return "i32.rotr"; + + // Numeric instructions - arithmetic (i64) + case 0x79: return "i64.clz"; + case 0x7A: return "i64.ctz"; + case 0x7B: return "i64.popcnt"; + case 0x7C: return "i64.add"; + case 0x7D: return "i64.sub"; + case 0x7E: return "i64.mul"; + case 0x7F: return "i64.div_s"; + case 0x80: return "i64.div_u"; + case 0x81: return "i64.rem_s"; + case 0x82: return "i64.rem_u"; + case 0x83: return "i64.and"; + case 0x84: return "i64.or"; + case 0x85: return "i64.xor"; + case 0x86: return "i64.shl"; + case 0x87: return "i64.shr_s"; + case 0x88: return "i64.shr_u"; + case 0x89: return "i64.rotl"; + case 0x8A: return "i64.rotr"; + + // Numeric instructions - arithmetic (f32) + case 0x8B: return "f32.abs"; + case 0x8C: return "f32.neg"; + case 0x8D: return "f32.ceil"; + case 0x8E: return "f32.floor"; + case 0x8F: return "f32.trunc"; + case 0x90: return "f32.nearest"; + case 0x91: return "f32.sqrt"; + case 0x92: return "f32.add"; + case 0x93: return "f32.sub"; + case 0x94: return "f32.mul"; + case 0x95: return "f32.div"; + case 0x96: return "f32.min"; + case 0x97: return "f32.max"; + case 0x98: return "f32.copysign"; + + // Numeric instructions - arithmetic (f64) + case 0x99: return "f64.abs"; + case 0x9A: return "f64.neg"; + case 0x9B: return "f64.ceil"; + case 0x9C: return "f64.floor"; + case 0x9D: return "f64.trunc"; + case 0x9E: return "f64.nearest"; + case 0x9F: return "f64.sqrt"; + case 0xA0: return "f64.add"; + case 0xA1: return "f64.sub"; + case 0xA2: return "f64.mul"; + case 0xA3: return "f64.div"; + case 0xA4: return "f64.min"; + case 0xA5: return "f64.max"; + case 0xA6: return "f64.copysign"; + + // Numeric instructions - conversions + case 0xA7: return "i32.wrap_i64"; + case 0xA8: return "i32.trunc_f32_s"; + case 0xA9: return "i32.trunc_f32_u"; + case 0xAA: return "i32.trunc_f64_s"; + case 0xAB: return "i32.trunc_f64_u"; + case 0xAC: return "i64.extend_i32_s"; + case 0xAD: return "i64.extend_i32_u"; + case 0xAE: return "i64.trunc_f32_s"; + case 0xAF: return "i64.trunc_f32_u"; + case 0xB0: return "i64.trunc_f64_s"; + case 0xB1: return "i64.trunc_f64_u"; + case 0xB2: return "f32.convert_i32_s"; + case 0xB3: return "f32.convert_i32_u"; + case 0xB4: return "f32.convert_i64_s"; + case 0xB5: return "f32.convert_i64_u"; + case 0xB6: return "f32.demote_f64"; + case 0xB7: return "f64.convert_i32_s"; + case 0xB8: return "f64.convert_i32_u"; + case 0xB9: return "f64.convert_i64_s"; + case 0xBA: return "f64.convert_i64_u"; + case 0xBB: return "f64.promote_f32"; + case 0xBC: return "i32.reinterpret_f32"; + case 0xBD: return "i64.reinterpret_f64"; + case 0xBE: return "f32.reinterpret_i32"; + case 0xBF: return "f64.reinterpret_i64"; + + // Sign extension instructions + case 0xC0: return "i32.extend8_s"; + case 0xC1: return "i32.extend16_s"; + case 0xC2: return "i64.extend8_s"; + case 0xC3: return "i64.extend16_s"; + case 0xC4: return "i64.extend32_s"; + + // Reference instructions + case 0xD0: return $"ref.null {ReadHeapType()}"; + case 0xD1: return "ref.is_null"; + case 0xD2: return $"ref.func {ReadU32()}"; + case 0xD3: return "ref.eq"; + case 0xD4: return "ref.as_non_null"; + case 0xD5: return $"br_on_null {ReadU32()}"; + case 0xD6: return $"br_on_non_null {ReadU32()}"; + + // GC instructions (0xFB prefix) + case 0xFB: return DecodeFBPrefixed(); + + // Saturating truncation and bulk memory (0xFC prefix) + case 0xFC: return DecodeFCPrefixed(); + + // SIMD (0xFD prefix) + case 0xFD: return DecodeFDPrefixed(); + + default: + return $""; + } + } + + private string DecodeFCPrefixed() + { + uint subOpcode = ReadU32(); + switch (subOpcode) + { + case 0: return "i32.trunc_sat_f32_s"; + case 1: return "i32.trunc_sat_f32_u"; + case 2: return "i32.trunc_sat_f64_s"; + case 3: return "i32.trunc_sat_f64_u"; + case 4: return "i64.trunc_sat_f32_s"; + case 5: return "i64.trunc_sat_f32_u"; + case 6: return "i64.trunc_sat_f64_s"; + case 7: return "i64.trunc_sat_f64_u"; + case 8: + { + uint dataIdx = ReadU32(); + uint memIdx = ReadU32(); + return $"memory.init {dataIdx} {memIdx}"; + } + case 9: return $"data.drop {ReadU32()}"; + case 10: + { + uint dstMem = ReadU32(); + uint srcMem = ReadU32(); + return $"memory.copy {dstMem} {srcMem}"; + } + case 11: return $"memory.fill {ReadU32()}"; + case 12: + { + uint elemIdx = ReadU32(); + uint tableIdx = ReadU32(); + return $"table.init {tableIdx} {elemIdx}"; + } + case 13: return $"elem.drop {ReadU32()}"; + case 14: + { + uint dstTable = ReadU32(); + uint srcTable = ReadU32(); + return $"table.copy {dstTable} {srcTable}"; + } + case 15: return $"table.grow {ReadU32()}"; + case 16: return $"table.size {ReadU32()}"; + case 17: return $"table.fill {ReadU32()}"; + default: + return $""; + } + } + + private string DecodeFBPrefixed() + { + uint subOpcode = ReadU32(); + switch (subOpcode) + { + case 0: return $"struct.new {ReadU32()}"; + case 1: return $"struct.new_default {ReadU32()}"; + case 2: + { + uint typeIdx = ReadU32(); + uint fieldIdx = ReadU32(); + return $"struct.get {typeIdx} {fieldIdx}"; + } + case 3: + { + uint typeIdx = ReadU32(); + uint fieldIdx = ReadU32(); + return $"struct.get_s {typeIdx} {fieldIdx}"; + } + case 4: + { + uint typeIdx = ReadU32(); + uint fieldIdx = ReadU32(); + return $"struct.get_u {typeIdx} {fieldIdx}"; + } + case 5: + { + uint typeIdx = ReadU32(); + uint fieldIdx = ReadU32(); + return $"struct.set {typeIdx} {fieldIdx}"; + } + case 6: return $"array.new {ReadU32()}"; + case 7: return $"array.new_default {ReadU32()}"; + case 8: + { + uint typeIdx = ReadU32(); + uint size = ReadU32(); + return $"array.new_fixed {typeIdx} {size}"; + } + case 9: + { + uint typeIdx = ReadU32(); + uint dataIdx = ReadU32(); + return $"array.new_data {typeIdx} {dataIdx}"; + } + case 10: + { + uint typeIdx = ReadU32(); + uint elemIdx = ReadU32(); + return $"array.new_elem {typeIdx} {elemIdx}"; + } + case 11: return $"array.get {ReadU32()}"; + case 12: return $"array.get_s {ReadU32()}"; + case 13: return $"array.get_u {ReadU32()}"; + case 14: return $"array.set {ReadU32()}"; + case 15: return "array.len"; + case 16: return $"array.fill {ReadU32()}"; + case 17: + { + uint dstType = ReadU32(); + uint srcType = ReadU32(); + return $"array.copy {dstType} {srcType}"; + } + case 18: + { + uint typeIdx = ReadU32(); + uint dataIdx = ReadU32(); + return $"array.init_data {typeIdx} {dataIdx}"; + } + case 19: + { + uint typeIdx = ReadU32(); + uint elemIdx = ReadU32(); + return $"array.init_elem {typeIdx} {elemIdx}"; + } + case 20: return $"ref.test (ref {ReadHeapType()})"; + case 21: return $"ref.test (ref null {ReadHeapType()})"; + case 22: return $"ref.cast (ref {ReadHeapType()})"; + case 23: return $"ref.cast (ref null {ReadHeapType()})"; + case 26: return "any.convert_extern"; + case 27: return "extern.convert_any"; + case 28: return $"ref.i31"; + case 29: return "i31.get_s"; + case 30: return "i31.get_u"; + default: + return $""; + } + } + + private string DecodeFDPrefixed() + { + uint subOpcode = ReadU32(); + // SIMD instructions - just show the sub-opcode for now + // Full SIMD decode would add hundreds of entries + if (subOpcode == 0) + { + // v128.load memarg + return $"v128.load {ReadMemArg()}"; + } + if (subOpcode == 11) + { + // v128.store memarg + return $"v128.store {ReadMemArg()}"; + } + if (subOpcode == 12) + { + // v128.const - 16 bytes immediate + var bytes = new byte[16]; + for (int i = 0; i < 16; i++) + bytes[i] = ReadByte(); + return $"v128.const 0x{BitConverter.ToString(bytes).Replace("-", "")}"; + } + return $""; + } + + private byte ReadByte() + { + if (_offset >= _endOffset) + return 0; + return _code[_offset++]; + } + + private uint ReadU32() + { + uint result = 0; + int shift = 0; + byte b; + do + { + b = ReadByte(); + result |= (uint)(b & 0x7F) << shift; + shift += 7; + } while ((b & 0x80) != 0); + return result; + } + + private int ReadI32() + { + int result = 0; + int shift = 0; + byte b; + do + { + b = ReadByte(); + result |= (int)(b & 0x7F) << shift; + shift += 7; + } while ((b & 0x80) != 0); + // Sign extend + if (shift < 32 && (b & 0x40) != 0) + result |= -(1 << shift); + return result; + } + + private long ReadI64() + { + long result = 0; + int shift = 0; + byte b; + do + { + b = ReadByte(); + result |= (long)(b & 0x7F) << shift; + shift += 7; + } while ((b & 0x80) != 0); + // Sign extend + if (shift < 64 && (b & 0x40) != 0) + result |= -(1L << shift); + return result; + } + + private float ReadF32() + { + if (_offset + 4 > _endOffset) + { + _offset = _endOffset; + return 0; + } + float val = BitConverter.ToSingle(_code, _offset); + _offset += 4; + return val; + } + + private double ReadF64() + { + if (_offset + 8 > _endOffset) + { + _offset = _endOffset; + return 0; + } + double val = BitConverter.ToDouble(_code, _offset); + _offset += 8; + return val; + } + + private string ReadMemArg() + { + uint align = ReadU32(); + // Bit 6 indicates multi-memory (memory index follows) + uint memIdx = 0; + if ((align & 0x40) != 0) + { + align &= ~0x40u; + memIdx = ReadU32(); + } + uint offset = ReadU32(); + if (memIdx != 0) + return $"align={1u << (int)align} offset={offset} mem={memIdx}"; + if (offset != 0) + return $"align={1u << (int)align} offset={offset}"; + return $"align={1u << (int)align}"; + } + + private string ReadBlockType() + { + if (_offset >= _endOffset) + return ""; + byte b = _code[_offset]; + if (b == 0x40) + { + _offset++; + return ""; + } + // Value type encoding uses single bytes 0x7F-0x70 range + if (b >= 0x70 && b <= 0x7F) + { + _offset++; + return $" (result {ValTypeName(b)})"; + } + // Otherwise it's a type index as a signed LEB128 + int typeIdx = ReadI32(); + return $" (type {typeIdx})"; + } + + private string ReadHeapType() + { + byte b = _code[_offset]; + // Abstract heap types are encoded as single bytes + switch (b) + { + case 0x73: _offset++; return "nofunc"; + case 0x72: _offset++; return "noextern"; + case 0x71: _offset++; return "none"; + case 0x70: _offset++; return "func"; + case 0x6F: _offset++; return "extern"; + case 0x6E: _offset++; return "any"; + case 0x6D: _offset++; return "eq"; + case 0x6C: _offset++; return "i31"; + case 0x6B: _offset++; return "struct"; + case 0x6A: _offset++; return "array"; + default: + // Type index as signed LEB128 + return ReadI32().ToString(); + } + } + + private static string ValTypeName(byte b) + { + return b switch + { + 0x7F => "i32", + 0x7E => "i64", + 0x7D => "f32", + 0x7C => "f64", + 0x7B => "v128", + 0x70 => "funcref", + 0x6F => "externref", + 0x6E => "anyref", + 0x6D => "eqref", + 0x6C => "i31ref", + 0x6B => "structref", + 0x6A => "arrayref", + _ => $"" + }; + } + } +} From 19586b89ef2b692ae9797ea8af279d46a1bf33d0 Mon Sep 17 00:00:00 2001 From: David Wrighton Date: Tue, 5 May 2026 16:48:11 -0700 Subject: [PATCH 42/43] Fix GC collection of pinned metadata array in WebcilAssemblyMetadata WebcilAssemblyMetadata was not retaining a reference to the pinned metadata byte array passed to its constructor. After GetStandaloneAssemblyMetadata returned, the array could be collected by the GC despite being allocated on the Pinned Object Heap, since no live reference existed. This caused an AccessViolationException when MetadataReader accessed the freed memory on larger files like system.private.corelib.wasm. Fix: store the metadata byte array in a field to keep it rooted for the lifetime of the MetadataReader. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../aot/ILCompiler.Reflection.ReadyToRun/WebcilImageReader.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/coreclr/tools/aot/ILCompiler.Reflection.ReadyToRun/WebcilImageReader.cs b/src/coreclr/tools/aot/ILCompiler.Reflection.ReadyToRun/WebcilImageReader.cs index 501e3f5e06f35a..6174ba5c7da123 100644 --- a/src/coreclr/tools/aot/ILCompiler.Reflection.ReadyToRun/WebcilImageReader.cs +++ b/src/coreclr/tools/aot/ILCompiler.Reflection.ReadyToRun/WebcilImageReader.cs @@ -533,10 +533,12 @@ internal sealed unsafe class WebcilAssemblyMetadata : IAssemblyMetadata { private readonly MetadataReader _metadataReader; private readonly WebcilImageReader _webcilReader; + private readonly byte[] _metadataBytes; public WebcilAssemblyMetadata(byte[] metadataBytes, WebcilImageReader webcilReader) { _webcilReader = webcilReader; + _metadataBytes = metadataBytes; fixed (byte* p = metadataBytes) { _metadataReader = new MetadataReader(p, metadataBytes.Length); From 54a66ddd431ab61c992740be4034bc09a53b53c9 Mon Sep 17 00:00:00 2001 From: David Wrighton Date: Tue, 5 May 2026 16:52:29 -0700 Subject: [PATCH 43/43] Use .wasm file extension for WASM R2R output in test scripts WASM R2R images should use .wasm extension instead of .dll. Update CLRTest.CrossGen.targets to: - Set output extension to .wasm for both composite and non-composite modes in bash and batch scripts when CrossGen2OutputFormat is 'wasm' - Pass -f flag to crossgen2 in batch scripts (matching bash behavior) Also set CrossGen2OutputFormat=wasm in Directory.Build.props for browser target OS so all tests targeting wasm use the correct format. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/tests/Common/CLRTest.CrossGen.targets | 21 +++++++++++++++++++-- src/tests/Directory.Build.props | 1 + 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/src/tests/Common/CLRTest.CrossGen.targets b/src/tests/Common/CLRTest.CrossGen.targets index ceaa10324ca08b..1add20a931f4d7 100644 --- a/src/tests/Common/CLRTest.CrossGen.targets +++ b/src/tests/Common/CLRTest.CrossGen.targets @@ -174,6 +174,9 @@ if [ ! -z ${RunCrossGen2+x} ]%3B then if [ "$(CrossGen2OutputFormat)" == "macho" ]; then __CompositeExt=".o" __CompositeExtFinal=".dylib" + elif [ "$(CrossGen2OutputFormat)" == "wasm" ]; then + __CompositeExt=".wasm" + __CompositeExtFinal=".wasm" fi RunCrossgen2OnFiles "$PWD/composite-r2r${__CompositeExt}" "$PWD/composite-r2r${__CompositeExtFinal}" "$PWD/IL-CG2/*.dll" else @@ -182,6 +185,9 @@ if [ ! -z ${RunCrossGen2+x} ]%3B then do echo $dllFile bareFileName="${dllFile##*/}" + if [ "$(CrossGen2OutputFormat)" == "wasm" ]; then + bareFileName="${bareFileName%.dll}.wasm" + fi RunCrossgen2OnFiles "$PWD/$bareFileName" "$PWD/$bareFileName" "$dllFile" if [ $__cg2ExitCode -ne 0 ]; then break @@ -255,7 +261,11 @@ if defined RunCrossGen2 ( if defined CompositeBuildMode ( set ExtraCrossGen2Args=!ExtraCrossGen2Args! --composite - set __OutputFile=!scriptPath!\composite-r2r.dll + if "$(CrossGen2OutputFormat)"=="wasm" ( + set __OutputFile=!scriptPath!\composite-r2r.wasm + ) else ( + set __OutputFile=!scriptPath!\composite-r2r.dll + ) set __PdbFile=!scriptPath!\composite-r2r.ni.pdb rem In composite mode, treat all dll's in the test folder as rooting inputs set __InputFile=!scriptPath!IL-CG2\*.dll @@ -264,7 +274,11 @@ if defined RunCrossGen2 ( ) else ( set ExtraCrossGen2Args=!ExtraCrossGen2Args! -r:!scriptPath!IL-CG2\*.dll for %%I in (!scriptPath!IL-CG2\*.dll) do ( - set __OutputFile=!scriptPath!%%~nI.dll + if "$(CrossGen2OutputFormat)"=="wasm" ( + set __OutputFile=!scriptPath!%%~nI.wasm + ) else ( + set __OutputFile=!scriptPath!%%~nI.dll + ) set __PdbFile=!scriptPath!%%~nI.ni.pdb set __InputFile=%%I call :CompileOneFileCrossgen2 @@ -302,6 +316,9 @@ if defined RunCrossGen2 ( echo --targetos:$(TargetOS)>>!__ResponseFile! echo --verify-type-and-field-layout>>!__ResponseFile! echo --method-layout:random>>!__ResponseFile! + if not "$(CrossGen2OutputFormat)"=="" ( + echo -f:$(CrossGen2OutputFormat)>>!__ResponseFile! + ) if defined CrossGen2SynthesizePgo ( echo --synthesize-random-mibc>>!__ResponseFile! echo --embed-pgo-data>>!__ResponseFile! diff --git a/src/tests/Directory.Build.props b/src/tests/Directory.Build.props index b6dce9ef455492..db8747e62a7edf 100644 --- a/src/tests/Directory.Build.props +++ b/src/tests/Directory.Build.props @@ -202,6 +202,7 @@ /p:MSBuildEnableWorkloadResolver=false /p:Configuration=$(Configuration) + wasm