You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
On wasm (CoreCLR, R2R), executing shared-generic R2R code that performs a generic-dictionary lookup (e.g. typeof(T) inside a __Canon instantiation) traps with:
Uncaught RuntimeError: null function or function signature mismatch
This is a call_indirect to wasm function-table index 0. The generic-lookup R2R import cell is emitted as a zero pointer and the runtime-lookup slow path calls through it.
Repro
Branch with the wasm R2R bring-up work (depends on #129766 / UCO→R2R dispatch to reach the R2R-native export). Reproduced deterministically on V8:
The shared-generic delegate invoke works; only the generic-dictionary lookup traps. (This refutes an earlier delegate _methodPtr hypothesis.)
Root cause
getReadyToRunHelper returns false on wasm (CorInfoImpl.ReadyToRun.cs, "WebAssembly doesn't use the compact ReadyToRun helpers"), so generic-handle lookups route through the runtime-lookup path (embedGenericHandle → ComputeRuntimeLookupForSharedGenericToken).
ProcessDynamicDictionaryLookup (src/coreclr/vm/prestub.cpp) resolves the general (non-MVAR/VAR-fast-path) case as CORINFO_USEHELPER with CORINFO_HELP_RUNTIMEHANDLE_METHOD / _CLASS — a helper call made through the generic-lookup import cell.
That import cell is a DelayLoadHelperImport. On wasm its delay-load helper is set to null for GenericLookupSignature, and EncodeData then emits a zero pointer:
if(factory.Target.Architecture==TargetArchitecture.Wasm32){if(instanceSignatureisGenericLookupSignature){// Generic lookups are resolved via eager fixups and don't need import thunks_delayLoadHelper=null;}else{_delayLoadHelper=factory.WasmImportThunkPortableEntrypoint(this);}}
The comment's assumption is incorrect for method/this-dependent dictionary lookups: they are inherently per-call dynamic (they need the runtime generic-context argument) and cannot be resolved by Module::RunEagerFixups (which only processes import sections flagged Eager and asserts *fixupCell != 0). So the cell stays 0, and the USEHELPER slow path does call_indirect to index 0 → "null function".
Introduced by #127483 (commit 2ac8dc55ec2, "[WASM] Add READYTORUN_FIXUP_InjectStringThunks and R2R thunk infrastructure"), which changed wasm from always using WasmImportThunkPortableEntrypoint to nulling the helper for GenericLookupSignature.
Why it isn't a one-line revert
Simply restoring WasmImportThunkPortableEntrypoint(this) for generic-lookup cells makes crossgen2 itself crash during R2R compilation of any assembly with a generic-dictionary lookup:
System.IndexOutOfRangeException
at ILCompiler.DependencyAnalysis.ReadyToRun.WasmImportThunk.EmitCode(...)
WasmImportThunk.EmitCode raises the wasm signature to a managed MethodSignature and walks it with an ArgIterator; the generic-lookup helper's hidden generic-context argument (HasGenericContextArg is hardcoded false) makes the offsets[] / wasm-locals arrays mismatch. This is the real reason the cell was nulled — emitting a generic-lookup delay-load thunk is deferred (WASM-TODO) work.
(Verified locally: applied the revert, rebuilt crossgen2-wasm, reproduced the IndexOutOfRangeException, then reverted again.)
Proposed fix
Implement generic-lookup (dynamic dictionary lookup) delay-load thunk support on wasm:
Compiler — WasmImportThunk.EmitCode to handle the generic-context argument in its arg spill/restore loops (and surface HasGenericContextArg for generic-lookup signatures).
VM — a matching delay-load helper that invokes ProcessDynamicDictionaryLookup with the generic context + signature and patches the cell, then wire the appropriate ReadyToRunHelper id for generic lookups.
Alternative: emit the dictionary-lookup slow path inline on wasm without a call through a delay-load cell (larger codegen change).
Summary
On wasm (CoreCLR, R2R), executing shared-generic R2R code that performs a generic-dictionary lookup (e.g.
typeof(T)inside a__Canoninstantiation) traps with:This is a
call_indirectto wasm function-table index0. The generic-lookup R2R import cell is emitted as a zero pointer and the runtime-lookup slow path calls through it.Repro
Branch with the wasm R2R bring-up work (depends on #129766 / UCO→R2R dispatch to reach the R2R-native export). Reproduced deterministically on V8:
Call chain:
JsExportIJSObject(R2R) → UCO/interp→R2R thunk →JsExportTest<System.__Canon>(R2R shared-generic) → traps at the first generic-dictionary access.Isolation (confirmed)
Staged diagnostics inside
JsExportTest<T>(3 V8 runs) pinpoint the trap precisely:JsExportTest<__Canon>invoke == nullcheckres = invoke(value, echoName)resnon-nulltypeof(T)The shared-generic delegate invoke works; only the generic-dictionary lookup traps. (This refutes an earlier delegate
_methodPtrhypothesis.)Root cause
getReadyToRunHelperreturns false on wasm (CorInfoImpl.ReadyToRun.cs, "WebAssembly doesn't use the compact ReadyToRun helpers"), so generic-handle lookups route through the runtime-lookup path (embedGenericHandle→ComputeRuntimeLookupForSharedGenericToken).ProcessDynamicDictionaryLookup(src/coreclr/vm/prestub.cpp) resolves the general (non-MVAR/VAR-fast-path) case asCORINFO_USEHELPERwithCORINFO_HELP_RUNTIMEHANDLE_METHOD/_CLASS— a helper call made through the generic-lookup import cell.DelayLoadHelperImport. On wasm its delay-load helper is set tonullforGenericLookupSignature, andEncodeDatathen emits a zero pointer:https://github.com/dotnet/runtime/blob/main/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/DelayLoadHelperImport.cs
The comment's assumption is incorrect for method/this-dependent dictionary lookups: they are inherently per-call dynamic (they need the runtime generic-context argument) and cannot be resolved by
Module::RunEagerFixups(which only processes import sections flaggedEagerand asserts*fixupCell != 0). So the cell stays0, and the USEHELPER slow path doescall_indirectto index0→ "null function".Introduced by #127483 (commit
2ac8dc55ec2, "[WASM] Add READYTORUN_FIXUP_InjectStringThunks and R2R thunk infrastructure"), which changed wasm from always usingWasmImportThunkPortableEntrypointto nulling the helper forGenericLookupSignature.Why it isn't a one-line revert
Simply restoring
WasmImportThunkPortableEntrypoint(this)for generic-lookup cells makes crossgen2 itself crash during R2R compilation of any assembly with a generic-dictionary lookup:WasmImportThunk.EmitCoderaises the wasm signature to a managedMethodSignatureand walks it with anArgIterator; the generic-lookup helper's hidden generic-context argument (HasGenericContextArgis hardcodedfalse) makes theoffsets[]/ wasm-locals arrays mismatch. This is the real reason the cell was nulled — emitting a generic-lookup delay-load thunk is deferred (WASM-TODO) work.(Verified locally: applied the revert, rebuilt crossgen2-wasm, reproduced the
IndexOutOfRangeException, then reverted again.)Proposed fix
Implement generic-lookup (dynamic dictionary lookup) delay-load thunk support on wasm:
WasmImportThunk.EmitCodeto handle the generic-context argument in its arg spill/restore loops (and surfaceHasGenericContextArgfor generic-lookup signatures).ProcessDynamicDictionaryLookupwith the generic context + signature and patches the cell, then wire the appropriateReadyToRunHelperid for generic lookups.Alternative: emit the dictionary-lookup slow path inline on wasm without a call through a delay-load cell (larger codegen change).
Relevant files
src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/DelayLoadHelperImport.cssrc/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/WasmImportThunk.cssrc/coreclr/tools/aot/ILCompiler.ReadyToRun/JitInterface/CorInfoImpl.ReadyToRun.cs(getReadyToRunHelper,embedGenericHandle)src/coreclr/vm/prestub.cpp(ProcessDynamicDictionaryLookup)src/coreclr/vm/ceeload.cpp(Module::RunEagerFixups)cc @davidwrighton @radekdoulik
Note
This issue was authored with the assistance of GitHub Copilot.