Uh oh!
There was an error while loading. Please reload this page.
Implement convertPInvokeCalliToCall for CoreCLR - #131654
Conversation
Tagging subscribers to this area: @agocke |
There was a problem hiding this comment.
Pull request overview
This PR updates CoreCLR to use the JIT-EE convertPInvokeCalliToCall path for unmanaged calli, aligning behavior with NativeAOT by creating/caching an IL stub at JIT-time (instead of the legacy runtime helper + per-arch assembly mechanism).
Changes:
- Implement
CEEInfo::convertPInvokeCalliToCalland plumb VM support to create CALLI IL stubs and defer/report stub-generation failures at stub-execution time. - Remove the legacy
GenericPInvokeCalliHelper/ cookie-based JIT path and associated helper/assembly plumbing across architectures, plus related frame/cDAC entries. - Update debugger stub tracing to recover the unmanaged target from the new trailing
native intargument.
Reviewed changes
Copilot reviewed 71 out of 71 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| src/coreclr/vm/wasm/helpers.cpp | Removes wasm placeholder for the deleted GenericPInvokeCalliHelper. |
| src/coreclr/vm/stubmgr.h | Removes mention of GenericPInvokeCalliHelper from comments. |
| src/coreclr/vm/stubmgr.cpp | Updates IL stub tracing to locate CALLI target from the last argument (not hidden arg); removes helper tracing paths. |
| src/coreclr/vm/stubhelpers.h | Adds QCall entrypoint declaration for throwing deferred interop exceptions. |
| src/coreclr/vm/stubhelpers.cpp | Adds QCall implementation for deferred interop exception throwing; removes old CALLI target mangling. |
| src/coreclr/vm/riscv64/pinvokestubs.S | Deletes CALLI helper stub macro usage and associated “secret” mangling logic. |
| src/coreclr/vm/qcallentrypoints.cpp | Registers new StubHelpers_ThrowInteropException QCall entrypoint. |
| src/coreclr/vm/mlinfo.cpp | Defers CALLI-stub marshal failures to runtime (records exception info instead of throwing during stub generation). |
| src/coreclr/vm/loongarch64/pinvokestubs.S | Deletes CALLI helper stub macro usage and associated “secret” mangling logic. |
| src/coreclr/vm/jitinterface.cpp | Implements convertPInvokeCalliToCall; marks IL stubs as non-inlineable. |
| src/coreclr/vm/i386/cgenx86.cpp | Removes x86 PInvokeCalliFrame regdisplay support (frame removed). |
| src/coreclr/vm/i386/asmhelpers.S | Removes x86 GenericPInvokeCalliHelper assembly helper. |
| src/coreclr/vm/i386/asmhelpers.asm | Removes x86 GenericPInvokeCalliHelper assembly helper and worker import. |
| src/coreclr/vm/FrameTypes.h | Removes PInvokeCalliFrame from frame type list. |
| src/coreclr/vm/frames.h | Removes PInvokeCalliFrame definition and updates CALLI documentation comments. |
| src/coreclr/vm/frames.cpp | Removes PInvokeCalliFrame promotion/logging and constructor. |
| src/coreclr/vm/dllimport.h | Adds VM API for creating CALLI IL stubs; adds stub linker state for deferred exceptions and target-arg index. |
| src/coreclr/vm/dllimport.cpp | Adds CALLI IL stub creation pipeline and deferred-throw stub generation; refactors vararg stub worker path. |
| src/coreclr/vm/datadescriptor/datadescriptor.inc | Removes cDAC descriptor for the deleted PInvokeCalliFrame. |
| src/coreclr/vm/corelib.h | Adds binder mapping for StubHelpers.ThrowInteropException. |
| src/coreclr/vm/cgensys.h | Removes GenericPInvokeCalli* extern declarations. |
| src/coreclr/vm/ceeload.h | Adds Module::GetLoaderModuleForSignature for standalone-signature-owned artifacts. |
| src/coreclr/vm/ceeload.cpp | Implements GetLoaderModuleForSignature and reuses it from GetVASigCookie. |
| src/coreclr/vm/callconvbuilder.cpp | Fixes module-independent signature modopt parsing for unmanaged calling conventions. |
| src/coreclr/vm/arm64/pinvokestubs.S | Deletes CALLI helper stub macro usage and associated “secret” mangling logic. |
| src/coreclr/vm/arm64/PInvokeStubs.asm | Deletes CALLI helper stub macro usage and associated “secret” mangling logic. |
| src/coreclr/vm/arm/pinvokestubs.S | Deletes CALLI helper stub macro usage. |
| src/coreclr/vm/amd64/pinvokestubs.S | Deletes GenericPInvokeCalliHelper implementation (Unix asm). |
| src/coreclr/vm/amd64/PInvokeStubs.asm | Deletes GenericPInvokeCalliHelper implementation (Windows asm). |
| src/coreclr/vm/amd64/asmconstants.h | Removes now-unused pinvoke-calli target register constants. |
| src/coreclr/tools/superpmi/superpmi/icorjitinfo.cpp | Removes SuperPMI interception for deleted JIT-EE method GetCookieForPInvokeCalliSig. |
| src/coreclr/tools/superpmi/superpmi-shim-simple/icorjitinfo_generated.cpp | Removes shim forwarding for deleted GetCookieForPInvokeCalliSig. |
| src/coreclr/tools/superpmi/superpmi-shim-counter/icorjitinfo_generated.cpp | Removes shim forwarding/counter for deleted GetCookieForPInvokeCalliSig. |
| src/coreclr/tools/superpmi/superpmi-shim-collector/icorjitinfo.cpp | Removes recording for deleted GetCookieForPInvokeCalliSig. |
| src/coreclr/tools/superpmi/superpmi-shared/methodcontext.h | Removes recording/replay surface for deleted GetCookieForPInvokeCalliSig. |
| src/coreclr/tools/superpmi/superpmi-shared/methodcontext.cpp | Removes recording/replay implementation for deleted GetCookieForPInvokeCalliSig. |
| src/coreclr/tools/superpmi/superpmi-shared/lwmlist.h | Removes LWM map entry for deleted GetCookieForPInvokeCalliSig. |
| src/coreclr/tools/superpmi/superpmi-shared/agnostic.h | Removes serialized key struct for deleted GetCookieForPInvokeCalliSig. |
| src/coreclr/tools/Common/JitInterface/ThunkGenerator/ThunkInput.txt | Removes deleted JIT-EE interface method from thunk generator input. |
| src/coreclr/tools/Common/JitInterface/CorInfoImpl.cs | Removes managed-side implementation stub for deleted GetCookieForPInvokeCalliSig. |
| src/coreclr/tools/Common/JitInterface/CorInfoImpl_generated.cs | Removes generated callback plumbing for deleted GetCookieForPInvokeCalliSig. |
| src/coreclr/tools/Common/JitInterface/CorInfoHelpFunc.cs | Removes helper id CORINFO_HELP_PINVOKE_CALLI. |
| src/coreclr/tools/aot/jitinterface/jitinterface_generated.h | Removes AOT wrapper method for deleted GetCookieForPInvokeCalliSig. |
| src/coreclr/tools/aot/ILCompiler.RyuJit/JitInterface/CorInfoImpl.RyuJit.cs | Removes setting hClass for converted calli stub (RyuJit AOT side). |
| src/coreclr/tools/aot/ILCompiler.ReadyToRun/JitInterface/CorInfoImpl.ReadyToRun.cs | Implements ReadyToRun behavior for convertPInvokeCalliToCall (throws to force runtime JIT when needed). |
| src/coreclr/System.Private.CoreLib/src/System/StubHelpers.cs | Adds managed QCall declaration for ThrowInteropException. |
| src/coreclr/jit/wellknownargs.h | Removes well-known args for the deleted cookie/target register convention. |
| src/coreclr/jit/targetx86.h | Removes x86 pinvoke-calli cookie/target register definitions. |
| src/coreclr/jit/targetwasm.h | Removes wasm pinvoke-calli cookie/target register definitions. |
| src/coreclr/jit/targetriscv64.h | Removes riscv64 pinvoke-calli cookie/target register definitions. |
| src/coreclr/jit/targetloongarch64.h | Removes loongarch64 pinvoke-calli cookie/target register definitions. |
| src/coreclr/jit/targetarm64.h | Removes arm64 pinvoke-calli cookie/target register definitions. |
| src/coreclr/jit/targetarm.h | Removes arm pinvoke-calli cookie/target register definitions. |
| src/coreclr/jit/targetamd64.h | Removes amd64 pinvoke-calli cookie/target register definitions. |
| src/coreclr/jit/morph.cpp | Removes cookie-based indirect-call morphing to CORINFO_HELP_PINVOKE_CALLI. |
| src/coreclr/jit/lower.h | Removes lowering hook that asserted cookie calls should not reach lowering. |
| src/coreclr/jit/lower.cpp | Removes lowering helper that asserted cookie calls do not exist. |
| src/coreclr/jit/importercalls.cpp | Always attempts calli conversion via convertPInvokeCalliToCall; removes old cookie-building calli path. |
| src/coreclr/jit/ICorJitInfo_wrapper_generated.hpp | Removes wrapper for deleted GetCookieForPInvokeCalliSig. |
| src/coreclr/jit/ICorJitInfo_names_generated.h | Removes API name entry for deleted GetCookieForPInvokeCalliSig. |
| src/coreclr/jit/handlekinds.h | Removes handle kind for the deleted pinvoke-calli cookie handle. |
| src/coreclr/jit/gentree.h | Removes GenTreeCall::gtCallCookie storage. |
| src/coreclr/jit/gentree.cpp | Removes cookie argument/register plumbing and updates call cloning/init. |
| src/coreclr/jit/ee_il_dll.cpp | Removes now-unused eeConvertToLookup helper (used by cookie path). |
| src/coreclr/jit/compiler.h | Removes eeConvertToLookup declaration. |
| src/coreclr/inc/jithelpers.h | Removes CORINFO_HELP_PINVOKE_CALLI helper mapping. |
| src/coreclr/inc/jiteeversionguid.h | Bumps JIT-EE interface version GUID (interface change). |
| src/coreclr/inc/icorjitinfoimpl_generated.h | Removes override for deleted GetCookieForPInvokeCalliSig. |
| src/coreclr/inc/corinfo.h | Removes CORINFO_HELP_PINVOKE_CALLI and deletes GetCookieForPInvokeCalliSig; clarifies convertPInvokeCalliToCall contract. |
| docs/design/coreclr/botr/guide-for-porting.md | Updates BOTR documentation to remove mention of the deleted CALLI helper. |
| docs/design/coreclr/botr/clr-abi.md | Removes documentation for the deleted CALLI pinvoke cookie/target register convention. |
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
9fd25b3 to
2d846b4CompareThere was a problem hiding this comment.
Pull request overview
Copilot reviewed 71 out of 71 changed files in this pull request and generated no new comments.
Suppressed comments (2)
src/coreclr/vm/stubhelpers.cpp:515
- The local
targetinStubHelpers::GetDelegateTargetis now unused on 64-bit builds, which is likely to trigger an unreferenced-local warning (often treated as error in CoreCLR builds). It looks like the old target-mangling logic was removed but the temporary wasn’t cleaned up.
src/coreclr/vm/stubmgr.cpp:1733 *pValue = ((TADDR*)®s)[index];type-puns anArgumentRegistersstruct as aTADDR[], which is undefined behavior under C++ strict-aliasing rules (and is a new pattern in this file). This can be made aliasing-safe by copying bytes instead of reinterpreting the pointer.
|
Azure Pipelines: Successfully started running 8 pipeline(s). 8 pipeline(s) were filtered out due to trigger conditions. There may be pipelines that require an authorized user to comment /azp run to run. |
MichalPetryka
commented
Aug 1, 2026
x86 failure seems related? |
Nothing calls GetCookieForPInvokeCalliSig or CORINFO_HELP_PINVOKE_CALLI now that an unmanaged calli is either expanded inline or converted to a call to a marshalling stub, so drop both and bump the JIT-EE version GUID. The generated files were regenerated with ThunkGenerator/gen.bat. CORINFO_HELP_PINVOKE_CALLI is not a ReadyToRun helper, so removing it does not version the ReadyToRun format - R2R images encode READYTORUN_HELPER_* ids, which are unaffected. Also removes the SuperPMI recording for the cookie. Its two historical packet ids are left in place, matching Packet_CanGetCookieForPInvokeCalliSig which has been unused for some time. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
- Remove unused #if HOST_64BIT 'target' variable in StubHelpers::GetDelegateTarget that was left over after removing the old GenericPInvokeCalliHelper mangling. - Fix BuildCalliILStubSignature to only prepend an extra leading native int for implicit HASTHIS (HASTHIS && !EXPLICITTHIS). For EXPLICITTHIS signatures the explicit this parameter is already counted in numArgs, so adding a second native int would double-count it and produce an incorrect stub signature. Co-authored-by: jkoritzinsky <1571408+jkoritzinsky@users.noreply.github.com>
TryGetUnmanagedCallingConventionFromModOpt reports its failures as an HRESULT - COR_E_INVALIDPROGRAM for conflicting calling convention modopts, and COR_E_BADIMAGEFORMAT for a malformed one - and the original code threw them with COMPlusThrowHR, which derives the exception kind from that HRESULT. When the throw became a deferred error carrying a RuntimeExceptionKind, the HRESULT was dropped and the kind hardcoded to TypeLoadException, so a calli with two calling convention modopts reported TypeLoadException instead of InvalidProgramException. Derive the kind from the HRESULT with EEException::GetKindFromHR, which is what COMPlusThrowHR does internally, so the deferred throw is identical to the original one. Fixes baseservices/callconvs/TestCallingConventions. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
LowerSpecialCopyArgs implements the x86 IJW copy-constructor semantics by mapping each argument of the unmanaged call onto the IL stub argument at the same index, and asserted that the two counts are equal. An unmanaged CALLI stub takes the call target as an extra trailing argument that is not passed on to the unmanaged call, so its argument count is one higher and the assert fired for a calli with IsCopyConstructed modreqs. The index mapping itself is unaffected - the extra argument is last, and the loop only walks indices below the unmanaged call's argument count - so only the assert needs to allow the stub to have more arguments than the call. Fixes Interop/PInvoke/Miscellaneous/CopyCtor on windows-x86. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Have convertPInvokeCalliToCall fill both hClass and hMethod in CoreCLR and NativeAOT. Record both handles in SuperPMI now that the JIT consumes the complete resolved token directly. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
… dead code. Also move calli stubs back to using a stack-local signature (as we always copy in this case).
bebabda to
9a716b5Comparejkoritzinsky
commented
Aug 24, 2026
Fixed the bad codegen by taking a fix from #132534 and applying it here. |
…orms in DAC stack walk (#131961) Fixes Issue #131606 main PR N/A (targeted servicing fix; the full fix for main is in #131642 / #131654, which are too invasive for a servicing branch) # Description On 64-bit platforms, an unmanaged `calli` target can be encoded into `InlinedCallFrame::m_Datum` as `(target << 1) | 1`. This left-shift moves bit 0 of the target address into bit 1 of `m_Datum`. Two sites in `src/coreclr/debug/daccess/dacdbiimplstackwalk.cpp` (`GetCountOfInternalFrames` and `EnumerateInternalFrames`) test bit 1 of `m_Datum` against `InlinedCallFrameMarker::ExceptionHandlingHelper` without first checking bit 0 to confirm the field actually holds a `MethodDesc*` (with the marker bit set) rather than a shifted-and-tagged raw `calli` target. As a result, when the linker happens to place the native `calli` target at an odd address, the shifted-in low bit is misread as the EH-helper marker, and the frame is incorrectly skipped during stack walking. This PR adds a `TARGET_64BIT`-guarded check for bit 0 before interpreting bit 1 as the `InlinedCallFrameMarker::ExceptionHandlingHelper` marker at both affected sites, matching the precedent already established in `InlinedCallFrame::GetFunction_Impl` (`src/coreclr/vm/frames.h`), which masks out the marker bits before treating `m_Datum` as a pointer. # Customer Impact Visual Studio's mixed-mode debugger fails to show native C++ call stack frames when a C++/CLI layer calls native code through an unmanaged `calli` whose target address happens to be odd (a common occurrence with delay-loaded imports and incrementally-linked thunks). This makes debugging native code called from managed C++/CLI unreliable and appears intermittent/random to customers, since it depends on a single bit of a linker-chosen address. # Regression Yes. This is a regression from .NET 8, introduced by the exception handling rewrite. # Testing Verified the corrected bit-check logic in isolation (bit 0 check first, then bit 1), confirming that on 64-bit an odd-tagged `calli` target (bit0=1, bit1=1) is no longer misidentified as the EH-helper marker, while behavior for actual EH-helper-marked frames (bit0=0, bit1=1) and for 32-bit platforms (unaffected, no shift-tagging) is unchanged. # Risk Low. The change is narrowly scoped to two conditional checks in the DAC stack-walk code, guarded by `TARGET_64BIT`, and does not alter behavior for 32-bit platforms or for frames that are genuinely marked as EH helpers. Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: jkotas <6668460+jkotas@users.noreply.github.com>
Uh oh!
There was an error while loading. Please reload this page.
…t-il-com-stubs Conflict resolved in stubmgr.cpp: the new CLRToCOMStubManager block abuts the InteropDispatchStubManager comment, which main updated when GenericPInvokeCalliHelper was removed (#131654). Kept the new block and took main's comment text. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
# Build vararg support only where it is reachable Stacked on #131654 — targets `dev/jkoritzinsky/calli-pinvoke`, not `main`. ## What and why The vararg calling convention is only supported on Windows: `ConvToJitSig` rejects both `IMAGE_CEE_CS_CALLCONV_VARARG` and `IMAGE_CEE_CS_CALLCONV_NATIVEVARARG` everywhere else. The supporting infrastructure was nonetheless compiled on every platform — assembly stubs, cookie allocation, GC reporting, marshalling helpers and their managed surface. This introduces `FEATURE_VARARGS` and builds that machinery only where it can be reached. **No behavior change on Windows.** `ConvToJitSig` now keys off the same feature instead of spelling out `TARGET_UNIX || TARGET_ARM`, so there is a single definition of "varargs are supported" rather than two that have to be kept in agreement. That is not a behavior change: `browser` and `wasi` both set `CLR_CMAKE_TARGET_UNIX` (`configureplatform.cmake` lines 434 and 439), so every target that threw before still throws. ## The feature is declared twice, deliberately ```cmake # clrdefinitions.cmake — native if (CLR_CMAKE_TARGET_WIN32) ``` ```xml <!-- clr.featuredefines.props — managed, alongside FeatureIjw / FeatureCominterop --> <PropertyGroup Condition="'$(TargetsWindows)' == 'true'"> ``` Both are required, and the reason is worth writing down because the failure mode is silent. `vm/corelib.h` is not only a C++ header. It is also parsed by the `CreateRuntimeRootILLinkDescriptorFile` MSBuild task (`System.Private.CoreLib/CreateRuntimeRootILLinkDescriptorFile.targets`) to generate the **ILLink root descriptor** for System.Private.CoreLib — and that task receives the **managed `$(DefineConstants)`**, not the native CMake defines. So a native-only `#ifdef` around a `DEFINE_METHOD` there still compiles, but the ILLink root quietly disappears, the trimmer removes a method the runtime resolves by name, and you get: ``` Assert failure: pMD != NULL && ("EE expects method to exist") CoreLibBinder::LookupMethodLocal ILCodeStream::EmitCALL ILArgIteratorMarshaler::EmitConvertSpaceAndContentsCLRToNative ``` which points at the marshaller and gives no hint that a *build-system* conditional is the cause. The existing `#ifdef FEATURE_COMINTEROP` in that header is not a counter-example: `FEATURE_COMINTEROP` is emitted on **both** sides by `clr.featuredefines.props`, so its root is dropped exactly when the managed code is also absent. Declaring `FeatureVarargs` in the same place gives `FEATURE_VARARGS` that same symmetry, which is what makes gating `corelib.h` safe here. ## Deleted outright Unix `VarargPInvokeStub` implementations in `amd64`, `arm`, `arm64`, `loongarch64` and `riscv64` `pinvokestubs.S` and in `i386/asmhelpers.S`; the wasm `PORTABILITY_ASSERT` placeholders in `vm/wasm/helpers.cpp`; and the `VASigCookie` offsets in the `arm`, `loongarch64` and `riscv64` `asmconstants.h`. None of those files is deletable whole — each also holds `JIT_PInvokeBegin`/`End`/`InitPInvokeFrame`. ## Gated — native The cookie machinery (`struct VASigCookie` and its cDAC data descriptor, `VASigCookieBlock`, `Module::GetVASigCookie`/`GetVASigCookieWorker`, `VASigCookieEx`), `VarargPInvokeStubWorker`, `GetILStubForVarargPInvoke`, `TheVarargPInvokeStub`, `InteropDispatchStubManager` (plus its `vptr_list.h` entry and `Init()`), `clrvarargs.cpp` and `varargsnative.cpp`, the `ArgIterator` → `va_list` marshaller and the `StubHelpers` entrypoints behind it, `TransitionFrame::GetVASigCookie`, and the vararg GC reporting in `eetwain.cpp` and `gc_unwind_x86.inl`. `Module::GetLoaderModuleForSignature` is **not** gated — the unmanaged-`calli` stub path calls it on every platform. The Windows `.asm` files are guarded rather than excluded. Strictly unnecessary, since every Windows arch enables the feature — but it buys a compile-time proxy: forcing `FEATURE_VARARGS` off on windows-x64 compiles the whole VM **and DAC** as a stand-in for the six targets that can't be built locally. That proxy earned its keep immediately by catching `promoteVarArgs` in `eetwain.cpp`, which takes `PTR_VASigCookie` and is compiled on every target — a real cross-platform break that inspection alone had missed. ## Gated — managed NativeAOT's throwing `ArgIterator` moves to the shared partition as `System/ArgIterator.PlatformNotSupported.cs` and is now used by CoreCLR too whenever the feature is off, so the throwing copy exists once instead of twice: | Build | `ArgIterator` source | |---|---| | CoreCLR, varargs supported | its own `ArgIterator.cs` (real implementation) | | CoreCLR, varargs unsupported | shared `ArgIterator.PlatformNotSupported.cs` | | NativeAOT | shared `ArgIterator.PlatformNotSupported.cs` | | Mono | its own real implementation, untouched | The `Shared.projitems` condition is `'$(FeatureVarargs)' != 'true' and '$(FeatureMono)' != 'true'`. The Mono clause matters: Mono's CoreLib also imports `Shared.projitems`, has its own real `ArgIterator`, and does not import `clr.featuredefines.props` — without it, Mono would pick up a duplicate `System.ArgIterator`. Because CoreCLR's `ArgIterator.cs` is now included conditionally, its `#if TARGET_WINDOWS` split and the ~50-line duplicated throwing branch both go away; the file has no preprocessor directives left. The three `StubHelpers` `va_list` members that only the marshaller calls are gated the same way. ## Deliberately unchanged - **`GCREFMAP_VASIG_COOKIE` (= 5)** — part of the ReadyToRun GC ref map format. Value kept; only its handling is gated. - **`DynamicMethodDesc::StubPInvokeVarArg` (= 4)** — the cDAC contract depends on the value. - **x86 GC info `varargs` bit / `FLIP_VARARGS`** — serialized format, and x86 *is* a supported vararg target. There is no unreachable x86 GC/unwind vararg code. - **`tools/Common/CallingConvention/ArgIterator.cs`** — crossgen2 cross-targets, so it cannot be gated at compile time. - **`inc/dacdbi.idl` / `debug/inc/dacdbiinterface.h`** — versioned interface shape untouched; only the `GetVarArgSig` implementation is gated. - **`getVarArgsHandle`** keeps its `ICorJitInfo` vtable slot and asserts instead of being removed. `ICorJitInfo` is not per-platform and varargs do work on Windows, so the JIT still needs the method there; this change does not reshape the interface. `GetStubForILStub` now throws `IDS_EE_VARARG_NOT_SUPPORTED` for every target without the feature rather than only under `FEATURE_PORTABLE_ENTRYPOINTS` — a strict generalization, since that feature is wasm-only and already outside `FEATURE_VARARGS`. ## cDAC `struct VASigCookie` and its `CDAC_TYPE` descriptor are gated, so a target without the feature stops advertising a type it can never allocate. The managed cDAC is unchanged and reads the **target's own** descriptor, so older runtimes are unaffected; the lookup is lazy and only reachable from `DacDbiImpl.GetVarArgSig` on a vararg frame. Verified by inspecting the emitted contract descriptor in both configurations. ## Renamed `PINVOKE_CALLI_SIGTOKEN_REGNUM`/`REGISTER` on amd64 → `PINVOKE_VARARG_SIGTOKEN_REGNUM`/`REGISTER`. That register (r11) carries the `VASigCookie*` into `VarargPInvokeStub`; it outlived the unmanaged-`calli` helpers removed in the parent layer, so the old name referred to a caller that no longer exists. ## Note for reviewers touching `asmconstants.h` `h2inc` runs at CMake **configure** time and `asmconstants.h` is not registered as a configure dependency, so editing a constant's name or value does not regenerate `AsmConstants.inc` on an incremental build — the assembler keeps consuming the stale copy. The rename above surfaced this as `error A2006: undefined symbol`. Pre-existing infrastructure behavior, not changed here; a reconfigure picks it up. Worth knowing because a silently stale *value* would be far less obvious than a stale name. ## Validation | Target | Result | |---|---| | windows-x64 `clr+libs -rc Checked` | builds clean | | windows-x86 | builds clean (exercises `i386/asmhelpers.asm` + asmconstants) | | windows-arm64 | builds clean (exercises `arm64/PInvokeStubs.asm` + asmconstants) | | windows-x64, `FEATURE_VARARGS` forced **off** on both sides | builds clean, **including the DAC** | | NativeAOT (`clr.nativeaotlibs`) | builds clean, picks up the shared file | | linux/osx x64, linux arm32/arm64, loongarch64, riscv64, browser-wasm | **not built locally** — covered by inspection plus the forced-off proxy | Tests (windows-x64 Checked): - `JIT/Directed/arglist/vararg_TargetWindows` — 241/241 passed - Interop — 333 total, 321 passed, 10 failed; the 10 are the known pre-existing out-of-proc COM and DisabledRuntimeMarshalling environment failures, unchanged from baseline. `VarargsTest`, `CrossAssemblyVarargsTest` and IJW `NativeVarargsTest` all pass. - JIT/Directed — 686 total, 679 passed, **0 failed**, 7 skipped Feature-off behavior was verified by inspecting build output rather than assuming: the contract descriptor drops `VASigCookie` (while `MethodTable` remains), and CoreLib drops `ArgIterator_Init`/`CalcVaListSize` while gaining `PlatformNotSupported_ArgIterator`. > [!NOTE] > This pull request description was generated by GitHub Copilot. --------- Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 544b2da7-b36a-4a7c-a2df-20e1487849af
Replaces CoreCLR's lazy, call-time marshalling mechanism for unmanaged
calliwith theconvertPInvokeCalliToCallJIT-EE path that NativeAOT already uses.Background
An unmanaged
callithat needed marshalling used to compile into a call toCORINFO_HELP_PINVOKE_CALLI. At run timeGenericPInvokeCalliHelper— hand-written assembly onevery architecture — pushed a
PInvokeCalliFrame, calledGenericPInvokeCalliStubWorker, andgenerated the marshalling IL stub from a
VASigCookieon first call. The unmanaged target wassmuggled through a secret register, shifted
<<1|1on 64-bit.Now the stub
MethodDescis created while the caller is being JIT-compiled, cached by signaturein the IL stub cache exactly as before, and the stub is JIT-compiled on first call. The unmanaged
target is passed as an ordinary trailing
native intparameter.Failures are reported when the stub is called
Because the stub is built while its caller is jitted, a failure detected while building it would
otherwise fail the compilation of a method that may never execute the call site.
Marshalling failures were already reported per parameter through
StubHelpers.ThrowInteropParamException. That is extended to the failures which are not tied to aparameter, so the whole set — a signature that needs marshalling, a non-blittable generic
instantiation, an unsupported calling convention, and an instance
thisthe runtime cannotexpress — becomes a stub body that throws when it is called. Classification records the failure and
returns a usable calling convention so stub creation runs to completion, and
FinishEmitdiscardsthe marshalling it produced in favour of the throw.
Only a signature that is not a standalone method signature at all — one carrying the generic bit or
an unknown calling convention nibble, from which no stub signature can be built — still fails
eagerly with
COR_E_BADIMAGEFORMAT.One case is a genuine behavior change: an unmanaged
calliwhose signature carries an instancethisis not expressible as a static stub and the JIT will not expand it inline. Previously theruntime declined to build a stub, which — once the helper fallback was gone — would have let the
JIT emit a plain managed indirect call to an unmanaged target. It now reports
InvalidProgramException/VLDTR_E_FMD_PINVOKENOTSTATIC, matching a P/Invoke declared on aninstance method. Native member functions are unaffected: they are static signatures whose first
parameter is the
thispointer.Removed
GenericPInvokeCalliHelperon amd64/arm/arm64/i386/loongarch64/riscv64/wasm,GenericPInvokeCalliStubWorker,PInvokeCalliFramewith its stack-walking and data-descriptorsupport, and the PInvoke-calli cookie mechanism in the JIT:
GenTreeCall::gtCallCookie,WellKnownArg::PInvokeCookie/PInvokeTargetwith their argument registers on all sevenarchitectures,
GTF_ICON_PINVKI_HDL, thefgMorphArgsrewrite into the helper call,LowerIndirectNonvirtCall, andeeConvertToLookup.Nothing then referenced
GetCookieForPInvokeCalliSigorCORINFO_HELP_PINVOKE_CALLI, so both aredropped from the JIT-EE interface and the JIT-EE version GUID is bumped. The generated files
were regenerated with
ThunkGenerator/gen.bat.CORINFO_HELP_PINVOKE_CALLIis not a ReadyToRunhelper, so removing it does not version the ReadyToRun format — R2R images encode
READYTORUN_HELPER_*ids, which are unaffected.ReadyToRun
ReadyToRun never fell back to the runtime helper:
GetCookieForPInvokeCalliSigthrewRequiresRuntimeJitException, so the cookie call was only ever an abort that left the wholemethod to the runtime JIT. crossgen2 now implements
convertPInvokeCalliToCalland throws forexactly the call sites the JIT cannot expand inline —
mustConvert, a fastcall convention, anunmanaged signature carrying an instance
this, or a signature that needs marshalling.Verified equivalent: an assembly exercising blittable stdcall / platform-default /
SuppressGCTransition/ managed / marshalling / fastcallcalliwas crossgen2'd before and after.The set of precompiled methods and their exact code sizes are byte-identical, and
MarshallingCalli/FastcallCalliare left to the runtime JIT in both.Debugger
ILStubManager::TraceManagercan no longer read the target from the removed hidden argument. Itlocates the trailing target parameter with
MetaSig/ArgIteratorand reads it from the contextcaptured at the stub's entry point — from the argument register the calling convention assigned, or
from the incoming stack arguments. The register mapping is implemented for x86/amd64/arm/arm64 and
returns
FALSE(no trace) elsewhere, matching the existingStubManagerHelpers::GetFirstArgprecedent.
Two latent fixes worth reviewing independently
In
CallConv::TryGetUnmanagedCallingConventionFromModOptSigStartingAtRetType, both pre-existing andboth required for generic
callistubs whose signatures are converted to module-independent form:ELEMENT_TYPE_CMOD_INTERNALmodifiers, inverted relative to the tokenpath.
ConvertToInternalExactlyOneencodesCMOD_OPTasrequired = 0andCallConv*modoptsare always optional, so the branch was dead.
tokenLookupModulebut passedpModuletoGetNameOfTypeRefOrDef, producing aspurious
InvalidProgramExceptionfordelegate* unmanaged<U,T>.These change behavior for any module-independent signature carrying calling-convention modopts.
Validation
windows-x64 Checked:
DisabledRuntimeMarshalling environment failures, unchanged from baseline.
JIT/Directed/pinvokeandJIT/Directed/callconv.cmdtests pass.calli, nested layout classes, fastcall,collectible ALC unload (5/5 assemblies collected), and a hand-written IL matrix of
calli instance unmanagedvariants.ArgIteratortarget recovery was checked against a synthetic entry-point context for targetslanding in RDX, R8, R9 and the first three stack slots.
Not built or executed locally: every non-windows-x64 target. The assembly deletions, the
non-x64 argument-register mappings in the stub manager, and the non-x64 target-header edits are
covered by inspection only.
Fixes#127473
Fixes#131606 in main (though this fix is probably not one we want to backport)
Note
This pull request description was generated by GitHub Copilot.