Skip to content
Open
89 changes: 89 additions & 0 deletions docs/design/datacontracts/EnC.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
# Contract EnC

This contract reports Edit and Continue (EnC) function version numbers for jitted
managed methods. EnC function versions are 1-based monotonically increasing counters
that the runtime assigns to each `EnC`-emitted instance of a method body.

## APIs of contract

``` csharp
// Returns the latest EnC version number associated with the method identified by
// (module, methodDef). If no EnC-jitted instance exists for that method, returns
// the default EnC function version (1).
TargetNUInt GetLatestEnCVersion(TargetPointer module, uint methodDef);

// Returns the EnC version number for the specific jitted instance of the method
// identified by (module, methodDef) whose hot region starts at the given native
// code address. If no matching jitted instance exists (for example, the method
// was never EnC-edited), returns the default EnC function version (1).
TargetNUInt GetEnCVersion(TargetPointer module, uint methodDef, TargetCodePointer nativeCodeAddress);
```

## Version 1

Data descriptors used:
| Data Descriptor Name | Field | Type | Purpose |
| --- | --- | --- | --- |
| `Module` | `EnCDataList` | nuint | Head of the singly linked list of `EnCData` entries for jitted EnC-versioned methods in this module |
| `EnCData` | `AddrOfCode` | nuint | Native code start (TADDR) for the jitted instance |
| `EnCData` | `Token` | uint32 | `mdMethodDef` token of the method |
| `EnCData` | `EnCVersion` | nuint | EnC function version number for this jitted instance |
| `EnCData` | `Next` | nuint | Next entry in the module's `EnCData` list, or null |

Global variables used:
| Global Name | Type | Purpose |
| --- | --- | --- |
| `CorDBDefaultEnCFunctionVersion` | nuint | Default EnC function version reported for methods that have never been EnC-edited (matches `CorDB_DEFAULT_ENC_FUNCTION_VERSION` in `src/coreclr/inc/cordbpriv.h`) |

Contracts used: none

``` csharp
// Returns the address of the first EnCData entry on module's EnCDataList whose Token
// matches methodDef and (when addrOrZero is non-null) whose AddrOfCode matches
// addrOrZero. Returns TargetPointer.Null if no entry matches.
TargetPointer FindEnCDataEntry(TargetPointer module, uint methodDef,
TargetPointer addrOrZero)
{
TargetPointer cur = _target.ReadPointer(module + /* Module::EnCDataList offset */);
while (cur != TargetPointer.Null)
{
uint token = _target.Read<uint>(cur + /* EnCData::Token offset */);
TargetPointer addrOfCode = _target.ReadPointer(cur + /* EnCData::AddrOfCode offset */);
if (token == methodDef &&
(addrOrZero == TargetPointer.Null || addrOfCode == addrOrZero))
{
return cur;
}
cur = _target.ReadPointer(cur + /* EnCData::Next offset */);
}
return TargetPointer.Null;
}
```

``` csharp
TargetNUInt GetLatestEnCVersion(TargetPointer module, uint methodDef)
{
TargetPointer entry = FindEnCDataEntry(module, methodDef, TargetPointer.Null);
if (entry == TargetPointer.Null)
return new TargetNUInt(/* CorDBDefaultEnCFunctionVersion global */);

return _target.ReadNUInt(entry + /* EnCData::EnCVersion offset */);
}
```

``` csharp
TargetNUInt GetEnCVersion(TargetPointer module, uint methodDef,
TargetCodePointer nativeCodeAddress)
{
if (nativeCodeAddress.Value == 0)
return new TargetNUInt(/* CorDBDefaultEnCFunctionVersion global */);

TargetPointer entry = FindEnCDataEntry(module, methodDef,
new TargetPointer(nativeCodeAddress.Value));
if (entry == TargetPointer.Null)
return new TargetNUInt(/* CorDBDefaultEnCFunctionVersion global */);

return _target.ReadNUInt(entry + /* EnCData::EnCVersion offset */);
}
```

