From f41a172797635064a21d1725dde40abba03d8ad1 Mon Sep 17 00:00:00 2001 From: David Wrighton Date: Wed, 8 Jul 2026 15:55:54 -0700 Subject: [PATCH] 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");