Wasm: Runtime_79354 fails with test assembly R2R
- Dominant language
- C#
- Stars
- 18.3k
- Forks
- 5.6k
- PR merge metrics
- PR metrics pending
Description
### Description
On wasm (FEATURE_PORTABLE_ENTRYPOINTS + cached interface dispatch), invoking an **open virtual/interface delegate** from ReadyToRun-compiled code asserts in the interpreter:
```
Assert failure: portableEntryPoint->IsValid()
src/coreclr/vm/precode_portable.cpp:109 ToPortableEntryPoint
Frame (InterpreterFrame): MyDelegate::IL_STUB_DelegateShuffleThunk, IR_0014
```
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)
1. Build `clr+libs -os browser` (Checked).
2. 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:
```csharp
public interface IGetContents { (string, int, string) GetContents(); }
public struct MyStruct : IGetContents { /* ... */ public (string,int,string) GetContents() => (s1, a, s2); }
public delegate (string, int, string) MyDelegate(IGetContents arg);
var mi = typeof(IGetContents).GetMethod("GetContents");
var func = (MyDelegate)mi.CreateDelegate(typeof(MyDelegate)); // open delegate over interface method
var r = func(new MyStruct { /* ... */ }); // 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.
### Notes
- Area: wasm interpreter / R2R (portable entry points).
> [!NOTE]
> This issue was drafted with GitHub Copilot.
Contributor guide
Assessment
This issue has not been assessed yet.