From fee66de56897e9bab810ba9f6849459744d03643 Mon Sep 17 00:00:00 2001 From: David Wrighton Date: Thu, 23 Apr 2026 15:03:55 -0700 Subject: [PATCH 01/42] 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/42] 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/42] 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/42] 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/42] 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/42] 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/42] 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/42] 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/42] 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/42] 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/42] 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/42] 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/42] 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/42] 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/42] 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/42] 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/42] 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/42] 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/42] 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/42] 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/42] 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/42] 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/42] 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/42] 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/42] 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/42] 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/42] 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/42] 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/42] - 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/42] - 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/42] 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/42] 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/42] 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/42] - 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/42] 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/42] 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 c07aa4505fbeed8e4a16427334e71df1d054934f Mon Sep 17 00:00:00 2001 From: David Wrighton Date: Tue, 5 May 2026 10:51:59 -0700 Subject: [PATCH 37/42] 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 92ac3399288216a28dc7b4f7abccfea6fd74c87c Mon Sep 17 00:00:00 2001 From: David Wrighton Date: Wed, 6 May 2026 14:11:55 -0700 Subject: [PATCH 38/42] Feedback --- docs/design/coreclr/botr/readytorun-format.md | 2 +- .../DependencyAnalysis/ReadyToRun/WasmImportThunk.cs | 8 ++++---- .../ReadyToRun/WasmInterpreterToR2RThunkNode.cs | 8 ++++---- .../ReadyToRun/WasmR2RToInterpreterThunkNode.cs | 6 +++--- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/docs/design/coreclr/botr/readytorun-format.md b/docs/design/coreclr/botr/readytorun-format.md index a54253862324aa..59dead55ab2bf3 100644 --- a/docs/design/coreclr/botr/readytorun-format.md +++ b/docs/design/coreclr/botr/readytorun-format.md @@ -1055,7 +1055,7 @@ it knows a hidden retbuf pointer argument is present in the Wasm parameter list. | `l` | `i64` parameter | | `f` | `f32` parameter | | `d` | `f64` parameter | -| `S` | struct parameter passed by reference, `N` is the struct size in bytes | +| `S` | struct parameter passed by reference, `` is the struct size in bytes | | `e` | empty struct parameter — elided from Wasm args but present in the string | **Suffix**: 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 ca66ab87016ac3..b5f200735f9894 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,14 +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]; - bool[] isIndirectArg = new bool[methodSignature.Length]; + bool[] isIndirectStructArg = new bool[methodSignature.Length]; int argIndex = 0; int argOffset; while ((argOffset = argit.GetNextOffset()) != TransitionBlock.InvalidOffset) { offsets[argIndex] = argOffset; - isIndirectArg[argIndex] = argit.IsArgPassedByRef() && argit.IsValueType(); + isIndirectStructArg[argIndex] = argit.IsArgPassedByRef() && argit.IsValueType(); argIndex++; } @@ -207,7 +207,7 @@ protected override void EmitCode(NodeFactory factory, ref Wasm.WasmEmitter instr int currentOffset = offsets[i]; - if (isIndirectArg[i]) + if (isIndirectStructArg[i]) { // Indirect struct — zero-fill the transition block slot instead of copying the byref pointer. int structSize = paramType.GetElementSize().AsInt; @@ -312,7 +312,7 @@ protected override void EmitCode(NodeFactory factory, ref Wasm.WasmEmitter instr continue; } - if (isIndirectArg[i]) + if (isIndirectStructArg[i]) { // Indirect struct — pass the original byref pointer from the caller expressions.Add(Local.Get(wasmLocalIndex)); 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 ed73571d05ef14..bf6ba702683af0 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 @@ -95,14 +95,14 @@ protected override void EmitCode(NodeFactory factory, ref Wasm.WasmEmitter instr // the byte offset into pArgs. int sizeOfTransitionBlock = transitionBlock.SizeOfTransitionBlock; int[] interpOffsets = new int[methodSignature.Length]; - bool[] isIndirectArg = new bool[methodSignature.Length]; + bool[] isIndirectStructArg = new bool[methodSignature.Length]; int argIndex = 0; int argOffset; while ((argOffset = argit.GetNextOffset()) != TransitionBlock.InvalidOffset) { interpOffsets[argIndex] = argOffset - sizeOfTransitionBlock; - isIndirectArg[argIndex] = argit.IsArgPassedByRef() && argit.IsValueType(); + isIndirectStructArg[argIndex] = argit.IsArgPassedByRef() && argit.IsValueType(); argIndex++; } @@ -181,7 +181,7 @@ protected override void EmitCode(NodeFactory factory, ref Wasm.WasmEmitter instr continue; } - if (isIndirectArg[i]) + if (isIndirectStructArg[i]) { // Byreference struct — pass a pointer into the incoming pArgs buffer expressions.Add(Local.Get(LocalPArgs)); @@ -219,7 +219,7 @@ protected override void EmitCode(NodeFactory factory, ref Wasm.WasmEmitter instr // call_indirect with the target R2R function's type signature expressions.Add(Local.Get(LocalPortableEntrypoint)); - expressions.Add(I32.Load(0)); // load the actual function index from the type node + expressions.Add(I32.Load(0)); // load the actual function index from the portable entrypoint expressions.Add(ControlFlow.CallIndirect(targetTypeIndex, 0)); // Handle wasm return value — pRet is already on the stack under the return value 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 b129fa441d64b8..a8bf4482c3e26a 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 @@ -99,7 +99,7 @@ protected override void EmitCode(NodeFactory factory, ref Wasm.WasmEmitter instr bool hasThis = !methodSignature.IsStatic; int[] offsets = new int[methodSignature.Length]; - bool[] isIndirectArg = new bool[methodSignature.Length]; + bool[] isIndirectStructArg = new bool[methodSignature.Length]; int argIndex = 0; int argOffset; @@ -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() && argit.IsValueType(); + isIndirectStructArg[argIndex] = argit.IsArgPassedByRef() && argit.IsValueType(); argIndex++; } @@ -189,7 +189,7 @@ protected override void EmitCode(NodeFactory factory, ref Wasm.WasmEmitter instr expressions.Add(I32.Const(0)); expressions.Add(I32.Store((ulong)currentOffset)); } - else if (isIndirectArg[i]) + else if (isIndirectStructArg[i]) { // Indirect struct — copy the exact contents from the incoming pointer int structSize = paramType.GetElementSize().AsInt; From 078c80b7766af7f328c76243ec5145495020cd14 Mon Sep 17 00:00:00 2001 From: David Wrighton Date: Wed, 6 May 2026 14:59:58 -0700 Subject: [PATCH 39/42] Add recordWasmManagedCallSig JIT-EE API for R2R thunk generation Add a new JIT-VM interface API that allows the JIT to report managed call site signatures to the R2R compiler for Wasm thunk generation. The VM implementation is a no-op. The ReadyToRun implementation generates R2R-to-interpreter thunks (same as recordCallSite). The RyuJit/NativeAOT implementation is a no-op. The JIT calls this API from codegenwasm.cpp for non-helper, non-unmanaged calls, resolving the signature from the method handle if not already available on the call node. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- compile-wasm-helperlib.ps1 | 118 +++++++++ prepare-wasm-test.ps1 | 239 ++++++++++++++++++ src/coreclr/inc/corjit.h | 6 + src/coreclr/inc/icorjitinfoimpl_generated.h | 3 + src/coreclr/inc/jiteeversionguid.h | 10 +- src/coreclr/jit/ICorJitInfo_names_generated.h | 1 + .../jit/ICorJitInfo_wrapper_generated.hpp | 8 + src/coreclr/jit/codegenwasm.cpp | 21 ++ .../JitInterface/CorInfoImpl_generated.cs | 16 ++ .../ThunkGenerator/ThunkInput.txt | 1 + .../JitInterface/CorInfoImpl.ReadyToRun.cs | 31 +++ .../JitInterface/CorInfoImpl.RyuJit.cs | 6 + .../aot/jitinterface/jitinterface_generated.h | 9 + .../superpmi-shim-collector/icorjitinfo.cpp | 7 + .../icorjitinfo_generated.cpp | 7 + .../icorjitinfo_generated.cpp | 6 + .../tools/superpmi/superpmi/icorjitinfo.cpp | 6 + src/coreclr/vm/jitinterface.cpp | 8 + 18 files changed, 498 insertions(+), 5 deletions(-) create mode 100644 compile-wasm-helperlib.ps1 create mode 100644 prepare-wasm-test.ps1 diff --git a/compile-wasm-helperlib.ps1 b/compile-wasm-helperlib.ps1 new file mode 100644 index 00000000000000..6fb805f3aa95fc --- /dev/null +++ b/compile-wasm-helperlib.ps1 @@ -0,0 +1,118 @@ +<# +.SYNOPSIS + Compiles HelperLib.dll to HelperLib.wasm using crossgen2 targeting browser/wasm. + +.DESCRIPTION + Uses crossgen2 to AOT-compile HelperLib.dll into a .wasm file, referencing + the managed DLLs in the IL directory. Output is placed into the IL directory. + + Prerequisites: + - Run .\build.cmd -os browser -c Debug -subset clr+libs + - Run .\prepare-wasm-test.ps1 to populate the IL directory +#> +param( + [ValidateSet("Debug", "Release")] + [string]$Configuration = "Debug" +) + +$ErrorActionPreference = "Stop" + +$RepoRoot = $PSScriptRoot +$DotnetExe = Join-Path $RepoRoot ".dotnet\dotnet.exe" +$Crossgen2Dll = Join-Path $RepoRoot "artifacts\bin\coreclr\windows.x64.debug\crossgen2\crossgen2.dll" +$WasmBinDir = Join-Path $RepoRoot "artifacts\bin\coreclr\browser.wasm.$Configuration" +$ILDir = Join-Path $WasmBinDir "IL" +$InputDll = Join-Path $ILDir "HelperLib.dll" +$baseName = [System.IO.Path]::GetFileNameWithoutExtension($InputDll) +$OutputWasm = Join-Path $WasmBinDir "$baseName.wasm" +$OutputVerifyWasm = Join-Path $WasmBinDir "$baseName.Verify.wasm" +$OutputWat = Join-Path $WasmBinDir "$baseName.wat" +$Wasm2Wat = "c:\git\wabt\bin\wasm2wat.exe" +$Wat2Wasm = "c:\git\wabt\bin\wat2wasm.exe" + +# --- Validate prerequisites --- + +if (-not (Test-Path $DotnetExe)) { + Write-Error "dotnet not found at $DotnetExe" + exit 1 +} +if (-not (Test-Path $Crossgen2Dll)) { + Write-Error "crossgen2.dll not found at $Crossgen2Dll. Build with: .\build.cmd clr -c Debug" + exit 1 +} +if (-not (Test-Path $InputDll)) { + Write-Error "HelperLib.dll not found at $InputDll. Run .\prepare-wasm-test.ps1 first." + exit 1 +} +if (-not (Test-Path $Wasm2Wat)) { + Write-Error "wasm2wat.exe not found at $Wasm2Wat" + exit 1 +} + +# --- Build reference assembly list --- + +$referenceDlls = Get-ChildItem "$ILDir\*.dll" +$referenceArgs = @() +foreach ($dll in $referenceDlls) { + $referenceArgs += "-r" + $referenceArgs += $dll.FullName +} + +# --- Run crossgen2 --- + +Write-Host "Compiling HelperLib.dll -> HelperLib.wasm" -ForegroundColor Cyan +Write-Host " Input : $InputDll" +Write-Host " Output: $OutputWasm" +Write-Host " References: $($referenceDlls.Count) DLLs from IL directory" +Write-Host "" + +$crossgen2Args = @( + "--targetos", "browser" + "--targetarch", "wasm" + "-f", "wasm" + "-o", $OutputWasm +) + $referenceArgs + @( + $InputDll +) + +$RspFile = Join-Path $WasmBinDir "crossgen2-helperlib.rsp" +$crossgen2Args | Set-Content -Path $RspFile +Write-Host "Response file: $RspFile" -ForegroundColor Yellow + +Write-Host "$DotnetExe $Crossgen2Dll @$RspFile" -ForegroundColor Yellow + +& $DotnetExe $Crossgen2Dll "@$RspFile" +if ($LASTEXITCODE -ne 0) { + Write-Error "crossgen2 compilation failed with exit code $LASTEXITCODE" + exit 1 +} + +Write-Host "" +Write-Host "=== crossgen2 succeeded ===" -ForegroundColor Green +Write-Host " Output: $OutputWasm" +Write-Host " Size : $((Get-Item $OutputWasm).Length) bytes" + +# --- Convert to WAT --- + +Write-Host "" +Write-Host "Converting HelperLib.wasm -> HelperLib.wat" -ForegroundColor Cyan + +& $Wasm2Wat $OutputWasm --enable-all --no-check -o $OutputWat +if ($LASTEXITCODE -ne 0) { + Write-Error "wasm2wat conversion failed with exit code $LASTEXITCODE" + exit 1 +} + +Write-Host "" +Write-Host "Converting HelperLib.wat -> HelperLib.Verify.wasm" -ForegroundColor Cyan + +& $Wat2Wasm $OutputWat --enable-extended-const -o $OutputVerifyWasm +if ($LASTEXITCODE -ne 0) { + Write-Error "wasm2wat conversion failed with exit code $LASTEXITCODE" + exit 1 +} + +Write-Host "" +Write-Host "=== WAT conversion succeeded ===" -ForegroundColor Green +Write-Host " Output: $OutputWat" +Write-Host " Size : $((Get-Item $OutputWat).Length) bytes" diff --git a/prepare-wasm-test.ps1 b/prepare-wasm-test.ps1 new file mode 100644 index 00000000000000..c91c823a581380 --- /dev/null +++ b/prepare-wasm-test.ps1 @@ -0,0 +1,239 @@ +<# +.SYNOPSIS + Prepares the IL directory for CoreCLR WASM testing with a two-DLL HelloWorld test app. + +.DESCRIPTION + This script: + 1. Builds a HelperLib class library and a HelloWorld console app that references it + 2. Copies the framework DLLs into the IL directory + 3. Copies the built test DLLs into the IL directory + + Prerequisites: Run the CoreCLR WASM build first: + .\build.cmd -os browser -c Debug -subset clr+libs + +.PARAMETER Configuration + Build configuration (Debug or Release). Must match the configuration used for the WASM build. + +.EXAMPLE + .\prepare-wasm-test.ps1 + .\prepare-wasm-test.ps1 -Configuration Release +#> +param( + [ValidateSet("Debug", "Release")] + [string]$Configuration = "Debug" +) + +$ErrorActionPreference = "Stop" + +$RepoRoot = $PSScriptRoot +$ArtifactsDir = Join-Path $RepoRoot "artifacts" +$WasmBinDir = Join-Path $ArtifactsDir "bin\coreclr\browser.wasm.$Configuration" +$ILDir = Join-Path $WasmBinDir "IL" +$FrameworkDllDir = Join-Path $ArtifactsDir "bin\microsoft.netcore.app.runtime.browser-wasm\$Configuration\runtimes\browser-wasm\lib\net11.0" +$DotnetExe = Join-Path $RepoRoot ".dotnet\dotnet.exe" +$TempDir = Join-Path $ArtifactsDir "tmp\wasm-hello-test" + +# --- Validate prerequisites --- + +if (-not (Test-Path $DotnetExe)) { + Write-Error "dotnet not found at $DotnetExe. Run '.\build.cmd -os browser -c $Configuration -subset clr+libs' first." + exit 1 +} + +if (-not (Test-Path $FrameworkDllDir)) { + Write-Error "Framework DLLs not found at $FrameworkDllDir. Run '.\build.cmd -os browser -c $Configuration -subset clr+libs' first." + exit 1 +} + +if (-not (Test-Path (Join-Path $WasmBinDir "corerun.js"))) { + Write-Error "corerun.js not found in $WasmBinDir. Run '.\build.cmd -os browser -c $Configuration -subset clr+libs' first." + exit 1 +} + +# --- Create temp workspace with test projects --- + +if (Test-Path $TempDir) { + Remove-Item $TempDir -Recurse -Force +} +New-Item -ItemType Directory -Path $TempDir -Force | Out-Null + +# HelperLib project +$helperDir = Join-Path $TempDir "HelperLib" +New-Item -ItemType Directory -Path $helperDir -Force | Out-Null + +Set-Content (Join-Path $helperDir "HelperLib.csproj") @" + + + net11.0 + true + + +"@ + +Set-Content (Join-Path $helperDir "Helper.cs") @" +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace HelperLib; + +public static class Helper +{ + public static unsafe string DoWork(string context, + delegate* managed callback, + int arg1, + int arg2, + int arg3, + int arg4, + int arg5, + int arg6, + int arg7, + int arg8, + int arg9, + int arg10, + int arg11, + int arg12, + int arg13, + int arg14, + int arg15) + { + callback(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12, arg13, arg14, arg15); + return `$"[HelperLib] Performed work for: {context} {OtherFunction()}"; + } + public static string OtherFunction() + { + return "in OtherFunction also compiled via R2R"; + } +} +"@ + +# HelloWorld project +$helloDir = Join-Path $TempDir "HelloWorld" +New-Item -ItemType Directory -Path $helloDir -Force | Out-Null + +Set-Content (Join-Path $helloDir "HelloWorld.csproj") @" + + + net11.0 + Exe + true + + + + + +"@ + +Set-Content (Join-Path $helloDir "Program.cs") @" +// 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 HelperLib; + +namespace HelloWorld; + +public static class Program +{ + + public static unsafe void Main() + { + Console.WriteLine("[HelloWorld] Starting up..."); + + Console.WriteLine(Helper.DoWork("Call1", &PrintEachArgOnNewLine, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15)); + Console.WriteLine(Helper.DoWork("Call2", &PrintEachArgOnNewLine, 11, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 115)); + Console.WriteLine(Helper.DoWork("Call3", &PrintEachArgOnNewLine, 21, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 115)); + + Console.WriteLine("[HelloWorld] All done!"); + } + + public static unsafe void PrintEachArgOnNewLine( + int a1, int a2, int a3, int a4, int a5, + int a6, int a7, int a8, int a9, int a10, + int a11, int a12, int a13, int a14, int a15) + { + Console.WriteLine(a1); + Console.WriteLine(a2); + Console.WriteLine(a3); + Console.WriteLine(a4); + Console.WriteLine(a5); + Console.WriteLine(a6); + Console.WriteLine(a7); + Console.WriteLine(a8); + Console.WriteLine(a9); + Console.WriteLine(a10); + Console.WriteLine(a11); + Console.WriteLine(a12); + Console.WriteLine(a13); + Console.WriteLine(a14); + Console.WriteLine(a15); + } +} +"@ + +# --- Build the test projects --- + +Write-Host "" +Write-Host "=== Building test projects ===" -ForegroundColor Cyan + +Write-Host "Building HelloWorld (includes HelperLib via ProjectReference)..." +& $DotnetExe build (Join-Path $helloDir "HelloWorld.csproj") -c Debug --nologo -v quiet +if ($LASTEXITCODE -ne 0) { + Write-Error "Build failed." + exit 1 +} +Write-Host "Build succeeded." -ForegroundColor Green + +# --- Populate the IL directory --- + +Write-Host "" +Write-Host "=== Populating IL directory ===" -ForegroundColor Cyan +Write-Host " Target: $ILDir" + +New-Item -ItemType Directory -Path $ILDir -Force | Out-Null + +# Copy framework DLLs (these are the managed BCL assemblies for browser-wasm) +Write-Host "Copying framework DLLs..." +$frameworkDlls = Get-ChildItem "$FrameworkDllDir\*.dll" +$frameworkDlls | Copy-Item -Destination $ILDir -Force +Write-Host " Copied $($frameworkDlls.Count) framework DLLs" + +# Copy built test DLLs (repo Directory.Build.props redirects output to artifacts/bin/) +$builtOutputDir = Join-Path $ArtifactsDir "bin\HelloWorld\Debug\net11.0" +$helloDll = Join-Path $builtOutputDir "HelloWorld.dll" +$helperDll = Join-Path $builtOutputDir "HelperLib.dll" + +if (-not (Test-Path $helloDll)) { + Write-Error "HelloWorld.dll not found at $builtOutputDir" + exit 1 +} +if (-not (Test-Path $helperDll)) { + Write-Error "HelperLib.dll not found at $builtOutputDir" + exit 1 +} + +Copy-Item $helloDll $ILDir -Force +Copy-Item $helperDll $ILDir -Force +Write-Host " Copied HelloWorld.dll" +Write-Host " Copied HelperLib.dll" + +# --- Summary --- + +$totalDlls = (Get-ChildItem "$ILDir\*.dll").Count + +# Convert to Unix-style absolute path for node usage (required even on Windows) +$unixILPath = ($ILDir -replace '\\', '/') -replace '^[A-Za-z]:', '' + +Write-Host "" +Write-Host "=== IL directory ready ===" -ForegroundColor Green +Write-Host " Location : $ILDir" +Write-Host " Total DLLs: $totalDlls" +Write-Host "" +Write-Host "--- To run with Node.js ---" -ForegroundColor Yellow +Write-Host " cd $WasmBinDir" +Write-Host " node ./corerun.js -c $unixILPath $unixILPath/HelloWorld.dll" +Write-Host "" +Write-Host "--- To run in browser ---" -ForegroundColor Yellow +Write-Host " 1. Edit src\coreclr\hosts\corerun\CMakeLists.txt -> set CORERUN_IN_BROWSER to 1" +Write-Host " 2. Rebuild: .\build.cmd -os browser -c $Configuration -subset clr" +Write-Host " 3. Serve: dotnet-serve --directory `"$WasmBinDir`"" +Write-Host " 4. Open corerun.html in your browser" diff --git a/src/coreclr/inc/corjit.h b/src/coreclr/inc/corjit.h index bf17388fd4bb64..649c38b171ba64 100644 --- a/src/coreclr/inc/corjit.h +++ b/src/coreclr/inc/corjit.h @@ -424,6 +424,12 @@ class ICorJitInfo : public ICorDynamicInfo CORINFO_METHOD_HANDLE methodHandle /* IN */ ) = 0; + // Records the signature of a managed call site for Wasm R2R thunk generation. + // This is a no-op on all targets except ReadyToRun Wasm compilation. + virtual void recordWasmManagedCallSig( + CORINFO_SIG_INFO * callSig /* IN */ + ) = 0; + // A relocation is recorded if we are pre-jitting. // A jump thunk may be inserted if we are jitting virtual void recordRelocation( diff --git a/src/coreclr/inc/icorjitinfoimpl_generated.h b/src/coreclr/inc/icorjitinfoimpl_generated.h index 7a67f9ea0ff3cd..8fc80cda20018c 100644 --- a/src/coreclr/inc/icorjitinfoimpl_generated.h +++ b/src/coreclr/inc/icorjitinfoimpl_generated.h @@ -742,6 +742,9 @@ void recordCallSite( CORINFO_SIG_INFO* callSig, CORINFO_METHOD_HANDLE methodHandle) override; +void recordWasmManagedCallSig( + CORINFO_SIG_INFO* callSig) override; + void recordRelocation( void* location, void* locationRW, diff --git a/src/coreclr/inc/jiteeversionguid.h b/src/coreclr/inc/jiteeversionguid.h index fe5749d03fdda1..d51dc20a9cca3d 100644 --- a/src/coreclr/inc/jiteeversionguid.h +++ b/src/coreclr/inc/jiteeversionguid.h @@ -37,11 +37,11 @@ #include -constexpr GUID JITEEVersionIdentifier = { /* e92fbf65-4856-4729-ab9e-f66f7adcecf9 */ - 0xe92fbf65, - 0x4856, - 0x4729, - {0xab, 0x9e, 0xf6, 0x6f, 0x7a, 0xdc, 0xec, 0xf9} +constexpr GUID JITEEVersionIdentifier = { /* bf284efa-a3fb-4420-a62f-6e1a0a1b2cfc */ + 0xbf284efa, + 0xa3fb, + 0x4420, + {0xa6, 0x2f, 0x6e, 0x1a, 0x0a, 0x1b, 0x2c, 0xfc} }; #endif // JIT_EE_VERSIONING_GUID_H diff --git a/src/coreclr/jit/ICorJitInfo_names_generated.h b/src/coreclr/jit/ICorJitInfo_names_generated.h index d03d03b1007970..57781f560ce0d5 100644 --- a/src/coreclr/jit/ICorJitInfo_names_generated.h +++ b/src/coreclr/jit/ICorJitInfo_names_generated.h @@ -178,6 +178,7 @@ DEF_CLR_API(reportFatalError) DEF_CLR_API(getPgoInstrumentationResults) DEF_CLR_API(allocPgoInstrumentationBySchema) DEF_CLR_API(recordCallSite) +DEF_CLR_API(recordWasmManagedCallSig) DEF_CLR_API(recordRelocation) DEF_CLR_API(getRelocTypeHint) DEF_CLR_API(getExpectedTargetArchitecture) diff --git a/src/coreclr/jit/ICorJitInfo_wrapper_generated.hpp b/src/coreclr/jit/ICorJitInfo_wrapper_generated.hpp index 80411912d6c9cc..87e541b608f155 100644 --- a/src/coreclr/jit/ICorJitInfo_wrapper_generated.hpp +++ b/src/coreclr/jit/ICorJitInfo_wrapper_generated.hpp @@ -1728,6 +1728,14 @@ void WrapICorJitInfo::recordCallSite( API_LEAVE(recordCallSite); } +void WrapICorJitInfo::recordWasmManagedCallSig( + CORINFO_SIG_INFO* callSig) +{ + API_ENTER(recordWasmManagedCallSig); + wrapHnd->recordWasmManagedCallSig(callSig); + API_LEAVE(recordWasmManagedCallSig); +} + void WrapICorJitInfo::recordRelocation( void* location, void* locationRW, diff --git a/src/coreclr/jit/codegenwasm.cpp b/src/coreclr/jit/codegenwasm.cpp index 92d724eee2da0f..9b216829fe9629 100644 --- a/src/coreclr/jit/codegenwasm.cpp +++ b/src/coreclr/jit/codegenwasm.cpp @@ -2493,6 +2493,27 @@ void CodeGen::genCallInstruction(GenTreeCall* call) GenTree* target = getCallTarget(call, ¶ms.methHnd); + // Report managed call signatures to the R2R compiler for thunk generation. + if (!call->IsHelperCall() && !call->IsUnmanaged()) + { + CORINFO_SIG_INFO sigInfoLocal; + CORINFO_SIG_INFO* sigInfoCall = call->callSig; + + if (sigInfoCall == nullptr) + { + if ((params.methHnd != NO_METHOD_HANDLE) && (Compiler::eeGetHelperNum(params.methHnd) == CORINFO_HELP_UNDEF)) + { + m_compiler->eeGetMethodSig(params.methHnd, &sigInfoLocal); + sigInfoCall = &sigInfoLocal; + } + } + + if (sigInfoCall != nullptr) + { + m_compiler->info.compCompHnd->recordWasmManagedCallSig(sigInfoCall); + } + } + ArrayStack typeStack(m_compiler->getAllocator(CMK_Codegen)); if (call->TypeIs(TYP_STRUCT)) diff --git a/src/coreclr/tools/Common/JitInterface/CorInfoImpl_generated.cs b/src/coreclr/tools/Common/JitInterface/CorInfoImpl_generated.cs index a1eebc3762aaa1..2ec1fc175a8d91 100644 --- a/src/coreclr/tools/Common/JitInterface/CorInfoImpl_generated.cs +++ b/src/coreclr/tools/Common/JitInterface/CorInfoImpl_generated.cs @@ -194,6 +194,7 @@ static ICorJitInfoCallbacks() s_callbacks.getPgoInstrumentationResults = &_getPgoInstrumentationResults; s_callbacks.allocPgoInstrumentationBySchema = &_allocPgoInstrumentationBySchema; s_callbacks.recordCallSite = &_recordCallSite; + s_callbacks.recordWasmManagedCallSig = &_recordWasmManagedCallSig; s_callbacks.recordRelocation = &_recordRelocation; s_callbacks.getRelocTypeHint = &_getRelocTypeHint; s_callbacks.getExpectedTargetArchitecture = &_getExpectedTargetArchitecture; @@ -376,6 +377,7 @@ static ICorJitInfoCallbacks() public delegate* unmanaged getPgoInstrumentationResults; public delegate* unmanaged allocPgoInstrumentationBySchema; public delegate* unmanaged recordCallSite; + public delegate* unmanaged recordWasmManagedCallSig; public delegate* unmanaged recordRelocation; public delegate* unmanaged getRelocTypeHint; public delegate* unmanaged getExpectedTargetArchitecture; @@ -2950,6 +2952,20 @@ private static void _recordCallSite(IntPtr thisHandle, IntPtr* ppException, uint } } + [UnmanagedCallersOnly] + private static void _recordWasmManagedCallSig(IntPtr thisHandle, IntPtr* ppException, CORINFO_SIG_INFO* callSig) + { + var _this = GetThis(thisHandle); + try + { + _this.recordWasmManagedCallSig(callSig); + } + catch (Exception ex) + { + *ppException = _this.AllocException(ex); + } + } + [UnmanagedCallersOnly] private static void _recordRelocation(IntPtr thisHandle, IntPtr* ppException, void* location, void* locationRW, void* target, CorInfoReloc fRelocType, int addlDelta) { diff --git a/src/coreclr/tools/Common/JitInterface/ThunkGenerator/ThunkInput.txt b/src/coreclr/tools/Common/JitInterface/ThunkGenerator/ThunkInput.txt index 8e605002f90174..384ca10f881504 100644 --- a/src/coreclr/tools/Common/JitInterface/ThunkGenerator/ThunkInput.txt +++ b/src/coreclr/tools/Common/JitInterface/ThunkGenerator/ThunkInput.txt @@ -347,6 +347,7 @@ FUNCTIONS JITINTERFACE_HRESULT getPgoInstrumentationResults(CORINFO_METHOD_HANDLE ftnHnd, ICorJitInfo::PgoInstrumentationSchema** pSchema, uint32_t* pCountSchemaItems, uint8_t**pInstrumentationData, ICorJitInfo::PgoSource* pPgoSource, bool* pDynamicPgo) JITINTERFACE_HRESULT allocPgoInstrumentationBySchema(CORINFO_METHOD_HANDLE ftnHnd, ICorJitInfo::PgoInstrumentationSchema* pSchema, uint32_t countSchemaItems, uint8_t** pInstrumentationData) void recordCallSite(uint32_t instrOffset, CORINFO_SIG_INFO* callSig, CORINFO_METHOD_HANDLE methodHandle) + void recordWasmManagedCallSig(CORINFO_SIG_INFO* callSig) void recordRelocation(void* location, void* locationRW, void* target, CorInfoReloc fRelocType, int32_t addlDelta) CorInfoReloc getRelocTypeHint(void* target) uint32_t getExpectedTargetArchitecture() 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 05f44f52703aa3..311cc912791007 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/JitInterface/CorInfoImpl.ReadyToRun.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/JitInterface/CorInfoImpl.ReadyToRun.cs @@ -3567,5 +3567,36 @@ private void recordCallSite(uint instrOffset, CORINFO_SIG_INFO* callSig, CORINFO } } } + + private void recordWasmManagedCallSig(CORINFO_SIG_INFO* callSig) + { + if ((callSig != null) && _compilation.NodeFactory.Target.IsWasm) + { + var sig = HandleToObject(callSig->methodSignature); + + WasmLowering.LoweringFlags flags = 0; + if (callSig->hasTypeArg()) + { + flags |= WasmLowering.LoweringFlags.HasGenericContextArg; + } + if (callSig->isAsyncCall()) + { + flags |= WasmLowering.LoweringFlags.IsAsyncCall; + } + if (((int)callSig->getCallConv() & 0xF) != 0) + { + flags |= WasmLowering.LoweringFlags.IsUnmanagedCallersOnly; + } + + WasmSignature wasmSig = WasmLowering.GetSignature(sig, flags); + + // 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"); + } + } + } } } 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 892a67cb2a4f15..135762578a5308 100644 --- a/src/coreclr/tools/aot/ILCompiler.RyuJit/JitInterface/CorInfoImpl.RyuJit.cs +++ b/src/coreclr/tools/aot/ILCompiler.RyuJit/JitInterface/CorInfoImpl.RyuJit.cs @@ -2507,6 +2507,12 @@ private bool notifyMethodInfoUsage(CORINFO_METHOD_STRUCT_* ftn) #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 + { + } + +#pragma warning disable CA1822 // Mark members as static + private void recordWasmManagedCallSig(CORINFO_SIG_INFO* callSig) #pragma warning restore CA1822 // Mark members as static { } diff --git a/src/coreclr/tools/aot/jitinterface/jitinterface_generated.h b/src/coreclr/tools/aot/jitinterface/jitinterface_generated.h index dec68b1eb53d4f..90ec07a3b446bc 100644 --- a/src/coreclr/tools/aot/jitinterface/jitinterface_generated.h +++ b/src/coreclr/tools/aot/jitinterface/jitinterface_generated.h @@ -185,6 +185,7 @@ struct JitInterfaceCallbacks JITINTERFACE_HRESULT (* getPgoInstrumentationResults)(void * thisHandle, CorInfoExceptionClass** ppException, CORINFO_METHOD_HANDLE ftnHnd, ICorJitInfo::PgoInstrumentationSchema** pSchema, uint32_t* pCountSchemaItems, uint8_t** pInstrumentationData, ICorJitInfo::PgoSource* pPgoSource, bool* pDynamicPgo); JITINTERFACE_HRESULT (* allocPgoInstrumentationBySchema)(void * thisHandle, CorInfoExceptionClass** ppException, CORINFO_METHOD_HANDLE ftnHnd, ICorJitInfo::PgoInstrumentationSchema* pSchema, uint32_t countSchemaItems, uint8_t** pInstrumentationData); void (* recordCallSite)(void * thisHandle, CorInfoExceptionClass** ppException, uint32_t instrOffset, CORINFO_SIG_INFO* callSig, CORINFO_METHOD_HANDLE methodHandle); + void (* recordWasmManagedCallSig)(void * thisHandle, CorInfoExceptionClass** ppException, CORINFO_SIG_INFO* callSig); void (* recordRelocation)(void * thisHandle, CorInfoExceptionClass** ppException, void* location, void* locationRW, void* target, CorInfoReloc fRelocType, int32_t addlDelta); CorInfoReloc (* getRelocTypeHint)(void * thisHandle, CorInfoExceptionClass** ppException, void* target); uint32_t (* getExpectedTargetArchitecture)(void * thisHandle, CorInfoExceptionClass** ppException); @@ -1910,6 +1911,14 @@ class JitInterfaceWrapper : public ICorJitInfo if (pException != nullptr) throw pException; } + virtual void recordWasmManagedCallSig( + CORINFO_SIG_INFO* callSig) +{ + CorInfoExceptionClass* pException = nullptr; + _callbacks->recordWasmManagedCallSig(_thisHandle, &pException, callSig); + if (pException != nullptr) throw pException; +} + virtual void recordRelocation( void* location, void* locationRW, diff --git a/src/coreclr/tools/superpmi/superpmi-shim-collector/icorjitinfo.cpp b/src/coreclr/tools/superpmi/superpmi-shim-collector/icorjitinfo.cpp index e29ba7b5cc97ff..bd82b684e91e11 100644 --- a/src/coreclr/tools/superpmi/superpmi-shim-collector/icorjitinfo.cpp +++ b/src/coreclr/tools/superpmi/superpmi-shim-collector/icorjitinfo.cpp @@ -2012,6 +2012,13 @@ void interceptor_ICJI::recordCallSite(uint32_t instrOffset, /* IN * mc->cr->recRecordCallSite(instrOffset, callSig, methodHandle); } +void interceptor_ICJI::recordWasmManagedCallSig(CORINFO_SIG_INFO* callSig /* IN */) +{ + mc->cr->AddCall("recordWasmManagedCallSig"); + original_ICorJitInfo->recordWasmManagedCallSig(callSig); + // No-op in the VM, nothing to record for replay. +} + // A relocation is recorded if we are pre-jitting. // A jump thunk may be inserted if we are jitting void interceptor_ICJI::recordRelocation(void* location, /* IN */ diff --git a/src/coreclr/tools/superpmi/superpmi-shim-counter/icorjitinfo_generated.cpp b/src/coreclr/tools/superpmi/superpmi-shim-counter/icorjitinfo_generated.cpp index 8058b6802159e8..874083bd15b940 100644 --- a/src/coreclr/tools/superpmi/superpmi-shim-counter/icorjitinfo_generated.cpp +++ b/src/coreclr/tools/superpmi/superpmi-shim-counter/icorjitinfo_generated.cpp @@ -1429,6 +1429,13 @@ void interceptor_ICJI::recordCallSite( original_ICorJitInfo->recordCallSite(instrOffset, callSig, methodHandle); } +void interceptor_ICJI::recordWasmManagedCallSig( + CORINFO_SIG_INFO* callSig) +{ + mcs->AddCall("recordWasmManagedCallSig"); + original_ICorJitInfo->recordWasmManagedCallSig(callSig); +} + void interceptor_ICJI::recordRelocation( void* location, void* locationRW, diff --git a/src/coreclr/tools/superpmi/superpmi-shim-simple/icorjitinfo_generated.cpp b/src/coreclr/tools/superpmi/superpmi-shim-simple/icorjitinfo_generated.cpp index 852a318f83c225..b870f0f947f2f6 100644 --- a/src/coreclr/tools/superpmi/superpmi-shim-simple/icorjitinfo_generated.cpp +++ b/src/coreclr/tools/superpmi/superpmi-shim-simple/icorjitinfo_generated.cpp @@ -1255,6 +1255,12 @@ void interceptor_ICJI::recordCallSite( original_ICorJitInfo->recordCallSite(instrOffset, callSig, methodHandle); } +void interceptor_ICJI::recordWasmManagedCallSig( + CORINFO_SIG_INFO* callSig) +{ + original_ICorJitInfo->recordWasmManagedCallSig(callSig); +} + void interceptor_ICJI::recordRelocation( void* location, void* locationRW, diff --git a/src/coreclr/tools/superpmi/superpmi/icorjitinfo.cpp b/src/coreclr/tools/superpmi/superpmi/icorjitinfo.cpp index 20372434076341..93262fd2279b01 100644 --- a/src/coreclr/tools/superpmi/superpmi/icorjitinfo.cpp +++ b/src/coreclr/tools/superpmi/superpmi/icorjitinfo.cpp @@ -1827,6 +1827,12 @@ void MyICJI::recordCallSite(uint32_t instrOffset, /* IN */ jitInstance->mc->cr->repRecordCallSite(instrOffset, callSig, methodHandle); } +void MyICJI::recordWasmManagedCallSig(CORINFO_SIG_INFO* callSig /* IN */) +{ + jitInstance->mc->cr->AddCall("recordWasmManagedCallSig"); + // No-op for SuperPMI replay. Only meaningful for ReadyToRun Wasm compilation. +} + // A relocation is recorded if we are pre-jitting. // A jump thunk may be inserted if we are jitting void MyICJI::recordRelocation(void* location, /* IN */ diff --git a/src/coreclr/vm/jitinterface.cpp b/src/coreclr/vm/jitinterface.cpp index 5f05c739c48c24..f34070e9e9dc18 100644 --- a/src/coreclr/vm/jitinterface.cpp +++ b/src/coreclr/vm/jitinterface.cpp @@ -15259,6 +15259,14 @@ void CEEInfo::recordCallSite( UNREACHABLE(); // only called on derived class. } +void CEEInfo::recordWasmManagedCallSig( + CORINFO_SIG_INFO * callSig /* IN */ + ) +{ + LIMITED_METHOD_CONTRACT; + // No-op for the VM. Only meaningful for ReadyToRun Wasm compilation. +} + void CEEInfo::recordRelocation( void * location, /* IN */ void * locationRW, /* IN */ From ff7067f76d40d234249b7b3459829e5f609d10ee Mon Sep 17 00:00:00 2001 From: David Wrighton Date: Wed, 6 May 2026 16:34:24 -0700 Subject: [PATCH 40/42] Revert recordCallSite emitter plumbing for thunk generation The previous approach plumbed callSig and isUnmanagedCall through the emitter to call recordCallSite during instruction output. This is no longer needed since recordWasmManagedCallSig is called directly during codegen. Reverted changes: - emit.h: Remove isUnmanagedCall field, sigInfo back to DEBUG-only - emit.cpp: emitRecordCallSite and debugInfoSize back to DEBUG-only - emitwasm.cpp: Remove emitOutputInstr call-site recording block and revert emitIns_Call sig plumbing to DEBUG-only - codegenwasm.cpp: Remove isUnmanagedCall/sigInfo param setting Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/coreclr/jit/codegenwasm.cpp | 6 ++--- src/coreclr/jit/emit.cpp | 4 +-- src/coreclr/jit/emit.h | 28 +++++++++------------ src/coreclr/jit/emitwasm.cpp | 44 +++------------------------------ 4 files changed, 18 insertions(+), 64 deletions(-) diff --git a/src/coreclr/jit/codegenwasm.cpp b/src/coreclr/jit/codegenwasm.cpp index 9b216829fe9629..456e911cb21ab3 100644 --- a/src/coreclr/jit/codegenwasm.cpp +++ b/src/coreclr/jit/codegenwasm.cpp @@ -2480,14 +2480,12 @@ void CodeGen::genCallInstruction(GenTreeCall* call) params.debugInfo = di; } -#if defined(DEBUG) || defined(TARGET_WASM) +#ifdef DEBUG // 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(call->callSig == NULL || params.hasAsyncRet == call->callSig->isAsyncCall()); - params.sigInfo = call->callSig; - params.isUnmanagedCall = call->IsUnmanaged(); + params.sigInfo = call->callSig; } #endif // DEBUG diff --git a/src/coreclr/jit/emit.cpp b/src/coreclr/jit/emit.cpp index 595ffbead10e80..ea343f496cce66 100644 --- a/src/coreclr/jit/emit.cpp +++ b/src/coreclr/jit/emit.cpp @@ -883,7 +883,7 @@ void emitter::emitBegCG(Compiler* comp, COMP_HANDLE cmpHandle) m_compiler = comp; emitCmpHandle = cmpHandle; m_debugInfoSize = sizeof(instrDescDebugInfo*); -#if !defined(DEBUG) && !defined(TARGET_WASM) +#ifndef DEBUG if (!comp->opts.disAsm) m_debugInfoSize = 0; #endif @@ -10590,7 +10590,7 @@ void emitter::emitRecordCallSite(ULONG instrOffset, /* IN */ CORINFO_SIG_INFO* callSig, /* IN */ CORINFO_METHOD_HANDLE methodHandle) /* IN */ { -#if defined(DEBUG) || defined(TARGET_WASM) +#if defined(DEBUG) // 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 bf86334ae92bc7..d871a91cd8a516 100644 --- a/src/coreclr/jit/emit.h +++ b/src/coreclr/jit/emit.h @@ -474,7 +474,7 @@ struct EmitCallParams { EmitCallType callType = EC_COUNT; CORINFO_METHOD_HANDLE methHnd = NO_METHOD_HANDLE; -#if defined(DEBUG) || defined(TARGET_WASM) +#ifdef DEBUG // Used to report call sites to the EE CORINFO_SIG_INFO* sigInfo = nullptr; #endif @@ -494,9 +494,6 @@ 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 @@ -644,19 +641,16 @@ 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' -#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 + 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' + 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 3a91252617cafc..3492406ccf2cbd 100644 --- a/src/coreclr/jit/emitwasm.cpp +++ b/src/coreclr/jit/emitwasm.cpp @@ -206,14 +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) { - 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; + INDEBUG(id->idDebugOnlyInfo()->idCallSig = params.sigInfo); + id->idDebugOnlyInfo()->idMemCookie = (size_t)params.methHnd; // method token + id->idDebugOnlyInfo()->idFlags = GTF_ICON_METHOD_HDL; } dispIns(id); @@ -857,41 +854,6 @@ 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; - 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); - 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, methodHandle); - } - *dp = dst; return sz; } From 44160fbbe37fe1028063851626c7d7f6cb2ae22f Mon Sep 17 00:00:00 2001 From: David Wrighton Date: Thu, 7 May 2026 14:40:52 -0700 Subject: [PATCH 41/42] - Remove unnecessarily added files - Use Reallocate to be a bit more efficient --- compile-wasm-helperlib.ps1 | 118 ---------- prepare-wasm-test.ps1 | 239 -------------------- src/coreclr/vm/pregeneratedstringthunks.cpp | 8 +- 3 files changed, 4 insertions(+), 361 deletions(-) delete mode 100644 compile-wasm-helperlib.ps1 delete mode 100644 prepare-wasm-test.ps1 diff --git a/compile-wasm-helperlib.ps1 b/compile-wasm-helperlib.ps1 deleted file mode 100644 index 6fb805f3aa95fc..00000000000000 --- a/compile-wasm-helperlib.ps1 +++ /dev/null @@ -1,118 +0,0 @@ -<# -.SYNOPSIS - Compiles HelperLib.dll to HelperLib.wasm using crossgen2 targeting browser/wasm. - -.DESCRIPTION - Uses crossgen2 to AOT-compile HelperLib.dll into a .wasm file, referencing - the managed DLLs in the IL directory. Output is placed into the IL directory. - - Prerequisites: - - Run .\build.cmd -os browser -c Debug -subset clr+libs - - Run .\prepare-wasm-test.ps1 to populate the IL directory -#> -param( - [ValidateSet("Debug", "Release")] - [string]$Configuration = "Debug" -) - -$ErrorActionPreference = "Stop" - -$RepoRoot = $PSScriptRoot -$DotnetExe = Join-Path $RepoRoot ".dotnet\dotnet.exe" -$Crossgen2Dll = Join-Path $RepoRoot "artifacts\bin\coreclr\windows.x64.debug\crossgen2\crossgen2.dll" -$WasmBinDir = Join-Path $RepoRoot "artifacts\bin\coreclr\browser.wasm.$Configuration" -$ILDir = Join-Path $WasmBinDir "IL" -$InputDll = Join-Path $ILDir "HelperLib.dll" -$baseName = [System.IO.Path]::GetFileNameWithoutExtension($InputDll) -$OutputWasm = Join-Path $WasmBinDir "$baseName.wasm" -$OutputVerifyWasm = Join-Path $WasmBinDir "$baseName.Verify.wasm" -$OutputWat = Join-Path $WasmBinDir "$baseName.wat" -$Wasm2Wat = "c:\git\wabt\bin\wasm2wat.exe" -$Wat2Wasm = "c:\git\wabt\bin\wat2wasm.exe" - -# --- Validate prerequisites --- - -if (-not (Test-Path $DotnetExe)) { - Write-Error "dotnet not found at $DotnetExe" - exit 1 -} -if (-not (Test-Path $Crossgen2Dll)) { - Write-Error "crossgen2.dll not found at $Crossgen2Dll. Build with: .\build.cmd clr -c Debug" - exit 1 -} -if (-not (Test-Path $InputDll)) { - Write-Error "HelperLib.dll not found at $InputDll. Run .\prepare-wasm-test.ps1 first." - exit 1 -} -if (-not (Test-Path $Wasm2Wat)) { - Write-Error "wasm2wat.exe not found at $Wasm2Wat" - exit 1 -} - -# --- Build reference assembly list --- - -$referenceDlls = Get-ChildItem "$ILDir\*.dll" -$referenceArgs = @() -foreach ($dll in $referenceDlls) { - $referenceArgs += "-r" - $referenceArgs += $dll.FullName -} - -# --- Run crossgen2 --- - -Write-Host "Compiling HelperLib.dll -> HelperLib.wasm" -ForegroundColor Cyan -Write-Host " Input : $InputDll" -Write-Host " Output: $OutputWasm" -Write-Host " References: $($referenceDlls.Count) DLLs from IL directory" -Write-Host "" - -$crossgen2Args = @( - "--targetos", "browser" - "--targetarch", "wasm" - "-f", "wasm" - "-o", $OutputWasm -) + $referenceArgs + @( - $InputDll -) - -$RspFile = Join-Path $WasmBinDir "crossgen2-helperlib.rsp" -$crossgen2Args | Set-Content -Path $RspFile -Write-Host "Response file: $RspFile" -ForegroundColor Yellow - -Write-Host "$DotnetExe $Crossgen2Dll @$RspFile" -ForegroundColor Yellow - -& $DotnetExe $Crossgen2Dll "@$RspFile" -if ($LASTEXITCODE -ne 0) { - Write-Error "crossgen2 compilation failed with exit code $LASTEXITCODE" - exit 1 -} - -Write-Host "" -Write-Host "=== crossgen2 succeeded ===" -ForegroundColor Green -Write-Host " Output: $OutputWasm" -Write-Host " Size : $((Get-Item $OutputWasm).Length) bytes" - -# --- Convert to WAT --- - -Write-Host "" -Write-Host "Converting HelperLib.wasm -> HelperLib.wat" -ForegroundColor Cyan - -& $Wasm2Wat $OutputWasm --enable-all --no-check -o $OutputWat -if ($LASTEXITCODE -ne 0) { - Write-Error "wasm2wat conversion failed with exit code $LASTEXITCODE" - exit 1 -} - -Write-Host "" -Write-Host "Converting HelperLib.wat -> HelperLib.Verify.wasm" -ForegroundColor Cyan - -& $Wat2Wasm $OutputWat --enable-extended-const -o $OutputVerifyWasm -if ($LASTEXITCODE -ne 0) { - Write-Error "wasm2wat conversion failed with exit code $LASTEXITCODE" - exit 1 -} - -Write-Host "" -Write-Host "=== WAT conversion succeeded ===" -ForegroundColor Green -Write-Host " Output: $OutputWat" -Write-Host " Size : $((Get-Item $OutputWat).Length) bytes" diff --git a/prepare-wasm-test.ps1 b/prepare-wasm-test.ps1 deleted file mode 100644 index c91c823a581380..00000000000000 --- a/prepare-wasm-test.ps1 +++ /dev/null @@ -1,239 +0,0 @@ -<# -.SYNOPSIS - Prepares the IL directory for CoreCLR WASM testing with a two-DLL HelloWorld test app. - -.DESCRIPTION - This script: - 1. Builds a HelperLib class library and a HelloWorld console app that references it - 2. Copies the framework DLLs into the IL directory - 3. Copies the built test DLLs into the IL directory - - Prerequisites: Run the CoreCLR WASM build first: - .\build.cmd -os browser -c Debug -subset clr+libs - -.PARAMETER Configuration - Build configuration (Debug or Release). Must match the configuration used for the WASM build. - -.EXAMPLE - .\prepare-wasm-test.ps1 - .\prepare-wasm-test.ps1 -Configuration Release -#> -param( - [ValidateSet("Debug", "Release")] - [string]$Configuration = "Debug" -) - -$ErrorActionPreference = "Stop" - -$RepoRoot = $PSScriptRoot -$ArtifactsDir = Join-Path $RepoRoot "artifacts" -$WasmBinDir = Join-Path $ArtifactsDir "bin\coreclr\browser.wasm.$Configuration" -$ILDir = Join-Path $WasmBinDir "IL" -$FrameworkDllDir = Join-Path $ArtifactsDir "bin\microsoft.netcore.app.runtime.browser-wasm\$Configuration\runtimes\browser-wasm\lib\net11.0" -$DotnetExe = Join-Path $RepoRoot ".dotnet\dotnet.exe" -$TempDir = Join-Path $ArtifactsDir "tmp\wasm-hello-test" - -# --- Validate prerequisites --- - -if (-not (Test-Path $DotnetExe)) { - Write-Error "dotnet not found at $DotnetExe. Run '.\build.cmd -os browser -c $Configuration -subset clr+libs' first." - exit 1 -} - -if (-not (Test-Path $FrameworkDllDir)) { - Write-Error "Framework DLLs not found at $FrameworkDllDir. Run '.\build.cmd -os browser -c $Configuration -subset clr+libs' first." - exit 1 -} - -if (-not (Test-Path (Join-Path $WasmBinDir "corerun.js"))) { - Write-Error "corerun.js not found in $WasmBinDir. Run '.\build.cmd -os browser -c $Configuration -subset clr+libs' first." - exit 1 -} - -# --- Create temp workspace with test projects --- - -if (Test-Path $TempDir) { - Remove-Item $TempDir -Recurse -Force -} -New-Item -ItemType Directory -Path $TempDir -Force | Out-Null - -# HelperLib project -$helperDir = Join-Path $TempDir "HelperLib" -New-Item -ItemType Directory -Path $helperDir -Force | Out-Null - -Set-Content (Join-Path $helperDir "HelperLib.csproj") @" - - - net11.0 - true - - -"@ - -Set-Content (Join-Path $helperDir "Helper.cs") @" -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -namespace HelperLib; - -public static class Helper -{ - public static unsafe string DoWork(string context, - delegate* managed callback, - int arg1, - int arg2, - int arg3, - int arg4, - int arg5, - int arg6, - int arg7, - int arg8, - int arg9, - int arg10, - int arg11, - int arg12, - int arg13, - int arg14, - int arg15) - { - callback(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12, arg13, arg14, arg15); - return `$"[HelperLib] Performed work for: {context} {OtherFunction()}"; - } - public static string OtherFunction() - { - return "in OtherFunction also compiled via R2R"; - } -} -"@ - -# HelloWorld project -$helloDir = Join-Path $TempDir "HelloWorld" -New-Item -ItemType Directory -Path $helloDir -Force | Out-Null - -Set-Content (Join-Path $helloDir "HelloWorld.csproj") @" - - - net11.0 - Exe - true - - - - - -"@ - -Set-Content (Join-Path $helloDir "Program.cs") @" -// 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 HelperLib; - -namespace HelloWorld; - -public static class Program -{ - - public static unsafe void Main() - { - Console.WriteLine("[HelloWorld] Starting up..."); - - Console.WriteLine(Helper.DoWork("Call1", &PrintEachArgOnNewLine, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15)); - Console.WriteLine(Helper.DoWork("Call2", &PrintEachArgOnNewLine, 11, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 115)); - Console.WriteLine(Helper.DoWork("Call3", &PrintEachArgOnNewLine, 21, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 115)); - - Console.WriteLine("[HelloWorld] All done!"); - } - - public static unsafe void PrintEachArgOnNewLine( - int a1, int a2, int a3, int a4, int a5, - int a6, int a7, int a8, int a9, int a10, - int a11, int a12, int a13, int a14, int a15) - { - Console.WriteLine(a1); - Console.WriteLine(a2); - Console.WriteLine(a3); - Console.WriteLine(a4); - Console.WriteLine(a5); - Console.WriteLine(a6); - Console.WriteLine(a7); - Console.WriteLine(a8); - Console.WriteLine(a9); - Console.WriteLine(a10); - Console.WriteLine(a11); - Console.WriteLine(a12); - Console.WriteLine(a13); - Console.WriteLine(a14); - Console.WriteLine(a15); - } -} -"@ - -# --- Build the test projects --- - -Write-Host "" -Write-Host "=== Building test projects ===" -ForegroundColor Cyan - -Write-Host "Building HelloWorld (includes HelperLib via ProjectReference)..." -& $DotnetExe build (Join-Path $helloDir "HelloWorld.csproj") -c Debug --nologo -v quiet -if ($LASTEXITCODE -ne 0) { - Write-Error "Build failed." - exit 1 -} -Write-Host "Build succeeded." -ForegroundColor Green - -# --- Populate the IL directory --- - -Write-Host "" -Write-Host "=== Populating IL directory ===" -ForegroundColor Cyan -Write-Host " Target: $ILDir" - -New-Item -ItemType Directory -Path $ILDir -Force | Out-Null - -# Copy framework DLLs (these are the managed BCL assemblies for browser-wasm) -Write-Host "Copying framework DLLs..." -$frameworkDlls = Get-ChildItem "$FrameworkDllDir\*.dll" -$frameworkDlls | Copy-Item -Destination $ILDir -Force -Write-Host " Copied $($frameworkDlls.Count) framework DLLs" - -# Copy built test DLLs (repo Directory.Build.props redirects output to artifacts/bin/) -$builtOutputDir = Join-Path $ArtifactsDir "bin\HelloWorld\Debug\net11.0" -$helloDll = Join-Path $builtOutputDir "HelloWorld.dll" -$helperDll = Join-Path $builtOutputDir "HelperLib.dll" - -if (-not (Test-Path $helloDll)) { - Write-Error "HelloWorld.dll not found at $builtOutputDir" - exit 1 -} -if (-not (Test-Path $helperDll)) { - Write-Error "HelperLib.dll not found at $builtOutputDir" - exit 1 -} - -Copy-Item $helloDll $ILDir -Force -Copy-Item $helperDll $ILDir -Force -Write-Host " Copied HelloWorld.dll" -Write-Host " Copied HelperLib.dll" - -# --- Summary --- - -$totalDlls = (Get-ChildItem "$ILDir\*.dll").Count - -# Convert to Unix-style absolute path for node usage (required even on Windows) -$unixILPath = ($ILDir -replace '\\', '/') -replace '^[A-Za-z]:', '' - -Write-Host "" -Write-Host "=== IL directory ready ===" -ForegroundColor Green -Write-Host " Location : $ILDir" -Write-Host " Total DLLs: $totalDlls" -Write-Host "" -Write-Host "--- To run with Node.js ---" -ForegroundColor Yellow -Write-Host " cd $WasmBinDir" -Write-Host " node ./corerun.js -c $unixILPath $unixILPath/HelloWorld.dll" -Write-Host "" -Write-Host "--- To run in browser ---" -ForegroundColor Yellow -Write-Host " 1. Edit src\coreclr\hosts\corerun\CMakeLists.txt -> set CORERUN_IN_BROWSER to 1" -Write-Host " 2. Rebuild: .\build.cmd -os browser -c $Configuration -subset clr" -Write-Host " 3. Serve: dotnet-serve --directory `"$WasmBinDir`"" -Write-Host " 4. Open corerun.html in your browser" diff --git a/src/coreclr/vm/pregeneratedstringthunks.cpp b/src/coreclr/vm/pregeneratedstringthunks.cpp index dbc84d6e28c094..6fd5e43c47d84c 100644 --- a/src/coreclr/vm/pregeneratedstringthunks.cpp +++ b/src/coreclr/vm/pregeneratedstringthunks.cpp @@ -50,7 +50,7 @@ void ProcessInjectStringThunksFixup(ReadyToRunInfo * pR2RInfo, PCCOR_SIGNATURE p _ASSERTE(existingTable != nullptr); // First pass: check if there are any new strings not already in the table. - bool hasNewEntries = false; + uint32_t newEntryCount = 0; { PCCOR_SIGNATURE pCurrent = pBlob; while (true) @@ -67,13 +67,13 @@ void ProcessInjectStringThunksFixup(ReadyToRunInfo * pR2RInfo, PCCOR_SIGNATURE p void* unused; if (!existingTable->Lookup(str, &unused)) { - hasNewEntries = true; + newEntryCount++; break; } } } - if (!hasNewEntries) + if (newEntryCount == 0) return; #ifdef TARGET_WASM @@ -86,7 +86,7 @@ void ProcessInjectStringThunksFixup(ReadyToRunInfo * pR2RInfo, PCCOR_SIGNATURE p // Build a new table with all existing entries plus new ones. StringToThunkHash* newTable = new StringToThunkHash(); - + newTable->Reallocate((existingTable->GetCount() + newEntryCount) * StringToThunkHash::s_density_factor_denominator / StringToThunkHash::s_density_factor_numerator + 1); // Copy existing entries for (auto iter = existingTable->Begin(); iter != existingTable->End(); ++iter) { From 69dc63e60019308f401318d71278ed70ec044200 Mon Sep 17 00:00:00 2001 From: David Wrighton Date: Thu, 7 May 2026 15:08:16 -0700 Subject: [PATCH 42/42] Fix review issues --- src/coreclr/jit/codegenwasm.cpp | 3 ++- src/coreclr/vm/pregeneratedstringthunks.cpp | 1 - src/tasks/WasmAppBuilder/coreclr/SignatureMapper.cs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/coreclr/jit/codegenwasm.cpp b/src/coreclr/jit/codegenwasm.cpp index 456e911cb21ab3..4f6f6309c90e9d 100644 --- a/src/coreclr/jit/codegenwasm.cpp +++ b/src/coreclr/jit/codegenwasm.cpp @@ -2499,7 +2499,8 @@ void CodeGen::genCallInstruction(GenTreeCall* call) if (sigInfoCall == nullptr) { - if ((params.methHnd != NO_METHOD_HANDLE) && (Compiler::eeGetHelperNum(params.methHnd) == CORINFO_HELP_UNDEF)) + if ((params.methHnd != NO_METHOD_HANDLE) && + (Compiler::eeGetHelperNum(params.methHnd) == CORINFO_HELP_UNDEF)) { m_compiler->eeGetMethodSig(params.methHnd, &sigInfoLocal); sigInfoCall = &sigInfoLocal; diff --git a/src/coreclr/vm/pregeneratedstringthunks.cpp b/src/coreclr/vm/pregeneratedstringthunks.cpp index 6fd5e43c47d84c..91d3fee06306ad 100644 --- a/src/coreclr/vm/pregeneratedstringthunks.cpp +++ b/src/coreclr/vm/pregeneratedstringthunks.cpp @@ -68,7 +68,6 @@ void ProcessInjectStringThunksFixup(ReadyToRunInfo * pR2RInfo, PCCOR_SIGNATURE p if (!existingTable->Lookup(str, &unused)) { newEntryCount++; - break; } } } diff --git a/src/tasks/WasmAppBuilder/coreclr/SignatureMapper.cs b/src/tasks/WasmAppBuilder/coreclr/SignatureMapper.cs index db7142945d1ba6..9d49f7bcfa9c44 100644 --- a/src/tasks/WasmAppBuilder/coreclr/SignatureMapper.cs +++ b/src/tasks/WasmAppBuilder/coreclr/SignatureMapper.cs @@ -237,7 +237,7 @@ public static List ParseSignatureTokens(string signature) /// /// 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. + /// Struct tokens (S<N>) consume max((size + 7) / 8, 1) slots; all others consume 1. /// public static int TokenToSlotCount(string token) {