11 changes: 11 additions & 0 deletions docs/design/datacontracts/Loader.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ TargetPointer GetILBase(ModuleHandle handle);
TargetPointer GetAssemblyLoadContext(ModuleHandle handle);
ModuleLookupTables GetLookupTables(ModuleHandle handle);
TargetPointer GetModuleLookupMapElement(TargetPointer table, uint token, out TargetNUInt flags);
TargetPointer LookupMemberRefAsMethod(ModuleHandle handle, uint memberRefToken);
IEnumerable<(TargetPointer, uint)> EnumerateModuleLookupMap(TargetPointer table);
bool IsCollectible(ModuleHandle handle);
bool IsDynamic(ModuleHandle handle);
Expand Down Expand Up @@ -254,6 +255,7 @@ enum ClrModifiableAssemblies : uint
| `MaxWebcilSections` | ushort | Maximum number of COFF sections supported in a Webcil image (must stay in sync with native `WEBCIL_MAX_SECTIONS`) | `16` |
| `DebuggerInfoMask` | uint | Mask for the debugger info bits within the Module's transient flags | `0x0000FC00` |
| `DebuggerInfoShift` | int | Bit shift for the debugger info bits within the Module's transient flags | `10` |
| `IS_FIELD_MEMBER_REF` | TADDR (target pointer-sized unsigned int) | Flag on `MemberRefToDescMap` entries indicating the entry is a FieldDesc, not a MethodDesc | `0x00000002` |
| `DEBUGGER_ALLOW_JIT_OPTS_PRIV` | uint | Debugger allows JIT optimizations (shifted in transient flags) | `0x00000800` |

Contracts used:
Expand Down Expand Up @@ -717,6 +719,15 @@ TargetPointer GetModuleLookupMapElement(TargetPointer table, uint token, out Tar
return TargetPointer.Null;
}

// Returns the MethodDesc pointer for the given mdMemberRef token, or TargetPointer.Null
// if the entry is a FieldDesc (flagged with IS_FIELD_MEMBER_REF) or not present.
TargetPointer LookupMemberRefAsMethod(ModuleHandle handle, uint memberRefToken)
{
ModuleLookupTables tables = GetLookupTables(handle);
TargetPointer ptr = GetModuleLookupMapElement(tables.MemberRefToDesc, memberRefToken, out TargetNUInt flags);
return (flags.Value & /* IS_FIELD_MEMBER_REF */) != 0 ? TargetPointer.Null : ptr;
Comment thread
rcj1 marked this conversation as resolved.
Comment thread
rcj1 marked this conversation as resolved.
}

IEnumerable<(TargetPointer, uint)> EnumerateModuleLookupMap(TargetPointer table)
{
Data.ModuleLookupMap lookupMap = new Data.ModuleLookupMap(table);
Expand Down
47 changes: 45 additions & 2 deletions docs/design/datacontracts/RuntimeTypeSystem.md
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,10 @@ partial interface IRuntimeTypeSystem : IContract
// Return true if the method is an async thunk method.
public virtual bool IsAsyncThunkMethod(MethodDescHandle methodDesc);

// Return the async variant MethodDesc for the given method, or null if not found.
// Mirrors the no-create path of native MethodDesc::GetAsyncVariantNoCreate.
public virtual MethodDescHandle? GetAsyncVariant(MethodDescHandle methodDesc);

// Return true if the method is a wrapper stub (unboxing or instantiating).
public virtual bool IsWrapperStub(MethodDescHandle methodDesc);

Expand Down Expand Up @@ -1212,8 +1216,11 @@ And the following enumeration definitions
[Flags]
internal enum AsyncMethodFlags : uint
{
None = 0,
Thunk = 16,
None = 0x0,
AsyncCall = 0x1,
IsAsyncVariant = 0x4,
Thunk = 0x10,
ReturnDroppingThunk = 0x20,
}

[Flags]
Expand Down Expand Up @@ -1698,6 +1705,42 @@ Determining if a method is a wrapper stub (unboxing or instantiating):
}
```

Resolving the async variant of an async thunk method (no-create):

```csharp
// Helper: matches native MatchesAsyncVariantLookup(AsyncVariantLookup::Async).
private bool IsAsyncVariantMethod(MethodDesc md)
{
if (!md.HasAsyncMethodData)
return false;

Data.AsyncMethodData asyncData = // Read AsyncMethodData from the async method data optional slot
AsyncMethodFlags flags = (AsyncMethodFlags)asyncData.Flags;
return flags.HasFlag(AsyncMethodFlags.IsAsyncVariant) && !flags.HasFlag(AsyncMethodFlags.ReturnDroppingThunk);
}

