diff --git a/src/coreclr/vm/method.cpp b/src/coreclr/vm/method.cpp index e6b95c9086fb09..296ca3aff02df7 100644 --- a/src/coreclr/vm/method.cpp +++ b/src/coreclr/vm/method.cpp @@ -2006,8 +2006,10 @@ PCODE MethodDesc::GetSingleCallableAddrOfCodeForUnmanagedCallersOnly() CONTRACTL { THROWS; - GC_NOTRIGGER; - MODE_ANY; + // On portable entrypoint platforms resolving the entrypoint may need to run the prestub + // (e.g. to publish R2R native code for the method), which can trigger a GC. + GC_TRIGGERS; + MODE_PREEMPTIVE; PRECONDITION(HasUnmanagedCallersOnlyAttribute()); } CONTRACTL_END; diff --git a/src/coreclr/vm/method.hpp b/src/coreclr/vm/method.hpp index 128ee354256306..4f30f8adbf2d4b 100644 --- a/src/coreclr/vm/method.hpp +++ b/src/coreclr/vm/method.hpp @@ -2318,6 +2318,13 @@ class MethodDesc PCODE PrepareInitialCode(CallerGCMode callerGCMode = CallerGCMode::Unknown); PCODE PrepareCode(PrepareCodeConfig* pConfig); +#ifdef FEATURE_PORTABLE_ENTRYPOINTS + // Probe for precompiled R2R native code for an UnmanagedCallersOnly method and, if present, + // publish it into this method's portable entrypoint WITHOUT compiling interpreter byte code. + // Returns true if native code was found and published, false otherwise. + bool TryPublishR2RCodeForUnmanagedCallersOnly(); +#endif // FEATURE_PORTABLE_ENTRYPOINTS + private: PCODE GetPrecompiledCode(PrepareCodeConfig* pConfig, bool shouldTier); PCODE GetPrecompiledR2RCode(PrepareCodeConfig* pConfig); diff --git a/src/coreclr/vm/precode_portable.cpp b/src/coreclr/vm/precode_portable.cpp index 0ffdaa130a8591..dc7879ae388f04 100644 --- a/src/coreclr/vm/precode_portable.cpp +++ b/src/coreclr/vm/precode_portable.cpp @@ -174,7 +174,8 @@ void* GetUnmanagedCallersOnlyThunk(MethodDesc* pMD); bool PortableEntryPoint::EnsureCodeForUnmanagedCallersOnly() { - LIMITED_METHOD_CONTRACT; + STANDARD_VM_CONTRACT; + _ASSERTE(IsValid()); if (HasFlags(_flags, kUnmanagedCallersOnly_Checked | kUnmanagedCallersOnly_Has)) diff --git a/src/coreclr/vm/prestub.cpp b/src/coreclr/vm/prestub.cpp index 71a23d73c3c6bc..5cbd48e096b05b 100644 --- a/src/coreclr/vm/prestub.cpp +++ b/src/coreclr/vm/prestub.cpp @@ -502,6 +502,28 @@ PCODE MethodDesc::GetPrecompiledR2RCode(PrepareCodeConfig* pConfig) return pCode; } +#ifdef FEATURE_PORTABLE_ENTRYPOINTS +bool MethodDesc::TryPublishR2RCodeForUnmanagedCallersOnly() +{ + STANDARD_VM_CONTRACT; + _ASSERTE(HasUnmanagedCallersOnlyAttribute()); + +#ifdef FEATURE_READYTORUN + PrepareCodeConfig config(NativeCodeVersion(this), TRUE, TRUE); + config.SetCallerGCMode(CallerGCMode::Preemptive); + + // GetPrecompiledR2RCode resolves the R2R entrypoint and, on portable-entrypoint (wasm) targets, + // publishes it into this method's portable entrypoint as a side effect (PortableEntryPoint::SetActualCode). + // It never compiles interpreter byte code, so purely interpreted methods simply return NULL here and + // are left unprepared for lazy byte code generation on first call. + PCODE pCode = GetPrecompiledR2RCode(&config); + return pCode != (PCODE)NULL; +#else // !FEATURE_READYTORUN + return false; +#endif // FEATURE_READYTORUN +} +#endif // FEATURE_PORTABLE_ENTRYPOINTS + PCODE MethodDesc::GetMulticoreJitCode(PrepareCodeConfig* pConfig, bool* pWasTier0) { STANDARD_VM_CONTRACT; @@ -2093,6 +2115,12 @@ extern "C" void* STDCALL ExecuteInterpretedMethod(TransitionBlock* pTransitionBl void ExecuteInterpretedMethodWithArgs(TADDR targetIp, int8_t* args, size_t argSize, void* retBuff, PCODE callerIp) { + // targetIp must point to valid interpreter byte code. A NULL here means a caller failed to route a + // method that has native (R2R) code but no interpreter code to InvokeManagedMethod. Dispatching a + // NULL byte code pointer would be (mis)interpreted as INTOP_INVALID and fail with a cryptic fatal + // error, so assert here at the actual point of misuse. + _ASSERTE(targetIp != (TADDR)NULL); + // Copy arguments to the stack if (argSize > 0) { @@ -2237,10 +2265,14 @@ extern "C" void ExecuteInterpretedMethodFromUnmanaged(MethodDesc* pMD, int8_t* a InterpByteCodeStart* targetIp = pMD->GetInterpreterCode(); if (targetIp == NULL) { - GCX_PREEMP(); - (void)pMD->DoPrestub(NULL /* MethodTable */, CallerGCMode::Coop); + (void)pMD->DoPrestub(NULL /* MethodTable */, CallerGCMode::Preemptive); targetIp = pMD->GetInterpreterCode(); } + // The g_ReverseThunks reverse thunk is only used for UnmanagedCallersOnly methods that are executed + // by the interpreter. Methods compiled to native (R2R) code are dispatched directly to their R2R + // entrypoint by GetUnmanagedCallersOnlyThunk and never reach this path, so the interpreter byte code + // must exist here. + _ASSERTE(targetIp != NULL); (void)ExecuteInterpretedMethodWithArgs((TADDR)targetIp, args, argSize, ret, callerIp); } #endif // FEATURE_INTERPRETER diff --git a/src/coreclr/vm/wasm/calldescrworkerwasm.cpp b/src/coreclr/vm/wasm/calldescrworkerwasm.cpp index ad7581265efb1a..6a290cfe6b60df 100644 --- a/src/coreclr/vm/wasm/calldescrworkerwasm.cpp +++ b/src/coreclr/vm/wasm/calldescrworkerwasm.cpp @@ -38,5 +38,17 @@ extern "C" void STDCALL CallDescrWorkerInternal(CallDescrData* pCallDescrData) retBuff = &pCallDescrData->returnValue; } + if (targetIp == NULL) + { + // The target method has no interpreter code because it was compiled to native (R2R) code. + // Invoke it as a compiled managed method through the interpreter->R2R thunk, mirroring the + // fallback already present in ExecuteInterpretedMethodWithArgs_PortableEntryPoint_Complex and + // the CALL_INTERP_METHOD path in InterpExecMethod. Without this, the NULL bytecode pointer + // would be handed to the interpreter and dispatched as INTOP_INVALID. + ManagedMethodParam param = { pMethod, args, (int8_t*)retBuff, (PCODE)NULL, nullptr }; + InvokeManagedMethod(¶m); + return; + } + ExecuteInterpretedMethodWithArgs((TADDR)targetIp, args, argsSize, retBuff, (PCODE)&CallDescrWorkerInternal); } diff --git a/src/coreclr/vm/wasm/helpers.cpp b/src/coreclr/vm/wasm/helpers.cpp index c787640c359313..2e108e3bfa02ae 100644 --- a/src/coreclr/vm/wasm/helpers.cpp +++ b/src/coreclr/vm/wasm/helpers.cpp @@ -1427,6 +1427,30 @@ void* GetUnmanagedCallersOnlyThunk(MethodDesc* pMD) _ASSERTE(pMD != NULL); _ASSERTE(pMD->HasUnmanagedCallersOnlyAttribute()); + // Prefer R2R-compiled native code for UnmanagedCallersOnly methods since it is emitted + // with the native (unmanaged) calling convention and its R2R code is itself the directly-callable + // unmanaged entrypoint. Resolve the method's code first and, if it has native (R2R) code, return that + // entrypoint directly. The g_ReverseThunks interpreter fallback below is only required for methods + // that are executed by the interpreter (no native code), and is intentionally unused for R2R methods. + PCODE entryPoint = pMD->GetPortableEntryPoint(); + if (!PortableEntryPoint::HasNativeEntryPoint(entryPoint) && pMD->GetInterpreterCode() == NULL) + { + // The method has not been prepared yet. Probe for precompiled R2R native code and, if present, + // publish it into the portable entrypoint WITHOUT compiling interpreter byte code. Purely + // interpreted methods are intentionally left unprepared here so that their byte code is generated + // lazily on first call through the reverse thunk below (better for startup). For R2R methods we + // run the prestub to perform the canonical full preparation; it finds the R2R code first and never + // falls back to compiling byte code. + if (pMD->TryPublishR2RCodeForUnmanagedCallersOnly()) + { + (void)pMD->DoPrestub(NULL /* MethodTable */, CallerGCMode::Preemptive); + } + } + if (PortableEntryPoint::HasNativeEntryPoint(entryPoint)) + { + return PortableEntryPoint::GetActualCode(entryPoint); + } + const ReverseThunkMapValue* value = LookupThunk(pMD); if (value == NULL) {