Conversation
… the point of jumping into the r2r code, and then it fails with a function type mismatch since portable entrypoints are not quite correctly hooked up
…entrypoints - This includes some initial work where there is a predefined set of new thunks generated into the coreclr codebase. I expect that we'll build some sort of hardcoded list here for invokes to/from R2R code, and then have R2R supplement the list with extra cases. - Adjust PortableEntryPoint calling convention to match the WASM version
…into locations in the R2R file work - Notably, a new level of indirection is needed
- This is used to make loads/stores more efficient. loads/stores have an offset baked into them, so instead of global.get 1 i32.const <reloc> add i32.load We can do global.get 1 i32.load offset=<reloc> Overall this saves 3 bytes of space per load from an RVA relative address
…rator to match the ArgIterator behavior in the runtime - Notably, the ArgIterator in the runtime follows interpreter semantics, so this logic adjusts the way we stash memory into the stack to match the interpreter behavior - And the WasmImportThunk code now utilizes the ArgIterator to do its layout. - We may need to reconcile behavior differences between the interpreter/RyuJIT abi, but they are fairly close, so this is probably reasonable to do for now. Alternatively, we can remove the whole GCRefMap infra in favor of a small bit of conservative reporting, and a completely different format.
|
Tagging subscribers to this area: @agocke |
|
|
||
| #elif defined(TARGET_WASM) | ||
|
|
||
| #define FCDECL0(rettype, funcname) rettype F_CALL_CONV funcname(uintptr_t stack_pointer, int32_t portableEntryPointContext) |
There was a problem hiding this comment.
This will need #122988 (comment) to deal with the stack_pointer
There was a problem hiding this comment.
Yes. I've implemented this for the calls into the interpreter stubs now, but I was holding off on finishing this up for FCalls until my next PR, as its not really needed for initial bring up, and the PR is already way too large.
There was a problem hiding this comment.
Pull request overview
This draft PR introduces the initial plumbing needed to load Webcil R2R artifacts on WebAssembly and execute R2R methods via portable entrypoints, including new thunking paths and relocation support across the runtime, JIT, and AOT toolchain.
Changes:
- Add PortableEntryPoint-aware thunk generation and call paths for wasm (including R2R→interpreter dispatch support).
- Extend Webcil decoding / PE layout plumbing to surface ReadyToRun headers and apply relocations.
- Introduce a new wasm relocation kind and update the wasm object writer and AOT thunk emission to use it.
Reviewed changes
Copilot reviewed 37 out of 37 changed files in this pull request and generated 8 comments.
Show a summary per file
| File | Description |
|---|---|
| src/tasks/WasmAppBuilder/coreclr/ManagedToNativeGenerator.cs | Adds pregenerated signatures and adjusts cache file encoding. |
| src/tasks/WasmAppBuilder/coreclr/InterpToNativeGenerator.cs | Generates PortableEntryPoint-aware call thunks (*_PE) and signature handling for p suffix. |
| src/coreclr/vm/wasm/helpers.cpp | Adds wasm PortableEntryPoint→interpreter thunk table and lookup/cache; implements DelayLoad_MethodCall for wasm. |
| src/coreclr/vm/wasm/callhelpers-interp-to-managed.cpp | Updates generated thunk table with *_PE entries and PortableEntryPoint-aware call helpers. |
| src/coreclr/vm/readytoruninfo.cpp | Wires R2R entrypoint resolution through PortableEntryPoint temporary entrypoints. |
| src/coreclr/vm/prestub.cpp | Adds PortableEntryPoint-based interpreted execution entry and lazy interpreter code init path. |
| src/coreclr/vm/precode_portable.hpp | Adds “prefers interpreter” flag plumbing on PortableEntryPoint. |
| src/coreclr/vm/precode_portable.cpp | Integrates “prefers interpreter” behavior into native-entrypoint checks and SetActualCode upgrade. |
| src/coreclr/vm/peimagelayout.inl | Routes more queries through the active decoder; adds Webcil table-base offset accessor; enables Webcil R2R header queries. |
| src/coreclr/vm/peimagelayout.h | Removes cached Webcil table-base offset storage and exposes decoder-driven accessor. |
| src/coreclr/vm/peimagelayout.cpp | Applies relocations for Webcil and adds wasm-specific relocation parsing handling. |
| src/coreclr/vm/method.cpp | Initializes PortableEntryPoint with optional R2R→interpreter stub and sets “prefer interpreter” behavior. |
| src/coreclr/vm/jithelpers.cpp | Updates wasm helper wrappers for new FCALL signatures. |
| src/coreclr/vm/interpexec.cpp | Adjusts helper call ABI for PortableEntryPoints (stack pointer + context) and prefers interpreter entrypoints where flagged. |
| src/coreclr/vm/ilstubcache.cpp | Moves temporary entrypoint assignment to after method flags are set for IL stubs. |
| src/coreclr/vm/gccover.cpp | Updates GCFrame usage to pass GC flags instead of a bool. |
| src/coreclr/vm/frames.h | Changes GCFrame ctor signature and introduces conservative GC protect macro for interpreter builds. |
| src/coreclr/vm/frames.cpp | Implements GCFrame flag-based scanning instead of the prior bool-based interior tracking. |
| src/coreclr/vm/fcall.h | Defines wasm-specific FCALL/HCALL signatures including stack pointer + PortableEntryPoint context. |
| src/coreclr/vm/cgensys.h | Aligns wasm DelayLoad_MethodCall declaration with C++ implementation signature. |
| src/coreclr/utilcode/webcildecoder.cpp | Adds ReadyToRun header discovery and manifest metadata support for Webcil. |
| src/coreclr/tools/Common/JitInterface/CorInfoTypes.cs | Adds CorInfoReloc value for relative wasm linear-memory loads/stores. |
| src/coreclr/tools/Common/JitInterface/CorInfoImpl.cs | Maps the new CorInfoReloc value to the compiler relocation type. |
| src/coreclr/tools/Common/Compiler/ObjectWriter/WasmObjectWriter.cs | Implements relocation fixup handling for relative wasm linear-memory address relocations. |
| src/coreclr/tools/Common/Compiler/ObjectWriter/WasmInstructions.cs | Adds relocation-capable memory-arg encoding and new LoadWithRVAOffset helper. |
| src/coreclr/tools/Common/Compiler/DependencyAnalysis/Relocation.cs | Adds the new relocation kind and supports read/write/size operations for it. |
| src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/WasmImportThunk.cs | Reworks delay-load helper thunk emission to use ArgIterator/TransitionBlock and new RVA-relative load encoding. |
| src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/TransitionBlock.cs | Updates wasm32 transition block sizing/alignment assumptions. |
| src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/RuntimeFunctionsTableNode.cs | Emits wasm-specific runtime function “BeginAddress” entries as table indices. |
| src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/GCRefMapBuilder.cs | Refactors ArgIterator construction into reusable helper used by wasm import thunk emission. |
| src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/ArgIterator.cs | Adjusts wasm32 argument sizing/alignment and stack offset computation. |
| src/coreclr/runtime/portable/AllocFast.cpp | Updates an FCALL callsite to pass wasm stack/context parameters. |
| src/coreclr/jit/morph.cpp | Adds wasm-specific extra indirection for R2R entrypoints treated as PortableEntryPoints. |
| src/coreclr/jit/lower.cpp | Adds wasm-specific extra indirection when materializing entrypoints for calls. |
| src/coreclr/jit/codegenwasm.cpp | Adjusts helper call emission to load through PortableEntryPoint indirections. |
| src/coreclr/inc/webcildecoder.h | Exposes Webcil R2R header / metadata APIs and stores ReadyToRun header cache state. |
| src/coreclr/inc/corinfo.h | Adds CorInfoReloc value for relative wasm linear-memory address relocations. |
Comments suppressed due to low confidence (1)
src/coreclr/utilcode/webcildecoder.cpp:92
WebcilDecoder::Reset()does not reset the newly added ReadyToRun-related cache fields (m_hasNoReadyToRunHeaderandm_pReadyToRunHeader). If aWebcilDecoderinstance is reused, stale cached state could causeHasReadyToRunHeader()/GetReadyToRunHeader()to return incorrect results.
Reset these fields in Reset() (and consider also reinitializing them in Init()) to keep decoder state consistent across uses.
void WebcilDecoder::Reset()
{
LIMITED_METHOD_CONTRACT;
m_base = 0;
m_size = 0;
m_hasContents = false;
m_pHeader = NULL;
m_sections = NULL;
m_pCorHeader = NULL;
m_relocated = FALSE;
}
src/coreclr/vm/prestub.cpp
Outdated
| if (targetIp == NULL) | ||
| { | ||
| GCPROTECT_BEGINCONSERVATIVE_ARRAY(args, (UINT)(argsSize/sizeof(OBJECTREF))); | ||
| GCX_PREEMP(); | ||
| (void)pMethod->DoPrestub(NULL /* MethodTable */, CallerGCMode::Coop); | ||
| targetIp = pMethod->GetInterpreterCode(); | ||
| GCPROTECT_END(); |
There was a problem hiding this comment.
GCPROTECT_BEGINCONSERVATIVE_ARRAY(args, ...) is protecting the local pointer variable args (i.e., &args) rather than the argument buffer it points to. This means the GC frame will conservatively scan unrelated stack memory adjacent to the pointer variable, not the actual argument slots, which can lead to incorrect root reporting and potential GC corruption/instability.
Instead, create a GCFrame over the actual buffer address (e.g., GCFrame gcFrame((OBJECTREF*)args, count, GC_CALL_INTERIOR | GC_CALL_PINNED)), or introduce a dedicated helper/macro for protecting a dynamic pointer+count buffer rather than using a macro that takes an array variable.
| COUNT_T dirPos = 0; | ||
| #ifdef TARGET_WASM | ||
| // WASM will padd out the reloc size to the next 16 byte boundary, so we need to validate we can safely read the IMAGE_BASE_RELOCATION struct before processing each entry. | ||
| while (dirPos < (dirSize - sizeof(IMAGE_BASE_RELOCATION))) | ||
| #else | ||
| while (dirPos < dirSize) | ||
| #endif | ||
| { | ||
| PIMAGE_BASE_RELOCATION r = (PIMAGE_BASE_RELOCATION)(dir + dirPos); |
There was a problem hiding this comment.
On TARGET_WASM, the loop condition dirPos < (dirSize - sizeof(IMAGE_BASE_RELOCATION)) can underflow when dirSize < sizeof(IMAGE_BASE_RELOCATION) (since COUNT_T is unsigned), leading to an out-of-bounds read of IMAGE_BASE_RELOCATION.
Consider rewriting the condition as dirPos + sizeof(IMAGE_BASE_RELOCATION) <= dirSize (or guarding with if (dirSize < sizeof(...)) return;) to avoid unsigned underflow.
| void SetPrefersInterpreterEntryPoint() | ||
| { | ||
| LIMITED_METHOD_CONTRACT; | ||
| _ASSERTE(IsValid()); | ||
| _flags = (_flags | kPrefersInterpreterEntryPoint); // TODO, use interlock operation here | ||
| } | ||
|
|
||
| void ClearPrefersInterpreterEntryPoint() | ||
| { | ||
| LIMITED_METHOD_CONTRACT; | ||
| _ASSERTE(IsValid()); | ||
| _flags = (_flags & ~kPrefersInterpreterEntryPoint); // TODO, use interlock operation here | ||
| } |
There was a problem hiding this comment.
SetPrefersInterpreterEntryPoint / ClearPrefersInterpreterEntryPoint update _flags via plain read/modify/write (_flags = _flags | ... / _flags = _flags & ...). Since _flags is also used for other state (e.g., UnmanagedCallersOnly flags) and can be accessed concurrently, this can drop updates or tear state.
Please switch these to interlocked bit operations (e.g., InterlockedOr / InterlockedAnd) similar to the existing SetFlags helper used elsewhere in this file.
| WRAPPER_NO_CONTRACT; | ||
| return RhpNewArrayFast(pMT, size); | ||
| // Since we know the implementation of RhpNewArrayFast is native we don't need to actually | ||
| // pass a stack poitner or portable entry point context, so we can just specify 0 for those parameters. |
There was a problem hiding this comment.
Typo in comment: stack poitner should be stack pointer. (Also consider whether the comment should mention the portableEntryPointContext parameter name consistently.)
| // pass a stack poitner or portable entry point context, so we can just specify 0 for those parameters. | |
| // pass a stack pointer or portableEntryPointContext, so we can just specify 0 for those parameters. |
| // All Signatures required for FCalls should be in this list, but we may also want to include pregenerated signatures for commonly used shapes used by | ||
| // R2R code to reduce duplication in generated R2R binaries. 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 = |
There was a problem hiding this comment.
The comment says the type characters in these pregenerated signatures should match SignatureMapper.CharToNativeType, but the added signatures include a 'p' suffix, which is not a valid type character for SignatureMapper and is handled specially elsewhere.
Please update the comment to document 'p' as a portable-entrypoint marker (hidden parameter) so future additions don't follow the comment literally and break generation.
| string [] pregeneratedInterpreterToNativeSignatures = | ||
| { |
There was a problem hiding this comment.
Minor style inconsistency: this file otherwise uses string[] but this new declaration uses string []. Consider switching to string[] to match the surrounding conventions.
|
|
||
| WASM_TABLE_INDEX_I32 = 0x207, // Wasm: a table index encoded as a 4-byte uint32, e.g. for storing the "address" of a function into linear memory | ||
| 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 varint32. Used as the immediate argument of a load or store instruction, |
There was a problem hiding this comment.
The enum comment says WASM_MEMORY_ADDR_REL_LEB is a "varint32", but the relocation is encoded/decoded using ULEB128 (WritePaddedULEB128/ReadULEB128) and is described as varuint32 elsewhere.
Please fix the comment to avoid implying signed encoding.
| WASM_MEMORY_ADDR_REL_LEB = 0x209, // Wasm: a relative linear memory index encoded as a 5-byte varint32. Used as the immediate argument of a load or store instruction, | |
| 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, |
src/coreclr/inc/corinfo.h
Outdated
| // e.g. in R2R scenarios as an offset from __image_base | ||
| WASM_TYPE_INDEX_LEB, // Wasm: a type index encoded as a 5-byte varuint32, e.g. the type immediate in a call_indirect. | ||
| WASM_GLOBAL_INDEX_LEB, // Wasm: a global index encoded as a 5-byte varuint32, e.g. the index immediate in a get_global. | ||
| WASM_MEMORY_ADDR_REL_LEB, // Wasm: a relative linear memory index encoded as a 5-byte varint32. Used as the immediate argument of a load or store instruction, |
There was a problem hiding this comment.
The enum comment describes WASM_MEMORY_ADDR_REL_LEB as a "varint32", but the writer uses ULEB128 for this relocation type. Please update the comment to varuint32 (or clarify why it is signed) to avoid misleading JIT/objwriter implementers.
| WASM_MEMORY_ADDR_REL_LEB, // Wasm: a relative linear memory index encoded as a 5-byte varint32. Used as the immediate argument of a load or store instruction, | |
| WASM_MEMORY_ADDR_REL_LEB, // Wasm: a relative linear memory index encoded as a 5-byte varuint32. Used as the immediate argument of a load or store instruction, |
src/coreclr/vm/prestub.cpp
Outdated
| InterpByteCodeStart* targetIp = pMethod->GetInterpreterCode(); | ||
| if (targetIp == NULL) | ||
| { | ||
| GCPROTECT_BEGINCONSERVATIVE_ARRAY(args, (UINT)(argsSize/sizeof(OBJECTREF))); |
There was a problem hiding this comment.
This is not reliable. CoreCLR is not compatible with general conservative reporting - it will cause dynamic methods and collectible assemblies to crash intermittently.
We can only conservatively double-report (I believe it is what the interpreter does). We cannot conservatively report random pointers.
There was a problem hiding this comment.
@jkotas, I disagree. I'm pretty sure this IS safe, but it may not be sufficient. Notably, calls to LCG/collectible code may ALSO need to report a loaderallocator/LCG method. I'd prefer to add to that detail, than try to figure out how to report exactly here.
There was a problem hiding this comment.
ALSO need to report a loaderallocator/LCG method
That's not the only problem. Conservative reporting like this causes dead objects to become alive. Collectible code is not robust against that. Sure, it can be fixed with more work but I am not sure whether we want to do it.
Example of sequence of events that causes problem:
- The loader allocator is identified as dead and it is scheduled for cleanup
- Some conservatively reported root resurrects an object from the loader allocator that is scheduled for cleanup
- The loader allocator is cleaned up
- The resurrected object is still on the GC heap, but points to a MethodTable that does not exist anymore
- The GC crashes accessing the non-existing MethodTable when it tries to walk the heap
There was a problem hiding this comment.
Also, conservative reporting has been a source of unpredictable/flaky behavior in Mono, sometimes even leading to OOMs, for example dotnet/aspnetcore#23451. It would be nice to avoid that on CoreCLR.
than try to figure out how to report exactly here.
I think it should be cheap and straightforward to do the proper reporting once the transition thunks are produced by crossgen.
There was a problem hiding this comment.
I don't agree. The issues with conservative gc resurrecting dead objects were fixed some years ago. (It was one of Chris Ahna's more subtle finds.) The only issues remaining are keeping things method frames live, and the general problems of having a conservative gc which can lead to ooms, etc. given the current expectation around around codegen, I don't see it as useful to avoid conservative gc here at this time.
There was a problem hiding this comment.
So what prevents the sequence of events in my comment above from crashing the GC?
It was one of Chris Ahna's more subtle finds.
That was in .NET Native that did not support collectible types (if you mean the same set of problems that I have in mind)
There was a problem hiding this comment.
(I am fine with using conservative GC as a bring up aid, as long as it is wrapped with TODOs that link to issue.)
There was a problem hiding this comment.
@jkotas That find, which Chris found on .NET Native, was also very interesting as my analysis at the time indicated that it actually addressed a number of issues that had been coming up in collectible assemblies for years but we had never managed to fix/explain to my satisfaction. Since then I haven't seen collectible assemblies fail in a way which would have been triggered by using conservative GC. Effectively, collectible assemblies had been relying on a property of garbage collection which should have always been true around marking dead objects as dead reliably, but had a very obscure bug in the GC, and when Chris fixed the issue in object liveness computation, this fundamentally fixed the problems that collectible assemblies had around walking over objects that had notionally been collected, but had not yet quite properly been considered free in the GC. The issue had been explained away for collectible assemblies as a detail that the GC sometimes walked over dead object memory that might not yet been marked as a Free region.
Effectively, Chris's fix was to the ability of the GC to determine if an object was live, and that is the fundamental
issue that collectible assemblies had had for years. It so happens that conservative GC has exactly the same requirements for correctness, but the failure mode with conservative GC was rather easier to understand (and frankly, Chris was never brought in to deal with the collectible types problems, and he is so intensely good at tracking these things down.)
Also, the current model for GC reporting on the interpreter in coreclr has a requirement on conservative GC, not just safe double-reporting. This is due to some rather awkward details around the CALL, and various call helpers instructions work. To fix this completely would require splitting the existing CALL/CALLVIRT/CALL_HELPER instructions into several separate instructions, and introducing a variety of different GC reporting rules at each. As noted when we put it together, it's a limited time with the conservative report on a given memory address, but it IS true conservative reporting where sometimes wild pointers are fed into the GC.
There was a problem hiding this comment.
Regardless, we could use the PrestubMethodFrame here, which would be fairly reasonable. I'll look into it.
| // All Signatures required for FCalls should be in this list, but we may also want to include pregenerated signatures for commonly used shapes used by | ||
| // R2R code to reduce duplication in generated R2R binaries. 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 = |
There was a problem hiding this comment.
InterpreterToR2RSignatures
This PR contains one of @davidwrighton's commits extracted from #126662 to reduce the PR size and complexity. It adds a relocation type that is essentially `R_WASM_MEMORY_ADDR_REL_LEB`, so that a relocation corresponding to a relative offset from r2r `imageBase` can be used directly as the `offset` in a load, like: ```wasm global.get $imageBase i32.load align=... offset=<reloc> ``` The functionality isn't used yet, but will be in #126662. --------- Co-authored-by: David Wrighton <davidwr@microsoft.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 36 out of 36 changed files in this pull request and generated 3 comments.
Comments suppressed due to low confidence (1)
src/coreclr/utilcode/webcildecoder.cpp:92
WebcilDecoder::Reset(andInit) do not reset the new ReadyToRun header cache fields (m_hasNoReadyToRunHeader/m_pReadyToRunHeader). If a previous image had no R2R header,m_hasNoReadyToRunHeaderstays true and subsequentHasReadyToRunHeader()calls will incorrectly return false even after reinitializing with an R2R-enabled image. Reset these fields inReset()and/orInit().
void WebcilDecoder::Reset()
{
LIMITED_METHOD_CONTRACT;
m_base = 0;
m_size = 0;
m_hasContents = false;
m_pHeader = NULL;
m_sections = NULL;
m_pCorHeader = NULL;
m_relocated = FALSE;
}
src/coreclr/vm/prestub.cpp
Outdated
| GCPROTECT_BEGINCONSERVATIVE_ARRAY(args, (UINT)(argsSize/sizeof(OBJECTREF))); | ||
| GCX_PREEMP(); | ||
| (void)pMethod->DoPrestub(NULL /* MethodTable */, CallerGCMode::Coop); | ||
| targetIp = pMethod->GetInterpreterCode(); | ||
|
|
||
| if (targetIp == NULL) | ||
| { | ||
| _ASSERTE(!PortableEntryPoint::PrefersInterpreterEntryPoint(portableEntrypoint)); | ||
| ManagedMethodParam param = { pMethod, args, retBuff, (PCODE)targetIp, nullptr /* WASM-TODO, handle RuntimeAsync */}; | ||
| return InvokeManagedMethod(¶m); | ||
| } | ||
|
|
||
| GCPROTECT_END(); |
There was a problem hiding this comment.
GCPROTECT_BEGINCONSERVATIVE_ARRAY(args, ...) is passing a pointer variable (int8_t* args) into a macro that takes the address of its argument. This ends up protecting the local pointer (int8_t**) rather than scanning/protecting the argument buffer contents, so GC refs in the args buffer can move during DoPrestub. Consider passing an lvalue representing the first element of the buffer (e.g., ((OBJECTREF*)args)[0]) or constructing a GCFrame directly over (OBJECTREF*)args (and also guard for args == nullptr / argsSize == 0).
| GCPROTECT_BEGINCONSERVATIVE_ARRAY(args, (UINT)(argsSize/sizeof(OBJECTREF))); | |
| GCX_PREEMP(); | |
| (void)pMethod->DoPrestub(NULL /* MethodTable */, CallerGCMode::Coop); | |
| targetIp = pMethod->GetInterpreterCode(); | |
| if (targetIp == NULL) | |
| { | |
| _ASSERTE(!PortableEntryPoint::PrefersInterpreterEntryPoint(portableEntrypoint)); | |
| ManagedMethodParam param = { pMethod, args, retBuff, (PCODE)targetIp, nullptr /* WASM-TODO, handle RuntimeAsync */}; | |
| return InvokeManagedMethod(¶m); | |
| } | |
| GCPROTECT_END(); | |
| UINT gcRefCount = (UINT)(argsSize / sizeof(OBJECTREF)); | |
| if (args != NULL && gcRefCount != 0) | |
| { | |
| GCPROTECT_BEGINCONSERVATIVE_ARRAY(((OBJECTREF*)args)[0], gcRefCount); | |
| GCX_PREEMP(); | |
| (void)pMethod->DoPrestub(NULL /* MethodTable */, CallerGCMode::Coop); | |
| targetIp = pMethod->GetInterpreterCode(); | |
| if (targetIp == NULL) | |
| { | |
| _ASSERTE(!PortableEntryPoint::PrefersInterpreterEntryPoint(portableEntrypoint)); | |
| ManagedMethodParam param = { pMethod, args, retBuff, (PCODE)targetIp, nullptr /* WASM-TODO, handle RuntimeAsync */}; | |
| return InvokeManagedMethod(¶m); | |
| } | |
| GCPROTECT_END(); | |
| } | |
| else | |
| { | |
| GCX_PREEMP(); | |
| (void)pMethod->DoPrestub(NULL /* MethodTable */, CallerGCMode::Coop); | |
| targetIp = pMethod->GetInterpreterCode(); | |
| if (targetIp == NULL) | |
| { | |
| _ASSERTE(!PortableEntryPoint::PrefersInterpreterEntryPoint(portableEntrypoint)); | |
| ManagedMethodParam param = { pMethod, args, retBuff, (PCODE)targetIp, nullptr /* WASM-TODO, handle RuntimeAsync */}; | |
| return InvokeManagedMethod(¶m); | |
| } | |
| } |
| // Pregenerated signatures for commonly used shapes used by R2R code to reduce duplication in generated R2R binaries. | ||
| // 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 = |
There was a problem hiding this comment.
The comment describing pregeneratedInterpreterToNativeSignatures says the type characters match SignatureMapper.CharToNativeType, but these signatures include the 'p' suffix which is handled as a portable-entrypoint marker (and is not recognized by CharToNativeType). Update the comment to reflect that 'p' is a special suffix, not a regular type code.
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 36 out of 36 changed files in this pull request and generated 6 comments.
Comments suppressed due to low confidence (1)
src/coreclr/utilcode/webcildecoder.cpp:92
WebcilDecoder::Init/Resetinitializem_hasContents,m_pHeader, etc., but they don't reset the new ReadyToRun-related cache fields (m_hasNoReadyToRunHeaderandm_pReadyToRunHeader). If the decoder instance is reused for another image,HasReadyToRunHeader()/GetReadyToRunHeader()could return stale results or pointers. These fields should be cleared inInitandReset.
void WebcilDecoder::Init(void *flatBase, COUNT_T size)
{
CONTRACTL
{
INSTANCE_CHECK;
PRECONDITION((size == 0) || CheckPointer(flatBase));
NOTHROW;
GC_NOTRIGGER;
}
CONTRACTL_END;
m_base = (TADDR)flatBase;
m_size = size;
m_hasContents = (size > 0);
m_pHeader = (m_hasContents && size >= sizeof(WebcilHeader)) ? (const WebcilHeader *)flatBase : NULL;
if (m_pHeader != NULL && m_pHeader->VersionMajor >= 1)
{
// For version 1 and above, the section headers start after the larger header
if (size < sizeof(WebcilHeader_1))
{
m_pHeader = NULL; // Not enough data for even the header
}
}
if (!m_pHeader)
{
m_sections = NULL;
}
else
{
m_sections = (const WebcilSectionHeader *)(((uint8_t*)flatBase) + (m_pHeader->VersionMajor >= 1 ? sizeof(WebcilHeader_1) : sizeof(WebcilHeader)));
}
m_pCorHeader = NULL;
m_relocated = FALSE;
}
void WebcilDecoder::Reset()
{
LIMITED_METHOD_CONTRACT;
m_base = 0;
m_size = 0;
m_hasContents = false;
m_pHeader = NULL;
m_sections = NULL;
m_pCorHeader = NULL;
m_relocated = FALSE;
}
| // pass a stack pointer or portable entry point context, so we can just specify 0 for those parameters. | ||
| return RhpNewArrayFast(0 /* stack_pointer */, pMT, size, 0 /* portableEntryPointContext */); |
There was a problem hiding this comment.
RhpNewArrayFast is declared via FCDECL2, which expands to a 2-arg function on non-WASM targets. This direct call with 4 arguments will not compile outside TARGET_WASM (and also makes the callsite inconsistent with the declaration on other platforms). Consider wrapping the extra stack_pointer / portableEntryPointContext arguments in #ifdef TARGET_WASM, or providing a small helper/macro that supplies the right argument list per target.
| // pass a stack pointer or portable entry point context, so we can just specify 0 for those parameters. | |
| return RhpNewArrayFast(0 /* stack_pointer */, pMT, size, 0 /* portableEntryPointContext */); | |
| // pass a stack pointer or portable entry point context, so we can just specify 0 for those parameters. | |
| #ifdef TARGET_WASM | |
| return RhpNewArrayFast(0 /* stack_pointer */, pMT, size, 0 /* portableEntryPointContext */); | |
| #else | |
| return RhpNewArrayFast(pMT, size); | |
| #endif |
| COUNT_T dirPos = 0; | ||
| #ifdef TARGET_WASM | ||
| // WASM will padd out the reloc size to the next 16 byte boundary, so we need to validate we can safely read the IMAGE_BASE_RELOCATION struct before processing each entry. | ||
| while (dirPos < (dirSize - sizeof(IMAGE_BASE_RELOCATION))) |
There was a problem hiding this comment.
On WASM, the loop condition dirPos < (dirSize - sizeof(IMAGE_BASE_RELOCATION)) can underflow when dirSize < sizeof(IMAGE_BASE_RELOCATION) because dirSize is unsigned. That would turn the bound into a huge value and can lead to out-of-bounds reads. Use a form like dirPos + sizeof(IMAGE_BASE_RELOCATION) <= dirSize (and keep the existing padding handling) to avoid unsigned underflow.
| while (dirPos < (dirSize - sizeof(IMAGE_BASE_RELOCATION))) | |
| while (dirPos + sizeof(IMAGE_BASE_RELOCATION) <= dirSize) |
| } | ||
|
|
||
| foreach (var nestedType in type.GetNestedTypes()) | ||
| ScanType(nestedType); | ||
| } |
There was a problem hiding this comment.
type.GetNestedTypes() without binding flags returns only public nested types. This will skip InternalCall methods on non-public nested types. Use type.GetNestedTypes(BindingFlags.Public | BindingFlags.NonPublic) (and consider BindingFlags.DeclaredOnly if desired) to ensure the scan is complete.
| // Pregenerated signatures for commonly used shapes used by R2R code to reduce duplication in generated R2R binaries. | ||
| // 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", | ||
| }; |
There was a problem hiding this comment.
The comment describing pregeneratedInterpreterToNativeSignatures says all characters match SignatureMapper.CharToNativeType, but these signatures now include the trailing 'p' portable-entrypoint marker (which is not a type char and isn't handled by CharToNativeType). Please update the comment to document the 'p' suffix convention (and ideally reference where it's interpreted) to avoid confusion.
| @@ -618,7 +859,7 @@ namespace | |||
| return NULL; | |||
| } | |||
|
|
|||
| uint32_t keyBufferLen = sig.NumFixedArgs() + (sig.HasThis() ? 1 : 0) + 2; | |||
| uint32_t keyBufferLen = sig.NumFixedArgs() + (sig.HasThis() ? 1 : 0) + 2 + ((callConv == IMAGE_CEE_CS_CALLCONV_DEFAULT) ? 1 : 0); | |||
| char* keyBuffer = (char*)alloca(keyBufferLen); | |||
There was a problem hiding this comment.
ComputeCalliSigThunk still allows non-default calling conventions (C/stdcall/fastcall/unmanaged), but GetSignatureKey only appends the 'p' marker for IMAGE_CEE_CS_CALLCONV_DEFAULT. With the thunk table updates, several floating-point shapes now only exist in the ...p form (e.g., ddp, ffp), so a non-default calli with double/float args/returns would compute keys like dd/ff and fail lookup. Either restrict the supported calling conventions here (return NULL for non-default until thunks exist), or ensure the thunk table/keying scheme continues to cover those non-default shapes.
| const StringToWasmSigThunk g_wasmThunks[] = { | ||
| { "dd", (void*)&CallFunc_F64_RetF64 }, | ||
| { "ddd", (void*)&CallFunc_F64_F64_RetF64 }, | ||
| { "dddd", (void*)&CallFunc_F64_F64_F64_RetF64 }, | ||
| { "ddi", (void*)&CallFunc_F64_I32_RetF64 }, | ||
| { "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 }, | ||
| { "ff", (void*)&CallFunc_F32_RetF32 }, | ||
| { "fff", (void*)&CallFunc_F32_F32_RetF32 }, | ||
| { "ffff", (void*)&CallFunc_F32_F32_F32_RetF32 }, | ||
| { "ffi", (void*)&CallFunc_F32_I32_RetF32 }, | ||
| { "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 }, |
There was a problem hiding this comment.
The thunk map no longer contains non-p floating-point signatures (e.g., dd, ddd, ff, etc.), but helpers.cpp can still compute and look up keys without the 'p' suffix for non-default calli calling conventions. If any such calli is exercised (C/stdcall/fastcall/unmanaged), it will now fail to find a thunk. Consider generating/keeping both the portable-entrypoint (...p) and non-portable variants for these float/double shapes, or tightening the runtime to reject non-default calling conventions until supported.
…rms the stack walker to stop walking - Put the callers frame pointer into the TransitionBlock. The current implementation drops it into the m_ReturnAddress field, which is a bit dodgy, but we can fix this later when we actually work on the stack walker - Replace use of conservative gc in the ExecuteInterpretedMethodWithArgs_PortableEntryPoint_Complex function in favor of reusing the PrestubMethodFrame which I believe is appropriate to the situation
| return RhpNewArrayFast(pMT, size); | ||
| // Since we know the implementation of RhpNewArrayFast is native we don't need to actually | ||
| // pass a stack pointer or portable entry point context, so we can just specify 0 for those parameters. | ||
| return RhpNewArrayFast(0 /* stack_pointer */, pMT, size, 0 /* portableEntryPointContext */); |
There was a problem hiding this comment.
| return RhpNewArrayFast(0 /* stack_pointer */, pMT, size, 0 /* portableEntryPointContext */); | |
| return RhpNewArrayFast_IMPL(pMT, size); |
Call the actual _IMPL method? I think this is going to overwrite the stack pointer with zero otherwise.
| { | ||
| FCALL_CONTRACT; | ||
| IL_Throw_Impl(obj, NULL); | ||
| IL_Throw_Impl(0, obj, NULL, 0); |
There was a problem hiding this comment.
| IL_Throw_Impl(0, obj, NULL, 0); | |
| IL_Throw_Impl_IMPL(obj, NULL); |
| { | ||
| FCALL_CONTRACT; | ||
| IL_Rethrow_Impl(NULL); | ||
| IL_Rethrow_Impl(0, NULL, 0); |
There was a problem hiding this comment.
| IL_Rethrow_Impl(0, NULL, 0); | |
| IL_Rethrow_Impl_IMPL(NULL); |
| { | ||
| FCALL_CONTRACT; | ||
| IL_ThrowExact_Impl(obj, NULL); | ||
| IL_ThrowExact_Impl(0, obj, NULL, 0); |
There was a problem hiding this comment.
| IL_ThrowExact_Impl(0, obj, NULL, 0); | |
| IL_ThrowExact_Impl_IMPL(obj, NULL); |
Initial change that actually makes it possible to load and execute an R2R method on WebAssembly.
Needed before merge