Logic to load R2R files on WebAssembly#127229
Logic to load R2R files on WebAssembly#127229davidwrighton wants to merge 1 commit intodotnet:mainfrom
Conversation
davidwrighton
commented
Apr 21, 2026
- Add better error checking to libCorerun.js
- Add handling of relocs/tableBase to WebCIL v1 images
- Actually handle PortableEntryPoint correctly in the ReadyToRun code
- Generate WASM_TABLE_INDEX_I32 relocs in the the RuntimeFunctionsTableNode
- Adjust DoesSlotCallPrestub to return false and not assert all the time on Wasm. In the absence of code versioning, returning false appears to be adequate.
- Add better error checking to libCorerun.js - Add handling of relocs/tableBase to WebCIL v1 images - Actually handle PortableEntryPoint correctly in the ReadyToRun code - Generate WASM_TABLE_INDEX_I32 relocs in the the RuntimeFunctionsTableNode - Adjust DoesSlotCallPrestub to return false and not assert all the time on Wasm. In the absence of code versioning, returning false appears to be adequate.
|
Tagging subscribers to this area: @agocke |
There was a problem hiding this comment.
Pull request overview
This PR enables loading and executing ReadyToRun (R2R) code from WebAssembly/Webcil images by extending Webcil decoding/relocation support and wiring portable entrypoints + runtime function table relocs for Wasm.
Changes:
- Extend Webcil decoding and PEImageLayout dispatch so Webcil images can expose R2R headers, manifest metadata, and apply base/table relocs.
- Update R2R runtime entrypoint lookup to correctly use portable entrypoints (temporary entrypoint + “actual code”).
- Emit Wasm table-index relocations for the runtime functions table and improve
libCorerun.jserror handling around module compile/table growth.
Reviewed changes
Copilot reviewed 9 out of 9 changed files in this pull request and generated 8 comments.
Show a summary per file
| File | Description |
|---|---|
| src/coreclr/vm/readytoruninfo.cpp | Uses portable entrypoints to route R2R entrypoint resolution via temporary entrypoints. |
| src/coreclr/vm/precode_portable.cpp | Makes DoesSlotCallPrestub return false under portable entrypoints (Wasm scenario). |
| src/coreclr/vm/peimagelayout.inl | Adds Webcil GetTableBaseOffset forwarding + changes Webcil dispatch behavior for R2R header queries. |
| src/coreclr/vm/peimagelayout.h | Removes legacy table-base setter/getter in favor of decoder-derived offset. |
| src/coreclr/vm/peimagelayout.cpp | Applies relocations for Webcil at init time and adds Wasm-specific relocation parsing logic. |
| src/coreclr/utilcode/webcildecoder.cpp | Adds R2R header discovery + manifest metadata access for Webcil. |
| src/coreclr/inc/webcildecoder.h | Exposes new Webcil R2R surface area and table-base offset from Webcil v1 header. |
| src/coreclr/tools/aot/ILCompiler.ReadyToRun/.../RuntimeFunctionsTableNode.cs | Emits WASM_TABLE_INDEX_I32 relocs for Wasm runtime function BeginAddress values. |
| src/coreclr/hosts/corerun/wasm/libCorerun.js | Adds error handling for Wasm module compilation and table growth failures. |
Comments suppressed due to low confidence (1)
src/coreclr/utilcode/webcildecoder.cpp:92
- WebcilDecoder::Reset() doesn't reset the new ReadyToRun header caches (m_hasNoReadyToRunHeader and m_pReadyToRunHeader). If the decoder instance is reused (e.g., via Reset/Init), stale cached state can incorrectly force HasReadyToRunHeader() to return false or return a pointer from a previous image. Reset should clear these fields (and any other R2R-related cached state) to the same defaults as the constructor.
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;
}
| 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 |
| if (_nodeFactory.Target.Architecture == TargetArchitecture.Wasm32) | ||
| { | ||
| // On Amd64, the 2nd word contains the EndOffset of the runtime function | ||
| Debug.Assert(frameInfo.StartOffset != frameInfo.EndOffset); | ||
| runtimeFunctionsBuilder.EmitReloc(symbol, RelocType.IMAGE_REL_BASED_ADDR32NB, delta: frameInfo.EndOffset); | ||
| runtimeFunctionsBuilder.EmitReloc(symbol, RelocType.WASM_TABLE_INDEX_I32, frameIndex); | ||
| } | ||
| else | ||
| { | ||
| runtimeFunctionsBuilder.EmitReloc(symbol, RelocType.IMAGE_REL_BASED_ADDR32NB, delta: frameInfo.StartOffset + _nodeFactory.Target.CodeDelta); | ||
| if (!relocsOnly && _nodeFactory.Target.Architecture == TargetArchitecture.X64) | ||
| { | ||
| // On Amd64, the 2nd word contains the EndOffset of the runtime function | ||
| Debug.Assert(frameInfo.StartOffset != frameInfo.EndOffset); | ||
| runtimeFunctionsBuilder.EmitReloc(symbol, RelocType.IMAGE_REL_BASED_ADDR32NB, delta: frameInfo.EndOffset); | ||
| } | ||
| } |
| #ifndef FEATURE_PORTABLE_ENTRYPOINTS | ||
| pEntryPoint = dac_cast<TADDR>(GetImage()->GetBase()) + m_pRuntimeFunctions[id].BeginAddress; | ||
| #else | ||
| // When we have portable entrypoints enable, the R2R image contains actual entrypoints. |
| READYTORUN_HEADER * WebcilDecoder::GetReadyToRunHeader() const | ||
| { | ||
| CONTRACT(READYTORUN_HEADER *) | ||
| { | ||
| INSTANCE_CHECK; | ||
| PRECONDITION(CheckNTHeaders()); | ||
| PRECONDITION(HasCorHeader()); | ||
| PRECONDITION(HasReadyToRunHeader()); | ||
| NOTHROW; | ||
| GC_NOTRIGGER; |
| TADDR WebcilDecoder::GetDirectoryData(IMAGE_DATA_DIRECTORY *pDir) const | ||
| { | ||
| CONTRACT(TADDR) | ||
| { | ||
| INSTANCE_CHECK; | ||
| PRECONDITION(CheckNTHeaders()); | ||
| PRECONDITION(CheckDirectory(pDir, 0, NULL_OK)); | ||
| NOTHROW; | ||
| GC_NOTRIGGER; | ||
| SUPPORTS_DAC; | ||
| POSTCONDITION(CheckPointer((void *)RETVAL, NULL_OK)); | ||
| CANNOT_TAKE_LOCK; | ||
| } | ||
| CONTRACT_END; | ||
|
|
||
| RETURN GetRvaData(VAL32(pDir->VirtualAddress)); | ||
| } | ||
|
|
||
| CHECK WebcilDecoder::CheckDirectory(IMAGE_DATA_DIRECTORY *pDir, int forbiddenFlags, IsNullOK ok) const | ||
| { | ||
| CONTRACT_CHECK | ||
| { | ||
| INSTANCE_CHECK; | ||
| PRECONDITION(CheckNTHeaders()); | ||
| PRECONDITION(CheckPointer(pDir)); | ||
| NOTHROW; |
|
|
||
| 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. |
| #ifdef FEATURE_WEBCIL | ||
| , m_tableBaseOffset(0) | ||
| #endif | ||
| , m_pOwner(NULL) |
| const errormessage = e instanceof Error ? e.message : String(e); | ||
| console.error("Failed to compile WebAssembly module for Webcil image:", {wasmPath, errormessage}); |
| { | ||
| LIMITED_METHOD_CONTRACT; | ||
| _ASSERTE(!"DoesSlotCallPrestub is not supported with Portable EntryPoints"); | ||
| /* On WASM slots never directly call the prestub. Instead we have the R2R to interpreter thunks |
There was a problem hiding this comment.
Does this need to be guarded with a #ifdef TARGET_WASM? I'm just wondering if we could have other platforms with portable entrypoints where this behavior may be different.
| BOOL IsNativeMachineFormat() const { return FALSE; } | ||
| PTR_CVOID GetNativeManifestMetadata(COUNT_T *pSize = NULL) const; | ||
| READYTORUN_HEADER *GetReadyToRunHeader() const; | ||
| BOOL IsNativeMachineFormat() const { return TRUE; } |
There was a problem hiding this comment.
TRUE only when you found R2R ?