From 2e8245d341d60411b3114067e69ad4134e85f19d Mon Sep 17 00:00:00 2001 From: Smaug123 <3138005+Smaug123@users.noreply.github.com> Date: Fri, 15 Aug 2025 14:11:48 +0100 Subject: [PATCH] WIP --- WoofWare.PawPrint.Domain/FieldInfo.fs | 12 +- WoofWare.PawPrint.Test/TestPureCases.fs | 10 + .../WoofWare.PawPrint.Test.fsproj | 2 + .../sourcesPure/AdvancedStructLayout.cs | 481 ++++++++++++++++++ .../sourcesPure/OverlappingStructs.cs | 364 +++++++++++++ WoofWare.PawPrint/AbstractMachine.fs | 5 +- WoofWare.PawPrint/BasicCliType.fs | 70 ++- WoofWare.PawPrint/Constants.fs | 10 + WoofWare.PawPrint/EvalStack.fs | 98 +++- .../EvalStackValueComparisons.fs | 21 +- WoofWare.PawPrint/FieldHandleRegistry.fs | 53 +- WoofWare.PawPrint/IlMachineState.fs | 21 +- WoofWare.PawPrint/Intrinsics.fs | 30 +- WoofWare.PawPrint/ManagedHeap.fs | 6 +- WoofWare.PawPrint/TypeHandleRegistry.fs | 28 +- WoofWare.PawPrint/UnaryMetadataIlOp.fs | 48 +- WoofWare.PawPrint/UnaryStringTokenIlOp.fs | 14 +- WoofWare.PawPrint/WoofWare.PawPrint.fsproj | 1 + 18 files changed, 1176 insertions(+), 98 deletions(-) create mode 100644 WoofWare.PawPrint.Test/sourcesPure/AdvancedStructLayout.cs create mode 100644 WoofWare.PawPrint.Test/sourcesPure/OverlappingStructs.cs create mode 100644 WoofWare.PawPrint/Constants.fs diff --git a/WoofWare.PawPrint.Domain/FieldInfo.fs b/WoofWare.PawPrint.Domain/FieldInfo.fs index 1a6a960..2f35705 100644 --- a/WoofWare.PawPrint.Domain/FieldInfo.fs +++ b/WoofWare.PawPrint.Domain/FieldInfo.fs @@ -34,6 +34,10 @@ type FieldInfo<'typeGeneric, 'fieldGeneric when 'typeGeneric : comparison and 't /// literal, and other characteristics. /// Attributes : FieldAttributes + + /// Static fields don't have an offset at all; also, instance fields which don't have an explicit offset (but + /// which of course do have one implicitly, which is most fields) are None here. + Offset : int option } member this.HasFieldRVA = this.Attributes.HasFlag FieldAttributes.HasFieldRVA @@ -62,12 +66,18 @@ module FieldInfo = let declaringType = ConcreteType.make' assembly declaringType declaringTypeNamespace declaringTypeName typeGenerics + let offset = + match def.GetOffset () with + | -1 -> None + | s -> Some s + { Name = name Signature = fieldSig DeclaringType = declaringType Handle = handle Attributes = def.Attributes + Offset = offset } let mapTypeGenerics<'a, 'b, 'field @@ -84,5 +94,5 @@ module FieldInfo = DeclaringType = declaringType Signature = input.Signature Attributes = input.Attributes - + Offset = input.Offset } diff --git a/WoofWare.PawPrint.Test/TestPureCases.fs b/WoofWare.PawPrint.Test/TestPureCases.fs index 3784d5f..c8b11e7 100644 --- a/WoofWare.PawPrint.Test/TestPureCases.fs +++ b/WoofWare.PawPrint.Test/TestPureCases.fs @@ -18,6 +18,16 @@ module TestPureCases = ExpectedReturnCode = 0 NativeImpls = MockEnv.make () } + { + FileName = "OverlappingStructs.cs" + ExpectedReturnCode = 0 + NativeImpls = MockEnv.make () + } + { + FileName = "AdvancedStructLayout.cs" + ExpectedReturnCode = 0 + NativeImpls = MockEnv.make () + } { FileName = "InitializeArray.cs" ExpectedReturnCode = 0 diff --git a/WoofWare.PawPrint.Test/WoofWare.PawPrint.Test.fsproj b/WoofWare.PawPrint.Test/WoofWare.PawPrint.Test.fsproj index ed3c6f6..334958b 100644 --- a/WoofWare.PawPrint.Test/WoofWare.PawPrint.Test.fsproj +++ b/WoofWare.PawPrint.Test/WoofWare.PawPrint.Test.fsproj @@ -45,6 +45,8 @@ + + diff --git a/WoofWare.PawPrint.Test/sourcesPure/AdvancedStructLayout.cs b/WoofWare.PawPrint.Test/sourcesPure/AdvancedStructLayout.cs new file mode 100644 index 0000000..b7b3341 --- /dev/null +++ b/WoofWare.PawPrint.Test/sourcesPure/AdvancedStructLayout.cs @@ -0,0 +1,481 @@ +using System; +using System.Runtime.InteropServices; +using System.Runtime.CompilerServices; + +// Compile with: csc /unsafe StructLayoutTestsAdvanced.cs +public class StructLayoutTestsAdvanced +{ + // Test structs + + [StructLayout(LayoutKind.Sequential)] + struct PointerTestStruct + { + public int A; + public byte B; + public short C; + public int D; + } + + [StructLayout(LayoutKind.Sequential)] + unsafe struct FixedBufferStruct + { + public int Header; + public fixed byte Buffer[64]; + public int Footer; + } + + [StructLayout(LayoutKind.Sequential)] + unsafe struct NestedFixedStruct + { + public fixed int IntArray[4]; + public fixed double DoubleArray[2]; + } + + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)] + struct MarshalStringStruct + { + public int Id; + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 32)] + public string Name; + public double Value; + } + + [StructLayout(LayoutKind.Sequential)] + struct MarshalArrayStruct + { + public int Count; + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 8)] + public int[] Values; + } + + [StructLayout(LayoutKind.Sequential)] + struct BlittableStruct + { + public int X; + public double Y; + public long Z; + } + + ref struct RefStruct + { + public int Value; + public Span Span; + + public RefStruct(int value) + { + Value = value; + Span = new Span(new int[] { value, value * 2, value * 3 }); + } + } + + readonly struct ReadOnlyStruct + { + public readonly int X; + public readonly int Y; + + public ReadOnlyStruct(int x, int y) + { + X = x; + Y = y; + } + + public int Sum => X + Y; + } + + readonly ref struct ReadOnlyRefStruct + { + public readonly int Value; + public readonly ReadOnlySpan Data; + + public ReadOnlyRefStruct(int value, ReadOnlySpan data) + { + Value = value; + Data = data; + } + } + + struct Generic where T : struct + { + public T Value; + public int Index; + + public Generic(T value, int index) + { + Value = value; + Index = index; + } + } + + struct DoubleGeneric + { + public T First; + public U Second; + } + + interface IIndexable + { + int GetIndex(); + void SetIndex(int value); + } + + struct StructWithInterface : IIndexable + { + public int Index; + public string Data; + + public int GetIndex() => Index; + public void SetIndex(int value) => Index = value; + } + + interface IMutable + { + void Mutate(); + } + + struct MutableStruct : IMutable + { + public int Counter; + + public void Mutate() + { + Counter++; + } + } + + struct RefReturnStruct + { + public int A; + public int B; + public int C; + + public ref int GetRef(int index) + { + if (index == 0) return ref A; + if (index == 1) return ref B; + return ref C; + } + } + + static unsafe int TestUnsafePointers() + { + var s = new PointerTestStruct { A = 0x12345678, B = 0xAB, C = 0x1234, D = 0xDEADBEEF }; + + // Test sizeof + int size = sizeof(PointerTestStruct); + if (size == 0) return 1; + + // Test pointer access + PointerTestStruct* ptr = &s; + if (ptr->A != 0x12345678) return 2; + if (ptr->B != 0xAB) return 3; + if (ptr->C != 0x1234) return 4; + if (ptr->D != unchecked((int)0xDEADBEEF)) return 5; + + // Test pointer arithmetic and casting + byte* bytePtr = (byte*)ptr; + int* intPtr = (int*)bytePtr; + if (*intPtr != 0x12345678) return 6; // First int field + + // Verify field offsets + int* dPtr = &(ptr->D); + int* aPtr = &(ptr->A); + long ptrDiff = (byte*)dPtr - (byte*)aPtr; + if (ptrDiff < 8) return 7; // D should be at least 8 bytes from A + + // Test modification through pointer + ptr->A = 999; + if (s.A != 999) return 8; + + return 0; + } + + static unsafe int TestFixedBuffers() + { + var f = new FixedBufferStruct(); + f.Header = 0xFEED; + f.Footer = 0xBEEF; + + // Test fixed buffer access + for (int i = 0; i < 64; i++) + { + f.Buffer[i] = (byte)(i % 256); + } + + if (f.Header != 0xFEED) return 10; + if (f.Footer != 0xBEEF) return 11; + + // Verify buffer contents + for (int i = 0; i < 64; i++) + { + if (f.Buffer[i] != (byte)(i % 256)) return 12; + } + + // Test pointer to fixed buffer + byte* bufPtr = f.Buffer; + bufPtr[0] = 255; + if (f.Buffer[0] != 255) return 13; + + // Test nested fixed arrays + var n = new NestedFixedStruct(); + n.IntArray[0] = 100; + n.IntArray[3] = 400; + n.DoubleArray[0] = 1.5; + n.DoubleArray[1] = 2.5; + + if (n.IntArray[0] != 100) return 14; + if (n.IntArray[3] != 400) return 15; + if (Math.Abs(n.DoubleArray[0] - 1.5) > 0.0001) return 16; + if (Math.Abs(n.DoubleArray[1] - 2.5) > 0.0001) return 17; + + return 0; + } + + static unsafe int TestMarshaling() + { + // Test string marshaling + var ms = new MarshalStringStruct + { + Id = 42, + Name = "TestString", + Value = 3.14159 + }; + + if (ms.Id != 42) return 20; + if (ms.Name != "TestString") return 21; + if (Math.Abs(ms.Value - 3.14159) > 0.00001) return 22; + + // Test Marshal.SizeOf + int marshalSize = Marshal.SizeOf(typeof(MarshalStringStruct)); + if (marshalSize == 0) return 23; + + // Test array marshaling + var ma = new MarshalArrayStruct + { + Count = 5, + Values = new int[] { 1, 2, 3, 4, 5, 6, 7, 8 } + }; + + if (ma.Count != 5) return 24; + if (ma.Values.Length != 8) return 25; + if (ma.Values[7] != 8) return 26; + + // Test StructureToPtr and PtrToStructure + var blittable = new BlittableStruct { X = 100, Y = 200.5, Z = 300 }; + IntPtr ptr = Marshal.AllocHGlobal(Marshal.SizeOf(typeof(BlittableStruct))); + try + { + Marshal.StructureToPtr(blittable, ptr, false); + var recovered = (BlittableStruct)Marshal.PtrToStructure(ptr, typeof(BlittableStruct)); + + if (recovered.X != 100) return 27; + if (Math.Abs(recovered.Y - 200.5) > 0.00001) return 28; + if (recovered.Z != 300) return 29; + } + finally + { + Marshal.FreeHGlobal(ptr); + } + + return 0; + } + + static int TestRefStructs() + { + // Test ref struct + var rs = new RefStruct(10); + if (rs.Value != 10) return 30; + if (rs.Span.Length != 3) return 31; + if (rs.Span[0] != 10) return 32; + if (rs.Span[1] != 20) return 33; + if (rs.Span[2] != 30) return 34; + + // Modify through span + rs.Span[0] = 100; + if (rs.Span[0] != 100) return 35; + + // Test readonly struct + var ros = new ReadOnlyStruct(5, 7); + if (ros.X != 5) return 36; + if (ros.Y != 7) return 37; + if (ros.Sum != 12) return 38; + + // Verify immutability - create new instance + var ros2 = new ReadOnlyStruct(10, 20); + if (ros.X != 5) return 39; // Original should be unchanged + + // Test readonly ref struct + byte[] data = { 1, 2, 3, 4 }; + var rors = new ReadOnlyRefStruct(42, new ReadOnlySpan(data)); + if (rors.Value != 42) return 40; + if (rors.Data.Length != 4) return 41; + if (rors.Data[3] != 4) return 42; + + return 0; + } + + static int TestGenerics() + { + // Test single generic parameter + var g1 = new Generic(42, 1); + if (g1.Value != 42) return 50; + if (g1.Index != 1) return 51; + + var g2 = new Generic(3.14, 2); + if (Math.Abs(g2.Value - 3.14) > 0.00001) return 52; + if (g2.Index != 2) return 53; + + // Test with custom struct + var inner = new ReadOnlyStruct(10, 20); + var g3 = new Generic(inner, 3); + if (g3.Value.X != 10) return 54; + if (g3.Value.Y != 20) return 55; + if (g3.Index != 3) return 56; + + // Test double generic + var dg = new DoubleGeneric { First = 100, Second = "test" }; + if (dg.First != 100) return 57; + if (dg.Second != "test") return 58; + + // Test with different type combinations + var dg2 = new DoubleGeneric { First = 2.718, Second = long.MaxValue }; + if (Math.Abs(dg2.First - 2.718) > 0.00001) return 59; + if (dg2.Second != long.MaxValue) return 60; + + return 0; + } + + static int TestByRefReturns() + { + var r = new RefReturnStruct { A = 10, B = 20, C = 30 }; + + // Test ref return + ref int refA = ref r.GetRef(0); + if (refA != 10) return 70; + + // Modify through ref + refA = 100; + if (r.A != 100) return 71; + + ref int refB = ref r.GetRef(1); + refB = 200; + if (r.B != 200) return 72; + + ref int refC = ref r.GetRef(2); + refC = 300; + if (r.C != 300) return 73; + + // Test ref local + ref int localRef = ref r.A; + localRef = 1000; + if (r.A != 1000) return 74; + + // Test that ref points to actual field + localRef = 2000; + if (refA != 2000) return 75; // Both should see the change + + return 0; + } + + static int TestStructInterfaces() + { + // Test struct implementing interface + var s = new StructWithInterface { Index = 42, Data = "test" }; + if (s.GetIndex() != 42) return 80; + + s.SetIndex(100); + if (s.Index != 100) return 81; + + // Test boxing to interface + IIndexable boxed = s; // Boxing occurs here + if (boxed.GetIndex() != 100) return 82; + + // Modify through interface (modifies boxed copy) + boxed.SetIndex(200); + if (boxed.GetIndex() != 200) return 83; + if (s.Index != 100) return 84; // Original should be unchanged + + // Test mutable interface + var m = new MutableStruct { Counter = 0 }; + m.Mutate(); + if (m.Counter != 1) return 85; + + // Box to interface and mutate + IMutable boxedMutable = m; // Boxing + boxedMutable.Mutate(); + if (m.Counter != 1) return 86; // Original unchanged + + // Cast back to see boxed mutation + var unboxed = (MutableStruct)boxedMutable; + if (unboxed.Counter != 2) return 87; + + // Direct interface call on boxed struct maintains state + boxedMutable.Mutate(); + boxedMutable.Mutate(); + var unboxed2 = (MutableStruct)boxedMutable; + if (unboxed2.Counter != 4) return 88; + + return 0; + } + + static unsafe int TestCombinedScenarios() + { + // Test generic with fixed buffer struct + var f = new FixedBufferStruct(); + f.Header = 999; + f.Buffer[0] = 123; + f.Footer = 111; + + var generic = new Generic(f, 42); + if (generic.Value.Header != 999) return 90; + if (generic.Value.Buffer[0] != 123) return 91; + if (generic.Value.Footer != 111) return 92; + if (generic.Index != 42) return 93; + + // Test marshaling with generic + var marshalable = new BlittableStruct { X = 10, Y = 20.0, Z = 30 }; + var genericMarshal = new Generic(marshalable, 5); + + if (genericMarshal.Value.X != 10) return 94; + if (Math.Abs(genericMarshal.Value.Y - 20.0) > 0.00001) return 95; + if (genericMarshal.Value.Z != 30) return 96; + + return 0; + } + + public static int Main() + { + int result = 0; + + unsafe + { + result = TestUnsafePointers(); + if (result != 0) return result; + + result = TestFixedBuffers(); + if (result != 0) return result; + } + + result = TestMarshaling(); + if (result != 0) return result; + + result = TestRefStructs(); + if (result != 0) return result; + + result = TestGenerics(); + if (result != 0) return result; + + result = TestByRefReturns(); + if (result != 0) return result; + + result = TestStructInterfaces(); + if (result != 0) return result; + + unsafe + { + result = TestCombinedScenarios(); + if (result != 0) return result; + } + + return 0; // All tests passed + } +} diff --git a/WoofWare.PawPrint.Test/sourcesPure/OverlappingStructs.cs b/WoofWare.PawPrint.Test/sourcesPure/OverlappingStructs.cs new file mode 100644 index 0000000..370eef4 --- /dev/null +++ b/WoofWare.PawPrint.Test/sourcesPure/OverlappingStructs.cs @@ -0,0 +1,364 @@ +using System; +using System.Runtime.InteropServices; + +public class StructLayoutTests +{ + // Test structs with various layouts + + [StructLayout(LayoutKind.Sequential)] + struct SequentialStruct + { + public int A; + public byte B; + public long C; + } + + [StructLayout(LayoutKind.Explicit)] + struct ExplicitUnion + { + [FieldOffset(0)] public int AsInt; + [FieldOffset(0)] public float AsFloat; + [FieldOffset(0)] public byte Byte0; + [FieldOffset(1)] public byte Byte1; + [FieldOffset(2)] public byte Byte2; + [FieldOffset(3)] public byte Byte3; + } + + [StructLayout(LayoutKind.Explicit, Size = 16)] + struct FixedSizeStruct + { + [FieldOffset(0)] public long First; + [FieldOffset(8)] public int Second; + [FieldOffset(12)] public short Third; + } + + [StructLayout(LayoutKind.Sequential, Pack = 1)] + struct PackedStruct + { + public byte A; + public int B; + public byte C; + } + + [StructLayout(LayoutKind.Auto)] + struct AutoLayoutStruct + { + public int X; + public string Y; + public double Z; + } + + [StructLayout(LayoutKind.Explicit)] + struct NestedUnion + { + [FieldOffset(0)] public ExplicitUnion Inner; + [FieldOffset(0)] public long AsLong; + [FieldOffset(4)] public int UpperInt; + } + + [StructLayout(LayoutKind.Explicit)] + struct LargeUnion + { + [FieldOffset(0)] public long Long1; + [FieldOffset(8)] public long Long2; + [FieldOffset(0)] public double Double1; + [FieldOffset(8)] public double Double2; + [FieldOffset(0)] public decimal AsDecimal; + } + + // Static fields for testing + static SequentialStruct staticSequential; + static ExplicitUnion staticUnion; + static FixedSizeStruct staticFixed; + + // Instance fields for testing + class FieldContainer + { + public SequentialStruct instanceSequential; + public ExplicitUnion instanceUnion; + public PackedStruct instancePacked; + public NestedUnion instanceNested; + } + + static int TestSequentialLayout() + { + var s = new SequentialStruct { A = 42, B = 255, C = long.MaxValue }; + + // Test field access + if (s.A != 42) return 1; + if (s.B != 255) return 2; + if (s.C != long.MaxValue) return 3; + + // Test copy semantics + var s2 = s; + s2.A = 100; + if (s.A != 42) return 4; // Should be unchanged (value type) + if (s2.A != 100) return 5; + + // Test static field storage + staticSequential = s; + if (staticSequential.A != 42) return 6; + if (staticSequential.C != long.MaxValue) return 7; + + return 0; + } + + static int TestExplicitUnion() + { + var u = new ExplicitUnion(); + + // Test overlapping int/float + u.AsInt = 0x3F800000; // IEEE 754 representation of 1.0f + if (Math.Abs(u.AsFloat - 1.0f) > 0.0001f) return 10; + + // Test byte-level access + u.AsInt = 0x12345678; + bool isLittleEndian = BitConverter.IsLittleEndian; + if (isLittleEndian) + { + if (u.Byte0 != 0x78) return 11; + if (u.Byte1 != 0x56) return 12; + if (u.Byte2 != 0x34) return 13; + if (u.Byte3 != 0x12) return 14; + } + else + { + if (u.Byte0 != 0x12) return 11; + if (u.Byte1 != 0x34) return 12; + if (u.Byte2 != 0x56) return 13; + if (u.Byte3 != 0x78) return 14; + } + + // Test static field + staticUnion = u; + if (staticUnion.AsInt != 0x12345678) return 15; + + return 0; + } + + static int TestFixedSizeStruct() + { + var f = new FixedSizeStruct { First = -1, Second = 42, Third = 1000 }; + + if (f.First != -1) return 20; + if (f.Second != 42) return 21; + if (f.Third != 1000) return 22; + + // Test size is respected + int size = Marshal.SizeOf(typeof(FixedSizeStruct)); + if (size != 16) return 23; + + staticFixed = f; + if (staticFixed.Second != 42) return 24; + + return 0; + } + + static int TestPackedStruct() + { + var p = new PackedStruct { A = 1, B = 0x12345678, C = 2 }; + + if (p.A != 1) return 30; + if (p.B != 0x12345678) return 31; + if (p.C != 2) return 32; + + // Packed struct should be 6 bytes (1 + 4 + 1) + int size = Marshal.SizeOf(typeof(PackedStruct)); + if (size != 6) return 33; + + return 0; + } + + static int TestInstanceFields() + { + var container = new FieldContainer(); + + container.instanceSequential = new SequentialStruct { A = 111, B = 222, C = 333 }; + if (container.instanceSequential.A != 111) return 40; + + container.instanceUnion = new ExplicitUnion { AsInt = 0xDEADBEEF }; + if (container.instanceUnion.AsInt != unchecked((int)0xDEADBEEF)) return 41; + + container.instancePacked = new PackedStruct { A = 10, B = 20, C = 30 }; + if (container.instancePacked.B != 20) return 42; + + container.instanceNested = new NestedUnion(); + container.instanceNested.Inner.AsInt = 100; + if (container.instanceNested.Inner.AsInt != 100) return 43; + + return 0; + } + + static int TestStructPassing() + { + var s = new SequentialStruct { A = 500, B = 50, C = 5000 }; + int result = ProcessSequential(s); + if (result != 555) return 50; // 500 + 50 + 5 (C % 1000) + + var u = new ExplicitUnion { AsInt = 1000 }; + u = TransformUnion(u); + if (u.AsInt != 2000) return 51; + + return 0; + } + + static int ProcessSequential(SequentialStruct s) + { + return s.A + s.B + (int)(s.C % 1000); + } + + static ExplicitUnion TransformUnion(ExplicitUnion u) + { + u.AsInt *= 2; + return u; + } + + static int TestNestedUnion() + { + var n = new NestedUnion(); + n.Inner.AsInt = 0x12345678; + + // Lower 32 bits should match Inner.AsInt + if ((n.AsLong & 0xFFFFFFFF) != 0x12345678) return 60; + + // Modify upper int + n.UpperInt = unchecked((int)0xABCDEF00); + + // Check both parts + if (n.Inner.AsInt != 0x12345678) return 61; + if (n.UpperInt != unchecked((int)0xABCDEF00)) return 62; + + return 0; + } + + static int TestLargeUnion() + { + var l = new LargeUnion(); + + // Test double/long overlap + l.Double1 = 1.0; + l.Double2 = 2.0; + + // IEEE 754: 1.0 = 0x3FF0000000000000 + if (l.Long1 != 0x3FF0000000000000) return 70; + // IEEE 754: 2.0 = 0x4000000000000000 + if (l.Long2 != 0x4000000000000000) return 71; + + // Test decimal overlap (decimal is 128 bits) + l.AsDecimal = 42m; + // Just verify it doesn't crash and maintains some structure + if (l.AsDecimal != 42m) return 72; + + return 0; + } + + static int TestAutoLayout() + { + // Auto layout structs can't use FieldOffset, but we can still test basic functionality + var a = new AutoLayoutStruct { X = 100, Y = "test", Z = 3.14159 }; + + if (a.X != 100) return 80; + if (a.Y != "test") return 81; + if (Math.Abs(a.Z - 3.14159) > 0.00001) return 82; + + // Test copy + var a2 = a; + a2.X = 200; + if (a.X != 100) return 83; // Original should be unchanged + if (a2.X != 200) return 84; + + return 0; + } + + static int TestStructArray() + { + var arr = new ExplicitUnion[3]; + arr[0].AsInt = 10; + arr[1].AsInt = 20; + arr[2].AsInt = 30; + + if (arr[0].AsInt != 10) return 90; + if (arr[1].AsInt != 20) return 91; + if (arr[2].AsInt != 30) return 92; + + // Modify through float view + arr[1].AsFloat = 2.5f; + if (Math.Abs(arr[1].AsFloat - 2.5f) > 0.0001f) return 93; + + return 0; + } + + static int TestBoxingUnboxing() + { + ExplicitUnion u = new ExplicitUnion { AsInt = 999 }; + object boxed = u; // Box + ExplicitUnion unboxed = (ExplicitUnion)boxed; // Unbox + + if (unboxed.AsInt != 999) return 100; + + // Modify original, boxed should remain unchanged + u.AsInt = 111; + ExplicitUnion fromBoxed = (ExplicitUnion)boxed; + if (fromBoxed.AsInt != 999) return 101; // Should still be 999 + + return 0; + } + + static int TestDefaultValues() + { + // Test that default struct initialization zeroes memory + var s = new SequentialStruct(); + if (s.A != 0) return 110; + if (s.B != 0) return 111; + if (s.C != 0) return 112; + + var u = new ExplicitUnion(); + if (u.AsInt != 0) return 113; + if (u.AsFloat != 0.0f) return 114; + + return 0; + } + + public static int Main() + { + int result = 0; + + result = TestSequentialLayout(); + if (result != 0) return result; + + result = TestExplicitUnion(); + if (result != 0) return result; + + result = TestFixedSizeStruct(); + if (result != 0) return result; + + result = TestPackedStruct(); + if (result != 0) return result; + + result = TestInstanceFields(); + if (result != 0) return result; + + result = TestStructPassing(); + if (result != 0) return result; + + result = TestNestedUnion(); + if (result != 0) return result; + + result = TestLargeUnion(); + if (result != 0) return result; + + result = TestAutoLayout(); + if (result != 0) return result; + + result = TestStructArray(); + if (result != 0) return result; + + result = TestBoxingUnboxing(); + if (result != 0) return result; + + result = TestDefaultValues(); + if (result != 0) return result; + + return 0; // All tests passed + } +} diff --git a/WoofWare.PawPrint/AbstractMachine.fs b/WoofWare.PawPrint/AbstractMachine.fs index 24e5fcf..152dc9f 100644 --- a/WoofWare.PawPrint/AbstractMachine.fs +++ b/WoofWare.PawPrint/AbstractMachine.fs @@ -1,6 +1,5 @@ namespace WoofWare.PawPrint -open System.Collections.Immutable open Microsoft.Extensions.Logging open Microsoft.FSharp.Core open WoofWare.PawPrint.ExternImplementations @@ -62,12 +61,12 @@ module AbstractMachine = let delegateToRun = state.ManagedHeap.NonArrayObjects.[delegateToRunAddr] let target = - match delegateToRun.Fields |> List.find (fun (x, _) -> x = "_target") |> snd with + match delegateToRun |> AllocatedNonArrayObject.DereferenceField "_target" with | CliType.ObjectRef addr -> addr | x -> failwith $"TODO: delegate target wasn't an object ref: %O{x}" let methodPtr = - match delegateToRun.Fields |> List.find (fun (x, _) -> x = "_methodPtr") |> snd with + match delegateToRun |> AllocatedNonArrayObject.DereferenceField "_methodPtr" with | CliType.Numeric (CliNumericType.NativeInt (NativeIntSource.FunctionPointer mi)) -> mi | d -> failwith $"unexpectedly not a method pointer in delegate invocation: {d}" diff --git a/WoofWare.PawPrint/BasicCliType.fs b/WoofWare.PawPrint/BasicCliType.fs index 6dd3f06..2e15791 100644 --- a/WoofWare.PawPrint/BasicCliType.fs +++ b/WoofWare.PawPrint/BasicCliType.fs @@ -160,11 +160,28 @@ type CliType = /// as a concatenated list of its fields. | ValueType of CliValueType +and CliField = + { + Name : string + Contents : CliType + /// "None" for "no explicit offset specified"; we expect most offsets to be None. + Offset : int option + } + and CliValueType = { - Fields : (string * CliType) list + Fields : CliField list } + static member OfFields (f : CliField list) = + { + Fields = f + } + + static member DereferenceField (name : string) (f : CliValueType) : CliType = + // TODO: this is wrong, it doesn't account for overlapping fields + f.Fields |> List.find (fun f -> f.Name = name) |> _.Contents + type CliTypeResolutionResult = | Resolved of CliType | FirstLoad of WoofWare.PawPrint.AssemblyReference @@ -200,10 +217,10 @@ module CliType = | CliType.ValueType vt -> match vt.Fields with | [] -> failwith "is it even possible to instantiate a value type with no fields" - | [ _, f ] -> sizeOf f + | [ field ] -> sizeOf field.Contents | fields -> // TODO: consider struct layout (there's an `Explicit` test that will exercise that) - fields |> List.map (snd >> sizeOf) |> List.sum + fields |> List.map (_.Contents >> sizeOf) |> List.sum let zeroOfPrimitive (primitiveType : PrimitiveType) : CliType = match primitiveType with @@ -227,19 +244,21 @@ module CliType = | PrimitiveType.TypedReference -> failwith "todo" | PrimitiveType.IntPtr -> { - Fields = - [ - "_value", CliType.RuntimePointer (CliRuntimePointer.Managed CliRuntimePointerSource.Null) - ] + Name = "_value" + Contents = CliType.RuntimePointer (CliRuntimePointer.Managed CliRuntimePointerSource.Null) + Offset = Some 0 } + |> List.singleton + |> CliValueType.OfFields |> CliType.ValueType | PrimitiveType.UIntPtr -> { - Fields = - [ - "_value", CliType.RuntimePointer (CliRuntimePointer.Managed CliRuntimePointerSource.Null) - ] + Name = "_value" + Contents = CliType.RuntimePointer (CliRuntimePointer.Managed CliRuntimePointerSource.Null) + Offset = Some 0 } + |> List.singleton + |> CliValueType.OfFields |> CliType.ValueType | PrimitiveType.Object -> CliType.ObjectRef None @@ -378,7 +397,7 @@ module CliType = // It's a value type - need to create zero values for all non-static fields let mutable currentConcreteTypes = concreteTypes - let fieldZeros = + let vt = typeDef.Fields |> List.filter (fun field -> not (field.Attributes.HasFlag FieldAttributes.Static)) |> List.map (fun field -> @@ -394,13 +413,14 @@ module CliType = zeroOfWithVisited currentConcreteTypes assemblies corelib fieldHandle visited currentConcreteTypes <- updatedConcreteTypes2 - (field.Name, fieldZero) - ) - let vt = - { - Fields = fieldZeros - } + { + Name = field.Name + Contents = fieldZero + Offset = field.Offset + } + ) + |> CliValueType.OfFields CliType.ValueType vt, currentConcreteTypes else @@ -465,8 +485,14 @@ module CliType = { Fields = cvt.Fields - |> List.replaceWhere (fun (fieldName, _existing) -> - if fieldName = field then Some (fieldName, value) else None + |> List.replaceWhere (fun f -> + if f.Name = field then + { f with + Contents = value + } + |> Some + else + None ) } |> CliType.ValueType @@ -478,4 +504,6 @@ module CliType = | CliType.Char (high, low) -> failwith "todo" | CliType.ObjectRef managedHeapAddressOption -> failwith "todo" | CliType.RuntimePointer cliRuntimePointer -> failwith "todo" - | CliType.ValueType cvt -> cvt.Fields |> List.pick (fun (n, v) -> if n = field then Some v else None) + | CliType.ValueType cvt -> + cvt.Fields + |> List.pick (fun f -> if f.Name = field then Some f.Contents else None) diff --git a/WoofWare.PawPrint/Constants.fs b/WoofWare.PawPrint/Constants.fs new file mode 100644 index 0000000..9dd1c11 --- /dev/null +++ b/WoofWare.PawPrint/Constants.fs @@ -0,0 +1,10 @@ +namespace WoofWare.PawPrint + +[] +module Constants = + + [] + let SIZEOF_INT = 4 + + [] + let SIZEOF_OBJ = 8 diff --git a/WoofWare.PawPrint/EvalStack.fs b/WoofWare.PawPrint/EvalStack.fs index 1ae76d2..d6e9d74 100644 --- a/WoofWare.PawPrint/EvalStack.fs +++ b/WoofWare.PawPrint/EvalStack.fs @@ -10,8 +10,7 @@ type EvalStackValue = | ObjectRef of ManagedHeapAddress // Fraser thinks this isn't really a thing in CoreCLR // | TransientPointer of TransientPointerSource - /// Mapping of field name to value - | UserDefinedValueType of (string * EvalStackValue) list + | UserDefinedValueType of EvalStackValueUserType override this.ToString () = match this with @@ -23,12 +22,39 @@ type EvalStackValue = | EvalStackValue.ObjectRef managedHeapAddress -> $"ObjectRef(%O{managedHeapAddress})" | EvalStackValue.UserDefinedValueType evalStackValues -> let desc = - evalStackValues - |> List.map (snd >> string) + evalStackValues.Fields + |> List.map (_.ContentsEval >> string) |> String.concat " | " $"Struct(%s{desc})" +and EvalStackValueField = + { + Name : string + ContentsEval : EvalStackValue + Offset : int option + } + +and EvalStackValueUserType = + { + Fields : EvalStackValueField list + } + + static member DereferenceField (name : string) (this : EvalStackValueUserType) = + // TODO: this doesn't account for overlapping fields + this.Fields + |> List.pick (fun stackField -> + if stackField.Name = name then + Some stackField.ContentsEval + else + None + ) + + static member OfFields (fields : EvalStackValueField list) = + { + Fields = fields + } + [] module EvalStackValue = /// The conversion performed by Conv_u. @@ -107,7 +133,11 @@ module EvalStackValue = | CliNumericType.Int32 _ -> match popped with | EvalStackValue.Int32 i -> CliType.Numeric (CliNumericType.Int32 i) - | EvalStackValue.UserDefinedValueType [ popped ] -> toCliTypeCoerced target (snd popped) + | EvalStackValue.UserDefinedValueType popped -> + match popped.Fields with + | [] -> failwith "unexpectedly empty" + | [ popped ] -> toCliTypeCoerced target popped.ContentsEval + | _ -> failwith $"TODO: %O{target}" | i -> failwith $"TODO: %O{i}" | CliNumericType.Int64 _ -> match popped with @@ -127,7 +157,11 @@ module EvalStackValue = | EvalStackValue.ManagedPointer ptrSrc -> CliNumericType.NativeInt (NativeIntSource.ManagedPointer ptrSrc) |> CliType.Numeric - | EvalStackValue.UserDefinedValueType [ (_, t) ] -> toCliTypeCoerced target t + | EvalStackValue.UserDefinedValueType vt -> + match vt.Fields with + | [] -> failwith "unexpected" + | [ vt ] -> toCliTypeCoerced target vt.ContentsEval + | _ -> failwith $"TODO: {popped}" | _ -> failwith $"TODO: {popped}" | CliNumericType.NativeFloat f -> failwith "todo" | CliNumericType.Int8 _ -> @@ -175,9 +209,9 @@ module EvalStackValue = | ManagedPointerSource.Null -> CliType.ObjectRef None | ManagedPointerSource.Heap s -> CliType.ObjectRef (Some s) | _ -> failwith "TODO" - | EvalStackValue.UserDefinedValueType fields -> - match fields with - | [ esv ] -> toCliTypeCoerced target (snd esv) + | EvalStackValue.UserDefinedValueType obj -> + match obj.Fields with + | [ esv ] -> toCliTypeCoerced target esv.ContentsEval | fields -> failwith $"TODO: don't know how to coerce struct of {fields} to a pointer" | _ -> failwith $"TODO: {popped}" | CliType.Bool _ -> @@ -219,28 +253,32 @@ module EvalStackValue = | CliType.ValueType vt -> match popped with | EvalStackValue.UserDefinedValueType popped -> - if vt.Fields.Length <> popped.Length then + if vt.Fields.Length <> popped.Fields.Length then + // TODO: overlapping fields failwith - $"mismatch: popped value type {popped} (length %i{popped.Length}) into {vt} (length %i{vt.Fields.Length})" + $"mismatch: popped value type {popped} (length %i{popped.Fields.Length}) into {vt} (length %i{vt.Fields.Length})" - let fields = - List.map2 - (fun (name1, v1) (name2, v2) -> - if name1 <> name2 then - failwith $"TODO: name mismatch, {name1} vs {name2}" + (vt.Fields, popped.Fields) + ||> List.map2 (fun field1 popped -> + if field1.Name <> popped.Name then + failwith $"TODO: name mismatch, {field1.Name} vs {popped.Name}" - name1, toCliTypeCoerced v1 v2 - ) - vt.Fields - popped + if field1.Offset <> popped.Offset then + failwith $"TODO: offset mismatch for {field1.Name}, {field1.Offset} vs {popped.Offset}" - { - Fields = fields - } + let contents = toCliTypeCoerced field1.Contents popped.ContentsEval + + { + CliField.Name = field1.Name + Contents = contents + Offset = field1.Offset + } + ) + |> CliValueType.OfFields |> CliType.ValueType | popped -> match vt.Fields with - | [ _, target ] -> toCliTypeCoerced target popped + | [ field ] -> toCliTypeCoerced field.Contents popped | _ -> failwith $"TODO: {popped} into value type {target}" let rec ofCliType (v : CliType) : EvalStackValue = @@ -285,8 +323,18 @@ module EvalStackValue = ManagedPointerSource.Field (failwith "TODO", fieldName) |> EvalStackValue.ManagedPointer | CliType.ValueType fields -> + // TODO: this is a bit dubious; we're being a bit sloppy with possibly-overlapping fields here fields.Fields - |> List.map (fun (name, f) -> name, ofCliType f) + |> List.map (fun field -> + let contents = ofCliType field.Contents + + { + Name = field.Name + Offset = field.Offset + ContentsEval = contents + } + ) + |> EvalStackValueUserType.OfFields |> EvalStackValue.UserDefinedValueType type EvalStack = diff --git a/WoofWare.PawPrint/EvalStackValueComparisons.fs b/WoofWare.PawPrint/EvalStackValueComparisons.fs index 37e212b..7a8fdf0 100644 --- a/WoofWare.PawPrint/EvalStackValueComparisons.fs +++ b/WoofWare.PawPrint/EvalStackValueComparisons.fs @@ -123,11 +123,24 @@ module EvalStackValueComparisons = let rec ceq (var1 : EvalStackValue) (var2 : EvalStackValue) : bool = // Table III.4 match var1, var2 with - | EvalStackValue.UserDefinedValueType [ _, u ], v -> ceq u v - | u, EvalStackValue.UserDefinedValueType [ _, v ] -> ceq u v - | EvalStackValue.UserDefinedValueType [], EvalStackValue.UserDefinedValueType [] -> true + | EvalStackValue.UserDefinedValueType { + Fields = [ f ] + }, + v -> ceq f.ContentsEval v + | u, + EvalStackValue.UserDefinedValueType { + Fields = [ f ] + } -> ceq u f.ContentsEval + | EvalStackValue.UserDefinedValueType { + Fields = [] + }, + EvalStackValue.UserDefinedValueType { + Fields = [] + } -> + // hmm, surely this can't happen, but :shrug: + true | EvalStackValue.UserDefinedValueType _, _ - | _, EvalStackValue.UserDefinedValueType _ -> failwith $"bad ceq: {var1} vs {var2}" + | _, EvalStackValue.UserDefinedValueType _ -> failwith $"TODO: ceq {var1} vs {var2}" | EvalStackValue.Int32 var1, EvalStackValue.Int32 var2 -> var1 = var2 | EvalStackValue.Int32 var1, EvalStackValue.NativeInt var2 -> failwith "TODO: int32 CEQ nativeint" | EvalStackValue.Int32 _, _ -> failwith $"bad ceq: Int32 vs {var2}" diff --git a/WoofWare.PawPrint/FieldHandleRegistry.fs b/WoofWare.PawPrint/FieldHandleRegistry.fs index b90a955..eb48925 100644 --- a/WoofWare.PawPrint/FieldHandleRegistry.fs +++ b/WoofWare.PawPrint/FieldHandleRegistry.fs @@ -34,7 +34,7 @@ module FieldHandleRegistry = let getOrAllocate (baseClassTypes : BaseClassTypes<'corelib>) (allocState : 'allocState) - (allocate : (string * CliType) list -> 'allocState -> ManagedHeapAddress * 'allocState) + (allocate : CliField list -> 'allocState -> ManagedHeapAddress * 'allocState) (declaringAssy : AssemblyName) (declaringType : ConcreteTypeHandle) (handle : FieldDefinitionHandle) @@ -54,8 +54,12 @@ module FieldHandleRegistry = failwith $"unexpected field name %s{field.Name} for BCL type RuntimeFieldHandle" { - Fields = [ "m_ptr", CliType.ofManagedObject runtimeFieldInfoStub ] + Name = "m_ptr" + Contents = CliType.ofManagedObject runtimeFieldInfoStub + Offset = None } + |> List.singleton + |> CliValueType.OfFields |> CliType.ValueType let handle = @@ -81,21 +85,52 @@ module FieldHandleRegistry = | TypeDefn.PrimitiveType PrimitiveType.IntPtr -> () | s -> failwith $"bad sig: {s}" + // https://github.com/dotnet/runtime/blob/2b21c73fa2c32fa0195e4a411a435dda185efd08/src/coreclr/System.Private.CoreLib/src/System/RuntimeHandles.cs#L1380 { - Fields = [ "m_handle", CliType.RuntimePointer (CliRuntimePointer.Unmanaged newHandle) ] + Name = "m_handle" + Contents = CliType.RuntimePointer (CliRuntimePointer.Unmanaged newHandle) + Offset = None // no struct layout was specified } + |> List.singleton + |> CliValueType.OfFields |> CliType.ValueType + // https://github.com/dotnet/runtime/blob/1d1bf92fcf43aa6981804dc53c5174445069c9e4/src/coreclr/System.Private.CoreLib/src/System/RuntimeHandles.cs#L1074 let runtimeFieldInfoStub = + // LayoutKind.Sequential [ // If we ever implement a GC, something should change here - "m_keepalive", CliType.ObjectRef None - "m_c", CliType.ObjectRef None - "m_d", CliType.ObjectRef None - "m_b", CliType.Numeric (CliNumericType.Int32 0) - "m_e", CliType.ObjectRef None + { + Name = "m_keepalive" + Contents = CliType.ObjectRef None + Offset = Some 0 + } + { + Name = "m_c" + Contents = CliType.ObjectRef None + Offset = Some SIZEOF_OBJ + } + { + Name = "m_d" + Contents = CliType.ObjectRef None + Offset = Some (SIZEOF_OBJ * 2) + } + { + Name = "m_b" + Contents = CliType.Numeric (CliNumericType.Int32 0) + Offset = Some (SIZEOF_OBJ * 3) + } + { + Name = "m_e" + Contents = CliType.ObjectRef None + Offset = Some (SIZEOF_OBJ * 3 + SIZEOF_INT) + } // RuntimeFieldHandleInternal: https://github.com/dotnet/runtime/blob/1d1bf92fcf43aa6981804dc53c5174445069c9e4/src/coreclr/System.Private.CoreLib/src/System/RuntimeHandles.cs#L1048 - "m_fieldHandle", runtimeFieldHandleInternal + { + Name = "m_fieldHandle" + Contents = runtimeFieldHandleInternal + Offset = Some (SIZEOF_OBJ * 4 + SIZEOF_INT) + } ] let alloc, state = allocate runtimeFieldInfoStub allocState diff --git a/WoofWare.PawPrint/IlMachineState.fs b/WoofWare.PawPrint/IlMachineState.fs index e4dabcb..28f6128 100644 --- a/WoofWare.PawPrint/IlMachineState.fs +++ b/WoofWare.PawPrint/IlMachineState.fs @@ -785,7 +785,7 @@ module IlMachineState = | ResolvedBaseType.ValueType -> let vt = { - Fields = constructed.Fields + CliValueType.Fields = constructed.Fields } state @@ -1165,7 +1165,7 @@ module IlMachineState = let allocateManagedObject<'generic, 'field> (typeInfo : WoofWare.PawPrint.TypeInfo<'generic, 'field>) - (fields : (string * CliType) list) + (fields : CliField list) (state : IlMachineState) : ManagedHeapAddress * IlMachineState = @@ -1484,9 +1484,18 @@ module IlMachineState = // Standard delegate fields in .NET are _target and _methodPtr // Update the fields with the target object and method pointer let updatedFields = - // TODO: field ordering here is probably wrong - ("_target", CliType.ObjectRef targetObj) - :: ("_methodPtr", methodPtr) + // Let's not consider field ordering for reference types like delegates. + // Nobody's going to be marshalling a reference type anyway, I hope. + { + Name = "_target" + Contents = CliType.ObjectRef targetObj + Offset = None + } + :: { + Name = "_methodPtr" + Contents = methodPtr + Offset = None + } :: heapObj.Fields let updatedObj = @@ -1602,5 +1611,5 @@ module IlMachineState = let obj = dereferencePointer state addr match obj with - | CliType.ValueType vt -> vt.Fields |> Map.ofList |> Map.find name + | CliType.ValueType vt -> vt |> CliValueType.DereferenceField name | v -> failwith $"could not find field {name} on object {v}" diff --git a/WoofWare.PawPrint/Intrinsics.fs b/WoofWare.PawPrint/Intrinsics.fs index c0240ae..ac5a78b 100644 --- a/WoofWare.PawPrint/Intrinsics.fs +++ b/WoofWare.PawPrint/Intrinsics.fs @@ -1,7 +1,6 @@ namespace WoofWare.PawPrint open System -open System.Collections.Immutable [] module Intrinsics = @@ -47,13 +46,15 @@ module Intrinsics = // https://github.com/dotnet/runtime/blob/108fa7856efcfd39bc991c2d849eabbf7ba5989c/src/coreclr/tools/Common/TypeSystem/IL/Stubs/UnsafeIntrinsics.cs#L192 match methodToCall.DeclaringType.Assembly.Name, methodToCall.DeclaringType.Name, methodToCall.Name with | "System.Private.CoreLib", "Type", "get_TypeHandle" -> + // https://github.com/dotnet/runtime/blob/ec11903827fc28847d775ba17e0cd1ff56cfbc2e/src/libraries/System.Private.CoreLib/src/System/Type.cs#L470 + // TODO: check return type is RuntimeTypeHandle match methodToCall.Signature.ParameterTypes with | _ :: _ -> failwith "bad signature Type.get_TypeHandle" | _ -> () - // https://github.com/dotnet/runtime/blob/ec11903827fc28847d775ba17e0cd1ff56cfbc2e/src/libraries/System.Private.CoreLib/src/System/Type.cs#L470 // no args, returns RuntimeTypeHandle, a struct with a single field (a RuntimeType class) + // https://github.com/dotnet/runtime/blob/1d1bf92fcf43aa6981804dc53c5174445069c9e4/src/coreclr/System.Private.CoreLib/src/System/RuntimeHandles.cs#L18 // The thing on top of the stack will be a RuntimeType. let arg, state = IlMachineState.popEvalStack currentThread state @@ -61,7 +62,10 @@ module Intrinsics = let arg = let rec go (arg : EvalStackValue) = match arg with - | EvalStackValue.UserDefinedValueType [ _, s ] -> go s + | EvalStackValue.UserDefinedValueType vt -> + match vt.Fields with + | [ field ] -> go field.ContentsEval + | _ -> failwith $"TODO: %O{vt}" | EvalStackValue.ManagedPointer ManagedPointerSource.Null -> failwith "TODO: throw NRE" | EvalStackValue.ManagedPointer (ManagedPointerSource.Heap addr) -> Some addr | s -> failwith $"TODO: called with unrecognised arg %O{s}" @@ -70,9 +74,14 @@ module Intrinsics = let state = let vt = + // https://github.com/dotnet/runtime/blob/2b21c73fa2c32fa0195e4a411a435dda185efd08/src/coreclr/System.Private.CoreLib/src/System/RuntimeHandles.cs#L92 { - Fields = [ "m_type", CliType.ObjectRef arg ] + Name = "m_type" + Contents = CliType.ObjectRef arg + Offset = Some 0 } + |> List.singleton + |> CliValueType.OfFields IlMachineState.pushToEvalStack (CliType.ValueType vt) currentThread state |> IlMachineState.advanceProgramCounter currentThread @@ -217,9 +226,16 @@ module Intrinsics = | EvalStackValue.ManagedPointer src -> IlMachineState.dereferencePointer state src | EvalStackValue.NativeInt src -> failwith "TODO" | EvalStackValue.ObjectRef ptr -> failwith "TODO" - | EvalStackValue.UserDefinedValueType [ _, field ] -> go field - | EvalStackValue.UserDefinedValueType [] - | EvalStackValue.UserDefinedValueType (_ :: _ :: _) + | EvalStackValue.UserDefinedValueType { + Fields = [ f ] + } -> go f.ContentsEval + | EvalStackValue.UserDefinedValueType { + Fields = [] + } -> failwith "unexpected no-fields object" + | EvalStackValue.UserDefinedValueType { + Fields = _ :: _ :: _ + } -> + failwith "TODO: check overlapping fields to see if this is a pointer" | EvalStackValue.Int32 _ | EvalStackValue.Int64 _ | EvalStackValue.Float _ -> failwith $"this isn't a pointer! {ptr}" diff --git a/WoofWare.PawPrint/ManagedHeap.fs b/WoofWare.PawPrint/ManagedHeap.fs index c037537..27bcdb9 100644 --- a/WoofWare.PawPrint/ManagedHeap.fs +++ b/WoofWare.PawPrint/ManagedHeap.fs @@ -8,11 +8,15 @@ type SyncBlock = type AllocatedNonArrayObject = { - Fields : (string * CliType) list + Fields : CliField list Type : WoofWare.PawPrint.TypeInfoCrate SyncBlock : SyncBlock } + static member DereferenceField (name : string) (f : AllocatedNonArrayObject) : CliType = + // TODO: this is wrong, it doesn't account for overlapping fields + f.Fields |> List.find (fun f -> f.Name = name) |> _.Contents + type AllocatedArray = { Length : int diff --git a/WoofWare.PawPrint/TypeHandleRegistry.fs b/WoofWare.PawPrint/TypeHandleRegistry.fs index 1c05103..b07e04b 100644 --- a/WoofWare.PawPrint/TypeHandleRegistry.fs +++ b/WoofWare.PawPrint/TypeHandleRegistry.fs @@ -18,7 +18,7 @@ module TypeHandleRegistry = /// Returns an allocated System.RuntimeType as well. let getOrAllocate (allocState : 'allocState) - (allocate : (string * CliType) list -> 'allocState -> ManagedHeapAddress * 'allocState) + (allocate : CliField list -> 'allocState -> ManagedHeapAddress * 'allocState) (def : ConcreteTypeHandle) (reg : TypeHandleRegistry) : ManagedHeapAddress * TypeHandleRegistry * 'allocState @@ -29,16 +29,34 @@ module TypeHandleRegistry = // Here follows the class System.RuntimeType, which is an internal class type with a constructor // whose only purpose is to throw. + // https://github.com/dotnet/runtime/blob/2b21c73fa2c32fa0195e4a411a435dda185efd08/src/libraries/System.Private.CoreLib/src/System/RuntimeType.cs#L14 + // and https://github.com/dotnet/runtime/blob/f0168ee80ba9aca18a7e7140b2bb436defda623c/src/coreclr/System.Private.CoreLib/src/System/RuntimeType.CoreCLR.cs#L44 let fields = [ // for the GC, I think? - "m_keepalive", CliType.ObjectRef None + { + Name = "m_keepalive" + Contents = CliType.ObjectRef None + Offset = None + } // TODO: this is actually a System.IntPtr https://github.com/dotnet/runtime/blob/ec11903827fc28847d775ba17e0cd1ff56cfbc2e/src/coreclr/nativeaot/Runtime.Base/src/System/Primitives.cs#L339 - "m_cache", CliType.Numeric (CliNumericType.NativeInt (NativeIntSource.Verbatim 0L)) - "m_handle", CliType.Numeric (CliNumericType.NativeInt (NativeIntSource.TypeHandlePtr def)) + { + Name = "m_cache" + Contents = CliType.Numeric (CliNumericType.NativeInt (NativeIntSource.Verbatim 0L)) + Offset = None + } + { + Name = "m_handle" + Contents = CliType.Numeric (CliNumericType.NativeInt (NativeIntSource.TypeHandlePtr def)) + Offset = None + } // This is the const -1, apparently?! // https://github.com/dotnet/runtime/blob/f0168ee80ba9aca18a7e7140b2bb436defda623c/src/coreclr/System.Private.CoreLib/src/System/RuntimeType.CoreCLR.cs#L2496 - "GenericParameterCountAny", CliType.Numeric (CliNumericType.Int32 -1) + { + Name = "GenericParameterCountAny" + Contents = CliType.Numeric (CliNumericType.Int32 -1) + Offset = None + } ] let alloc, state = allocate fields allocState diff --git a/WoofWare.PawPrint/UnaryMetadataIlOp.fs b/WoofWare.PawPrint/UnaryMetadataIlOp.fs index c0bfd21..98b6a8d 100644 --- a/WoofWare.PawPrint/UnaryMetadataIlOp.fs +++ b/WoofWare.PawPrint/UnaryMetadataIlOp.fs @@ -394,7 +394,14 @@ module internal UnaryMetadataIlOp = ImmutableArray.Empty state - state, (field.Name, zero) :: zeros + let field = + { + Name = field.Name + Contents = zero + Offset = field.Offset + } + + state, field :: zeros ) let fields = List.rev fieldZeros @@ -639,8 +646,14 @@ module internal UnaryMetadataIlOp = { v with Fields = v.Fields - |> List.replaceWhere (fun (x, _) -> - if x = field.Name then Some (x, valueToStore) else None + |> List.replaceWhere (fun f -> + if f.Name = field.Name then + { f with + Contents = valueToStore + } + |> Some + else + None ) } @@ -836,7 +849,7 @@ module internal UnaryMetadataIlOp = | false, _ -> failwith $"todo: array {managedHeapAddress}" | true, v -> IlMachineState.pushToEvalStack - (v.Fields |> List.find (fun (x, _) -> field.Name = x) |> snd) + (v.Fields |> List.find (fun f -> field.Name = f.Name) |> _.Contents) thread state | EvalStackValue.ManagedPointer (ManagedPointerSource.ArrayIndex (arr, index)) -> @@ -848,9 +861,8 @@ module internal UnaryMetadataIlOp = failwith "TODO: raise NullReferenceException" | EvalStackValue.ManagedPointer (ManagedPointerSource.Field _) -> failwith "TODO: get a field on a field ptr" - | EvalStackValue.UserDefinedValueType fields -> - let result = - fields |> List.pick (fun (k, v) -> if k = field.Name then Some v else None) + | EvalStackValue.UserDefinedValueType vt -> + let result = vt |> EvalStackValueUserType.DereferenceField field.Name IlMachineState.pushToEvalStack' result thread state @@ -1182,16 +1194,19 @@ module internal UnaryMetadataIlOp = let zero, state = IlMachineState.cliTypeZeroOfHandle state baseClassTypes fieldHandle - state, (field.Name, zero) :: acc + let field = + { + Name = field.Name + Contents = zero + Offset = failwith "offset" + } + + state, field :: acc ) |> fun (state, fields) -> state, List.rev fields // Create the value type with zero-initialized fields - let newValue = - CliType.ValueType - { - Fields = zeroFields - } + let newValue = zeroFields |> CliValueType.OfFields |> CliType.ValueType state |> IlMachineState.setLocalVariable thread frame var newValue | ManagedPointerSource.Argument (thread, frame, arg) -> failwith "TODO: Argument" @@ -1319,9 +1334,14 @@ module internal UnaryMetadataIlOp = let alloc, state = IlMachineState.getOrAllocateType baseClassTypes handle state let vt = + // https://github.com/dotnet/runtime/blob/2b21c73fa2c32fa0195e4a411a435dda185efd08/src/coreclr/System.Private.CoreLib/src/System/RuntimeHandles.cs#L92 { - Fields = [ "m_type", CliType.ObjectRef (Some alloc) ] + Name = "m_type" + Contents = CliType.ObjectRef (Some alloc) + Offset = None } + |> List.singleton + |> CliValueType.OfFields IlMachineState.pushToEvalStack (CliType.ValueType vt) thread state diff --git a/WoofWare.PawPrint/UnaryStringTokenIlOp.fs b/WoofWare.PawPrint/UnaryStringTokenIlOp.fs index 1b164e0..47ae8db 100644 --- a/WoofWare.PawPrint/UnaryStringTokenIlOp.fs +++ b/WoofWare.PawPrint/UnaryStringTokenIlOp.fs @@ -25,6 +25,8 @@ module internal UnaryStringTokenIlOp = let state = state |> IlMachineState.setStringData dataAddr stringToAllocate + // String type is: + // https://github.com/dotnet/runtime/blob/f0168ee80ba9aca18a7e7140b2bb436defda623c/src/libraries/System.Private.CoreLib/src/System/String.cs#L26 let stringInstanceFields = baseClassTypes.String.Fields |> List.choose (fun field -> @@ -47,8 +49,16 @@ module internal UnaryStringTokenIlOp = let fields = [ - "_firstChar", CliType.ofChar state.ManagedHeap.StringArrayData.[dataAddr] - "_stringLength", CliType.Numeric (CliNumericType.Int32 stringToAllocate.Length) + { + Name = "_firstChar" + Contents = CliType.ofChar state.ManagedHeap.StringArrayData.[dataAddr] + Offset = None + } + { + Name = "_stringLength" + Contents = CliType.Numeric (CliNumericType.Int32 stringToAllocate.Length) + Offset = None + } ] let addr, state = diff --git a/WoofWare.PawPrint/WoofWare.PawPrint.fsproj b/WoofWare.PawPrint/WoofWare.PawPrint.fsproj index 299d17d..d885456 100644 --- a/WoofWare.PawPrint/WoofWare.PawPrint.fsproj +++ b/WoofWare.PawPrint/WoofWare.PawPrint.fsproj @@ -9,6 +9,7 @@ +