public MethodDescHandle? GetAsyncVariant(MethodDescHandle methodDescHandle)
{
MethodDesc md = _methodDescs[methodDescHandle.Address];
uint token = md.Token;
TypeHandle typeHandle = GetTypeHandle(GetMethodTable(methodDescHandle));

// For typical method definitions (non-generic or generic-with-formal-vars), native's
// FindOrCreateAssociatedMethodDesc returns the result of MethodTable::GetParallelMethodDesc
// directly. The canonical MethodTable holds the chunk of sibling MethodDescs that share
// a token; one of them carries the async-variant flag combination.
TypeHandle canonMT = GetTypeHandle(GetCanonicalMethodTable(typeHandle));
foreach (MethodDescHandle candidate in GetIntroducedMethods(canonMT))
{
MethodDesc candidateMd = _methodDescs[candidate.Address];
if (candidateMd.Token == token && IsAsyncVariantMethod(candidateMd))
return candidate;
}

return null;
}
```

Extracting a pointer to the `MethodDescVersioningState` data for a given method

```csharp
Expand Down
69 changes: 20 additions & 49 deletions src/coreclr/debug/daccess/dacdbiimpl.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1255,8 +1255,7 @@ HRESULT STDMETHODCALLTYPE DacDbiInterfaceImpl::GetNativeCodeInfo(VMPTR_Assembly
if (pCodeInfo->m_rgCodeRegions[kHot].pAddress != (CORDB_ADDRESS)NULL)
{
pCodeInfo->isInstantiatedGeneric = pMethodDesc->HasClassOrMethodInstantiation();
LookupEnCVersions(pModule,
pCodeInfo->vmNativeCodeMethodDescToken,
LookupEnCVersions(pCodeInfo->vmNativeCodeMethodDescToken,
functionToken,
pCodeInfo->m_rgCodeRegions[kHot].pAddress,
&(pCodeInfo->encVersion));
Expand Down Expand Up @@ -1337,11 +1336,10 @@ HRESULT STDMETHODCALLTYPE DacDbiInterfaceImpl::GetNativeCodeInfoForAddr(CORDB_AD
pCodeInfo->isInstantiatedGeneric = pMethodDesc->HasClassOrMethodInstantiation();
pCodeInfo->vmNativeCodeMethodDescToken = vmMethodDesc;

SIZE_T unusedLatestEncVersion;
ULONG64 unusedLatestEncVersion;
Module * pModule = pMethodDesc->GetModule();
_ASSERTE(pModule != NULL);
LookupEnCVersions(pModule,
vmMethodDesc,
LookupEnCVersions(vmMethodDesc,
pMethodDesc->GetMemberDef(),
codeStartAddr,
&unusedLatestEncVersion, //unused by caller
Expand Down Expand Up @@ -5602,60 +5600,33 @@ HRESULT STDMETHODCALLTYPE DacDbiInterfaceImpl::GetPartialUserState(VMPTR_Thread
// thinking is that some of the RS data structures will remain, most likely in a reduced form.
//

void DacDbiInterfaceImpl::LookupEnCVersions(Module* pModule,
VMPTR_MethodDesc vmMethodDesc,
void DacDbiInterfaceImpl::LookupEnCVersions(VMPTR_MethodDesc vmMethodDesc,
mdMethodDef mdMethod,
CORDB_ADDRESS pNativeStartAddress,
SIZE_T * pLatestEnCVersion,
SIZE_T * pJittedInstanceEnCVersion /* = NULL */)
ULONG64 * pLatestEnCVersion,
ULONG64 * pJittedInstanceEnCVersion /* = NULL */)
{
MethodDesc * pMD = vmMethodDesc.GetDacPtr();

// make sure the vmMethodDesc and mdMethod match
_ASSERTE(pMD->GetMemberDef() == mdMethod);

MethodDesc * pMD = vmMethodDesc.GetDacPtr();
_ASSERTE(pLatestEnCVersion != NULL);

// @dbgtodo inspection - once we do EnC, stop using DMIs.
// If the method wasn't EnCed, DMIs may not exist. And since this is DAC, we can't create them.

// We may not have the memory for the DebuggerMethodInfos in a minidump.
// When dump debugging EnC information isn't very useful so just fallback
// to default version.
DebuggerMethodInfo * pDMI = NULL;
DebuggerJitInfo * pDJI = NULL;
EX_TRY_ALLOW_DATATARGET_MISSING_MEMORY
#ifndef FEATURE_METADATA_UPDATER
if (pJittedInstanceEnCVersion != NULL)
{
if (g_pDebugger != NULL)
{
pDMI = g_pDebugger->GetOrCreateMethodInfo(pModule, mdMethod);
if (pDMI != NULL)
{
pDJI = pDMI->FindJitInfo(pMD, CORDB_ADDRESS_TO_TADDR(pNativeStartAddress));
}
}
*pJittedInstanceEnCVersion = CorDB_DEFAULT_ENC_FUNCTION_VERSION;
}
EX_END_CATCH_ALLOW_DATATARGET_MISSING_MEMORY;
if (pDJI != NULL)
if (pLatestEnCVersion != NULL)
{
if (pJittedInstanceEnCVersion != NULL)
{
*pJittedInstanceEnCVersion = pDJI->m_encVersion;
}
*pLatestEnCVersion = pDMI->GetCurrentEnCVersion();
*pLatestEnCVersion = CorDB_DEFAULT_ENC_FUNCTION_VERSION;
}
else
#else
Module* pLoaderModule = pMD->GetLoaderModule();
PTR_EnCData pEnCData = pLoaderModule->FindEncData(mdMethod, CORDB_ADDRESS_TO_TADDR(pNativeStartAddress));
PTR_EnCData pLatestEncData = pLoaderModule->FindLatestEncData(mdMethod);
Comment on lines +5621 to +5623
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is fine now because heap dumps will include this as it is off the loader allocator

@hoyosjs

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about mini or triage dumps? I would not expect them to contain all the memory from the loader allocator.

Comment thread
rcj1 marked this conversation as resolved.
if (pJittedInstanceEnCVersion != NULL)
{
// If we have no DMI/DJI, then we must never have EnCed. So we can use default EnC info
// Several cases where we don't have a DMI/DJI:
// - LCG methods
// - method was never "touched" by debugger. (DJIs are created lazily).
if (pJittedInstanceEnCVersion != NULL)
{
*pJittedInstanceEnCVersion = CorDB_DEFAULT_ENC_FUNCTION_VERSION;
}
*pLatestEnCVersion = CorDB_DEFAULT_ENC_FUNCTION_VERSION;
*pJittedInstanceEnCVersion = (pEnCData != NULL) ? pEnCData->encVersion : CorDB_DEFAULT_ENC_FUNCTION_VERSION;
}
*pLatestEnCVersion = (pLatestEncData != NULL) ? pLatestEncData->encVersion : CorDB_DEFAULT_ENC_FUNCTION_VERSION;
#endif // FEATURE_METADATA_UPDATER
}

// Get the address of the Debugger control block on the helper thread
Expand Down
7 changes: 3 additions & 4 deletions src/coreclr/debug/daccess/dacdbiimpl.h
Original file line number Diff line number Diff line change
Expand Up @@ -883,12 +883,11 @@ class DacDbiInterfaceImpl :
BOOL UnwindRuntimeStackFrame(StackFrameIterator * pIter);

// Look up the EnC version number of a particular jitted instance of a managed method.
void LookupEnCVersions(Module* pModule,
VMPTR_MethodDesc vmMethodDesc,
void LookupEnCVersions(VMPTR_MethodDesc vmMethodDesc,
mdMethodDef mdMethod,
CORDB_ADDRESS pNativeStartAddress,
SIZE_T * pLatestEnCVersion,
SIZE_T * pJittedInstanceEnCVersion = NULL);
ULONG64 * pLatestEnCVersion,
ULONG64 * pJittedInstanceEnCVersion = NULL);

// @dbgtodo - This method should be removed once CordbFunctionBreakpoint and SetIP are moved OOP and
// no longer use nativeCodeJITInfoToken.
Expand Down
2 changes: 1 addition & 1 deletion src/coreclr/debug/di/divalue.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2718,7 +2718,7 @@ HRESULT CordbObjectValue::GetFunctionHelper(ICorDebugFunction **ppFunction)
IfFailThrow(pDAC->GetNativeCodeInfo(functionAssembly, functionMethodDef, &nativeCodeForDelFunc));

RSSmartPtr<CordbModule> funcModule(GetAppDomain()->LookupOrCreateModule(functionAssembly));
func.Assign(funcModule->LookupOrCreateFunction(functionMethodDef, nativeCodeForDelFunc.encVersion));
func.Assign(funcModule->LookupOrCreateFunction(functionMethodDef, (SIZE_T)nativeCodeForDelFunc.encVersion));
}

*ppFunction = static_cast<ICorDebugFunction*> (func.GetValue());
Expand Down
4 changes: 2 additions & 2 deletions src/coreclr/debug/di/module.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3902,7 +3902,7 @@ HRESULT CordbVariableHome::GetOffset(LONG *pOffset)
CordbNativeCode::CordbNativeCode(CordbFunction * pFunction,
const NativeCodeFunctionData * pJitData,
BOOL fIsInstantiatedGeneric)
: CordbCode(pFunction, (UINT_PTR)pJitData->m_rgCodeRegions[kHot].pAddress, pJitData->encVersion, FALSE),
: CordbCode(pFunction, (UINT_PTR)pJitData->m_rgCodeRegions[kHot].pAddress, (SIZE_T)pJitData->encVersion, FALSE),
m_vmNativeCodeMethodDescToken(pJitData->vmNativeCodeMethodDescToken),
m_fCodeAvailable(TRUE),
m_fIsInstantiatedGeneric(fIsInstantiatedGeneric != FALSE)
Expand Down Expand Up @@ -5093,7 +5093,7 @@ CordbNativeCode * CordbModule::LookupOrCreateNativeCode(mdMethodDef methodToken,
codeInfo.m_rgCodeRegions[kHot].cbSize));

// Lookup the function object that this code should be bound to
CordbFunction* pFunction = CordbModule::LookupOrCreateFunction(methodToken, codeInfo.encVersion);
CordbFunction* pFunction = CordbModule::LookupOrCreateFunction(methodToken, (SIZE_T)codeInfo.encVersion);
_ASSERTE(pFunction != NULL);

// There are bugs with the on-demand class load performed by CordbFunction in some cases. The old stack
Expand Down
11 changes: 11 additions & 0 deletions src/coreclr/debug/ee/functioninfo.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1226,6 +1226,17 @@ void DebuggerJitInfo::Init(TADDR newAddress)
this->m_sizeOfCode = this->m_codeRegionInfo.getSizeOfTotalCode();

this->m_encVersion = this->m_methodInfo->GetCurrentEnCVersion();
#ifdef FEATURE_METADATA_UPDATER
if (this->m_encVersion != CorDB_DEFAULT_ENC_FUNCTION_VERSION)
{
Module* pModule = this->m_pLoaderModule;
EnCData* pEnCData = (EnCData*)(void*)pModule->GetLoaderAllocator()->GetLowFrequencyHeap()->AllocMem(S_SIZE_T(sizeof(EnCData)));
pEnCData->addrOfCode = (TADDR)this->m_addrOfCode;
pEnCData->token = this->m_methodInfo->m_token;
pEnCData->encVersion = this->m_encVersion;
pModule->AddEncData(pEnCData);
Comment thread
rcj1 marked this conversation as resolved.
Comment on lines 1228 to +1237
Copy link
Copy Markdown
Contributor Author

@rcj1 rcj1 May 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good catch. Only possibly relevant in

HRESULT CordbObjectValue::GetFunction(ICorDebugFunction **ppFunction)
, but could return the latest jitted version as opposed to the latest version total. This is probably worth fixing

}
#endif // FEATURE_METADATA_UPDATER

this->InitFuncletAddress();

Expand Down
2 changes: 1 addition & 1 deletion src/coreclr/debug/inc/dacdbistructures.h
Original file line number Diff line number Diff line change
Expand Up @@ -509,7 +509,7 @@ class MSLAYOUT NativeCodeFunctionData
VMPTR_MethodDesc vmNativeCodeMethodDescToken;

// EnC version number of the function
SIZE_T encVersion;
ULONG64 encVersion;
Comment thread
rcj1 marked this conversation as resolved.
Comment thread
rcj1 marked this conversation as resolved.
};

//----------------------------------------------------------------------------------
Expand Down
Loading
Loading