From cac5a192c13fbca3159a37e3898b9583be2f32ba Mon Sep 17 00:00:00 2001 From: Jakob Botsch Nielsen Date: Thu, 16 Jul 2026 15:02:15 +0200 Subject: [PATCH 1/6] JIT: Removing boxing from value type instance calls Readds the optimization that we removed in 3206a8efa315b1244ee6b1620336932d26869caa, this time applying the optimization only when permitted via detecting `RefSafetyRulesAttribute` and `UnscopedRef`. --- src/coreclr/inc/corinfo.h | 12 +++ src/coreclr/inc/icorjitinfoimpl_generated.h | 3 + src/coreclr/inc/jiteeversionguid.h | 10 +- src/coreclr/jit/ICorJitInfo_names_generated.h | 1 + .../jit/ICorJitInfo_wrapper_generated.hpp | 9 ++ src/coreclr/jit/compiler.h | 1 + src/coreclr/jit/gentree.cpp | 64 ++++++++++++- src/coreclr/jit/importercalls.cpp | 94 ++++++++++++++++--- src/coreclr/jit/jitmetadatalist.h | 1 + .../tools/Common/JitInterface/CorInfoImpl.cs | 38 ++++++++ .../JitInterface/CorInfoImpl_generated.cs | 17 ++++ .../ThunkGenerator/ThunkInput.txt | 1 + .../ReadyToRun/AttributePresenceFilterNode.cs | 9 ++ .../aot/jitinterface/jitinterface_generated.h | 10 ++ .../tools/superpmi/superpmi-shared/lwmlist.h | 1 + .../superpmi-shared/methodcontext.cpp | 22 +++++ .../superpmi/superpmi-shared/methodcontext.h | 5 + .../superpmi-shim-collector/icorjitinfo.cpp | 8 ++ .../icorjitinfo_generated.cpp | 7 ++ .../icorjitinfo_generated.cpp | 6 ++ .../tools/superpmi/superpmi/icorjitinfo.cpp | 6 ++ src/coreclr/vm/jitinterface.cpp | 71 ++++++++++++++ src/coreclr/vm/wellknownattributes.h | 9 ++ 23 files changed, 386 insertions(+), 19 deletions(-) diff --git a/src/coreclr/inc/corinfo.h b/src/coreclr/inc/corinfo.h index 65288617affdce..032e92065be4c3 100644 --- a/src/coreclr/inc/corinfo.h +++ b/src/coreclr/inc/corinfo.h @@ -2114,6 +2114,18 @@ class ICorStaticInfo // Quick check whether the method is a jit intrinsic. Returns the same value as getMethodAttribs(ftn) & CORINFO_FLG_INTRINSIC, except faster. virtual bool isIntrinsic(CORINFO_METHOD_HANDLE ftn) = 0; + // Check whether the value type instance pointer ('this') passed to a value type instance + // method 'ftn' could possibly escape the method when it is called. This works for both direct + // and virtual/interface calls: since a method that lets 'this' escape must, together with every + // method it overrides, be annotated with [UnscopedRef], checking the called method (whether the + // base or the derived method) is sufficient. + // + // A 'false' result means the runtime can guarantee, based on the ECMA-335 augment tied to + // 'RefSafetyRulesAttribute', that the instance pointer does not escape 'ftn'. This allows the + // JIT to replace a heap box with a stack-allocated copy when devirtualizing calls onto boxed + // value types. A 'true' (conservative) result means no such guarantee can be made. + virtual bool canValueClassInstancePointerEscape(CORINFO_METHOD_HANDLE ftn) = 0; + // Notify EE about intent to rely on given MethodInfo in the current method // EE returns false if we're not allowed to do so and the methodinfo may change. // Example of a scenario addressed by notifyMethodInfoUsage: diff --git a/src/coreclr/inc/icorjitinfoimpl_generated.h b/src/coreclr/inc/icorjitinfoimpl_generated.h index fca6cdb5198cbc..c8f1cfbf002487 100644 --- a/src/coreclr/inc/icorjitinfoimpl_generated.h +++ b/src/coreclr/inc/icorjitinfoimpl_generated.h @@ -24,6 +24,9 @@ bool isIntrinsic( CORINFO_METHOD_HANDLE ftn) override; +bool canValueClassInstancePointerEscape( + CORINFO_METHOD_HANDLE ftn) override; + bool notifyMethodInfoUsage( CORINFO_METHOD_HANDLE ftn) override; diff --git a/src/coreclr/inc/jiteeversionguid.h b/src/coreclr/inc/jiteeversionguid.h index b66d35a6d89981..82ecfe03d54aba 100644 --- a/src/coreclr/inc/jiteeversionguid.h +++ b/src/coreclr/inc/jiteeversionguid.h @@ -37,11 +37,11 @@ #include -constexpr GUID JITEEVersionIdentifier = { /* 24c0b78a-9173-40cd-a6b3-e290fb3c0a22 */ - 0x24c0b78a, - 0x9173, - 0x40cd, - {0xa6, 0xb3, 0xe2, 0x90, 0xfb, 0x3c, 0x0a, 0x22} +constexpr GUID JITEEVersionIdentifier = { /* ba811465-4827-44e5-9445-6d00091c7eba */ + 0xba811465, + 0x4827, + 0x44e5, + {0x94, 0x45, 0x6d, 0x00, 0x09, 0x1c, 0x7e, 0xba} }; #endif // JIT_EE_VERSIONING_GUID_H diff --git a/src/coreclr/jit/ICorJitInfo_names_generated.h b/src/coreclr/jit/ICorJitInfo_names_generated.h index 91ab585e857e8f..b2bbbc186f5564 100644 --- a/src/coreclr/jit/ICorJitInfo_names_generated.h +++ b/src/coreclr/jit/ICorJitInfo_names_generated.h @@ -5,6 +5,7 @@ // To regenerate run the gen script in src/coreclr/tools/Common/JitInterface/ThunkGenerator // and follow the instructions in docs/project/updating-jitinterface.md DEF_CLR_API(isIntrinsic) +DEF_CLR_API(canValueClassInstancePointerEscape) DEF_CLR_API(notifyMethodInfoUsage) DEF_CLR_API(getMethodAttribs) DEF_CLR_API(setMethodAttribs) diff --git a/src/coreclr/jit/ICorJitInfo_wrapper_generated.hpp b/src/coreclr/jit/ICorJitInfo_wrapper_generated.hpp index b81544fa82cee6..4f34bffeda96ca 100644 --- a/src/coreclr/jit/ICorJitInfo_wrapper_generated.hpp +++ b/src/coreclr/jit/ICorJitInfo_wrapper_generated.hpp @@ -21,6 +21,15 @@ bool WrapICorJitInfo::isIntrinsic( return temp; } +bool WrapICorJitInfo::canValueClassInstancePointerEscape( + CORINFO_METHOD_HANDLE ftn) +{ + API_ENTER(canValueClassInstancePointerEscape); + bool temp = wrapHnd->canValueClassInstancePointerEscape(ftn); + API_LEAVE(canValueClassInstancePointerEscape); + return temp; +} + bool WrapICorJitInfo::notifyMethodInfoUsage( CORINFO_METHOD_HANDLE ftn) { diff --git a/src/coreclr/jit/compiler.h b/src/coreclr/jit/compiler.h index b2acfa5e21f7db..b6ef40836f7377 100644 --- a/src/coreclr/jit/compiler.h +++ b/src/coreclr/jit/compiler.h @@ -4065,6 +4065,7 @@ class Compiler BR_REMOVE_BUT_NOT_NARROW, // remove effects, return original source tree BR_DONT_REMOVE, // check if removal is possible, return copy source tree BR_DONT_REMOVE_WANT_TYPE_HANDLE, // check if removal is possible, return type handle tree + BR_MAKE_LOCAL_COPY // revise box to copy to temp local and return local's address }; GenTree* gtTryRemoveBoxUpstreamEffects(GenTree* tree, BoxRemovalOptions options = BR_REMOVE_AND_NARROW); diff --git a/src/coreclr/jit/gentree.cpp b/src/coreclr/jit/gentree.cpp index bbec675a6f07a1..86ccc2c5b7d1b8 100644 --- a/src/coreclr/jit/gentree.cpp +++ b/src/coreclr/jit/gentree.cpp @@ -16625,9 +16625,10 @@ GenTree* Compiler::gtTryRemoveBoxUpstreamEffects(GenTree* op, BoxRemovalOptions Statement* allocStmt = box->gtDefStmtWhenInlinedBoxValue; Statement* copyStmt = box->gtCopyStmtWhenInlinedBoxValue; - JITDUMP("gtTryRemoveBoxUpstreamEffects: %s of BOX (valuetype)" + JITDUMP("gtTryRemoveBoxUpstreamEffects: %s to %s of BOX (valuetype)" " [%06u] (assign/newobj " FMT_STMT " copy " FMT_STMT "\n", - (options == BR_DONT_REMOVE) ? "checking if it is possible" : "attempting", dspTreeID(op), + (options == BR_DONT_REMOVE) ? "checking if it is possible" : "attempting", + (options == BR_MAKE_LOCAL_COPY) ? "make local unboxed version" : "remove side effects", dspTreeID(op), allocStmt->GetID(), copyStmt->GetID()); // If we don't recognize the form of the store, bail. @@ -16703,6 +16704,65 @@ GenTree* Compiler::gtTryRemoveBoxUpstreamEffects(GenTree* op, BoxRemovalOptions return nullptr; } + // Handle case where we are optimizing the box into a local copy + if (options == BR_MAKE_LOCAL_COPY) + { + // Drill into the box to get at the box temp local and the box type + GenTree* boxTemp = box->BoxOp(); + assert(boxTemp->IsLocal()); + const unsigned boxTempLcl = boxTemp->AsLclVar()->GetLclNum(); + assert(lvaTable[boxTempLcl].lvType == TYP_REF); + CORINFO_CLASS_HANDLE boxClass = lvaTable[boxTempLcl].lvClassHnd; + assert(boxClass != nullptr); + + // Verify that the copy has the expected shape + // (store_blk|store_ind (add (boxTempLcl, ptr-size))) + // + // The shape here is constrained to the patterns we produce + // over in impImportAndPushBox for the inlined box case. + // + GenTree* copyDstAddr = copy->AsIndir()->Addr(); + if (!copyDstAddr->OperIs(GT_ADD)) + { + JITDUMP("Unexpected copy dest address tree\n"); + return nullptr; + } + + GenTree* copyDstAddrOp1 = copyDstAddr->AsOp()->gtOp1; + if (!copyDstAddrOp1->OperIs(GT_LCL_VAR) || (copyDstAddrOp1->AsLclVarCommon()->GetLclNum() != boxTempLcl)) + { + JITDUMP("Unexpected copy dest address 1st addend\n"); + return nullptr; + } + + GenTree* copyDstAddrOp2 = copyDstAddr->AsOp()->gtOp2; + if (!copyDstAddrOp2->IsIntegralConst(TARGET_POINTER_SIZE)) + { + JITDUMP("Unexpected copy dest address 2nd addend\n"); + return nullptr; + } + + // Screening checks have all passed. Do the transformation. + // + // Retype the box temp to be a struct + JITDUMP("Retyping box temp V%02u to struct %s\n", boxTempLcl, eeGetClassName(boxClass)); + lvaTable[boxTempLcl].lvType = TYP_UNDEF; + const bool isUnsafeValueClass = false; + lvaSetStruct(boxTempLcl, boxClass, isUnsafeValueClass); + + // Remove the newobj and store to box temp + JITDUMP("Bashing NEWOBJ [%06u] to NOP\n", dspTreeID(boxLclDef)); + boxLclDef->gtBashToNOP(); + + // Update the copy from the value to be boxed to the box temp + copy->AsIndir()->Addr() = gtNewLclVarAddrNode(boxTempLcl, TYP_BYREF); + + // Return the address of the now-struct typed box temp + GenTree* retValue = gtNewLclVarAddrNode(boxTempLcl, TYP_BYREF); + + return retValue; + } + // If the copy is a struct copy, make sure we know how to isolate any source side effects. GenTree* copySrc = copy->Data(); diff --git a/src/coreclr/jit/importercalls.cpp b/src/coreclr/jit/importercalls.cpp index b1322a05ef7c76..f033fe951e9bb2 100644 --- a/src/coreclr/jit/importercalls.cpp +++ b/src/coreclr/jit/importercalls.cpp @@ -9638,8 +9638,62 @@ void Compiler::impTransformDevirtualizedCall(GenTreeCall* call, CORINFO_SIG_INFO unboxedEntrySig; info.compCompHnd->getMethodSig(unboxedEntryMethod, &unboxedEntrySig); - bool canUseUnboxedEntry = true; + bool canUseUnboxedEntry = true; + bool madeLocalCopy = false; + GenTree* boxTypeHandle = nullptr; + bool const needsClassTypeArg = + unboxedEntrySig.hasTypeArg() && + (((SIZE_T)dcInfo->tokenLookupContext & CORINFO_CONTEXTFLAGS_MASK) == CORINFO_CONTEXTFLAGS_CLASS); + + // If the 'this' object is a local box, and the unboxed entry provably keeps the receiver + // from escaping, replace the heap allocation with a stack-local copy of the boxed value. + // + if (thisObj->IsBoxedValue() && + !info.compCompHnd->canValueClassInstancePointerEscape(unboxedEntryMethod)) + { + // If the shared unboxed entry needs the exact class as a type arg, recover the type + // handle statically from the box before it is bashed; the stack copy won't carry a + // method table at runtime. + // + bool haveTypeArg = true; + if (needsClassTypeArg) + { + boxTypeHandle = gtTryRemoveBoxUpstreamEffects(thisObj, BR_DONT_REMOVE_WANT_TYPE_HANDLE); + haveTypeArg = (boxTypeHandle != nullptr); + } + + GenTree* localCopyThis = + haveTypeArg ? gtTryRemoveBoxUpstreamEffects(thisObj, BR_MAKE_LOCAL_COPY) : nullptr; + + if (localCopyThis != nullptr) + { + JITDUMP("Success! invoking unboxed entry point on local copy\n"); + assert(localCopyThis->IsLclVarAddr()); + assert(thisObj == thisArg->GetEarlyNode()); + thisArg->SetEarlyNode(localCopyThis); + + // We may end up inlining this call, so the local copy must be marked as "aliased", + // making sure the inlinee importer will know when to spill references to its value. + // + lvaGetDesc(localCopyThis->AsLclFld())->lvHasLdAddrOp = true; + madeLocalCopy = true; + +#if FEATURE_TAILCALL_OPT + if (call->IsImplicitTailCall()) + { + // We just introduced a new address taken local variable, so clear the + // implicit tail call flag. + // + JITDUMP("Clearing the implicit tail call flag\n"); + call->gtCallMoreFlags &= ~GTF_CALL_M_IMPLICIT_TAILCALL; + } +#endif // FEATURE_TAILCALL_OPT + } + } + + // Compute the instantiation parameter the shared unboxed entry may need. + // if (unboxedEntrySig.hasTypeArg()) { if (((SIZE_T)dcInfo->tokenLookupContext & CORINFO_CONTEXTFLAGS_MASK) == CORINFO_CONTEXTFLAGS_METHOD) @@ -9650,10 +9704,17 @@ void Compiler::impTransformDevirtualizedCall(GenTreeCall* call, instParam = getLookupTree(dcInfo->pInstParamLookup, GTF_ICON_METHOD_HDL, exactMethodHandle); JITDUMP("revising call to invoke unboxed entry with additional method desc arg\n"); } + else if (madeLocalCopy) + { + // The exact class handle is known statically from the box. + // + assert(needsClassTypeArg && (boxTypeHandle != nullptr)); + instParam = boxTypeHandle; + JITDUMP("revising call to invoke unboxed entry with additional method table arg from box\n"); + } else { - assert(((SIZE_T)dcInfo->tokenLookupContext & CORINFO_CONTEXTFLAGS_MASK) == - CORINFO_CONTEXTFLAGS_CLASS); + assert(needsClassTypeArg); // Get the method table from the boxed object. // @@ -9681,16 +9742,20 @@ void Compiler::impTransformDevirtualizedCall(GenTreeCall* call, if (canUseUnboxedEntry) { - // Rewrite the call to target the unboxed entry on the box payload. Keep the heap box, - // since the callee may return an interior managed pointer into it; object stack allocation - // can later promote the box to the stack when escape analysis proves the receiver does not - // escape. - // - GenTree* const payloadOffset = gtNewIconNode(TARGET_POINTER_SIZE, TYP_I_IMPL); - GenTree* const boxPayload = - gtNewOperNode(GT_ADD, TYP_BYREF, thisArg->GetEarlyNode(), payloadOffset); + if (!madeLocalCopy) + { + // Rewrite the call to target the unboxed entry on the box payload. Keep the heap box, + // since the callee may return an interior managed pointer into it; object stack + // allocation can later promote the box to the stack when escape analysis proves the + // receiver does not escape. + // + GenTree* const payloadOffset = gtNewIconNode(TARGET_POINTER_SIZE, TYP_I_IMPL); + GenTree* const boxPayload = + gtNewOperNode(GT_ADD, TYP_BYREF, thisArg->GetEarlyNode(), payloadOffset); + + thisArg->SetEarlyNode(boxPayload); + } - thisArg->SetEarlyNode(boxPayload); call->gtCallMethHnd = unboxedEntryMethod; INDEBUG(call->gtCallDebugFlags |= GTF_CALL_MD_UNBOXED); @@ -9706,6 +9771,11 @@ void Compiler::impTransformDevirtualizedCall(GenTreeCall* call, derivedMethod = unboxedEntryMethod; pDerivedResolvedToken = dcInfo->pUnboxedResolvedToken; + + if (madeLocalCopy) + { + Metrics.DevirtualizedCallRemovedBox++; + } Metrics.DevirtualizedCallUnboxedEntry++; } } diff --git a/src/coreclr/jit/jitmetadatalist.h b/src/coreclr/jit/jitmetadatalist.h index 7fc8865617b9dc..db90f3a2932f41 100644 --- a/src/coreclr/jit/jitmetadatalist.h +++ b/src/coreclr/jit/jitmetadatalist.h @@ -58,6 +58,7 @@ JITMETADATAMETRIC(ImporterBranchFold, int, 0) JITMETADATAMETRIC(ImporterSwitchFold, int, 0) JITMETADATAMETRIC(DevirtualizedCall, int, 0) JITMETADATAMETRIC(DevirtualizedCallUnboxedEntry, int, 0) +JITMETADATAMETRIC(DevirtualizedCallRemovedBox, int, 0) JITMETADATAMETRIC(GDV, int, 0) JITMETADATAMETRIC(ClassGDV, int, 0) JITMETADATAMETRIC(MethodGDV, int, 0) diff --git a/src/coreclr/tools/Common/JitInterface/CorInfoImpl.cs b/src/coreclr/tools/Common/JitInterface/CorInfoImpl.cs index 37badc2c91ed76..0b9820665f983b 100644 --- a/src/coreclr/tools/Common/JitInterface/CorInfoImpl.cs +++ b/src/coreclr/tools/Common/JitInterface/CorInfoImpl.cs @@ -1082,6 +1082,44 @@ private bool isIntrinsic(CORINFO_METHOD_STRUCT_* ftn) return method.IsIntrinsic || HardwareIntrinsicHelpers.IsHardwareIntrinsic(method); } + private bool canValueClassInstancePointerEscape(CORINFO_METHOD_STRUCT_* ftn) + { + MethodDesc method = HandleToObject(ftn); + + // We only reason about the receiver of an instance method. This works for both direct and + // virtual/interface calls, and regardless of whether 'ftn' is the base (e.g. interface) + // method or the derived method: a method that lets 'this' escape must, together with every + // method it overrides, be annotated with [UnscopedRef], so inspecting the called method + // here is sufficient. + if (method.Signature.IsStatic) + return true; + + if (method.GetTypicalMethodDefinition() is not EcmaMethod ecmaMethod) + return true; + + // The guarantee only holds for modules that opted into the ref safety rules augment + // (RefSafetyRulesAttribute version 11 or above). + if (!ModuleOptsIntoRefSafetyRules(ecmaMethod.Module, 11)) + return true; + + // Per the augment, the 'this' pointer of a value type instance method does not escape the + // method unless the method is annotated with [UnscopedRef]. + return ecmaMethod.HasCustomAttribute("System.Diagnostics.CodeAnalysis", "UnscopedRefAttribute"); + } + + private static bool ModuleOptsIntoRefSafetyRules(EcmaModule module, int minVersion) + { + var reader = module.MetadataReader; + var handle = reader.GetCustomAttributeHandle( + reader.GetModuleDefinition().GetCustomAttributes(), + "System.Runtime.CompilerServices", "RefSafetyRulesAttribute"); + if (handle.IsNil) + return false; + + var value = reader.GetCustomAttribute(handle).DecodeValue(new CustomAttributeTypeProvider(module)); + return value.FixedArguments.Length == 1 && value.FixedArguments[0].Value is int version && version >= minVersion; + } + private uint getMethodAttribsInternal(MethodDesc method) { CorInfoFlag result = 0; diff --git a/src/coreclr/tools/Common/JitInterface/CorInfoImpl_generated.cs b/src/coreclr/tools/Common/JitInterface/CorInfoImpl_generated.cs index 9566e1788240ae..b0cdf1fb6c853e 100644 --- a/src/coreclr/tools/Common/JitInterface/CorInfoImpl_generated.cs +++ b/src/coreclr/tools/Common/JitInterface/CorInfoImpl_generated.cs @@ -21,6 +21,7 @@ private struct ICorJitInfoCallbacks static ICorJitInfoCallbacks() { s_callbacks.isIntrinsic = &_isIntrinsic; + s_callbacks.canValueClassInstancePointerEscape = &_canValueClassInstancePointerEscape; s_callbacks.notifyMethodInfoUsage = &_notifyMethodInfoUsage; s_callbacks.getMethodAttribs = &_getMethodAttribs; s_callbacks.setMethodAttribs = &_setMethodAttribs; @@ -204,6 +205,7 @@ static ICorJitInfoCallbacks() } public delegate* unmanaged isIntrinsic; + public delegate* unmanaged canValueClassInstancePointerEscape; public delegate* unmanaged notifyMethodInfoUsage; public delegate* unmanaged getMethodAttribs; public delegate* unmanaged setMethodAttribs; @@ -406,6 +408,21 @@ private static byte _isIntrinsic(IntPtr thisHandle, IntPtr* ppException, CORINFO } } + [UnmanagedCallersOnly] + private static byte _canValueClassInstancePointerEscape(IntPtr thisHandle, IntPtr* ppException, CORINFO_METHOD_STRUCT_* ftn) + { + var _this = GetThis(thisHandle); + try + { + return _this.canValueClassInstancePointerEscape(ftn) ? (byte)1 : (byte)0; + } + catch (Exception ex) + { + *ppException = _this.AllocException(ex); + return default; + } + } + [UnmanagedCallersOnly] private static byte _notifyMethodInfoUsage(IntPtr thisHandle, IntPtr* ppException, CORINFO_METHOD_STRUCT_* ftn) { diff --git a/src/coreclr/tools/Common/JitInterface/ThunkGenerator/ThunkInput.txt b/src/coreclr/tools/Common/JitInterface/ThunkGenerator/ThunkInput.txt index 68a74186c816b9..93c6cd35f50979 100644 --- a/src/coreclr/tools/Common/JitInterface/ThunkGenerator/ThunkInput.txt +++ b/src/coreclr/tools/Common/JitInterface/ThunkGenerator/ThunkInput.txt @@ -175,6 +175,7 @@ ICorJitInfo::errorTrapFunction,void* FUNCTIONS bool isIntrinsic( CORINFO_METHOD_HANDLE ftn ); + bool canValueClassInstancePointerEscape( CORINFO_METHOD_HANDLE ftn ); bool notifyMethodInfoUsage( CORINFO_METHOD_HANDLE ftn ); uint32_t getMethodAttribs( CORINFO_METHOD_HANDLE ftn ); void setMethodAttribs( CORINFO_METHOD_HANDLE ftn, CorInfoMethodRuntimeFlags attribs ); diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/AttributePresenceFilterNode.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/AttributePresenceFilterNode.cs index f6f85fb14fe7df..4bf6941c0335d6 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/AttributePresenceFilterNode.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/AttributePresenceFilterNode.cs @@ -80,6 +80,15 @@ private List GetCustomAttributeEntries() addToTable = true; } } + else if (customAttributeTypeNamespace == "System.Diagnostics.CodeAnalysis") + { + // Consulted by the JIT (via canValueClassInstancePointerEscape) to reason + // about escaping receivers of value type instance methods. + if (customAttributeTypeName == "UnscopedRefAttribute") + { + addToTable = true; + } + } if (!addToTable) continue; diff --git a/src/coreclr/tools/aot/jitinterface/jitinterface_generated.h b/src/coreclr/tools/aot/jitinterface/jitinterface_generated.h index f544bfd4b6484e..0f1e582f7e48f3 100644 --- a/src/coreclr/tools/aot/jitinterface/jitinterface_generated.h +++ b/src/coreclr/tools/aot/jitinterface/jitinterface_generated.h @@ -12,6 +12,7 @@ struct JitInterfaceCallbacks { bool (* isIntrinsic)(void * thisHandle, CorInfoExceptionClass** ppException, CORINFO_METHOD_HANDLE ftn); + bool (* canValueClassInstancePointerEscape)(void * thisHandle, CorInfoExceptionClass** ppException, CORINFO_METHOD_HANDLE ftn); bool (* notifyMethodInfoUsage)(void * thisHandle, CorInfoExceptionClass** ppException, CORINFO_METHOD_HANDLE ftn); uint32_t (* getMethodAttribs)(void * thisHandle, CorInfoExceptionClass** ppException, CORINFO_METHOD_HANDLE ftn); void (* setMethodAttribs)(void * thisHandle, CorInfoExceptionClass** ppException, CORINFO_METHOD_HANDLE ftn, CorInfoMethodRuntimeFlags attribs); @@ -216,6 +217,15 @@ class JitInterfaceWrapper : public ICorJitInfo return temp; } + virtual bool canValueClassInstancePointerEscape( + CORINFO_METHOD_HANDLE ftn) +{ + CorInfoExceptionClass* pException = nullptr; + bool temp = _callbacks->canValueClassInstancePointerEscape(_thisHandle, &pException, ftn); + if (pException != nullptr) throw pException; + return temp; +} + virtual bool notifyMethodInfoUsage( CORINFO_METHOD_HANDLE ftn) { diff --git a/src/coreclr/tools/superpmi/superpmi-shared/lwmlist.h b/src/coreclr/tools/superpmi/superpmi-shared/lwmlist.h index d46488982aa8fa..4d85fb3c7969ff 100644 --- a/src/coreclr/tools/superpmi/superpmi-shared/lwmlist.h +++ b/src/coreclr/tools/superpmi/superpmi-shared/lwmlist.h @@ -174,6 +174,7 @@ LWM(DoesFieldBelongToClass, DLDL, DWORD) DENSELWM(SigInstHandleMap, DWORDLONG) LWM(GetWasmTypeSymbol, Agnostic_GetWasmTypeSymbol, DWORDLONG) LWM(GetAddressAlignment, DWORDLONG, DWORD) +LWM(CanValueClassInstancePointerEscape, DWORDLONG, DWORD) #undef LWM #undef DENSELWM diff --git a/src/coreclr/tools/superpmi/superpmi-shared/methodcontext.cpp b/src/coreclr/tools/superpmi/superpmi-shared/methodcontext.cpp index 9d50ef78786dcd..8672c98cc5352d 100644 --- a/src/coreclr/tools/superpmi/superpmi-shared/methodcontext.cpp +++ b/src/coreclr/tools/superpmi/superpmi-shared/methodcontext.cpp @@ -7760,6 +7760,28 @@ bool MethodContext::IsStringContentEqual(LightWeightMap* prev, Lig } } +void MethodContext::recCanValueClassInstancePointerEscape(CORINFO_METHOD_HANDLE ftn, bool result) +{ + if (CanValueClassInstancePointerEscape == nullptr) + CanValueClassInstancePointerEscape = new LightWeightMap(); + + DWORDLONG key = CastHandle(ftn); + DWORD value = result ? 1 : 0; + CanValueClassInstancePointerEscape->Add(key, value); + DEBUG_REC(dmpCanValueClassInstancePointerEscape(key, value)); +} +void MethodContext::dmpCanValueClassInstancePointerEscape(DWORDLONG key, DWORD value) +{ + printf("CanValueClassInstancePointerEscape key ftn-%016" PRIX64 ", value res-%u", key, value); +} +bool MethodContext::repCanValueClassInstancePointerEscape(CORINFO_METHOD_HANDLE ftn) +{ + DWORDLONG key = CastHandle(ftn); + DWORD value = LookupByKeyOrMiss(CanValueClassInstancePointerEscape, key, ": key %016" PRIX64 "", key); + DEBUG_REP(dmpCanValueClassInstancePointerEscape(key, value)); + return value != 0; +} + bool g_debugRec = false; bool g_debugRep = false; diff --git a/src/coreclr/tools/superpmi/superpmi-shared/methodcontext.h b/src/coreclr/tools/superpmi/superpmi-shared/methodcontext.h index 8d38ab29b240c4..707a91c06b212b 100644 --- a/src/coreclr/tools/superpmi/superpmi-shared/methodcontext.h +++ b/src/coreclr/tools/superpmi/superpmi-shared/methodcontext.h @@ -122,6 +122,10 @@ class MethodContext void dmpIsIntrinsic(DWORDLONG key, DWORD value); bool repIsIntrinsic(CORINFO_METHOD_HANDLE ftn); + void recCanValueClassInstancePointerEscape(CORINFO_METHOD_HANDLE ftn, bool result); + void dmpCanValueClassInstancePointerEscape(DWORDLONG key, DWORD value); + bool repCanValueClassInstancePointerEscape(CORINFO_METHOD_HANDLE ftn); + void recNotifyMethodInfoUsage(CORINFO_METHOD_HANDLE ftn, bool result); void dmpNotifyMethodInfoUsage(DWORDLONG key, DWORD value); bool repNotifyMethodInfoUsage(CORINFO_METHOD_HANDLE ftn); @@ -1219,6 +1223,7 @@ enum mcPackets Packet_GetAsyncOtherVariant = 237, Packet_GetAwaitReturnCall = 238, Packet_GetAddressAlignment = 239, + Packet_CanValueClassInstancePointerEscape = 240, }; void SetDebugDumpVariables(); diff --git a/src/coreclr/tools/superpmi/superpmi-shim-collector/icorjitinfo.cpp b/src/coreclr/tools/superpmi/superpmi-shim-collector/icorjitinfo.cpp index a026e616aca6d9..95151403cbbcd1 100644 --- a/src/coreclr/tools/superpmi/superpmi-shim-collector/icorjitinfo.cpp +++ b/src/coreclr/tools/superpmi/superpmi-shim-collector/icorjitinfo.cpp @@ -28,6 +28,14 @@ bool interceptor_ICJI::isIntrinsic(CORINFO_METHOD_HANDLE ftn) return temp; } +bool interceptor_ICJI::canValueClassInstancePointerEscape(CORINFO_METHOD_HANDLE ftn) +{ + mc->cr->AddCall("canValueClassInstancePointerEscape"); + bool temp = original_ICorJitInfo->canValueClassInstancePointerEscape(ftn); + mc->recCanValueClassInstancePointerEscape(ftn, temp); + return temp; +} + bool interceptor_ICJI::notifyMethodInfoUsage(CORINFO_METHOD_HANDLE ftn) { mc->cr->AddCall("notifyMethodInfoUsage"); diff --git a/src/coreclr/tools/superpmi/superpmi-shim-counter/icorjitinfo_generated.cpp b/src/coreclr/tools/superpmi/superpmi-shim-counter/icorjitinfo_generated.cpp index 8671f7c0b459fa..0eff102387771e 100644 --- a/src/coreclr/tools/superpmi/superpmi-shim-counter/icorjitinfo_generated.cpp +++ b/src/coreclr/tools/superpmi/superpmi-shim-counter/icorjitinfo_generated.cpp @@ -19,6 +19,13 @@ bool interceptor_ICJI::isIntrinsic( return original_ICorJitInfo->isIntrinsic(ftn); } +bool interceptor_ICJI::canValueClassInstancePointerEscape( + CORINFO_METHOD_HANDLE ftn) +{ + mcs->AddCall("canValueClassInstancePointerEscape"); + return original_ICorJitInfo->canValueClassInstancePointerEscape(ftn); +} + bool interceptor_ICJI::notifyMethodInfoUsage( CORINFO_METHOD_HANDLE ftn) { diff --git a/src/coreclr/tools/superpmi/superpmi-shim-simple/icorjitinfo_generated.cpp b/src/coreclr/tools/superpmi/superpmi-shim-simple/icorjitinfo_generated.cpp index c0ce45ed34f4c1..7456ecdbe006f4 100644 --- a/src/coreclr/tools/superpmi/superpmi-shim-simple/icorjitinfo_generated.cpp +++ b/src/coreclr/tools/superpmi/superpmi-shim-simple/icorjitinfo_generated.cpp @@ -18,6 +18,12 @@ bool interceptor_ICJI::isIntrinsic( return original_ICorJitInfo->isIntrinsic(ftn); } +bool interceptor_ICJI::canValueClassInstancePointerEscape( + CORINFO_METHOD_HANDLE ftn) +{ + return original_ICorJitInfo->canValueClassInstancePointerEscape(ftn); +} + bool interceptor_ICJI::notifyMethodInfoUsage( CORINFO_METHOD_HANDLE ftn) { diff --git a/src/coreclr/tools/superpmi/superpmi/icorjitinfo.cpp b/src/coreclr/tools/superpmi/superpmi/icorjitinfo.cpp index 6ba6066929752d..65015f0d77fe3a 100644 --- a/src/coreclr/tools/superpmi/superpmi/icorjitinfo.cpp +++ b/src/coreclr/tools/superpmi/superpmi/icorjitinfo.cpp @@ -31,6 +31,12 @@ bool MyICJI::isIntrinsic(CORINFO_METHOD_HANDLE ftn) return jitInstance->mc->repIsIntrinsic(ftn); } +bool MyICJI::canValueClassInstancePointerEscape(CORINFO_METHOD_HANDLE ftn) +{ + jitInstance->mc->cr->AddCall("canValueClassInstancePointerEscape"); + return jitInstance->mc->repCanValueClassInstancePointerEscape(ftn); +} + bool MyICJI::notifyMethodInfoUsage(CORINFO_METHOD_HANDLE ftn) { jitInstance->mc->cr->AddCall("notifyMethodInfoUsage"); diff --git a/src/coreclr/vm/jitinterface.cpp b/src/coreclr/vm/jitinterface.cpp index 784edce5d72b78..e306e47052d01d 100644 --- a/src/coreclr/vm/jitinterface.cpp +++ b/src/coreclr/vm/jitinterface.cpp @@ -37,6 +37,7 @@ #include "ecall.h" #include "generics.h" #include "typestring.h" +#include "caparser.h" #include "typedesc.h" #include "genericdict.h" #include "array.h" @@ -6433,6 +6434,76 @@ bool CEEInfo::isIntrinsic(CORINFO_METHOD_HANDLE ftn) return ret; } +// Determine whether 'pModule' opted into the ECMA-335 augment for escaping value type instance +// pointers by applying RefSafetyRulesAttribute with a version of at least 'minVersion' (11 for the +// initial augment). The attribute is emitted at module scope by the C# compiler. +static bool ModuleOptsIntoRefSafetyRules(Module* pModule, int minVersion) +{ + STANDARD_VM_CONTRACT; + + const void* pData = NULL; + ULONG cbData = 0; + HRESULT hr = + pModule->GetCustomAttribute(TokenFromRid(1, mdtModule), WellKnownAttribute::RefSafetyRules, &pData, &cbData); + if (hr != S_OK) + { + return false; + } + + // RefSafetyRulesAttribute(int version): a 2-byte prolog followed by the int32 version argument. + CustomAttributeParser cap(pData, cbData); + if (FAILED(cap.SkipProlog())) + { + return false; + } + + INT32 version; + if (FAILED(cap.GetI4(&version))) + { + return false; + } + + return version >= minVersion; +} + +bool CEEInfo::canValueClassInstancePointerEscape(CORINFO_METHOD_HANDLE ftn) +{ + CONTRACTL { + THROWS; + GC_TRIGGERS; + MODE_PREEMPTIVE; + } CONTRACTL_END; + + // Conservatively assume the value type instance pointer may escape the callee. + bool result = true; + + JIT_TO_EE_TRANSITION(); + + _ASSERTE(ftn != NULL); + + MethodDesc* pMD = GetMethod(ftn); + + // We only reason about the receiver of an instance method. This works for both direct and + // virtual/interface calls, and regardless of whether 'ftn' is the base (e.g. interface) method or + // the derived method: a method that lets 'this' escape must, together with every method it + // overrides, be annotated with [UnscopedRef], so inspecting the called method here is sufficient. + if (!pMD->IsStatic()) + { + // The guarantee only holds for modules that opted into the ref safety rules augment + // (RefSafetyRulesAttribute version 11 or above). + if (ModuleOptsIntoRefSafetyRules(pMD->GetModule(), 11)) + { + // Per the augment, the 'this' pointer of a value type instance method does not escape + // the method unless the method is annotated with [UnscopedRef]. + result = (pMD->GetCustomAttribute(WellKnownAttribute::UnscopedRef, NULL, NULL) == S_OK); + } + } + + EE_TO_JIT_TRANSITION(); + + return result; +} + bool CEEInfo::notifyMethodInfoUsage(CORINFO_METHOD_HANDLE ftn) { CONTRACTL { diff --git a/src/coreclr/vm/wellknownattributes.h b/src/coreclr/vm/wellknownattributes.h index df72f546c95bae..498aae64d1f71f 100644 --- a/src/coreclr/vm/wellknownattributes.h +++ b/src/coreclr/vm/wellknownattributes.h @@ -38,6 +38,8 @@ enum class WellKnownAttribute : DWORD UnsafeAccessorAttribute, UnsafeAccessorTypeAttribute, ExtendedLayoutAttribute, + UnscopedRef, + RefSafetyRules, CountOfWellKnownAttributes }; @@ -145,6 +147,12 @@ inline const char *GetWellKnownAttributeName(WellKnownAttribute attribute) case WellKnownAttribute::ExtendedLayoutAttribute: ret = "System.Runtime.InteropServices.ExtendedLayoutAttribute"; break; + case WellKnownAttribute::UnscopedRef: + ret = "System.Diagnostics.CodeAnalysis.UnscopedRefAttribute"; + break; + case WellKnownAttribute::RefSafetyRules: + ret = "System.Runtime.CompilerServices.RefSafetyRulesAttribute"; + break; case WellKnownAttribute::CountOfWellKnownAttributes: default: ret = nullptr; @@ -163,6 +171,7 @@ inline const char *GetWellKnownAttributeName(WellKnownAttribute attribute) attribute == WellKnownAttribute::ThreadStatic || attribute == WellKnownAttribute::ParamArray || attribute == WellKnownAttribute::DefaultMember + || attribute == WellKnownAttribute::UnscopedRef || strncmp(prefix, ret, ARRAY_SIZE(prefix) - 1) == 0; _ASSERTE(readyToRunAware); #endif // _DEBUG From 8fc1a6da7e1c65406478dbb8be52249bad694491 Mon Sep 17 00:00:00 2001 From: Jakob Botsch Nielsen Date: Wed, 22 Jul 2026 15:30:56 +0200 Subject: [PATCH 2/6] Assert nonstatic instead --- .../tools/Common/JitInterface/CorInfoImpl.cs | 7 +++---- src/coreclr/vm/jitinterface.cpp | 19 +++++++++---------- 2 files changed, 12 insertions(+), 14 deletions(-) diff --git a/src/coreclr/tools/Common/JitInterface/CorInfoImpl.cs b/src/coreclr/tools/Common/JitInterface/CorInfoImpl.cs index dae7a75ec31630..a79a9ff903d575 100644 --- a/src/coreclr/tools/Common/JitInterface/CorInfoImpl.cs +++ b/src/coreclr/tools/Common/JitInterface/CorInfoImpl.cs @@ -1086,13 +1086,12 @@ private bool canValueClassInstancePointerEscape(CORINFO_METHOD_STRUCT_* ftn) { MethodDesc method = HandleToObject(ftn); - // We only reason about the receiver of an instance method. This works for both direct and - // virtual/interface calls, and regardless of whether 'ftn' is the base (e.g. interface) + // The JIT only asks about the receiver of an instance method. This works for both direct + // and virtual/interface calls, and regardless of whether 'ftn' is the base (e.g. interface) // method or the derived method: a method that lets 'this' escape must, together with every // method it overrides, be annotated with [UnscopedRef], so inspecting the called method // here is sufficient. - if (method.Signature.IsStatic) - return true; + Debug.Assert(!method.Signature.IsStatic); if (method.GetTypicalMethodDefinition() is not EcmaMethod ecmaMethod) return true; diff --git a/src/coreclr/vm/jitinterface.cpp b/src/coreclr/vm/jitinterface.cpp index 62dcb2ba1dff8c..0026d66b0b7c54 100644 --- a/src/coreclr/vm/jitinterface.cpp +++ b/src/coreclr/vm/jitinterface.cpp @@ -6483,20 +6483,19 @@ bool CEEInfo::canValueClassInstancePointerEscape(CORINFO_METHOD_HANDLE ftn) MethodDesc* pMD = GetMethod(ftn); - // We only reason about the receiver of an instance method. This works for both direct and + // The JIT only asks about the receiver of an instance method. This works for both direct and // virtual/interface calls, and regardless of whether 'ftn' is the base (e.g. interface) method or // the derived method: a method that lets 'this' escape must, together with every method it // overrides, be annotated with [UnscopedRef], so inspecting the called method here is sufficient. - if (!pMD->IsStatic()) + _ASSERTE(!pMD->IsStatic()); + + // The guarantee only holds for modules that opted into the ref safety rules augment + // (RefSafetyRulesAttribute version 11 or above). + if (ModuleOptsIntoRefSafetyRules(pMD->GetModule(), 11)) { - // The guarantee only holds for modules that opted into the ref safety rules augment - // (RefSafetyRulesAttribute version 11 or above). - if (ModuleOptsIntoRefSafetyRules(pMD->GetModule(), 11)) - { - // Per the augment, the 'this' pointer of a value type instance method does not escape - // the method unless the method is annotated with [UnscopedRef]. - result = (pMD->GetCustomAttribute(WellKnownAttribute::UnscopedRef, NULL, NULL) == S_OK); - } + // Per the augment, the 'this' pointer of a value type instance method does not escape + // the method unless the method is annotated with [UnscopedRef]. + result = (pMD->GetCustomAttribute(WellKnownAttribute::UnscopedRef, NULL, NULL) == S_OK); } EE_TO_JIT_TRANSITION(); From 0c6ae8d52250be6f6859e13de07e433d689fa624 Mon Sep 17 00:00:00 2001 From: Jakob Botsch Nielsen Date: Wed, 22 Jul 2026 17:15:34 +0200 Subject: [PATCH 3/6] Make comments a bit more concise --- src/coreclr/inc/corinfo.h | 6 +++--- .../tools/Common/JitInterface/CorInfoImpl.cs | 20 +++++++------------ src/coreclr/vm/jitinterface.cpp | 16 ++++----------- 3 files changed, 14 insertions(+), 28 deletions(-) diff --git a/src/coreclr/inc/corinfo.h b/src/coreclr/inc/corinfo.h index 2ab768a15ba2fc..95a8883b4907b6 100644 --- a/src/coreclr/inc/corinfo.h +++ b/src/coreclr/inc/corinfo.h @@ -2135,9 +2135,9 @@ class ICorStaticInfo // base or the derived method) is sufficient. // // A 'false' result means the runtime can guarantee, based on the ECMA-335 augment tied to - // 'RefSafetyRulesAttribute', that the instance pointer does not escape 'ftn'. This allows the - // JIT to replace a heap box with a stack-allocated copy when devirtualizing calls onto boxed - // value types. A 'true' (conservative) result means no such guarantee can be made. + // 'RefSafetyRulesAttribute', that the instance pointer does not escape 'ftn'. A 'true' + // (conservative) result means no such guarantee can be made. + // virtual bool canValueClassInstancePointerEscape(CORINFO_METHOD_HANDLE ftn) = 0; // Notify EE about intent to rely on given MethodInfo in the current method diff --git a/src/coreclr/tools/Common/JitInterface/CorInfoImpl.cs b/src/coreclr/tools/Common/JitInterface/CorInfoImpl.cs index a79a9ff903d575..5a0c37b14e523b 100644 --- a/src/coreclr/tools/Common/JitInterface/CorInfoImpl.cs +++ b/src/coreclr/tools/Common/JitInterface/CorInfoImpl.cs @@ -6,10 +6,11 @@ using System.Collections.Generic; using System.Diagnostics; using System.IO; -using System.Text; +using System.Reflection.Metadata; using System.Runtime.CompilerServices; using System.Runtime.ExceptionServices; using System.Runtime.InteropServices; +using System.Text; using System.Text.Unicode; using Internal.Text; @@ -1086,36 +1087,29 @@ private bool canValueClassInstancePointerEscape(CORINFO_METHOD_STRUCT_* ftn) { MethodDesc method = HandleToObject(ftn); - // The JIT only asks about the receiver of an instance method. This works for both direct - // and virtual/interface calls, and regardless of whether 'ftn' is the base (e.g. interface) - // method or the derived method: a method that lets 'this' escape must, together with every - // method it overrides, be annotated with [UnscopedRef], so inspecting the called method - // here is sufficient. Debug.Assert(!method.Signature.IsStatic); if (method.GetTypicalMethodDefinition() is not EcmaMethod ecmaMethod) return true; - // The guarantee only holds for modules that opted into the ref safety rules augment - // (RefSafetyRulesAttribute version 11 or above). + // ECMA augment III.1.7.7 allows making this escaping assumption + // based on RefSafetyRules and UnscopedRef attributes. if (!ModuleOptsIntoRefSafetyRules(ecmaMethod.Module, 11)) return true; - // Per the augment, the 'this' pointer of a value type instance method does not escape the - // method unless the method is annotated with [UnscopedRef]. return ecmaMethod.HasCustomAttribute("System.Diagnostics.CodeAnalysis", "UnscopedRefAttribute"); } private static bool ModuleOptsIntoRefSafetyRules(EcmaModule module, int minVersion) { - var reader = module.MetadataReader; - var handle = reader.GetCustomAttributeHandle( + MetadataReader reader = module.MetadataReader; + CustomAttributeHandle handle = reader.GetCustomAttributeHandle( reader.GetModuleDefinition().GetCustomAttributes(), "System.Runtime.CompilerServices", "RefSafetyRulesAttribute"); if (handle.IsNil) return false; - var value = reader.GetCustomAttribute(handle).DecodeValue(new CustomAttributeTypeProvider(module)); + CustomAttributeValue value = reader.GetCustomAttribute(handle).DecodeValue(new CustomAttributeTypeProvider(module)); return value.FixedArguments.Length == 1 && value.FixedArguments[0].Value is int version && version >= minVersion; } diff --git a/src/coreclr/vm/jitinterface.cpp b/src/coreclr/vm/jitinterface.cpp index 0026d66b0b7c54..e88aec674d194a 100644 --- a/src/coreclr/vm/jitinterface.cpp +++ b/src/coreclr/vm/jitinterface.cpp @@ -6434,9 +6434,8 @@ bool CEEInfo::isIntrinsic(CORINFO_METHOD_HANDLE ftn) return ret; } -// Determine whether 'pModule' opted into the ECMA-335 augment for escaping value type instance -// pointers by applying RefSafetyRulesAttribute with a version of at least 'minVersion' (11 for the -// initial augment). The attribute is emitted at module scope by the C# compiler. +// Determine whether 'pModule' has the RefSafetyRulesAttribute with a version +// of at least 'minVersion'. static bool ModuleOptsIntoRefSafetyRules(Module* pModule, int minVersion) { STANDARD_VM_CONTRACT; @@ -6482,19 +6481,12 @@ bool CEEInfo::canValueClassInstancePointerEscape(CORINFO_METHOD_HANDLE ftn) _ASSERTE(ftn != NULL); MethodDesc* pMD = GetMethod(ftn); - - // The JIT only asks about the receiver of an instance method. This works for both direct and - // virtual/interface calls, and regardless of whether 'ftn' is the base (e.g. interface) method or - // the derived method: a method that lets 'this' escape must, together with every method it - // overrides, be annotated with [UnscopedRef], so inspecting the called method here is sufficient. _ASSERTE(!pMD->IsStatic()); - // The guarantee only holds for modules that opted into the ref safety rules augment - // (RefSafetyRulesAttribute version 11 or above). + // ECMA augment III.1.7.7 allows making this escaping assumption based on + // RefSafetyRules and UnscopedRef attributes. if (ModuleOptsIntoRefSafetyRules(pMD->GetModule(), 11)) { - // Per the augment, the 'this' pointer of a value type instance method does not escape - // the method unless the method is annotated with [UnscopedRef]. result = (pMD->GetCustomAttribute(WellKnownAttribute::UnscopedRef, NULL, NULL) == S_OK); } From 2f9877103d5471ebdca45c3f4357c2cf022610d2 Mon Sep 17 00:00:00 2001 From: Jakob Botsch Nielsen Date: Wed, 22 Jul 2026 17:16:44 +0200 Subject: [PATCH 4/6] Rewrite another comment --- src/coreclr/inc/corinfo.h | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/src/coreclr/inc/corinfo.h b/src/coreclr/inc/corinfo.h index 95a8883b4907b6..649c467d7ec5a8 100644 --- a/src/coreclr/inc/corinfo.h +++ b/src/coreclr/inc/corinfo.h @@ -2128,15 +2128,13 @@ class ICorStaticInfo // Quick check whether the method is a jit intrinsic. Returns the same value as getMethodAttribs(ftn) & CORINFO_FLG_INTRINSIC, except faster. virtual bool isIntrinsic(CORINFO_METHOD_HANDLE ftn) = 0; - // Check whether the value type instance pointer ('this') passed to a value type instance - // method 'ftn' could possibly escape the method when it is called. This works for both direct - // and virtual/interface calls: since a method that lets 'this' escape must, together with every - // method it overrides, be annotated with [UnscopedRef], checking the called method (whether the - // base or the derived method) is sufficient. - // - // A 'false' result means the runtime can guarantee, based on the ECMA-335 augment tied to - // 'RefSafetyRulesAttribute', that the instance pointer does not escape 'ftn'. A 'true' - // (conservative) result means no such guarantee can be made. + // Check whether the value type instance pointer ('this') passed to a value + // type instance method 'ftn' could possibly escape the method when it is + // called. + // + // A 'false' result means the runtime can guarantee, based on the ECMA-335 + // augment III.1.7.7, that the instance pointer does not escape 'ftn'. A + // 'true' (conservative) result means no such guarantee can be made. // virtual bool canValueClassInstancePointerEscape(CORINFO_METHOD_HANDLE ftn) = 0; From be289eeb1ff2a72446dcf04bcbab075d28010283 Mon Sep 17 00:00:00 2001 From: Jakob Botsch Nielsen Date: Wed, 22 Jul 2026 17:21:18 +0200 Subject: [PATCH 5/6] Nit --- src/coreclr/vm/jitinterface.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/src/coreclr/vm/jitinterface.cpp b/src/coreclr/vm/jitinterface.cpp index e88aec674d194a..ade1b87735848c 100644 --- a/src/coreclr/vm/jitinterface.cpp +++ b/src/coreclr/vm/jitinterface.cpp @@ -6473,7 +6473,6 @@ bool CEEInfo::canValueClassInstancePointerEscape(CORINFO_METHOD_HANDLE ftn) MODE_PREEMPTIVE; } CONTRACTL_END; - // Conservatively assume the value type instance pointer may escape the callee. bool result = true; JIT_TO_EE_TRANSITION(); From 5f6673835de2e294f150808fe38e00c5211bad5d Mon Sep 17 00:00:00 2001 From: Jakob Botsch Nielsen Date: Thu, 23 Jul 2026 15:39:09 +0200 Subject: [PATCH 6/6] Cache module lookup --- src/coreclr/vm/ceeload.cpp | 36 +++++++++++++++++++++++++++++++++ src/coreclr/vm/ceeload.h | 17 ++++++++++++++++ src/coreclr/vm/jitinterface.cpp | 34 +------------------------------ 3 files changed, 54 insertions(+), 33 deletions(-) diff --git a/src/coreclr/vm/ceeload.cpp b/src/coreclr/vm/ceeload.cpp index 7d7e9e40d1692f..8eee6001cc6fbd 100644 --- a/src/coreclr/vm/ceeload.cpp +++ b/src/coreclr/vm/ceeload.cpp @@ -1226,6 +1226,42 @@ BOOL Module::IsRuntimeMarshallingEnabled() return hr != S_OK; } +BOOL Module::OptsIntoRefSafetyRulesV11() +{ + CONTRACTL + { + THROWS; + if (OptsIntoRefSafetyRulesV11Cached()) GC_NOTRIGGER; else GC_TRIGGERS; + MODE_ANY; + } + CONTRACTL_END + + if (OptsIntoRefSafetyRulesV11Cached()) + { + return !!(m_dwPersistedFlags & REF_SAFETY_RULES_V11); + } + + bool optsIn = false; + + const void *pVal; + ULONG cbVal; + if (GetCustomAttribute(TokenFromRid(1, mdtModule), WellKnownAttribute::RefSafetyRules, &pVal, &cbVal) == S_OK) + { + // RefSafetyRulesAttribute(int version): a 2-byte prolog followed by the int32 version argument. + CustomAttributeParser cap(pVal, cbVal); + INT32 version; + if (SUCCEEDED(cap.SkipProlog()) && SUCCEEDED(cap.GetI4(&version)) && version >= 11) + { + optsIn = true; + } + } + + InterlockedOr((LONG*)&m_dwPersistedFlags, REF_SAFETY_RULES_V11_IS_CACHED | + (optsIn ? REF_SAFETY_RULES_V11 : 0)); + + return optsIn; +} + //--------------------------------------------------------------------------------------- // // Returns managed representation of the module (Module or ModuleBuilder). diff --git a/src/coreclr/vm/ceeload.h b/src/coreclr/vm/ceeload.h index 076bfe497c5c06..cde7da2d65d1c5 100644 --- a/src/coreclr/vm/ceeload.h +++ b/src/coreclr/vm/ceeload.h @@ -693,6 +693,11 @@ class Module : public ModuleBase RUNTIME_MARSHALLING_ENABLED = 0x00010000, SKIP_TYPE_VALIDATION = 0x00020000, + + //If the RefSafetyRules >= v11 setting has been cached + REF_SAFETY_RULES_V11_IS_CACHED = 0x00040000, + //If this module opted into RefSafetyRules version 11 or above + REF_SAFETY_RULES_V11 = 0x00080000, }; Volatile m_dwTransientFlags; @@ -1594,6 +1599,18 @@ class Module : public ModuleBase return (m_dwPersistedFlags & RUNTIME_MARSHALLING_ENABLED_IS_CACHED); } + //----------------------------------------------------------------------------------------- + // If true, this module opted into the ECMA-335 augment tied to RefSafetyRulesAttribute with a + // version of at least 11 (i.e. RefSafetyRulesAttribute(version) with version >= 11). + //----------------------------------------------------------------------------------------- + BOOL OptsIntoRefSafetyRulesV11(); + + BOOL OptsIntoRefSafetyRulesV11Cached() + { + LIMITED_METHOD_CONTRACT; + return (m_dwPersistedFlags & REF_SAFETY_RULES_V11_IS_CACHED); + } + protected: // For reflection emit modules we set this flag when we emit the attribute, and always consider // the current setting of the flag to be set. diff --git a/src/coreclr/vm/jitinterface.cpp b/src/coreclr/vm/jitinterface.cpp index ade1b87735848c..2ebb2f4ad3a432 100644 --- a/src/coreclr/vm/jitinterface.cpp +++ b/src/coreclr/vm/jitinterface.cpp @@ -37,7 +37,6 @@ #include "ecall.h" #include "generics.h" #include "typestring.h" -#include "caparser.h" #include "typedesc.h" #include "genericdict.h" #include "array.h" @@ -6434,37 +6433,6 @@ bool CEEInfo::isIntrinsic(CORINFO_METHOD_HANDLE ftn) return ret; } -// Determine whether 'pModule' has the RefSafetyRulesAttribute with a version -// of at least 'minVersion'. -static bool ModuleOptsIntoRefSafetyRules(Module* pModule, int minVersion) -{ - STANDARD_VM_CONTRACT; - - const void* pData = NULL; - ULONG cbData = 0; - HRESULT hr = - pModule->GetCustomAttribute(TokenFromRid(1, mdtModule), WellKnownAttribute::RefSafetyRules, &pData, &cbData); - if (hr != S_OK) - { - return false; - } - - // RefSafetyRulesAttribute(int version): a 2-byte prolog followed by the int32 version argument. - CustomAttributeParser cap(pData, cbData); - if (FAILED(cap.SkipProlog())) - { - return false; - } - - INT32 version; - if (FAILED(cap.GetI4(&version))) - { - return false; - } - - return version >= minVersion; -} - bool CEEInfo::canValueClassInstancePointerEscape(CORINFO_METHOD_HANDLE ftn) { CONTRACTL { @@ -6484,7 +6452,7 @@ bool CEEInfo::canValueClassInstancePointerEscape(CORINFO_METHOD_HANDLE ftn) // ECMA augment III.1.7.7 allows making this escaping assumption based on // RefSafetyRules and UnscopedRef attributes. - if (ModuleOptsIntoRefSafetyRules(pMD->GetModule(), 11)) + if (pMD->GetModule()->OptsIntoRefSafetyRulesV11()) { result = (pMD->GetCustomAttribute(WellKnownAttribute::UnscopedRef, NULL, NULL) == S_OK); }