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
Tracking issue for replacing the workaround added for #130585 with a real fix.
Background
#130585: on browser-wasm, crossgen2 dies with NotImplementedException while the object writer materializes import signatures, because a TypeHandle fixup was emitted for a type that cannot be reverse-mapped to a module token (canonically [S.P.CoreLib]System.Reflection.MemberInfo). This blocked 4 of 7 browser-wasm jobs in #132339, which turns PublishReadyToRun on by default for CoreCLR browser-wasm.
The workaround that landed
A guard in embedGenericHandle's CORINFO_HANDLETYPE_CLASS branch in src/coreclr/tools/aot/ILCompiler.ReadyToRun/JitInterface/CorInfoImpl.ReadyToRun.cs, which rejects the method for R2R (RequiresRuntimeJitException) when the type handle could not be encoded as a module token:
// Mirrors SignatureContext.GetTargetModule: only an EcmaType *definition* is looked up in the// resolver, everything else is encoded against the local context and cannot fail.privateboolCanEncodeTypeHandleFixup(TypeDesctype){if(type.IsPrimitive||type.IsString||type.IsObject||type.IsWellKnownType(WellKnownType.TypedReference))returntrue;if(type.GetTypeDefinition()is not EcmaTypeecmaType)returntrue;return!_compilation.NodeFactory.Resolver.GetModuleTokenForType(ecmaType,allowDynamicallyCreatedReference:true,throwIfNotFound:false).IsNull;}
This is early enough to prevent the crash: the rejected method never reaches SetCode, so its Import node is never marked, ImportSectionNode.AddImport never runs, and the un-encodable signature is never materialized.
It is a workaround, not a fix. The affected methods silently lose ReadyToRun and fall back to the runtime JIT. That is a correctness-safe outcome but it costs exactly the startup benefit R2R exists to provide, and on wasm the runtime JIT is the expensive path.
Measured cost
Against xunit.runner.utility.netcoreapp10.dll (browser-wasm, Release), crossgen2 --verbose:
count
Rejected by the pre-existing embedClassHandle guard (MessageHandler<T> over out-of-bubble interfaces)
68
Rejected by this workaround
1 (System.Reflection.MemberInfo, in Xunit.RunnerReporterUtility.GetAvailableRunnerReporters)
So on this assembly the incremental cost is small. It has not been measured across the whole framework/test closure, and the affected set grows with any assembly containing a non-devirtualizable cross-bubble virtual call.
Proper fix (F1)
Take the method-keyed ReadyToRun fast path on wasm instead of the generic helper, so the declaring type handle is never needed. The fixup then keys on the callee MemberRef, which the consuming assembly always has a token for.
// Wasm R2R cannot use the CORINFO_HELP_READYTORUN_VIRTUAL_FUNC_PTR fast path because it// relies on DelayLoad_Helper_Obj dynamic-helper thunks, which are not implemented on wasm.// Fall through to the runtime CORINFO_HELP_VIRTUAL_FUNC_PTR helper instead.
#if defined(FEATURE_READYTORUN) && !defined(TARGET_WASM)
With TARGET_WASM the block is preprocessed out, so impImportLdvirtftn falls through to CORINFO_HELP_VIRTUAL_FUNC_PTR, which takes the declaring type handle as an argument — that argument is the whole reason the un-encodable TypeHandle fixup exists.
2. Implement the dynamic-helper thunks.src/coreclr/vm/wasm/dynamichelpers.cpp:37-45 — both are stubs today:
extern"C"voidSTDCALLDelayLoad_Helper_Obj()
{
PORTABILITY_ASSERT("DelayLoad_Helper_Obj is not implemented on wasm");
}
extern"C"voidSTDCALLDelayLoad_Helper_ObjObj()
{
PORTABILITY_ASSERT("DelayLoad_Helper_ObjObj is not implemented on wasm");
}
Plain DelayLoad_Helper in the same file is implemented for wasm and is the template: a naked function that saves/restores __stack_pointer around a call to a C++ worker ending in DynamicHelperWorker. The _Obj variants additionally have to carry an object reference through the TransitionBlock so the worker can resolve against the actual this. Registration is already wired up in jitinterface.cpp (READYTORUN_HELPER_DelayLoad_Helper_Obj → GetEEFuncEntryPoint(DelayLoad_Helper_Obj)).
3. Decide how the VirtualEntry cell is invoked on wasm.WasmImportThunk.cs:46-49 currently hard-rejects the virtual-call thunk:
if(useVirtualCall){// In wasm we should always be using a helper to get the function pointer target, and then dispatching on that instead of using a thunkthrownewSystem.NotSupportedException(nameof(useVirtualCall));}
This is deliberate — wasm has no arbitrary indirect branches, only call_indirect through a typed table, so the design avoids indirect-branch-through-thunk. Making the method-keyed path work likely means keeping the "helper returns a function pointer, then call_indirect" shape but keying the cell on the method rather than passing a type handle. That is a design decision for the wasm R2R owners, not a mechanical port. Note WasmImportThunk already accepts DelayLoad_Helper_Obj/_ObjObj in its non-virtual branch, so the compiler side is partly in place.
4. Don't forget the second route into the same helper.Compiler::getVirtMethodPointerTree (src/coreclr/jit/morph.cpp:5739, reached from fgMorphTailCallViaHelpers at morph.cpp:5445) embeds the parent type handle the same way and has noREADYTORUN_VIRTUAL_FUNC_PTR alternative on any target. Either fix it too, or confirm the tail-call-via-helper path is unreachable on wasm — otherwise the same un-encodable fixup can still be produced.
Alternative if F1 stays blocked (F2)
Synthesize a TypeRef in the manifest metadata for out-of-bubble types instead of failing. ModuleTokenResolver already has a manifest fallback that looks up existing entities (_manifestMutableModule.TryGetExistingEntityHandle), an AddModuleTokenForType registration path, and MutableModule is by design a module new tokens can be created in. Concerns:
_manifestMutableModule is written from parallel compile threads; MutableModule's concurrency contract for new-token creation was not verified.
Every synthesized TypeRef adds a manifest assembly-ref + MVID entry, hard-binding the R2R image to that MVID — a real version-resilience behaviour change beyond wasm.
It removes the crash for all targets but leaves wasm on the slow generic helper, so it is strictly worse than F1 for performance.
Done when
The workaround guard in embedGenericHandle can be removed (or demoted to an assert) without reintroducing the crash.
Methods with non-devirtualizable cross-bubble virtual calls are R2R-compiled on browser-wasm rather than rejected.
This issue body was generated by GitHub Copilot from a local investigation and reviewed before posting. The code references were verified against the tree; the F1 design direction is a proposal, not a validated implementation.
Tracking issue for replacing the workaround added for #130585 with a real fix.
Background
#130585: on browser-wasm, crossgen2 dies with
NotImplementedExceptionwhile the object writer materializes import signatures, because aTypeHandlefixup was emitted for a type that cannot be reverse-mapped to a module token (canonically[S.P.CoreLib]System.Reflection.MemberInfo). This blocked 4 of 7 browser-wasm jobs in #132339, which turnsPublishReadyToRunon by default for CoreCLR browser-wasm.The workaround that landed
A guard in
embedGenericHandle'sCORINFO_HANDLETYPE_CLASSbranch insrc/coreclr/tools/aot/ILCompiler.ReadyToRun/JitInterface/CorInfoImpl.ReadyToRun.cs, which rejects the method for R2R (RequiresRuntimeJitException) when the type handle could not be encoded as a module token:This is early enough to prevent the crash: the rejected method never reaches
SetCode, so itsImportnode is never marked,ImportSectionNode.AddImportnever runs, and the un-encodable signature is never materialized.It is a workaround, not a fix. The affected methods silently lose ReadyToRun and fall back to the runtime JIT. That is a correctness-safe outcome but it costs exactly the startup benefit R2R exists to provide, and on wasm the runtime JIT is the expensive path.
Measured cost
Against
xunit.runner.utility.netcoreapp10.dll(browser-wasm, Release),crossgen2 --verbose:embedClassHandleguard (MessageHandler<T>over out-of-bubble interfaces)System.Reflection.MemberInfo, inXunit.RunnerReporterUtility.GetAvailableRunnerReporters)So on this assembly the incremental cost is small. It has not been measured across the whole framework/test closure, and the affected set grows with any assembly containing a non-devirtualizable cross-bubble virtual call.
Proper fix (F1)
Take the method-keyed ReadyToRun fast path on wasm instead of the generic helper, so the declaring type handle is never needed. The fixup then keys on the callee
MemberRef, which the consuming assembly always has a token for.1. Drop the carve-out.
src/coreclr/jit/importer.cpp:2726:With
TARGET_WASMthe block is preprocessed out, soimpImportLdvirtftnfalls through toCORINFO_HELP_VIRTUAL_FUNC_PTR, which takes the declaring type handle as an argument — that argument is the whole reason the un-encodableTypeHandlefixup exists.2. Implement the dynamic-helper thunks.
src/coreclr/vm/wasm/dynamichelpers.cpp:37-45— both are stubs today:Plain
DelayLoad_Helperin the same file is implemented for wasm and is the template: anakedfunction that saves/restores__stack_pointeraround a call to a C++ worker ending inDynamicHelperWorker. The_Objvariants additionally have to carry an object reference through theTransitionBlockso the worker can resolve against the actualthis. Registration is already wired up injitinterface.cpp(READYTORUN_HELPER_DelayLoad_Helper_Obj→GetEEFuncEntryPoint(DelayLoad_Helper_Obj)).3. Decide how the
VirtualEntrycell is invoked on wasm.WasmImportThunk.cs:46-49currently hard-rejects the virtual-call thunk:This is deliberate — wasm has no arbitrary indirect branches, only
call_indirectthrough a typed table, so the design avoids indirect-branch-through-thunk. Making the method-keyed path work likely means keeping the "helper returns a function pointer, thencall_indirect" shape but keying the cell on the method rather than passing a type handle. That is a design decision for the wasm R2R owners, not a mechanical port. NoteWasmImportThunkalready acceptsDelayLoad_Helper_Obj/_ObjObjin its non-virtual branch, so the compiler side is partly in place.4. Don't forget the second route into the same helper.
Compiler::getVirtMethodPointerTree(src/coreclr/jit/morph.cpp:5739, reached fromfgMorphTailCallViaHelpersatmorph.cpp:5445) embeds the parent type handle the same way and has noREADYTORUN_VIRTUAL_FUNC_PTRalternative on any target. Either fix it too, or confirm the tail-call-via-helper path is unreachable on wasm — otherwise the same un-encodable fixup can still be produced.Alternative if F1 stays blocked (F2)
Synthesize a
TypeRefin the manifest metadata for out-of-bubble types instead of failing.ModuleTokenResolveralready has a manifest fallback that looks up existing entities (_manifestMutableModule.TryGetExistingEntityHandle), anAddModuleTokenForTyperegistration path, andMutableModuleis by design a module new tokens can be created in. Concerns:_manifestMutableModuleis written from parallel compile threads;MutableModule's concurrency contract for new-token creation was not verified.TypeRefadds a manifest assembly-ref + MVID entry, hard-binding the R2R image to that MVID — a real version-resilience behaviour change beyond wasm.Done when
embedGenericHandlecan be removed (or demoted to an assert) without reintroducing the crash.Related: #130585, #132339.
Note
This issue body was generated by GitHub Copilot from a local investigation and reviewed before posting. The code references were verified against the tree; the F1 design direction is a proposal, not a validated implementation.