diff --git a/docs/design/datacontracts/Object.md b/docs/design/datacontracts/Object.md new file mode 100644 index 00000000000000..7e2f1cf4aff10e --- /dev/null +++ b/docs/design/datacontracts/Object.md @@ -0,0 +1,53 @@ +# Contract Object + +This contract is for getting information about well-known managed objects + +## APIs of contract + +``` csharp +// Get the method table address for the object +TargetPointer GetMethodTableAddress(TargetPointer address); + +// Get the string corresponding to a managed string object. Error if address does not represent a string. +string GetStringValue(TargetPointer address); +``` + +## Version 1 + +Data descriptors used: +| Data Descriptor Name | Field | Meaning | +| --- | --- | --- | +| `Object` | `m_pMethTab` | Method table for the object | +| `String` | `m_FirstChar` | First character of the string - `m_StringLength` can be used to read the full string (encoded in UTF-16) | +| `String` | `m_StringLength` | Length of the string in characters (encoded in UTF-16) | + +Global variables used: +| Global Name | Type | Purpose | +| --- | --- | --- | +| `ObjectToMethodTableUnmask` | uint8 | Bits to clear for converting to a method table address | +| `StringMethodTable` | TargetPointer | The method table for System.String | + +``` csharp +TargetPointer GetMethodTableAddress(TargetPointer address) +{ + TargetPointer mt = _targetPointer.ReadPointer(address + /* Object::m_pMethTab offset */); + return mt.Value & ~target.ReadGlobal("ObjectToMethodTableUnmask"); +} + +string GetStringValue(TargetPointer address) +{ + TargetPointer mt = GetMethodTableAddress(address); + TargetPointer stringMethodTable = target.ReadPointer(target.ReadGlobalPointer("StringMethodTable")); + if (mt != stringMethodTable) + throw new ArgumentException("Address does not represent a string object", nameof(address)); + + // Validates the method table + _ = target.Contracts.RuntimeTypeSystem.GetTypeHandle(mt); + + Data.String str = _target.ProcessedData.GetOrAdd(address); + uint length = target.Read(address + /* String::m_StringLength offset */); + Span span = stackalloc byte[(int)length * sizeof(char)]; + target.ReadBuffer(address + /* String::m_FirstChar offset */, span); + return new string(MemoryMarshal.Cast(span)); +} +``` diff --git a/src/coreclr/debug/daccess/dacimpl.h b/src/coreclr/debug/daccess/dacimpl.h index f3d12c3427263c..5b9f59575a6922 100644 --- a/src/coreclr/debug/daccess/dacimpl.h +++ b/src/coreclr/debug/daccess/dacimpl.h @@ -1241,6 +1241,7 @@ class ClrDataAccess HRESULT GetMethodTableForEEClassImpl (CLRDATA_ADDRESS eeClassReallyMT, CLRDATA_ADDRESS *value); HRESULT GetMethodTableNameImpl(CLRDATA_ADDRESS mt, unsigned int count, _Inout_updates_z_(count) WCHAR *mtName, unsigned int *pNeeded); HRESULT GetObjectExceptionDataImpl(CLRDATA_ADDRESS objAddr, struct DacpExceptionObjectData *data); + HRESULT GetObjectStringDataImpl(CLRDATA_ADDRESS obj, unsigned int count, _Inout_updates_z_(count) WCHAR *stringData, unsigned int *pNeeded); BOOL IsExceptionFromManagedCode(EXCEPTION_RECORD * pExceptionRecord); #ifndef TARGET_UNIX diff --git a/src/coreclr/debug/daccess/request.cpp b/src/coreclr/debug/daccess/request.cpp index 5c759faeac4f93..615c041210bbb2 100644 --- a/src/coreclr/debug/daccess/request.cpp +++ b/src/coreclr/debug/daccess/request.cpp @@ -1601,7 +1601,7 @@ ClrDataAccess::GetDomainFromContext(CLRDATA_ADDRESS contextAddr, CLRDATA_ADDRESS HRESULT -ClrDataAccess::GetObjectStringData(CLRDATA_ADDRESS obj, unsigned int count, _Inout_updates_z_(count) WCHAR *stringData, unsigned int *pNeeded) +ClrDataAccess::GetObjectStringData(CLRDATA_ADDRESS obj, unsigned int count, _Inout_updates_z_(count) WCHAR* stringData, unsigned int* pNeeded) { if (obj == 0) return E_INVALIDARG; @@ -1611,44 +1611,73 @@ ClrDataAccess::GetObjectStringData(CLRDATA_ADDRESS obj, unsigned int count, _Ino SOSDacEnter(); + if (m_cdacSos != NULL) + { + hr = m_cdacSos->GetObjectStringData(obj, count, stringData, pNeeded); + if (FAILED(hr)) + { + hr = GetObjectStringDataImpl(obj, count, stringData, pNeeded); + } +#ifdef _DEBUG + else + { + unsigned int neededLocal; + SString stringDataLocal; + HRESULT hrLocal = GetObjectStringDataImpl(obj, count, stringDataLocal.OpenUnicodeBuffer(count), &neededLocal); + _ASSERTE(hr == hrLocal); + _ASSERTE(pNeeded == NULL || *pNeeded == neededLocal); + _ASSERTE(u16_strncmp(stringData, stringDataLocal, count) == 0); + } +#endif + } + else + { + hr = GetObjectStringDataImpl(obj, count, stringData, pNeeded); + } + + SOSDacLeave(); + return hr; +} + +HRESULT +ClrDataAccess::GetObjectStringDataImpl(CLRDATA_ADDRESS obj, unsigned int count, _Inout_updates_z_(count) WCHAR *stringData, unsigned int *pNeeded) +{ TADDR mtTADDR = DACGetMethodTableFromObjectPointer(TO_TADDR(obj), m_pTarget); PTR_MethodTable mt = PTR_MethodTable(mtTADDR); // Object must be a string BOOL bFree = FALSE; if (!DacValidateMethodTable(mt, bFree)) - hr = E_INVALIDARG; - else if (HOST_CDADDR(mt) != HOST_CDADDR(g_pStringClass)) - hr = E_INVALIDARG; + return E_INVALIDARG; - if (SUCCEEDED(hr)) - { - PTR_StringObject str(TO_TADDR(obj)); - ULONG32 needed = (ULONG32)str->GetStringLength() + 1; + if (HOST_CDADDR(mt) != HOST_CDADDR(g_pStringClass)) + return E_INVALIDARG; - if (stringData && count > 0) - { - if (count > needed) - count = needed; + PTR_StringObject str(TO_TADDR(obj)); + ULONG32 needed = (ULONG32)str->GetStringLength() + 1; - TADDR pszStr = TO_TADDR(obj)+offsetof(StringObject, m_FirstChar); - hr = m_pTarget->ReadVirtual(pszStr, (PBYTE)stringData, count * sizeof(WCHAR), &needed); + HRESULT hr; + if (stringData && count > 0) + { + if (count > needed) + count = needed; - if (SUCCEEDED(hr)) - stringData[count - 1] = W('\0'); - else - stringData[0] = W('\0'); - } - else - { - hr = E_INVALIDARG; - } + TADDR pszStr = TO_TADDR(obj)+offsetof(StringObject, m_FirstChar); + hr = m_pTarget->ReadVirtual(pszStr, (PBYTE)stringData, count * sizeof(WCHAR), &needed); - if (pNeeded) - *pNeeded = needed; + if (SUCCEEDED(hr)) + stringData[count - 1] = W('\0'); + else + stringData[0] = W('\0'); + } + else + { + hr = E_INVALIDARG; } - SOSDacLeave(); + if (pNeeded) + *pNeeded = needed; + return hr; } diff --git a/src/coreclr/debug/runtimeinfo/contracts.jsonc b/src/coreclr/debug/runtimeinfo/contracts.jsonc index 7d477b00cd858d..7ccabcb41dc75d 100644 --- a/src/coreclr/debug/runtimeinfo/contracts.jsonc +++ b/src/coreclr/debug/runtimeinfo/contracts.jsonc @@ -12,6 +12,7 @@ "DacStreams": 1, "Exception": 1, "Loader": 1, + "Object": 1, "RuntimeTypeSystem": 1, "Thread": 1 } diff --git a/src/coreclr/debug/runtimeinfo/datadescriptor.h b/src/coreclr/debug/runtimeinfo/datadescriptor.h index da83bebcfea39f..f51ddd2745fb0c 100644 --- a/src/coreclr/debug/runtimeinfo/datadescriptor.h +++ b/src/coreclr/debug/runtimeinfo/datadescriptor.h @@ -172,6 +172,17 @@ CDAC_TYPE_BEGIN(GCHandle) CDAC_TYPE_SIZE(sizeof(OBJECTHANDLE)) CDAC_TYPE_END(GCHandle) +CDAC_TYPE_BEGIN(Object) +CDAC_TYPE_INDETERMINATE(Object) +CDAC_TYPE_FIELD(Object, /*pointer*/, m_pMethTab, cdac_offsets::m_pMethTab) +CDAC_TYPE_END(Object) + +CDAC_TYPE_BEGIN(String) +CDAC_TYPE_INDETERMINATE(String) +CDAC_TYPE_FIELD(String, /*pointer*/, m_FirstChar, cdac_offsets::m_FirstChar) +CDAC_TYPE_FIELD(String, /*uint32*/, m_StringLength, cdac_offsets::m_StringLength) +CDAC_TYPE_END(String) + // Loader CDAC_TYPE_BEGIN(Module) @@ -264,8 +275,15 @@ CDAC_GLOBAL(FeatureEHFunclets, uint8, 1) #else CDAC_GLOBAL(FeatureEHFunclets, uint8, 0) #endif +// See Object::GetGCSafeMethodTable +#ifdef TARGET_64BIT +CDAC_GLOBAL(ObjectToMethodTableUnmask, uint8, 1 | 1 << 1 | 1 << 2) +#else +CDAC_GLOBAL(ObjectToMethodTableUnmask, uint8, 1 | 1 << 1) +#endif //TARGET_64BIT CDAC_GLOBAL(SOSBreakingChangeVersion, uint8, SOS_BREAKING_CHANGE_VERSION) CDAC_GLOBAL_POINTER(FreeObjectMethodTable, &::g_pFreeObjectMethodTable) +CDAC_GLOBAL_POINTER(StringMethodTable, &::g_pStringClass) CDAC_GLOBAL_POINTER(MiniMetaDataBuffAddress, &::g_MiniMetaDataBuffAddress) CDAC_GLOBAL_POINTER(MiniMetaDataBuffMaxSize, &::g_MiniMetaDataBuffMaxSize) CDAC_GLOBALS_END() diff --git a/src/coreclr/vm/object.h b/src/coreclr/vm/object.h index 4b9df2e18275f1..222c729f7dce41 100644 --- a/src/coreclr/vm/object.h +++ b/src/coreclr/vm/object.h @@ -462,6 +462,14 @@ class Object private: VOID ValidateInner(BOOL bDeep, BOOL bVerifyNextHeader, BOOL bVerifySyncBlock); + + template friend struct ::cdac_offsets; +}; + +template<> +struct cdac_offsets +{ + static constexpr size_t m_pMethTab = offsetof(Object, m_pMethTab); }; /* @@ -930,6 +938,15 @@ class StringObject : public Object private: static STRINGREF* EmptyStringRefPtr; static bool EmptyStringIsFrozen; + + template friend struct ::cdac_offsets; +}; + +template<> +struct cdac_offsets +{ + static constexpr size_t m_FirstChar = offsetof(StringObject, m_FirstChar); + static constexpr size_t m_StringLength = offsetof(StringObject, m_StringLength); }; /*================================GetEmptyString================================ diff --git a/src/native/managed/cdacreader/src/Constants.cs b/src/native/managed/cdacreader/src/Constants.cs index 6ca10e46a949bb..d713a5e69598e3 100644 --- a/src/native/managed/cdacreader/src/Constants.cs +++ b/src/native/managed/cdacreader/src/Constants.cs @@ -14,9 +14,11 @@ internal static class Globals internal const string GCThread = nameof(GCThread); internal const string FeatureEHFunclets = nameof(FeatureEHFunclets); + internal const string ObjectToMethodTableUnmask = nameof(ObjectToMethodTableUnmask); internal const string SOSBreakingChangeVersion = nameof(SOSBreakingChangeVersion); internal const string FreeObjectMethodTable = nameof(FreeObjectMethodTable); + internal const string StringMethodTable = nameof(StringMethodTable); internal const string MiniMetaDataBuffAddress = nameof(MiniMetaDataBuffAddress); internal const string MiniMetaDataBuffMaxSize = nameof(MiniMetaDataBuffMaxSize); diff --git a/src/native/managed/cdacreader/src/Contracts/Object.cs b/src/native/managed/cdacreader/src/Contracts/Object.cs new file mode 100644 index 00000000000000..24dc8a8062fcd3 --- /dev/null +++ b/src/native/managed/cdacreader/src/Contracts/Object.cs @@ -0,0 +1,31 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; + +namespace Microsoft.Diagnostics.DataContractReader.Contracts; + +internal interface IObject : IContract +{ + static string IContract.Name { get; } = nameof(Object); + static IContract IContract.Create(Target target, int version) + { + ulong methodTableOffset = (ulong)target.GetTypeInfo(DataType.Object).Fields["m_pMethTab"].Offset; + byte objectToMethodTableUnmask = target.ReadGlobal(Constants.Globals.ObjectToMethodTableUnmask); + TargetPointer stringMethodTable = target.ReadPointer( + target.ReadGlobalPointer(Constants.Globals.StringMethodTable)); + return version switch + { + 1 => new Object_1(target, methodTableOffset, objectToMethodTableUnmask, stringMethodTable), + _ => default(Object), + }; + } + + public virtual TargetPointer GetMethodTableAddress(TargetPointer address) => throw new NotImplementedException(); + public virtual string GetStringValue(TargetPointer address) => throw new NotImplementedException(); +} + +internal readonly struct Object : IObject +{ + // Everything throws NotImplementedException +} diff --git a/src/native/managed/cdacreader/src/Contracts/Object_1.cs b/src/native/managed/cdacreader/src/Contracts/Object_1.cs new file mode 100644 index 00000000000000..1f30b15a7091cf --- /dev/null +++ b/src/native/managed/cdacreader/src/Contracts/Object_1.cs @@ -0,0 +1,41 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Runtime.InteropServices; + +namespace Microsoft.Diagnostics.DataContractReader.Contracts; + +internal readonly struct Object_1 : IObject +{ + private readonly Target _target; + private readonly ulong _methodTableOffset; + private readonly TargetPointer _stringMethodTable; + private readonly byte _objectToMethodTableUnmask; + + internal Object_1(Target target, ulong methodTableOffset, byte objectToMethodTableUnmask, TargetPointer stringMethodTable) + { + _target = target; + _methodTableOffset = methodTableOffset; + _stringMethodTable = stringMethodTable; + _objectToMethodTableUnmask = objectToMethodTableUnmask; + } + + public TargetPointer GetMethodTableAddress(TargetPointer address) + { + TargetPointer mt = _target.ReadPointer(address + _methodTableOffset); + return mt.Value & (ulong)~_objectToMethodTableUnmask; + } + + string IObject.GetStringValue(TargetPointer address) + { + TargetPointer mt = GetMethodTableAddress(address); + if (mt != _stringMethodTable) + throw new ArgumentException("Address does not represent a string object", nameof(address)); + + Data.String str = _target.ProcessedData.GetOrAdd(address); + Span span = stackalloc byte[(int)str.StringLength * sizeof(char)]; + _target.ReadBuffer(str.FirstChar, span); + return new string(MemoryMarshal.Cast(span)); + } +} diff --git a/src/native/managed/cdacreader/src/Contracts/Registry.cs b/src/native/managed/cdacreader/src/Contracts/Registry.cs index 6a5bf35f64d648..0d6894e9fce27a 100644 --- a/src/native/managed/cdacreader/src/Contracts/Registry.cs +++ b/src/native/managed/cdacreader/src/Contracts/Registry.cs @@ -20,6 +20,7 @@ public Registry(Target target) public IException Exception => GetContract(); public ILoader Loader => GetContract(); + public IObject Object => GetContract(); public IThread Thread => GetContract(); public IRuntimeTypeSystem RuntimeTypeSystem => GetContract(); public IDacStreams DacStreams => GetContract(); diff --git a/src/native/managed/cdacreader/src/Data/String.cs b/src/native/managed/cdacreader/src/Data/String.cs new file mode 100644 index 00000000000000..088d1060a7d960 --- /dev/null +++ b/src/native/managed/cdacreader/src/Data/String.cs @@ -0,0 +1,21 @@ +// 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; + +internal sealed class String : IData +{ + static String IData.Create(Target target, TargetPointer address) + => new String(target, address); + + public String(Target target, TargetPointer address) + { + Target.TypeInfo type = target.GetTypeInfo(DataType.String); + + FirstChar = address + (ulong)type.Fields["m_FirstChar"].Offset; + StringLength = target.Read(address + (ulong)type.Fields["m_StringLength"].Offset); + } + + public TargetPointer FirstChar { get; init; } + public uint StringLength { get; init; } +} diff --git a/src/native/managed/cdacreader/src/DataType.cs b/src/native/managed/cdacreader/src/DataType.cs index ef037be8ed1c8c..244d8ce4af12c4 100644 --- a/src/native/managed/cdacreader/src/DataType.cs +++ b/src/native/managed/cdacreader/src/DataType.cs @@ -37,4 +37,6 @@ public enum DataType TypeVarTypeDesc, FnPtrTypeDesc, DynamicMetadata, + Object, + String, } diff --git a/src/native/managed/cdacreader/src/Legacy/SOSDacImpl.cs b/src/native/managed/cdacreader/src/Legacy/SOSDacImpl.cs index e4f2968f29f797..9faba1459ad37c 100644 --- a/src/native/managed/cdacreader/src/Legacy/SOSDacImpl.cs +++ b/src/native/managed/cdacreader/src/Legacy/SOSDacImpl.cs @@ -308,7 +308,21 @@ public unsafe int GetObjectExceptionData(ulong objectAddress, DacpExceptionObjec return HResults.S_OK; } - public unsafe int GetObjectStringData(ulong obj, uint count, char* stringData, uint* pNeeded) => HResults.E_NOTIMPL; + public unsafe int GetObjectStringData(ulong obj, uint count, char* stringData, uint* pNeeded) + { + try + { + Contracts.IObject contract = _target.Contracts.Object; + string str = contract.GetStringValue(obj); + CopyStringToTargetBuffer(stringData, count, pNeeded, str); + } + catch (System.Exception ex) + { + return ex.HResult; + } + + return HResults.S_OK; + } public unsafe int GetOOMData(ulong oomAddr, void* data) => HResults.E_NOTIMPL; public unsafe int GetOOMStaticData(void* data) => HResults.E_NOTIMPL; public unsafe int GetPEFileBase(ulong addr, ulong* peBase) => HResults.E_NOTIMPL; diff --git a/src/native/managed/cdacreader/tests/ObjectTests.cs b/src/native/managed/cdacreader/tests/ObjectTests.cs new file mode 100644 index 00000000000000..b95b7cac82c8ff --- /dev/null +++ b/src/native/managed/cdacreader/tests/ObjectTests.cs @@ -0,0 +1,169 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Runtime.InteropServices; +using System.Text; +using Xunit; + +namespace Microsoft.Diagnostics.DataContractReader.UnitTests; + +public unsafe class ObjectTests +{ + const ulong TestStringMethodTableGlobalAddress = 0x00000000_100000a0; + const ulong TestStringMethodTableAddress = 0x00000000_100000a8; + + private static readonly Target.TypeInfo ObjectTypeInfo = new() + { + Fields = { + { "m_pMethTab", new() { Offset = 0, Type = DataType.pointer} }, + } + }; + + private static readonly Target.TypeInfo StringTypeInfo = new Target.TypeInfo() + { + Fields = { + { "m_StringLength", new() { Offset = 0x8, Type = DataType.uint32} }, + { "m_FirstChar", new() { Offset = 0xc, Type = DataType.uint16} }, + } + }; + + private static readonly (DataType Type, Target.TypeInfo Info)[] ObjectTypes = + [ + (DataType.Object, ObjectTypeInfo), + (DataType.String, StringTypeInfo), + ]; + + const ulong TestObjectToMethodTableUnmask = 0x7; + private static (string Name, ulong Value, string? Type)[] ObjectGlobals = + [ + (nameof(Constants.Globals.ObjectToMethodTableUnmask), TestObjectToMethodTableUnmask, "uint8"), + (nameof(Constants.Globals.StringMethodTable), TestStringMethodTableGlobalAddress, null), + ]; + + private static MockMemorySpace.Builder AddStringMethodTablePointer(TargetTestHelpers targetTestHelpers, MockMemorySpace.Builder builder) + { + MockMemorySpace.HeapFragment fragment = new() { Name = "Address of String Method Table", Address = TestStringMethodTableGlobalAddress, Data = new byte[targetTestHelpers.PointerSize] }; + targetTestHelpers.WritePointer(fragment.Data, TestStringMethodTableAddress); + return builder.AddHeapFragments([ + fragment, + new () { Name = "String Method Table", Address = TestStringMethodTableAddress, Data = new byte[targetTestHelpers.PointerSize] } + ]); + } + + private delegate MockMemorySpace.Builder ConfigureContextBuilder(MockMemorySpace.Builder builder); + + private static void ObjectContractHelper(MockTarget.Architecture arch, ConfigureContextBuilder configure, Action testCase) + { + TargetTestHelpers targetTestHelpers = new(arch); + string typesJson = TargetTestHelpers.MakeTypesJson(ObjectTypes); + string globalsJson = TargetTestHelpers.MakeGlobalsJson(ObjectGlobals); + byte[] json = Encoding.UTF8.GetBytes($$""" + { + "version": 0, + "baseline": "empty", + "contracts": { + "{{nameof(Contracts.Object)}}": 1 + }, + "types": { {{typesJson}} }, + "globals": { {{globalsJson}} } + } + """); + Span descriptor = stackalloc byte[targetTestHelpers.ContractDescriptorSize]; + targetTestHelpers.ContractDescriptorFill(descriptor, json.Length, ObjectGlobals.Length); + + int pointerSize = targetTestHelpers.PointerSize; + Span pointerData = stackalloc byte[ObjectGlobals.Length * pointerSize]; + for (int i = 0; i < ObjectGlobals.Length; i++) + { + var (_, value, _) = ObjectGlobals[i]; + targetTestHelpers.WritePointer(pointerData.Slice(i * pointerSize), value); + } + + fixed (byte* jsonPtr = json) + { + MockMemorySpace.Builder builder = new(); + builder = builder.SetDescriptor(descriptor) + .SetJson(json) + .SetPointerData(pointerData); + + builder = AddStringMethodTablePointer(targetTestHelpers, builder); + + if (configure != null) + { + builder = configure(builder); + } + + using MockMemorySpace.ReadContext context = builder.Create(); + + bool success = MockMemorySpace.TryCreateTarget(&context, out Target? target); + Assert.True(success); + testCase(target); + } + + GC.KeepAlive(json); + } + + private static MockMemorySpace.Builder AddObject(TargetTestHelpers targetTestHelpers, MockMemorySpace.Builder builder, TargetPointer address, TargetPointer methodTable) + { + MockMemorySpace.HeapFragment fragment = new() { Name = $"Object : MT = '{methodTable}'", Address = address, Data = new byte[targetTestHelpers.SizeOfTypeInfo(ObjectTypeInfo)] }; + Span dest = fragment.Data; + targetTestHelpers.WritePointer(dest.Slice(ObjectTypeInfo.Fields["m_pMethTab"].Offset), methodTable); + return builder.AddHeapFragment(fragment); + } + + private static MockMemorySpace.Builder AddStringObject(TargetTestHelpers targetTestHelpers, MockMemorySpace.Builder builder, TargetPointer address, string value) + { + int size = targetTestHelpers.SizeOfTypeInfo(ObjectTypeInfo) + targetTestHelpers.SizeOfTypeInfo(StringTypeInfo) + value.Length * sizeof(char); + MockMemorySpace.HeapFragment fragment = new() { Name = $"String = '{value}'", Address = address, Data = new byte[size] }; + Span dest = fragment.Data; + targetTestHelpers.WritePointer(dest.Slice(ObjectTypeInfo.Fields["m_pMethTab"].Offset), TestStringMethodTableAddress); + targetTestHelpers.Write(dest.Slice(StringTypeInfo.Fields["m_StringLength"].Offset), (uint)value.Length); + MemoryMarshal.Cast(value).CopyTo(dest.Slice(StringTypeInfo.Fields["m_FirstChar"].Offset)); + return builder.AddHeapFragment(fragment); + } + + [Theory] + [ClassData(typeof(MockTarget.StdArch))] + public void UnmaskMethodTableAddress(MockTarget.Architecture arch) + { + const ulong TestObjectAddress = 0x00000000_10000010; + const ulong TestMethodTableAddress = 0x00000000_10000027; + TargetTestHelpers targetTestHelpers = new(arch); + ObjectContractHelper(arch, + (builder) => + { + builder = AddObject(targetTestHelpers, builder, TestObjectAddress, TestMethodTableAddress); + return builder; + }, + (target) => + { + Contracts.IObject contract = target.Contracts.Object; + Assert.NotNull(contract); + TargetPointer mt = contract.GetMethodTableAddress(TestObjectAddress); + Assert.Equal(TestMethodTableAddress & ~TestObjectToMethodTableUnmask, mt.Value); + }); + } + + [Theory] + [ClassData(typeof(MockTarget.StdArch))] + public void StringValue(MockTarget.Architecture arch) + { + const ulong TestStringAddress = 0x00000000_10000010; + string expected = "test_string_value"; + TargetTestHelpers targetTestHelpers = new(arch); + ObjectContractHelper(arch, + (builder) => + { + builder = AddStringObject(targetTestHelpers, builder, TestStringAddress, expected); + return builder; + }, + (target) => + { + Contracts.IObject contract = target.Contracts.Object; + Assert.NotNull(contract); + string actual = contract.GetStringValue(TestStringAddress); + Assert.Equal(expected, actual); + }); + } +}