Repository navigation
Description
Summary
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:
.\dotnet.cmd build -bl /p:TargetOS=browser /p:TargetArchitecture=wasm /p:Configuration=Debug /p:RuntimeFlavor=CoreCLR /t:Test .\src\libraries\System.Runtime.InteropServices.JavaScript\tests\System.Runtime.InteropServices.JavaScript.UnitTests\System.Runtime.InteropServices.JavaScript.Tests.csproj /p:Scenario="WasmTestOnV8" /p:WasmTestAppArgs="-method System.Runtime.InteropServices.JavaScript.Tests.JSExportTest.JsExportIJSObject"
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:
| step | operation | result |
|---|---|---|
enter JsExportTest<__Canon> |
(no dictionary) | OK |
invoke == null check |
(no dictionary) | OK |
res = invoke(value, echoName) |
shared-generic delegate invoke | OK, res non-null |
typeof(T) |
generic-dictionary lookup | TRAPS "null function" |
The shared-generic delegate invoke works; only the generic-dictionary lookup traps. (This refutes an earlier delegate _methodPtr hypothesis.)
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.- That import cell is a
DelayLoadHelperImport. On wasm its delay-load helper is set tonullforGenericLookupSignature, andEncodeDatathen emits a zero pointer:
if (factory.Target.Architecture == TargetArchitecture.Wasm32)
{
if (instanceSignature is GenericLookupSignature)
{
// 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.EmitCodeto handle the generic-context argument in its arg spill/restore loops (and surfaceHasGenericContextArgfor generic-lookup signatures). - VM — a matching delay-load helper that invokes
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.
Activity
dotnet-policy-service commentedon Jun 24, 2026
Tagging subscribers to 'arch-wasm': @lewing, @pavelsavara
See info in area-owners.md if you want to be subscribed.
davidwrighton commentedon Jun 24, 2026
I'm working on this right now.
[-][wasm][coreCLR] R2R generic dictionary lookup traps with ull function (zero import cell for GenericLookupSignature)[/-][+][mono][amd64] Fix calling-convention mismatch for small straddling vtypes in shared generics[/+][-][mono][amd64] Fix calling-convention mismatch for small straddling vtypes in shared generics[/-][+][wasm][coreCLR] R2R generic dictionary lookup traps with ull function (zero import cell for GenericLookupSignature)[/+]AndyAyersMS commentedon Jun 25, 2026
With this and other fixes I'm able to get part way into running something where SPC is R2R'd. See #129850.
Also re-checking JIT subtree pri-1 tests where just the test assemblies are R2R'd. This should fix the 7 or so remaining failures (and there is still one test I need to exclude with > 1000 parameters).
[-][wasm][coreCLR] R2R generic dictionary lookup traps with ull function (zero import cell for GenericLookupSignature)[/-][+][wasm][coreCLR] R2R generic dictionary lookup traps with null function (zero import cell for GenericLookupSignature)[/+]Fix generic lookups and thunks (#129825)
Fix generic lookups and thunks (#129825)