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 (FEATURE_PORTABLE_ENTRYPOINTS + cached interface dispatch), invoking an open virtual/interface delegate from ReadyToRun-compiled code asserts in the interpreter:
Found while ReadyToRun-compiling and running the src/tests/JIT tree on wasm. The failing case is src/tests/JIT/Regression/JitBlue/Runtime_79354/Runtime_79354.cs — an open-instance delegate over an interface method (IGetContents.GetContents()), created via MethodInfo.CreateDelegate. IL/interpreter execution passes; only R2R fails.
Root cause
For an open virtual/interface delegate, COMDelegate sets _methodPtrAux = GetVirtualCallStub(...), which under cached interface dispatch is (PCODE)CID_VirtualOpenDelegateDispatch — a native helper, not a portable entry point (comdelegate.cpp:1201, :961).
The delegate's _methodPtr is the IL shuffle thunk (CreateILDelegateShuffleThunk), which does LDFLD _methodPtrAux; calli (comdelegate.cpp:853-854). On wasm the shuffle thunk runs interpreted.
The interpreter's INTOP_CALLDELEGATE path already handles the open-virtual case (detects the CID_VirtualOpenDelegateDispatch / VSD marker and resolves the real target — interpexec.cpp:3422-3448). But that path is bypassed when the delegate is invoked from R2R-compiled code, which calls the shuffle thunk directly. The shuffle thunk's own calli then reaches INTOP_CALLI, which does PortableEntryPoint::GetMethodDesc(calliFunctionPointer) (interpexec.cpp:3326) on the native CID_VirtualOpenDelegateDispatch helper -> ToPortableEntryPoint -> IsValid() assert.
In a release build this is not a checked assert, but the calli target is still not a valid portable entry point, so the dispatch is incorrect.
Repro (wasm R2R)
Build clr+libs -os browser (Checked).
ReadyToRun-compile Runtime_79354.dll (crossgen2, --targetarch:wasm --targetos:browser) and run under corerun.js with the R2R companion enabled (PLATFORM_NATIVE_R2R=1, APP_ASSEMBLIES=EXTERNAL).
Minimal shape:
publicinterfaceIGetContents{(string,int,string)GetContents();}publicstructMyStruct:IGetContents{/* ... */public(string,int,string)GetContents()=>(s1,a,s2);}publicdelegate(string,int,string)MyDelegate(IGetContentsarg);varmi=typeof(IGetContents).GetMethod("GetContents");varfunc=(MyDelegate)mi.CreateDelegate(typeof(MyDelegate));// open delegate over interface methodvarr=func(newMyStruct{/* ... */});// asserts under wasm R2R
Suggested fix
Teach the interpreter's INTOP_CALLI (portable-entrypoints path) to recognize the open-virtual dispatch marker — calliFunctionPointer == CID_VirtualOpenDelegateDispatch (cached dispatch) or VirtualCallStubManager::isStubStatic(...) (VSD) — before treating the target as a portable entry point, and resolve the real target the same way INTOP_CALLDELEGATE does (from the shuffle-thunk delegate this + receiver). A working prototype exists.
Description
On wasm (FEATURE_PORTABLE_ENTRYPOINTS + cached interface dispatch), invoking an open virtual/interface delegate from ReadyToRun-compiled code asserts in the interpreter:
Found while ReadyToRun-compiling and running the
src/tests/JITtree on wasm. The failing case issrc/tests/JIT/Regression/JitBlue/Runtime_79354/Runtime_79354.cs— an open-instance delegate over an interface method (IGetContents.GetContents()), created viaMethodInfo.CreateDelegate. IL/interpreter execution passes; only R2R fails.Root cause
For an open virtual/interface delegate,
COMDelegatesets_methodPtrAux = GetVirtualCallStub(...), which under cached interface dispatch is(PCODE)CID_VirtualOpenDelegateDispatch— a native helper, not a portable entry point (comdelegate.cpp:1201,:961).The delegate's
_methodPtris the IL shuffle thunk (CreateILDelegateShuffleThunk), which doesLDFLD _methodPtrAux; calli(comdelegate.cpp:853-854). On wasm the shuffle thunk runs interpreted.The interpreter's
INTOP_CALLDELEGATEpath already handles the open-virtual case (detects theCID_VirtualOpenDelegateDispatch/ VSD marker and resolves the real target —interpexec.cpp:3422-3448). But that path is bypassed when the delegate is invoked from R2R-compiled code, which calls the shuffle thunk directly. The shuffle thunk's owncallithen reachesINTOP_CALLI, which doesPortableEntryPoint::GetMethodDesc(calliFunctionPointer)(interpexec.cpp:3326) on the nativeCID_VirtualOpenDelegateDispatchhelper ->ToPortableEntryPoint->IsValid()assert.In a release build this is not a checked assert, but the calli target is still not a valid portable entry point, so the dispatch is incorrect.
Repro (wasm R2R)
clr+libs -os browser(Checked).Runtime_79354.dll(crossgen2,--targetarch:wasm --targetos:browser) and run undercorerun.jswith the R2R companion enabled (PLATFORM_NATIVE_R2R=1,APP_ASSEMBLIES=EXTERNAL).Minimal shape:
Suggested fix
Teach the interpreter's
INTOP_CALLI(portable-entrypoints path) to recognize the open-virtual dispatch marker —calliFunctionPointer == CID_VirtualOpenDelegateDispatch(cached dispatch) orVirtualCallStubManager::isStubStatic(...)(VSD) — before treating the target as a portable entry point, and resolve the real target the same wayINTOP_CALLDELEGATEdoes (from the shuffle-thunk delegatethis+ receiver). A working prototype exists.Notes
Note
This issue was drafted with GitHub Copilot.