diff --git a/docs/design/datacontracts/DebugInfo.md b/docs/design/datacontracts/DebugInfo.md index 529b9db44d30b2..6ff8348cc01e42 100644 --- a/docs/design/datacontracts/DebugInfo.md +++ b/docs/design/datacontracts/DebugInfo.md @@ -389,3 +389,46 @@ Each variable entry in the Vars section is nibble-encoded as follows: | `VLT_FIXED_VA` | offset (encoded unsigned) | Signed integers are encoded using the same unsigned scheme, with the sign bit stored in bit 0 (`value = unsigned >> 1`, negate if `unsigned & 1`). On x86, stack offsets are DWORD-aligned and stored divided by `sizeof(DWORD)`. + +### Async Suspension Point APIs (Version 2+) + +We also support decoding async suspension points (and their captured continuation-object locals) from the `AsyncInfo` chunk of the debug info blob. The chunk is present only for methods that the JIT compiled with runtime-async suspension points; for all other methods, `AsyncInfoSize` is `0` in the FAT header and the API returns an empty list. + +Additional types: +```csharp +// A native code location at which an async method may suspend, together with +// the continuation-object locals captured at that point. +public readonly struct AsyncSuspensionInfo +{ + public uint NativeOffset { get; init; } + public IReadOnlyList Locals { get; init; } +} + +// A single local captured into the continuation object at a suspension point. +public readonly struct AsyncLocalInfo +{ + // Offset of the local within the continuation object's data area. + public uint Offset { get; init; } + // IL var number of the local (or a synthetic marker such as MAX_ILNUM-relative values). + public uint ILVarNumber { get; init; } +} +``` + +```csharp +IReadOnlyList GetAsyncSuspensionPoints(TargetCodePointer pCode); +``` + +### AsyncInfo Data Encoding + +Each entry is nibble-encoded as follows: + +1. `NumSuspensionPoints` — encoded unsigned 32-bit integer. +2. Total var count across all suspension points — encoded unsigned 32-bit integer. Informational only; the decoder reads it but does not need it, since the per-suspension-point counts read in step 3 already cover the entire flat var list. +3. For each of the `NumSuspensionPoints` suspension points (in order): + * `DiagnosticNativeOffset` — encoded signed delta from the previous suspension point's offset (the first delta is from `0`). Deltas are not required to be monotonic. + * `NumContinuationVars` — encoded unsigned 32-bit integer giving the number of continuation locals captured at this suspension point. +4. For each var (a single flat sequence, partitioned by the `NumContinuationVars` counts from step 3, in suspension-point order): + * `VarNumber - MAX_ILNUM` — encoded unsigned 32-bit integer. The `MAX_ILNUM` bias keeps the synthetic negative IL var numbers (e.g. `VARARGS_HND_ILNUM`, `RETBUF_ILNUM`, `TYPECTXT_ILNUM`) representable as unsigned values; the decoder reverses the bias by adding `MAX_ILNUM` back. + * `Offset` — encoded unsigned 32-bit integer giving the byte offset of the local within the continuation object's data area. + +`AsyncSuspensionInfo.Locals[i]` for the `n`-th suspension point therefore corresponds to the `i`-th var in the flat sequence whose flat index is the prefix sum of `NumContinuationVars[0..n-1]` plus `i`. diff --git a/docs/design/datacontracts/Object.md b/docs/design/datacontracts/Object.md index e81e73feef3aa2..503d1aed964daf 100644 --- a/docs/design/datacontracts/Object.md +++ b/docs/design/datacontracts/Object.md @@ -17,6 +17,12 @@ public readonly record struct DelegateInfo( TargetCodePointer TargetMethodPtr, DelegateType DelegateType); +// DiagnosticIP is TargetPointer.Null when the continuation has no ResumeInfo. +public readonly record struct ContinuationInfo( + TargetPointer Next, + TargetPointer DiagnosticIP, + uint State); + // Get the method table address for the object TargetPointer GetMethodTableAddress(TargetPointer address); @@ -37,6 +43,9 @@ int TryGetHashCode(TargetPointer address); TargetPointer GetSyncBlockAddress(TargetPointer address); DelegateInfo GetDelegateInfo(TargetPointer address); + +// Get the linked-list / diagnostic-IP / state triple for a runtime-async continuation object. +ContinuationInfo GetContinuationInfo(TargetPointer address); ``` ## Version 1 @@ -55,6 +64,10 @@ Data descriptors used: | `Delegate` | `MethodPtr` | Primary method pointer | | `Delegate` | `MethodPtrAux` | Auxiliary method pointer | | `Delegate` | `InvocationCount` | Invocation count (non-zero for multicast/wrapper/unmanaged/special delegates) | +| `ContinuationObject` | `Next` | Pointer to the next continuation in the linked list | +| `ContinuationObject` | `ResumeInfo` | Pointer to the `ResumeInfo` for this suspension point (may be null) | +| `ContinuationObject` | `State` | State index identifying the suspension point within the resumed method | +| `AsyncResumeInfo` | `DiagnosticIP` | Native IP into the resumed method used for diagnostics (may be null) | Global variables used: | Global Name | Type | Purpose | @@ -210,4 +223,21 @@ DelegateInfo GetDelegateInfo(TargetPointer address) return new DelegateInfo(targetObject, targetMethodPtr, delegateType); } + +ContinuationInfo GetContinuationInfo(TargetPointer address) +{ + TargetPointer next = target.ReadPointer(address + /* ContinuationObject::Next offset */); + TargetPointer resumeInfo = target.ReadPointer(address + /* ContinuationObject::ResumeInfo offset */); + uint state = (uint)target.Read(address + /* ContinuationObject::State offset */); + + // ResumeInfo may be null + TargetPointer diagnosticIP = resumeInfo != TargetPointer.Null + ? target.ReadPointer(resumeInfo + /* AsyncResumeInfo::DiagnosticIP offset */) + : TargetPointer.Null; + + return new ContinuationInfo( + Next: next, + DiagnosticIP: diagnosticIP, + State: state); +} ``` diff --git a/src/coreclr/debug/daccess/dacdbiimpl.cpp b/src/coreclr/debug/daccess/dacdbiimpl.cpp index db9423528f089f..0716eee6ce6d15 100644 --- a/src/coreclr/debug/daccess/dacdbiimpl.cpp +++ b/src/coreclr/debug/daccess/dacdbiimpl.cpp @@ -7410,10 +7410,12 @@ static BYTE* DebugInfoStoreNew(void * pData, size_t cBytes) return new (nothrow) BYTE[cBytes]; } -HRESULT STDMETHODCALLTYPE DacDbiInterfaceImpl::GetAsyncLocals(VMPTR_MethodDesc vmMethod, CORDB_ADDRESS codeAddr, UINT32 state, OUT DacDbiArrayList* pAsyncLocals) +HRESULT STDMETHODCALLTYPE DacDbiInterfaceImpl::EnumerateAsyncLocals(VMPTR_MethodDesc vmMethod, CORDB_ADDRESS codeAddr, UINT32 state, FP_ASYNC_LOCAL_CALLBACK fpCallback, CALLBACK_DATA pUserData) { DD_ENTER_MAY_THROW; + _ASSERTE(fpCallback != NULL); + HRESULT hr = S_OK; EX_TRY { @@ -7465,13 +7467,14 @@ HRESULT STDMETHODCALLTYPE DacDbiInterfaceImpl::GetAsyncLocals(VMPTR_MethodDesc v } UINT32 varCount = asyncSuspensionPoints[state].NumContinuationVars; - pAsyncLocals->Alloc(varCount); _ASSERTE(varBeginIndex + varCount <= cAsyncVars); for (UINT32 i = 0; i < varCount; i++) { - (*pAsyncLocals)[i].offset = asyncVars[varBeginIndex + i].Offset; - (*pAsyncLocals)[i].ilVarNum = asyncVars[varBeginIndex + i].VarNumber; + AsyncLocalData local; + local.offset = asyncVars[varBeginIndex + i].Offset; + local.ilVarNum = asyncVars[varBeginIndex + i].VarNumber; + fpCallback(&local, pUserData); } } EX_CATCH_HRESULT(hr); diff --git a/src/coreclr/debug/daccess/dacdbiimpl.h b/src/coreclr/debug/daccess/dacdbiimpl.h index a59bedda6e60c3..cefe6d2ae0f94b 100644 --- a/src/coreclr/debug/daccess/dacdbiimpl.h +++ b/src/coreclr/debug/daccess/dacdbiimpl.h @@ -151,7 +151,7 @@ class DacDbiInterfaceImpl : OUT PCODE *pDiagnosticIP, OUT CORDB_ADDRESS *pNextContinuation, OUT UINT32 *pState); - HRESULT STDMETHODCALLTYPE GetAsyncLocals(VMPTR_MethodDesc vmMethod, CORDB_ADDRESS codeAddr, UINT32 state, OUT DacDbiArrayList* pAsyncLocals); + HRESULT STDMETHODCALLTYPE EnumerateAsyncLocals(VMPTR_MethodDesc vmMethod, CORDB_ADDRESS codeAddr, UINT32 state, FP_ASYNC_LOCAL_CALLBACK fpCallback, CALLBACK_DATA pUserData); HRESULT STDMETHODCALLTYPE GetGenericArgTokenIndex(VMPTR_MethodDesc vmMethod, OUT UINT32* pIndex); private: diff --git a/src/coreclr/debug/di/rspriv.h b/src/coreclr/debug/di/rspriv.h index c61d532dde6680..f9b36d43106159 100644 --- a/src/coreclr/debug/di/rspriv.h +++ b/src/coreclr/debug/di/rspriv.h @@ -11432,8 +11432,7 @@ class CordbAsyncFrame : public CordbBase, public ICorDebugILFrame, public ICorDe CORDB_ADDRESS m_diagnosticIP; CORDB_ADDRESS m_continuationAddress; UINT32 m_state; - int m_nNumberOfVars; - DacDbiArrayList m_asyncVars; + CQuickArrayList m_asyncVars; Instantiation m_genericArgs; // the generics type arguments BOOL m_genericArgsLoaded; // whether we have loaded and cached the generics type arguments diff --git a/src/coreclr/debug/di/rsthread.cpp b/src/coreclr/debug/di/rsthread.cpp index fc8dbb8e0706e6..3de12c93cec2f0 100644 --- a/src/coreclr/debug/di/rsthread.cpp +++ b/src/coreclr/debug/di/rsthread.cpp @@ -10996,7 +10996,21 @@ HRESULT CordbAsyncFrame::Init() // LookupOrCreateNativeCode is marked INTERNAL_SYNC_API_ENTRY and requires the StopGoLock. m_pCode.Assign(pModule->LookupOrCreateNativeCode(m_methodDef, m_vmMethodDesc, m_pCodeStart)); m_pCode->LoadNativeInfo(); - IfFailThrow(GetProcess()->GetDAC()->GetAsyncLocals(m_vmMethodDesc, m_pCodeStart, m_state, &m_asyncVars)); + + { + CallbackAccumulator acc; + IfFailThrow(GetProcess()->GetDAC()->EnumerateAsyncLocals( + m_vmMethodDesc, m_pCodeStart, m_state, + &CallbackAccumulator::PushCallback, + &acc)); + IfFailThrow(acc.hrError); + + SIZE_T cLocals = acc.items.Size(); + for (SIZE_T i = 0; i < cLocals; i++) + { + m_asyncVars.Push(acc.items[i]); + } + } // Initialize function and IL code m_pFunction.Assign(m_pCode->GetFunction()); @@ -11038,7 +11052,6 @@ void CordbAsyncFrame::Neuter() return; m_pCode.Clear(); - m_asyncVars.Dealloc(); m_pAppDomain.Clear(); m_pFunction.Clear(); @@ -11259,7 +11272,7 @@ HRESULT CordbAsyncFrame::GetArgument(DWORD dwIndex, ICorDebugValue ** ppValue) CordbType * pType; LoadGenericArgs(); m_pCode->GetArgumentType(dwIndex, &m_genericArgs, &pType); - for (unsigned int i = 0; i < m_asyncVars.Count(); i++) + for (unsigned int i = 0; i < m_asyncVars.Size(); i++) { if (m_asyncVars[i].ilVarNum == dwIndex) { @@ -11438,7 +11451,7 @@ HRESULT CordbAsyncFrame::GetLocalVariableEx(ILCodeKind flags, DWORD dwIndex, ICo #endif // FEATURE_CODE_VERSIONING IfFailThrow(pActiveCode->GetLocalVariableType(dwIndex, &m_genericArgs, &pType)); - for (unsigned int i = 0 ; i < m_asyncVars.Count(); i++) + for (unsigned int i = 0 ; i < m_asyncVars.Size(); i++) { if (m_asyncVars[i].ilVarNum == dwIndex+argCount) { @@ -11533,7 +11546,7 @@ void CordbAsyncFrame::LoadGenericArgs() CORDB_ADDRESS genericTypeParam = 0; if (result == S_OK) { - for (unsigned int i = 0 ; i < m_asyncVars.Count(); i++) + for (unsigned int i = 0 ; i < m_asyncVars.Size(); i++) { if (m_asyncVars[i].ilVarNum == genericArgIndex) { diff --git a/src/coreclr/debug/inc/dacdbiinterface.h b/src/coreclr/debug/inc/dacdbiinterface.h index 5665c617629706..d10d8ce5b8faba 100644 --- a/src/coreclr/debug/inc/dacdbiinterface.h +++ b/src/coreclr/debug/inc/dacdbiinterface.h @@ -2265,7 +2265,30 @@ IDacDbiInterface : public IUnknown OUT CORDB_ADDRESS* pNextContinuation, OUT UINT32* pState) = 0; - virtual HRESULT STDMETHODCALLTYPE GetAsyncLocals(VMPTR_MethodDesc vmMethod, CORDB_ADDRESS codeAddr, UINT32 state, OUT DacDbiArrayList* pAsyncLocals) = 0; + // Callback invoked once per async local enumerated by EnumerateAsyncLocals. + // The callback must not throw. Implementations typically push the value into an + // accumulator stashed in pUserData (see CallbackAccumulator). + typedef void (*FP_ASYNC_LOCAL_CALLBACK)(AsyncLocalData * pLocal, CALLBACK_DATA pUserData); + + // Enumerate the async locals captured at a given async suspension point. + // + // Arguments: + // vmMethod - the async method in question + // codeAddr - native code address used to disambiguate code versions; when 0 + // the active native code of vmMethod is used + // state - index of the async suspension point whose locals are requested + // fpCallback - callback invoked once per AsyncLocalData; must not be NULL and + // must not throw + // pUserData - opaque user data passed through to the callback + // + // Notes: + // Returns S_OK with no callbacks invoked when: + // - vmMethod refers to an async thunk method + // - codeAddr is non-zero but does not resolve to a valid native code version + // - state is past the number of suspension points reported for the method + // Otherwise returns S_OK after invoking fpCallback for every local captured at + // suspension point `state`. + virtual HRESULT STDMETHODCALLTYPE EnumerateAsyncLocals(VMPTR_MethodDesc vmMethod, CORDB_ADDRESS codeAddr, UINT32 state, FP_ASYNC_LOCAL_CALLBACK fpCallback, CALLBACK_DATA pUserData) = 0; virtual HRESULT STDMETHODCALLTYPE GetGenericArgTokenIndex( VMPTR_MethodDesc vmMethod, diff --git a/src/coreclr/inc/dacdbi.idl b/src/coreclr/inc/dacdbi.idl index 700bd4c85e2618..d683dea29b0a2e 100644 --- a/src/coreclr/inc/dacdbi.idl +++ b/src/coreclr/inc/dacdbi.idl @@ -30,7 +30,6 @@ struct DebuggerIPCE_ObjectData; struct MonitorLockInfo; struct DacGcReference; struct DacSharedReJitInfo; -struct AsyncLocalData; // // Types MIDL cannot handle natively. These dummy definitions satisfy the MIDL @@ -42,6 +41,9 @@ cpp_quote("#if 0") // Passed by value in IsMatchingParentFrame, GetFramePointer, and the internal frame callback. typedef struct { UINT_PTR m_sp; } FramePointer; +// AsyncLocalData - struct passed by pointer to FP_ASYNC_LOCAL_CALLBACK. +typedef struct { UINT_PTR dummy; } AsyncLocalData; + // VMPTR types - opaque pointer-sized wrappers typedef ULONG64 VMPTR_AppDomain; typedef ULONG64 VMPTR_OBJECTHANDLE; @@ -129,7 +131,6 @@ typedef struct { void *pList; int nEntries; } DacDbiArrayList_CORDB_ADDRESS; typedef struct { void *pList; int nEntries; } DacDbiArrayList_GUID; typedef struct { void *pList; int nEntries; } DacDbiArrayList_COR_SEGMENT; typedef struct { void *pList; int nEntries; } DacDbiArrayList_COR_MEMORY_RANGE; -typedef struct { void *pList; int nEntries; } DacDbiArrayList_AsyncLocalData; cpp_quote("#endif") @@ -142,6 +143,7 @@ typedef BOOL (*FP_INTERNAL_FRAME_ENUMERATION_CALLBACK)(FramePointer fpFrame, CAL typedef void (*FP_HEAPSEGMENT_CALLBACK)(CORDB_ADDRESS rangeStart, CORDB_ADDRESS rangeEnd, int generation, ULONG heap, CALLBACK_DATA pUserData); typedef void (*FP_RCW_INTERFACE_CALLBACK)(CORDB_ADDRESS itfPtr, CALLBACK_DATA pUserData); typedef void (*FP_EXCEPTION_STACK_FRAME_CALLBACK)(VMPTR_AppDomain vmAppDomain, VMPTR_Assembly vmAssembly, CORDB_ADDRESS ip, mdMethodDef methodDef, BOOL isLastForeignExceptionFrame, CALLBACK_DATA pUserData); +typedef void (*FP_ASYNC_LOCAL_CALLBACK)(AsyncLocalData *pLocal, CALLBACK_DATA pUserData); typedef void (*FP_FIELDDATA_CALLBACK)(struct FieldData * pFieldData, CALLBACK_DATA pUserData); @@ -435,7 +437,7 @@ interface IDacDbiInterface : IUnknown [out] PCODE * pDiagnosticIP, [out] CORDB_ADDRESS * pNextContinuation, [out] UINT32 * pState); - HRESULT GetAsyncLocals([in] VMPTR_MethodDesc vmMethod, [in] CORDB_ADDRESS codeAddr, [in] UINT32 state, [out] DacDbiArrayList_AsyncLocalData * pAsyncLocals); + HRESULT EnumerateAsyncLocals([in] VMPTR_MethodDesc vmMethod, [in] CORDB_ADDRESS codeAddr, [in] UINT32 state, [in] FP_ASYNC_LOCAL_CALLBACK fpCallback, [in] CALLBACK_DATA pUserData); // Generic Arg Token HRESULT GetGenericArgTokenIndex([in] VMPTR_MethodDesc vmMethod, [out] UINT32 * pTokenIndex); diff --git a/src/coreclr/vm/datadescriptor/datadescriptor.inc b/src/coreclr/vm/datadescriptor/datadescriptor.inc index 70dc6b27ebff2d..d4dd99d7b34be4 100644 --- a/src/coreclr/vm/datadescriptor/datadescriptor.inc +++ b/src/coreclr/vm/datadescriptor/datadescriptor.inc @@ -186,8 +186,16 @@ CDAC_TYPE_END(String) CDAC_TYPE_BEGIN(ContinuationObject) CDAC_TYPE_SIZE(sizeof(ContinuationObject)) +CDAC_TYPE_FIELD(ContinuationObject, T_POINTER, Next, cdac_data::Next) +CDAC_TYPE_FIELD(ContinuationObject, T_POINTER, ResumeInfo, cdac_data::ResumeInfo) +CDAC_TYPE_FIELD(ContinuationObject, T_INT32, State, cdac_data::State) CDAC_TYPE_END(ContinuationObject) +CDAC_TYPE_BEGIN(AsyncResumeInfo) +CDAC_TYPE_INDETERMINATE(AsyncResumeInfo) +CDAC_TYPE_FIELD(AsyncResumeInfo, T_POINTER, DiagnosticIP, offsetof(CORINFO_AsyncResumeInfo, DiagnosticIP)) +CDAC_TYPE_END(AsyncResumeInfo) + CDAC_TYPE_BEGIN(Array) CDAC_TYPE_SIZE(sizeof(ArrayBase)) CDAC_TYPE_FIELD(Array, T_UINT32, m_NumComponents, cdac_data::m_NumComponents) diff --git a/src/coreclr/vm/object.h b/src/coreclr/vm/object.h index f473dc738b8909..9bb0074acc1060 100644 --- a/src/coreclr/vm/object.h +++ b/src/coreclr/vm/object.h @@ -2122,6 +2122,7 @@ class GenericCacheStruct class ContinuationObject : public Object { friend class CoreLibBinder; + friend struct ::cdac_data; public: CorInfoContinuationFlags GetFlags() const @@ -2234,6 +2235,14 @@ class ContinuationObject : public Object int32_t State; }; +template<> +struct cdac_data +{ + static constexpr size_t Next = offsetof(ContinuationObject, Next); + static constexpr size_t ResumeInfo = offsetof(ContinuationObject, ResumeInfo); + static constexpr size_t State = offsetof(ContinuationObject, State); +}; + // This class corresponds to Exception on the managed side. typedef DPTR(class ExceptionObject) PTR_ExceptionObject; #include "pshpack4.h" diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/IDebugInfo.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/IDebugInfo.cs index a05b2d4315740b..5624ba43f66ac6 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/IDebugInfo.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/IDebugInfo.cs @@ -74,6 +74,29 @@ public readonly struct DebugVarInfo public int StackOffset2 { get; init; } } +/// +/// A native code location at which an async method may suspend, together with the +/// continuation-object locals captured at that point. +/// +public readonly struct AsyncSuspensionInfo +{ + /// The native code offset of the suspension point. + public uint NativeOffset { get; init; } + /// The continuation-object locals live at this suspension point. + public IReadOnlyList Locals { get; init; } +} + +/// +/// A single local captured into the continuation object at a suspension point. +/// +public readonly struct AsyncLocalInfo +{ + /// Offset of the local within the continuation object's data area. + public uint Offset { get; init; } + /// IL var number of the local (or a synthetic marker such as MAX_ILNUM-relative values). + public uint ILVarNumber { get; init; } +} + public interface IDebugInfo : IContract { static string IContract.Name { get; } = nameof(DebugInfo); @@ -91,6 +114,13 @@ public interface IDebugInfo : IContract /// Each entry describes where a variable is stored at a particular native offset range. /// IEnumerable GetMethodVarInfo(TargetCodePointer pCode, out uint codeOffset) => throw new NotImplementedException(); + /// + /// Given a code pointer, return the async-suspension points for the method together with the + /// continuation-object locals captured at each suspension point. Returns an empty list when + /// the method has no async debug info. + /// + IReadOnlyList GetAsyncSuspensionPoints(TargetCodePointer pCode) => + Array.Empty(); } public readonly struct DebugInfo : IDebugInfo diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/IObject.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/IObject.cs index 3348f2a20c9638..70c81df2437b1c 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/IObject.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/IObject.cs @@ -17,6 +17,11 @@ public readonly record struct DelegateInfo( TargetCodePointer TargetMethodPtr, DelegateType DelegateType); +public readonly record struct ContinuationInfo( + TargetPointer Next, + TargetPointer DiagnosticIP, + uint State); + public interface IObject : IContract { static string IContract.Name { get; } = nameof(Object); @@ -28,6 +33,7 @@ public interface IObject : IContract // Returns the SyncBlock address for the object, or TargetPointer.Null if no sync block is associated with it. TargetPointer GetSyncBlockAddress(TargetPointer address) => throw new NotImplementedException(); DelegateInfo GetDelegateInfo(TargetPointer address) => throw new NotImplementedException(); + ContinuationInfo GetContinuationInfo(TargetPointer address) => throw new NotImplementedException(); } public readonly struct Object : IObject diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/DebugInfo/DebugInfoHelpers.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/DebugInfo/DebugInfoHelpers.cs index ab13d1f24b46af..7cda315c5b2824 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/DebugInfo/DebugInfoHelpers.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/DebugInfo/DebugInfoHelpers.cs @@ -188,4 +188,56 @@ private static int ReadEncodedStackOffset(NibbleReader reader, bool isX86) // On x86, stack offsets are DWORD-aligned and stored divided by sizeof(DWORD) return isX86 ? value * sizeof(int) : value; } + + /// + /// Decodes the AsyncInfo chunk produced by EECodeInfo::SetAsyncInfo in debuginfostore.cpp. + /// Layout (NibbleWriter): + /// ReadUInt: NumSuspensionPoints + /// ReadUInt: total var count (informational, unused here) + /// for each suspension point: + /// ReadInt: delta from previous DiagnosticNativeOffset (first delta is from 0) + /// ReadUInt: NumContinuationVars for this suspension point + /// for each var (flat, in suspension-point order): + /// ReadUInt: VarNumber - MAX_ILNUM (sentinel-adjusted to keep negative IL var nums positive) + /// ReadUInt: Offset + /// + internal static IReadOnlyList DoAsyncInfo(NativeReader nativeReader) + { + NibbleReader reader = new(nativeReader, 0); + + uint numSuspensionPoints = reader.ReadUInt(); + // Total var count - not used directly; we read each suspension point's count below. + _ = reader.ReadUInt(); + + if (numSuspensionPoints == 0) + return Array.Empty(); + + // Phase 1: read suspension-point headers (offset + per-point var count). + uint[] nativeOffsets = new uint[numSuspensionPoints]; + uint[] varCounts = new uint[numSuspensionPoints]; + + long lastOffset = 0; + for (uint i = 0; i < numSuspensionPoints; i++) + { + lastOffset += reader.ReadInt(); + nativeOffsets[i] = (uint)lastOffset; + varCounts[i] = reader.ReadUInt(); + } + + // Phase 2: read the flat var list and slice it per suspension point. + AsyncSuspensionInfo[] result = new AsyncSuspensionInfo[numSuspensionPoints]; + for (uint i = 0; i < numSuspensionPoints; i++) + { + uint n = varCounts[i]; + AsyncLocalInfo[] locals = n == 0 ? Array.Empty() : new AsyncLocalInfo[n]; + for (uint v = 0; v < n; v++) + { + uint ilVarNumber = reader.ReadUInt() + MAX_ILNUM; + uint offset = reader.ReadUInt(); + locals[v] = new AsyncLocalInfo { Offset = offset, ILVarNumber = ilVarNumber }; + } + result[i] = new AsyncSuspensionInfo { NativeOffset = nativeOffsets[i], Locals = locals }; + } + return result; + } } diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/DebugInfo/DebugInfo_2.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/DebugInfo/DebugInfo_2.cs index 1a21ea1465c1c0..67967368bb70ff 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/DebugInfo/DebugInfo_2.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/DebugInfo/DebugInfo_2.cs @@ -149,4 +149,22 @@ IEnumerable IDebugInfo.GetMethodVarInfo(TargetCodePointer pCode, o return []; } + + IReadOnlyList IDebugInfo.GetAsyncSuspensionPoints(TargetCodePointer pCode) + { + if (_eman.GetCodeBlockHandle(pCode) is not CodeBlockHandle cbh) + throw new InvalidOperationException($"No CodeBlockHandle found for native code {pCode}."); + TargetPointer debugInfo = _eman.GetDebugInfo(cbh, out bool _); + + if (debugInfo == TargetPointer.Null) + return Array.Empty(); + + DebugInfoChunks chunks = DecodeChunks(debugInfo); + + if (chunks.AsyncInfoSize == 0) + return Array.Empty(); + + NativeReader asyncNativeReader = new(new TargetStream(_target, chunks.AsyncInfoStart, chunks.AsyncInfoSize), _target.IsLittleEndian); + return DebugInfoHelpers.DoAsyncInfo(asyncNativeReader); + } } diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/Object_1.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/Object_1.cs index 89bcf6590abd4f..c4dd0ddc569f8e 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/Object_1.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/Object_1.cs @@ -175,4 +175,16 @@ public DelegateInfo GetDelegateInfo(TargetPointer address) TargetMethodPtr: targetMethodPtr, DelegateType: delegateType); } + + public ContinuationInfo GetContinuationInfo(TargetPointer address) + { + Data.ContinuationObject cont = _target.ProcessedData.GetOrAdd(address); + TargetPointer diagnosticIP = cont.ResumeInfo != TargetPointer.Null + ? _target.ProcessedData.GetOrAdd(cont.ResumeInfo).DiagnosticIP + : TargetPointer.Null; + return new ContinuationInfo( + Next: cont.Next, + DiagnosticIP: diagnosticIP, + State: (uint)cont.State); + } } diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/AsyncResumeInfo.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/AsyncResumeInfo.cs new file mode 100644 index 00000000000000..3780f39cda546c --- /dev/null +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/AsyncResumeInfo.cs @@ -0,0 +1,10 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Diagnostics.DataContractReader.Data; + +[CdacType(nameof(DataType.AsyncResumeInfo))] +internal sealed partial class AsyncResumeInfo : IData +{ + [Field] public TargetPointer DiagnosticIP { get; } +} diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/ContinuationObject.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/ContinuationObject.cs new file mode 100644 index 00000000000000..2674b3519890f9 --- /dev/null +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/ContinuationObject.cs @@ -0,0 +1,12 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Diagnostics.DataContractReader.Data; + +[CdacType(nameof(DataType.ContinuationObject))] +internal sealed partial class ContinuationObject : IData +{ + [Field] public TargetPointer Next { get; } + [Field] public TargetPointer ResumeInfo { get; } + [Field] public int State { get; } +} diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/DataType.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/DataType.cs index 8267818e6ba13d..91a16f32fb6aef 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/DataType.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/DataType.cs @@ -79,6 +79,7 @@ public enum DataType StressMsgHeader, Object, ContinuationObject, + AsyncResumeInfo, NativeObjectWrapperObject, ManagedObjectWrapperHolderObject, ManagedObjectWrapperLayout, diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/Dbi/DacDbiImpl.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/Dbi/DacDbiImpl.cs index 0bcbc3fededf64..d462d540f4eccc 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/Dbi/DacDbiImpl.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/Dbi/DacDbiImpl.cs @@ -4379,10 +4379,140 @@ public int GetAssemblyFromModule(ulong vmModule, ulong* pVmAssembly) } public int ParseContinuation(ulong continuationAddress, ulong* pDiagnosticIP, ulong* pNextContinuation, uint* pState) - => LegacyFallbackHelper.CanFallback() && _legacy is not null ? _legacy.ParseContinuation(continuationAddress, pDiagnosticIP, pNextContinuation, pState) : HResults.E_NOTIMPL; + { + int hr = HResults.S_OK; + try + { + if (pDiagnosticIP is null || pNextContinuation is null || pState is null) + throw new ArgumentException("Output pointers must not be null."); + + ContinuationInfo info = _target.Contracts.Object.GetContinuationInfo(new TargetPointer(continuationAddress)); + *pDiagnosticIP = info.DiagnosticIP.Value; + *pNextContinuation = info.Next.Value; + *pState = info.State; + } + catch (System.Exception ex) + { + hr = ex.HResult; + } +#if DEBUG + if (_legacy is not null) + { + ulong diagnosticIPLocal; + ulong nextLocal; + uint stateLocal; + int hrLocal = _legacy.ParseContinuation(continuationAddress, &diagnosticIPLocal, &nextLocal, &stateLocal); + Debug.ValidateHResult(hr, hrLocal); + if (hr == HResults.S_OK) + { + Debug.Assert(*pDiagnosticIP == diagnosticIPLocal, $"cDAC: {*pDiagnosticIP:x}, DAC: {diagnosticIPLocal:x}"); + Debug.Assert(*pNextContinuation == nextLocal, $"cDAC: {*pNextContinuation:x}, DAC: {nextLocal:x}"); + Debug.Assert(*pState == stateLocal, $"cDAC: {*pState}, DAC: {stateLocal}"); + } + } +#endif + return hr; + } + +#if DEBUG + [ThreadStatic] + private static List? _debugEnumerateAsyncLocals; + + private static List DebugEnumerateAsyncLocals + => _debugEnumerateAsyncLocals ??= new(); + + [UnmanagedCallersOnly] + private static void EnumerateAsyncLocalsDebugCallback(AsyncLocalData* pLocal, nint _) + { + DebugEnumerateAsyncLocals.Add(*pLocal); + } +#endif + + public int EnumerateAsyncLocals(ulong vmMethod, ulong codeAddr, uint state, + delegate* unmanaged fpCallback, nint pUserData) + { + int hr = HResults.S_OK; +#if DEBUG + List locals = new(); +#endif + try + { + if (vmMethod == 0) + throw new ArgumentException("vmMethod must not be zero.", nameof(vmMethod)); + if (fpCallback is null) + throw new ArgumentNullException(nameof(fpCallback)); + + Contracts.IRuntimeTypeSystem rts = _target.Contracts.RuntimeTypeSystem; + Contracts.MethodDescHandle md = rts.GetMethodDescHandle(new TargetPointer(vmMethod)); + + if (!rts.IsAsyncThunkMethod(md)) + { + TargetCodePointer pCode; + if (codeAddr != 0) + { + Contracts.ICodeVersions cv = _target.Contracts.CodeVersions; + NativeCodeVersionHandle ncvh = cv.GetNativeCodeVersionForIP(new TargetCodePointer(codeAddr)); + pCode = ncvh.Valid ? cv.GetNativeCode(ncvh) : TargetCodePointer.Null; + } + else + { + pCode = rts.GetNativeCode(md); + } + + if (pCode != TargetCodePointer.Null) + { + IReadOnlyList suspensionPoints = _target.Contracts.DebugInfo.GetAsyncSuspensionPoints(pCode); + if ((int)state < suspensionPoints.Count) + { + IReadOnlyList localInfos = suspensionPoints[(int)state].Locals; + int varCount = localInfos.Count; + for (int i = 0; i < varCount; i++) + { + AsyncLocalData local = new() + { + Offset = localInfos[i].Offset, + IlVarNum = localInfos[i].ILVarNumber, + }; +#if DEBUG + locals.Add(local); +#endif + fpCallback(&local, pUserData); + } + } + } + } + } + catch (System.Exception ex) + { + hr = ex.HResult; + } + +#if DEBUG + if (_legacy is not null) + { + DebugEnumerateAsyncLocals.Clear(); + delegate* unmanaged debugCallbackPtr = &EnumerateAsyncLocalsDebugCallback; + int hrLocal = _legacy.EnumerateAsyncLocals(vmMethod, codeAddr, state, debugCallbackPtr, 0); + Debug.ValidateHResult(hr, hrLocal); - public int GetAsyncLocals(ulong vmMethod, ulong codeAddr, uint state, nint pAsyncLocals) - => LegacyFallbackHelper.CanFallback() && _legacy is not null ? _legacy.GetAsyncLocals(vmMethod, codeAddr, state, pAsyncLocals) : HResults.E_NOTIMPL; + if (hr == HResults.S_OK) + { + List legacyLocals = DebugEnumerateAsyncLocals; + Debug.Assert(locals.Count == legacyLocals.Count, + $"cDAC: {locals.Count} async locals, DAC: {legacyLocals.Count}"); + for (int i = 0; i < locals.Count; i++) + { + Debug.Assert(locals[i].Offset == legacyLocals[i].Offset, + $"cDAC[{i}].Offset {locals[i].Offset} != DAC {legacyLocals[i].Offset}"); + Debug.Assert(locals[i].IlVarNum == legacyLocals[i].IlVarNum, + $"cDAC[{i}].IlVarNum {locals[i].IlVarNum} != DAC {legacyLocals[i].IlVarNum}"); + } + } + DebugEnumerateAsyncLocals.Clear(); + } +#endif + return hr; + } public int GetGenericArgTokenIndex(ulong vmMethod, uint* pIndex) { diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/Dbi/IDacDbiInterface.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/Dbi/IDacDbiInterface.cs index 48f9c08fe109b8..0ab3dd29855df0 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/Dbi/IDacDbiInterface.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/Dbi/IDacDbiInterface.cs @@ -112,6 +112,13 @@ public struct DacDbiExceptionCallStackData public Interop.BOOL isLastForeignExceptionFrame; } +[StructLayout(LayoutKind.Sequential)] +public struct AsyncLocalData +{ + public uint Offset; + public uint IlVarNum; +} + [StructLayout(LayoutKind.Sequential)] public struct COR_HEAPINFO { @@ -744,7 +751,8 @@ int EnumerateTypeHandleParams(ulong vmTypeHandle, int ParseContinuation(ulong continuationAddress, ulong* pDiagnosticIP, ulong* pNextContinuation, uint* pState); [PreserveSig] - int GetAsyncLocals(ulong vmMethod, ulong codeAddr, uint state, nint pAsyncLocals); + int EnumerateAsyncLocals(ulong vmMethod, ulong codeAddr, uint state, + delegate* unmanaged fpCallback, nint pUserData); [PreserveSig] int GetGenericArgTokenIndex(ulong vmMethod, uint* pIndex); diff --git a/src/native/managed/cdac/tests/UnitTests/MockDescriptors/MockDescriptors.Object.cs b/src/native/managed/cdac/tests/UnitTests/MockDescriptors/MockDescriptors.Object.cs index 4fef96a60ee26e..54e9454cedf117 100644 --- a/src/native/managed/cdac/tests/UnitTests/MockDescriptors/MockDescriptors.Object.cs +++ b/src/native/managed/cdac/tests/UnitTests/MockDescriptors/MockDescriptors.Object.cs @@ -194,6 +194,84 @@ public long InvocationCount } } +internal sealed class MockContinuationObjectData : TypedView +{ + public const string MethodTableFieldName = "m_pMethTab"; + public const string NextFieldName = "Next"; + public const string ResumeInfoFieldName = "ResumeInfo"; + public const string FlagsFieldName = "Flags"; + public const string StateFieldName = "State"; + + public static Layout CreateLayout(MockTarget.Architecture architecture) + { + SequentialLayoutBuilder builder = new("ContinuationObject", architecture); + return builder + .AddPointerField(MethodTableFieldName) + .AddPointerField(NextFieldName) + .AddPointerField(ResumeInfoFieldName) + .AddField(FlagsFieldName, sizeof(int)) + .AddField(StateFieldName, sizeof(int)) + .Build(); + } + + public ulong MethodTable + { + get => ReadPointerField(MethodTableFieldName); + set => WritePointerField(MethodTableFieldName, value); + } + + public ulong Next + { + get => ReadPointerField(NextFieldName); + set => WritePointerField(NextFieldName, value); + } + + public ulong ResumeInfo + { + get => ReadPointerField(ResumeInfoFieldName); + set => WritePointerField(ResumeInfoFieldName, value); + } + + public int Flags + { + get => unchecked((int)ReadUInt32Field(FlagsFieldName)); + set => WriteUInt32Field(FlagsFieldName, unchecked((uint)value)); + } + + public int State + { + get => unchecked((int)ReadUInt32Field(StateFieldName)); + set => WriteUInt32Field(StateFieldName, unchecked((uint)value)); + } +} + +internal sealed class MockAsyncResumeInfoData : TypedView +{ + public const string ResumeFieldName = "Resume"; + public const string DiagnosticIPFieldName = "DiagnosticIP"; + + public static Layout CreateLayout(MockTarget.Architecture architecture) + { + SequentialLayoutBuilder builder = new("AsyncResumeInfo", architecture); + return builder + .AddPointerField(ResumeFieldName) + .AddPointerField(DiagnosticIPFieldName) + .Build(); + } + + public ulong Resume + { + get => ReadPointerField(ResumeFieldName); + set => WritePointerField(ResumeFieldName, value); + } + + public ulong DiagnosticIP + { + get => ReadPointerField(DiagnosticIPFieldName); + set => WritePointerField(DiagnosticIPFieldName, value); + } +} + internal partial class MockDescriptors { internal sealed class MockObjectBuilder @@ -219,6 +297,8 @@ internal sealed class MockObjectBuilder internal Layout StringLayout { get; } internal Layout ArrayLayout { get; } internal Layout DelegateLayout { get; } + internal Layout ContinuationLayout { get; } + internal Layout AsyncResumeInfoLayout { get; } internal Layout SyncTableEntryLayout { get; } internal ulong TestStringMethodTableAddress { get; private set; } internal Layout SyncBlockLayout => SyncBlockBuilder.SyncBlockLayout; @@ -243,6 +323,8 @@ public MockObjectBuilder(RuntimeTypeSystem rtsBuilder, (ulong Start, ulong End) StringLayout = MockStringObjectData.CreateLayout(Builder.TargetTestHelpers.Arch); ArrayLayout = MockArrayObjectData.CreateLayout(Builder.TargetTestHelpers.Arch); DelegateLayout = MockDelegateObjectData.CreateLayout(Builder.TargetTestHelpers.Arch); + ContinuationLayout = MockContinuationObjectData.CreateLayout(Builder.TargetTestHelpers.Arch); + AsyncResumeInfoLayout = MockAsyncResumeInfoData.CreateLayout(Builder.TargetTestHelpers.Arch); SyncTableEntryLayout = MockSyncTableEntry.CreateLayout(Builder.TargetTestHelpers.Arch); Debug.Assert(ArrayLayout.Size == Builder.TargetTestHelpers.ArrayBaseSize); @@ -342,6 +424,27 @@ internal ulong AddDelegateObject(ulong methodTable, ulong target, ulong methodPt return fragment.Address; } + internal ulong AddContinuationObject(ulong methodTable, ulong next, ulong resumeInfo, int state, int flags = 0) + { + MockMemorySpace.HeapFragment fragment = ManagedObjectAllocator.Allocate((uint)ContinuationLayout.Size, $"Continuation : MT = '{methodTable}'"); + MockContinuationObjectData mockContinuation = ContinuationLayout.Create(fragment); + mockContinuation.MethodTable = methodTable; + mockContinuation.Next = next; + mockContinuation.ResumeInfo = resumeInfo; + mockContinuation.Flags = flags; + mockContinuation.State = state; + return fragment.Address; + } + + internal ulong AddAsyncResumeInfo(ulong diagnosticIP, ulong resume = 0) + { + MockMemorySpace.HeapFragment fragment = ManagedObjectAllocator.Allocate((uint)AsyncResumeInfoLayout.Size, $"AsyncResumeInfo : DiagnosticIP = '{diagnosticIP}'"); + MockAsyncResumeInfoData mockResumeInfo = AsyncResumeInfoLayout.Create(fragment); + mockResumeInfo.Resume = resume; + mockResumeInfo.DiagnosticIP = diagnosticIP; + return fragment.Address; + } + private void AddStringMethodTablePointer() { TargetTestHelpers targetTestHelpers = Builder.TargetTestHelpers; diff --git a/src/native/managed/cdac/tests/UnitTests/ObjectTests.cs b/src/native/managed/cdac/tests/UnitTests/ObjectTests.cs index ede2b54cfb8abf..28c7a99ea1150a 100644 --- a/src/native/managed/cdac/tests/UnitTests/ObjectTests.cs +++ b/src/native/managed/cdac/tests/UnitTests/ObjectTests.cs @@ -61,6 +61,8 @@ private static TestPlaceholderTarget CreateObjectTarget( [DataType.String] = TargetTestHelpers.CreateTypeInfo(objectBuilder.StringLayout), [DataType.Array] = TargetTestHelpers.CreateTypeInfo(objectBuilder.ArrayLayout), [DataType.Delegate] = TargetTestHelpers.CreateTypeInfo(objectBuilder.DelegateLayout), + [DataType.ContinuationObject] = TargetTestHelpers.CreateTypeInfo(objectBuilder.ContinuationLayout), + [DataType.AsyncResumeInfo] = TargetTestHelpers.CreateTypeInfo(objectBuilder.AsyncResumeInfoLayout), [DataType.SyncTableEntry] = TargetTestHelpers.CreateTypeInfo(objectBuilder.SyncTableEntryLayout), [DataType.SyncBlock] = TargetTestHelpers.CreateTypeInfo(objectBuilder.SyncBlockLayout), [DataType.InteropSyncBlockInfo] = TargetTestHelpers.CreateTypeInfo(objectBuilder.InteropSyncBlockInfoLayout), @@ -386,4 +388,87 @@ public void GetDelegateInfo_Multicast(MockTarget.Architecture arch) Assert.Equal(0ul, info.TargetObject.Value); Assert.Equal(0ul, info.TargetMethodPtr.Value); } + + [Theory] + [ClassData(typeof(MockTarget.StdArch))] + public void GetContinuationInfo_WithResumeInfo(MockTarget.Architecture arch) + { + const ulong TestMethodTable = 0x00000000_10000200; + const ulong TestNext = 0x00000000_10000600; + const ulong TestDiagnosticIP = 0x00000000_aaaa1000; + const int TestState = 0x1234_5678; + TargetPointer continuationAddress = default; + + IObject contract = CreateObjectContract( + arch, + objectBuilder => + { + ulong resumeInfoAddress = objectBuilder.AddAsyncResumeInfo(diagnosticIP: TestDiagnosticIP); + continuationAddress = objectBuilder.AddContinuationObject( + methodTable: TestMethodTable, + next: TestNext, + resumeInfo: resumeInfoAddress, + state: TestState); + }); + + ContinuationInfo info = contract.GetContinuationInfo(continuationAddress); + + Assert.Equal(TestNext, info.Next.Value); + Assert.Equal(TestDiagnosticIP, info.DiagnosticIP.Value); + Assert.Equal(unchecked((uint)TestState), info.State); + } + + [Theory] + [ClassData(typeof(MockTarget.StdArch))] + public void GetContinuationInfo_NullResumeInfo(MockTarget.Architecture arch) + { + const ulong TestMethodTable = 0x00000000_10000200; + const ulong TestNext = 0x00000000_10000600; + const int TestState = 42; + TargetPointer continuationAddress = default; + + IObject contract = CreateObjectContract( + arch, + objectBuilder => + { + continuationAddress = objectBuilder.AddContinuationObject( + methodTable: TestMethodTable, + next: TestNext, + resumeInfo: 0, + state: TestState); + }); + + ContinuationInfo info = contract.GetContinuationInfo(continuationAddress); + + Assert.Equal(TestNext, info.Next.Value); + Assert.Equal(TargetPointer.Null, info.DiagnosticIP); + Assert.Equal((uint)TestState, info.State); + } + + [Theory] + [ClassData(typeof(MockTarget.StdArch))] + public void GetContinuationInfo_NullDiagnosticIP(MockTarget.Architecture arch) + { + const ulong TestMethodTable = 0x00000000_10000200; + const int TestState = 0; + TargetPointer continuationAddress = default; + + IObject contract = CreateObjectContract( + arch, + objectBuilder => + { + ulong resumeInfoAddress = objectBuilder.AddAsyncResumeInfo(diagnosticIP: 0); + continuationAddress = objectBuilder.AddContinuationObject( + methodTable: TestMethodTable, + next: 0, + resumeInfo: resumeInfoAddress, + state: TestState); + }); + + ContinuationInfo info = contract.GetContinuationInfo(continuationAddress); + + Assert.Equal(TargetPointer.Null, info.Next); + Assert.Equal(TargetPointer.Null, info.DiagnosticIP); + Assert.Equal((uint)TestState, info.State); + } }