From f41a172797635064a21d1725dde40abba03d8ad1 Mon Sep 17 00:00:00 2001 From: David Wrighton Date: Wed, 8 Jul 2026 15:55:54 -0700 Subject: [PATCH 1/3] Add support for inline pinvokes to Wasm Ryujit - They should always be READYTORUN_FIXUP_PInvokeTarget instead of READYTORUN_FIXUP_IndirectPInvokeTarget - There is a debug version of JIT_PinvokeEnd which validates that sp == the __stack_pointer global. I believe our codegen will maintain this invariant, so we should be ok to skip resetting the __stack_pointer global before calls to the C++ implementation of JIT_PInvokeEnd - One detail which is no longer true after this work is merged is that the __stack_pointer global will be set to increasingly unpredictable values during execution. This is not a new phenomena, as it was happening during EH flow, but now it will happen in normal execution flow. Since the interpreter to R2R thunks will reset the stack before we return to any emscripten compiled code, we should be ok with this. --- .../JitInterface/CorInfoImpl.ReadyToRun.cs | 6 +- src/coreclr/vm/jitinterface.cpp | 2 + src/coreclr/vm/wasm/helpers.cpp | 79 +++++++++++++++++-- 3 files changed, 78 insertions(+), 9 deletions(-) diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/JitInterface/CorInfoImpl.ReadyToRun.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/JitInterface/CorInfoImpl.ReadyToRun.cs index 66de25c3df3523..549076b405d440 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/JitInterface/CorInfoImpl.ReadyToRun.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/JitInterface/CorInfoImpl.ReadyToRun.cs @@ -3220,8 +3220,12 @@ private void getAddressOfPInvokeTarget(CORINFO_METHOD_STRUCT_* method, ref CORIN ModuleToken moduleToken = new ModuleToken(ecmaMethod.Module, ecmaMethod.Handle); MethodWithToken methodWithToken = new MethodWithToken(ecmaMethod, moduleToken, constrainedType: null, unboxing: false, genericContextObject: null); - if ((ecmaMethod.GetPInvokeMethodCallingConventions() & UnmanagedCallingConventions.IsSuppressGcTransition) != 0) + if (((ecmaMethod.GetPInvokeMethodCallingConventions() & UnmanagedCallingConventions.IsSuppressGcTransition) != 0) + || _compilation.NodeFactory.Target.IsWasm) { + // Suppress GC transition pinvokes are called directly, since we can't do a gc transition at this point. + // On Wasm, we also call directly, as the runtime doesn't generate stubs for the pinvoke calls which can produce errors. Instead + // the error is produced as we fixup the method in the first place. pLookup.addr = (void*)ObjectToHandle(_compilation.SymbolNodeFactory.GetPInvokeTargetNode(methodWithToken)); pLookup.accessType = InfoAccessType.IAT_PVALUE; } diff --git a/src/coreclr/vm/jitinterface.cpp b/src/coreclr/vm/jitinterface.cpp index 75197f0ddd4087..9e924b470cd408 100644 --- a/src/coreclr/vm/jitinterface.cpp +++ b/src/coreclr/vm/jitinterface.cpp @@ -14313,6 +14313,7 @@ BOOL LoadDynamicInfoEntry(Module *currentModule, } break; +#ifdef HAS_PINVOKE_IMPORT_PRECODE case READYTORUN_FIXUP_IndirectPInvokeTarget: { MethodDesc *pMethod = ZapSig::DecodeMethod(currentModule, pInfoModule, pBlob); @@ -14322,6 +14323,7 @@ BOOL LoadDynamicInfoEntry(Module *currentModule, result = (size_t)(LPVOID)&(pMD->m_pPInvokeTarget); } break; +#endif // HAS_PINVOKE_IMPORT_PRECODE case READYTORUN_FIXUP_PInvokeTarget: { diff --git a/src/coreclr/vm/wasm/helpers.cpp b/src/coreclr/vm/wasm/helpers.cpp index d844c7796aa92b..a53e6ef01f5a31 100644 --- a/src/coreclr/vm/wasm/helpers.cpp +++ b/src/coreclr/vm/wasm/helpers.cpp @@ -13,6 +13,7 @@ #define WASM_STRINGIFY_HELPER(value) #value #define WASM_STRINGIFY(value) WASM_STRINGIFY_HELPER(value) +#define INLINED_PINVOKE_FROM_R2R 0 void ExecuteInterpretedMethodWithArgs_PortableEntryPoint(PCODE portableEntrypoint, TransitionBlock* block, size_t argsSize, int8_t* retBuff); @@ -491,12 +492,21 @@ void InlinedCallFrame::UpdateRegDisplay_Impl(const PREGDISPLAY pRD, bool updateF return; } - pRD->pCurrentContext->InterpreterIP = *(DWORD *)&m_pCallerReturnAddress; - pRD->IsCallerContextValid = FALSE; - pRD->pCurrentContext->InterpreterSP = *(DWORD *)&m_pCallSiteSP; - pRD->pCurrentContext->InterpreterFP = *(DWORD *)&m_pCalleeSavedFP; + if (m_CallerReturnAddress == INLINED_PINVOKE_FROM_R2R) + { + pRD->pCurrentContext->InterpreterSP = m_pCallSiteSP; + pRD->pCurrentContext->InterpreterIP = GetWasmVirtualIPFromStackPointer(m_pCallSiteSP); + _ASSERTE(pRD->pCurrentContext->InterpreterIP != 0); // We should be in RyuJit compiled code here + pRD->pCurrentContext->InterpreterFP = GetWasmFramePointerFromStackPointer(m_pCallSiteSP, pRD->pCurrentContext->InterpreterIP); + } + else + { + pRD->pCurrentContext->InterpreterIP = *(DWORD *)&m_pCallerReturnAddress; + pRD->pCurrentContext->InterpreterSP = *(DWORD *)&m_pCallSiteSP; + pRD->pCurrentContext->InterpreterFP = *(DWORD *)&m_pCalleeSavedFP; + } #define CALLEE_SAVED_REGISTER(regname) pRD->pCurrentContextPointers->regname = NULL; ENUM_CALLEE_SAVED_REGISTERS(); @@ -687,16 +697,69 @@ extern "C" void STDCALL GenericPInvokeCalliHelper(void) PORTABILITY_ASSERT("GenericPInvokeCalliHelper is not implemented on wasm"); } -EXTERN_C void JIT_PInvokeBegin(InlinedCallFrame* pFrame) +// Does the pinvoke frame transition; the naked wrappers below have already set the wasm +// __stack_pointer global to sp so it is safe to run native code here. +EXTERN_C void JIT_PInvokeBeginImpl(void* sp, InlinedCallFrame* pFrame) { - PORTABILITY_ASSERT("JIT_PInvokeBegin is not implemented on wasm"); + Thread* pThread = GetThread(); + + // Initialize the JIT-provided frame storage, deriving its state from sp/pep since wasm + // has no machine registers to read the caller SP / return address from. + ::new ((void*)pFrame) InlinedCallFrame(); + pFrame->m_pCallSiteSP = sp; + pFrame->m_pCallerReturnAddress = INLINED_PINVOKE_FROM_R2R; // When this is tru the UpdateRegisters function will do all work based on m_pCallerReturnAddress + pFrame->m_pCalleeSavedFP = 0; + pFrame->m_pThread = pThread; + + // Link the frame and transition to preemptive GC mode for the native call. + pFrame->Push(); + pThread->EnablePreemptiveGC(); } -EXTERN_C void JIT_PInvokeEnd(InlinedCallFrame* pFrame) +// R2R keeps its shadow SP in a local and leaves the __stack_pointer global stale, so publish +// the incoming sp to __stack_pointer before any native code runs and leave it there so the +// subsequent native pinvoke target is also safe. +extern "C" __attribute__((naked)) void JIT_PInvokeBegin(void* sp, InlinedCallFrame* pFrame, PCODE pep) { - PORTABILITY_ASSERT("JIT_PInvokeEnd is not implemented on wasm"); + asm("local.get 0\n" /* sp */ + "global.set __stack_pointer\n" /* __stack_pointer = sp before any native code runs */ + "local.get 0\n" /* sp */ + "local.get 1\n" /* pFrame */ + "call %0\n" + "return" ::"i"(JIT_PInvokeBeginImpl)); } +#ifdef DEBUG +// Debug variant of these apis tests that sp and __stack_pointer are in sync +EXTERN_C void* JIT_PInvokeEndImpl(TADDR sp, TADDR stack_pointer_global_value, InlinedCallFrame* pFrame) +{ + _ASSERTE(sp == stack_pointer_global_value); + Thread* pThread = (Thread*)pFrame->m_pThread; + + // Transition back to cooperative GC mode and unlink the frame. + pThread->DisablePreemptiveGC(); + pFrame->Pop(); +} + +extern "C" __attribute__((naked)) void JIT_PInvokeEnd(void* sp, InlinedCallFrame* pFrame, PCODE pep) +{ + asm("local.get 0\n" /* sp */ + "global.set __stack_pointer\n" /* __stack_pointer = sp before any native code runs */ + "local.get 1\n" /* pFrame */ + "call %0\n" + "return" ::"i"(JIT_PInvokeEndImpl)); +} +#else +extern "C" void JIT_PInvokeEnd(void* sp, InlinedCallFrame* pFrame, PCODE pep) +{ + Thread* pThread = (Thread*)pFrame->m_pThread; + + // Transition back to cooperative GC mode and unlink the frame. + pThread->DisablePreemptiveGC(); + pFrame->Pop(); +} +#endif + extern "C" void STDCALL JIT_StackProbe() { PORTABILITY_ASSERT("JIT_StackProbe is not implemented on wasm"); From 84e2cc4439a8b8ea7174e2c618eb490aed739772 Mon Sep 17 00:00:00 2001 From: David Wrighton Date: Mon, 20 Jul 2026 14:39:24 -0700 Subject: [PATCH 2/3] Fixes --- .../JitInterface/CorInfoImpl.ReadyToRun.cs | 6 +-- src/coreclr/vm/jithelpers.cpp | 2 +- src/coreclr/vm/wasm/helpers.cpp | 46 +++++++++++++------ 3 files changed, 37 insertions(+), 17 deletions(-) diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/JitInterface/CorInfoImpl.ReadyToRun.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/JitInterface/CorInfoImpl.ReadyToRun.cs index cd0b20cc29fd71..1c33cd073fd26d 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/JitInterface/CorInfoImpl.ReadyToRun.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/JitInterface/CorInfoImpl.ReadyToRun.cs @@ -3223,9 +3223,9 @@ private void getAddressOfPInvokeTarget(CORINFO_METHOD_STRUCT_* method, ref CORIN if (((ecmaMethod.GetPInvokeMethodCallingConventions() & UnmanagedCallingConventions.IsSuppressGcTransition) != 0) || _compilation.NodeFactory.Target.IsWasm) { - // Suppress GC transition pinvokes are called directly, since we can't do a gc transition at this point. - // On Wasm, we also call directly, as the runtime doesn't generate stubs for the pinvoke calls which can produce errors. Instead - // the error is produced as we fixup the method in the first place. + // Suppress GC transition P/Invokes are called directly, since we can't do a GC transition at this point. + // On Wasm, we also call directly because the runtime doesn't generate P/Invoke import precodes/stubs; instead, + // errors are reported when we fix up the method. pLookup.addr = (void*)ObjectToHandle(_compilation.SymbolNodeFactory.GetPInvokeTargetNode(methodWithToken)); pLookup.accessType = InfoAccessType.IAT_PVALUE; } diff --git a/src/coreclr/vm/jithelpers.cpp b/src/coreclr/vm/jithelpers.cpp index 42b5bc31a938f9..efd486ca917a46 100644 --- a/src/coreclr/vm/jithelpers.cpp +++ b/src/coreclr/vm/jithelpers.cpp @@ -1114,7 +1114,7 @@ HRESULT EEToProfInterfaceImpl::SetEnterLeaveFunctionHooksForJit(FunctionEnter3 * // tailored to the post-pinvoke operations. extern "C" VOID JIT_PInvokeEndRarePath(); -void JIT_PInvokeEndRarePath() +NOINLINE void JIT_PInvokeEndRarePath() { PreserveLastErrorHolder preserveLastError; diff --git a/src/coreclr/vm/wasm/helpers.cpp b/src/coreclr/vm/wasm/helpers.cpp index 3ee96e451edb68..64666de55c976e 100644 --- a/src/coreclr/vm/wasm/helpers.cpp +++ b/src/coreclr/vm/wasm/helpers.cpp @@ -496,10 +496,10 @@ void InlinedCallFrame::UpdateRegDisplay_Impl(const PREGDISPLAY pRD, bool updateF if (m_pCallerReturnAddress == INLINED_PINVOKE_FROM_R2R) { - pRD->pCurrentContext->InterpreterSP = m_pCallSiteSP; - pRD->pCurrentContext->InterpreterIP = GetWasmVirtualIPFromStackPointer(m_pCallSiteSP); + pRD->pCurrentContext->InterpreterSP = (TADDR)m_pCallSiteSP; + pRD->pCurrentContext->InterpreterIP = GetWasmVirtualIPFromStackPointer((TADDR)m_pCallSiteSP); _ASSERTE(pRD->pCurrentContext->InterpreterIP != 0); // We should be in RyuJit compiled code here - pRD->pCurrentContext->InterpreterFP = GetWasmFramePointerFromStackPointer(m_pCallSiteSP, pRD->pCurrentContext->InterpreterIP); + pRD->pCurrentContext->InterpreterFP = GetWasmFramePointerFromStackPointer((TADDR)m_pCallSiteSP, (PCODE)pRD->pCurrentContext->InterpreterIP); } else { @@ -701,7 +701,7 @@ EXTERN_C void JIT_PInvokeBeginImpl(void* sp, InlinedCallFrame* pFrame) // has no machine registers to read the caller SP / return address from. ::new ((void*)pFrame) InlinedCallFrame(); pFrame->m_pCallSiteSP = sp; - pFrame->m_pCallerReturnAddress = INLINED_PINVOKE_FROM_R2R; // When this is tru the UpdateRegisters function will do all work based on m_pCallerReturnAddress + pFrame->m_pCallerReturnAddress = INLINED_PINVOKE_FROM_R2R; // When this is true, UpdateRegDisplay_Impl derives state from m_pCallSiteSP. pFrame->m_pCalleeSavedFP = 0; pFrame->m_pThread = pThread; @@ -723,34 +723,54 @@ extern "C" __attribute__((naked)) void JIT_PInvokeBegin(void* sp, InlinedCallFra "return" ::"i"(JIT_PInvokeBeginImpl)); } +extern "C" VOID JIT_PInvokeEndRarePath(); + #ifdef DEBUG // Debug variant of these apis tests that sp and __stack_pointer are in sync -EXTERN_C void* JIT_PInvokeEndImpl(TADDR sp, TADDR stack_pointer_global_value, InlinedCallFrame* pFrame) +EXTERN_C void JIT_PInvokeEndImpl(TADDR sp, TADDR stack_pointer_global_value, InlinedCallFrame* pFrame) { _ASSERTE(sp == stack_pointer_global_value); Thread* pThread = (Thread*)pFrame->m_pThread; - // Transition back to cooperative GC mode and unlink the frame. - pThread->DisablePreemptiveGC(); - pFrame->Pop(); + pThread->m_fPreemptiveGCDisabled.StoreWithoutBarrier(1); + if (g_TrapReturningThreads) + { + JIT_PInvokeEndRarePath(); + } + else + { + pFrame->Pop(); + } } extern "C" __attribute__((naked)) void JIT_PInvokeEnd(void* sp, InlinedCallFrame* pFrame, PCODE pep) { - asm("local.get 0\n" /* sp */ - "global.set __stack_pointer\n" /* __stack_pointer = sp before any native code runs */ + asm( + "local.get 0\n" /* sp */ + "global.get __stack_pointer\n" /* __stack_pointer */ "local.get 1\n" /* pFrame */ + "local.get 0\n" /* sp */ + "global.set __stack_pointer\n" /* __stack_pointer = sp before any native code runs, set this here, so that if the assumption around sp == __stack_pointer is wrong the assert logic will work correctly. */ "call %0\n" "return" ::"i"(JIT_PInvokeEndImpl)); } #else extern "C" void JIT_PInvokeEnd(void* sp, InlinedCallFrame* pFrame, PCODE pep) { + UNREFERENCED_PARAMETER(sp); + UNREFERENCED_PARAMETER(pep); + Thread* pThread = (Thread*)pFrame->m_pThread; - // Transition back to cooperative GC mode and unlink the frame. - pThread->DisablePreemptiveGC(); - pFrame->Pop(); + pThread->m_fPreemptiveGCDisabled.StoreWithoutBarrier(1); + if (g_TrapReturningThreads) + { + JIT_PInvokeEndRarePath(); + } + else + { + pFrame->Pop(); + } } #endif From bbe903f0dad198b53011efc5d316789fa26ab9fd Mon Sep 17 00:00:00 2001 From: David Wrighton Date: Mon, 20 Jul 2026 14:48:16 -0700 Subject: [PATCH 3/3] Fix comment and indentation formatting from review feedback Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: a4d21b59-7518-4932-99eb-6f229cb57a7b --- .../JitInterface/CorInfoImpl.ReadyToRun.cs | 4 ++-- src/coreclr/vm/wasm/helpers.cpp | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/JitInterface/CorInfoImpl.ReadyToRun.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/JitInterface/CorInfoImpl.ReadyToRun.cs index 0d8a8267b7129c..69af35cc9ad8b1 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/JitInterface/CorInfoImpl.ReadyToRun.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/JitInterface/CorInfoImpl.ReadyToRun.cs @@ -3224,8 +3224,8 @@ private void getAddressOfPInvokeTarget(CORINFO_METHOD_STRUCT_* method, ref CORIN || _compilation.NodeFactory.Target.IsWasm) { // Suppress GC transition P/Invokes are called directly, since we can't do a GC transition at this point. - // On Wasm, we also call directly because the runtime doesn't generate P/Invoke import precodes/stubs; instead, - // errors are reported when we fix up the method. + // On Wasm, we also call directly because the runtime doesn't generate P/Invoke import precodes/stubs; instead, + // errors are reported when we fix up the method. pLookup.addr = (void*)ObjectToHandle(_compilation.SymbolNodeFactory.GetPInvokeTargetNode(methodWithToken)); pLookup.accessType = InfoAccessType.IAT_PVALUE; } diff --git a/src/coreclr/vm/wasm/helpers.cpp b/src/coreclr/vm/wasm/helpers.cpp index 64666de55c976e..3708b02e8d2af3 100644 --- a/src/coreclr/vm/wasm/helpers.cpp +++ b/src/coreclr/vm/wasm/helpers.cpp @@ -757,8 +757,8 @@ extern "C" __attribute__((naked)) void JIT_PInvokeEnd(void* sp, InlinedCallFrame #else extern "C" void JIT_PInvokeEnd(void* sp, InlinedCallFrame* pFrame, PCODE pep) { - UNREFERENCED_PARAMETER(sp); - UNREFERENCED_PARAMETER(pep); + UNREFERENCED_PARAMETER(sp); + UNREFERENCED_PARAMETER(pep); Thread* pThread = (Thread*)pFrame->m_pThread;