diff --git a/src/Platforms/Echo.Platforms.AsmResolver/Emulation/CilVirtualMachine.cs b/src/Platforms/Echo.Platforms.AsmResolver/Emulation/CilVirtualMachine.cs index 50b7c5a6..cc93fabe 100644 --- a/src/Platforms/Echo.Platforms.AsmResolver/Emulation/CilVirtualMachine.cs +++ b/src/Platforms/Echo.Platforms.AsmResolver/Emulation/CilVirtualMachine.cs @@ -141,7 +141,7 @@ public IObjectAllocator Allocator { get; set; - } = DefaultAllocators.VirtualHeap; + } = DefaultAllocators.String.WithFallback(DefaultAllocators.VirtualHeap); /// /// Gets the service that is responsible for invoking external functions or methods. diff --git a/src/Platforms/Echo.Platforms.AsmResolver/Emulation/Heap/ManagedObjectHeap.cs b/src/Platforms/Echo.Platforms.AsmResolver/Emulation/Heap/ManagedObjectHeap.cs index dc1369fb..dc4dfd2c 100644 --- a/src/Platforms/Echo.Platforms.AsmResolver/Emulation/Heap/ManagedObjectHeap.cs +++ b/src/Platforms/Echo.Platforms.AsmResolver/Emulation/Heap/ManagedObjectHeap.cs @@ -42,6 +42,17 @@ public ManagedObjectHeap(IHeap backingHeap, ValueFactory factory) /// public AddressRange AddressRange => _backingHeap.AddressRange; + /// + /// Allocates flat unmanaged memory in the heap (i.e., without any object header). + /// + /// The size in bytes of the memory region to allocate. + /// A value indicating whether the object should be initialized with zeroes. + /// The address of the memory that was allocated. + public long AllocateFlat(uint size, bool initialize) + { + return _backingHeap.Allocate(size, initialize); + } + /// /// Allocates a managed object of the provided type in the heap. /// @@ -153,7 +164,7 @@ public long AllocateString(BitVector contents) chunkSpan.SliceStringData(_factory).Write(contents); // Write null-terminator. - chunkSpan.Slice(chunkSpan.Count - 16 - 1).U16 = 0; + chunkSpan.Slice(chunkSpan.Count - 16).Write((ushort) 0); return address; } diff --git a/src/Platforms/Echo.Platforms.AsmResolver/Emulation/Invocation/DefaultAllocators.cs b/src/Platforms/Echo.Platforms.AsmResolver/Emulation/Invocation/DefaultAllocators.cs index eaf638a8..767656c8 100644 --- a/src/Platforms/Echo.Platforms.AsmResolver/Emulation/Invocation/DefaultAllocators.cs +++ b/src/Platforms/Echo.Platforms.AsmResolver/Emulation/Invocation/DefaultAllocators.cs @@ -14,6 +14,11 @@ public static class DefaultAllocators /// Gets an allocator that allocates objects in the virtualalized heap of the underlying virtual machine. /// public static VirtualHeapAllocator VirtualHeap => VirtualHeapAllocator.Instance; + + /// + /// Gets an allocator that handles System.String constructors. + /// + public static StringAllocator String => StringAllocator.Instance; /// /// Chains the first object allocator with the provided object allocator in such a way that if the result of the diff --git a/src/Platforms/Echo.Platforms.AsmResolver/Emulation/Invocation/StringAllocator.cs b/src/Platforms/Echo.Platforms.AsmResolver/Emulation/Invocation/StringAllocator.cs new file mode 100644 index 00000000..a308e198 --- /dev/null +++ b/src/Platforms/Echo.Platforms.AsmResolver/Emulation/Invocation/StringAllocator.cs @@ -0,0 +1,252 @@ +using System; +using System.Collections.Generic; +using AsmResolver.DotNet; +using AsmResolver.DotNet.Signatures.Types; +using AsmResolver.PE.DotNet.Metadata.Tables.Rows; +using Echo.Memory; +using Echo.Platforms.AsmResolver.Emulation.Dispatch; + +namespace Echo.Platforms.AsmResolver.Emulation.Invocation; + +/// +/// Provides a shim allocator that handles System.String constructors. +/// +public class StringAllocator : IObjectAllocator +{ + /// + /// Gets the singleton instance of the class. + /// + public static StringAllocator Instance + { + get; + } = new(); + + /// + public AllocationResult Allocate(CilExecutionContext context, IMethodDescriptor ctor, IList arguments) + { + if ((!ctor.DeclaringType?.IsTypeOf("System", "String") ?? true) || ctor.Signature is null) + return AllocationResult.Inconclusive(); + + // TODO: We may want to make this configurable. + foreach (var argument in arguments) + { + if (!argument.IsFullyKnown) + return AllocationResult.Inconclusive(); + } + + // TODO: Add all string constructors. + var types = ctor.Signature.ParameterTypes; + switch (types.Count) + { + case 1: + return types[0] switch + { + // .ctor(char[]) + SzArrayTypeSignature { BaseType.ElementType: ElementType.Char } + => ConstructCharArrayString(context, arguments), + + // .ctor(char*) + PointerTypeSignature { BaseType.ElementType: ElementType.Char } + => ConstructCharPointerString(context, arguments), + + // .ctor(sbyte*) + PointerTypeSignature { BaseType.ElementType: ElementType.I1 } + => ConstructSBytePointerString(context, arguments), + + // other + _ => AllocationResult.Inconclusive() + }; + + case 2: + if (types[0].ElementType == ElementType.Char && types[1].ElementType == ElementType.I4) + { + // .ctor(char, int32) + return ConstructRepeatedCharString(context, arguments); + } + + // other + return AllocationResult.Inconclusive(); + + case 3: + return types[0] switch + { + // .ctor(char[], int32, int32) + SzArrayTypeSignature { BaseType.ElementType: ElementType.Char } + => ConstructSizedCharArrayString(context, arguments), + + // // .ctor(char*, int32, int32) + PointerTypeSignature { BaseType.ElementType: ElementType.Char } + => ConstructSizedCharPointerString(context, arguments), + + // // .ctor(sbyte*, int32, int32) + PointerTypeSignature { BaseType.ElementType: ElementType.I1 } + => ConstructSizedSBytePointerString(context, arguments), + + // other + _ => AllocationResult.Inconclusive() + }; + } + + return AllocationResult.Inconclusive(); + } + + private static AllocationResult ConstructRepeatedCharString(CilExecutionContext context, IList arguments) + { + char c = (char)arguments[0].AsSpan().U16; + int length = arguments[1].AsSpan().I32; + + long result = context.Machine.Heap.AllocateString(new string(c, length)); + + return AllocationResult.FullyConstructed(context.Machine.ValueFactory.RentNativeInteger(result)); + } + + private static AllocationResult ConstructCharArrayString(CilExecutionContext context, IList arguments) + { + // Get array behind object. + var array = arguments[0].AsObjectHandle(context.Machine); + + // Read chars from array. + long result = array.Address != 0 + ? context.Machine.Heap.AllocateString(array.ReadArrayData()) + : 0; + + return AllocationResult.FullyConstructed(context.Machine.ValueFactory.RentNativeInteger(result)); + } + + private static AllocationResult ConstructSizedCharArrayString(CilExecutionContext context, + IList arguments) + { + // Get array behind object. + var array = arguments[0].AsObjectHandle(context.Machine); + int startIndex = arguments[1].AsSpan().I32; + int length = arguments[2].AsSpan().I32; + + // Read chars from array. + long result = array.Address != 0 + ? context.Machine.Heap.AllocateString(array.ReadArrayData(startIndex, length)) + : 0; + + return AllocationResult.FullyConstructed(context.Machine.ValueFactory.RentNativeInteger(result)); + } + + private static AllocationResult ConstructCharPointerString(CilExecutionContext context, IList arguments) + { + // Measure bounds. + long startAddress = arguments[0].AsSpan().ReadNativeInteger(context.Machine.Is32Bit); + long length = GetNullTerminatedStringLength(context, startAddress, sizeof(char)); + + // Construct string. + return ConstructSizedCharPointerString(context, startAddress, 0, (int)length); + } + + private static AllocationResult ConstructSizedCharPointerString(CilExecutionContext context, + IList arguments) + { + // Measure bounds. + long startAddress = arguments[0].AsSpan().ReadNativeInteger(context.Machine.Is32Bit); + int startIndex = arguments[1].AsSpan().I32; + int length = arguments[2].AsSpan().I32; + + // Construct string. + return ConstructSizedCharPointerString(context, startAddress, startIndex, length); + } + + private static AllocationResult ConstructSizedCharPointerString( + CilExecutionContext context, + long startAddress, + int startIndex, + int length) + { + // Read Unicode bytes. + var totalData = new BitVector(length * sizeof(char) * 8, false); + context.Machine.Memory.Read(startAddress + startIndex * sizeof(char), totalData); + + // Construct string. + long result = context.Machine.Heap.AllocateString(totalData); + return AllocationResult.FullyConstructed(context.Machine.ValueFactory.RentNativeInteger(result)); + } + + private static AllocationResult ConstructSBytePointerString(CilExecutionContext context, IList arguments) + { + // Measure bounds. + long startAddress = arguments[0].AsSpan().ReadNativeInteger(context.Machine.Is32Bit); + long length = GetNullTerminatedStringLength(context, startAddress, sizeof(sbyte)); + + // Construct string. + return ConstructSizedSBytePointerString(context, startAddress, (int)length); + } + + private static AllocationResult ConstructSizedSBytePointerString(CilExecutionContext context, + IList arguments) + { + // Measure bounds. + long startAddress = arguments[0].AsSpan().ReadNativeInteger(context.Machine.Is32Bit); + int startIndex = arguments[1].AsSpan().I32; + int length = arguments[2].AsSpan().I32; + + // Construct string. + return ConstructSizedSBytePointerString(context, startAddress + startIndex, length); + } + + private static AllocationResult ConstructSizedSBytePointerString(CilExecutionContext context, long startAddress, + int length) + { + // Read ASCII bytes. + var totalData = new BitVector(length * 8, false); + context.Machine.Memory.Read(startAddress, totalData); + + // Convert all ASCII bytes to Unicode bytes. + var widened = new BitVector(totalData.Count * 2, false); + for (int i = 0; i < totalData.ByteCount; i++) + { + widened.Bits[i * 2] = totalData.Bits[i]; + widened.KnownMask[i * 2] = totalData.KnownMask[i]; + widened.KnownMask[i * 2 + 1] = 0xFF; + } + + // Construct string. + long result = context.Machine.Heap.AllocateString(widened); + return AllocationResult.FullyConstructed(context.Machine.ValueFactory.RentNativeInteger(result)); + } + + private static long GetNullTerminatedStringLength(CilExecutionContext context, long startAddress, int charSize) + { + long endAddress = startAddress; + + var singleChar = context.Machine.ValueFactory.BitVectorPool.Rent(charSize * 8, false); + try + { + bool foundNullTerminator = false; + while (!foundNullTerminator) + { + context.Machine.Memory.Read(endAddress, singleChar); + switch (singleChar.AsSpan().IsZero.Value) + { + case TrileanValue.True: + // We definitely found a zero. + foundNullTerminator = true; + break; + + case TrileanValue.False: + // We definitely found a non-zero value. + endAddress += charSize; + break; + + case TrileanValue.Unknown: + // We are not sure this is a zero. We cannot continue. + throw new CilEmulatorException( + $"Attempted to read a null-terminated string at 0x{startAddress:X8} where the final size is uncertain."); + + default: + throw new ArgumentOutOfRangeException(); + } + } + } + finally + { + context.Machine.ValueFactory.BitVectorPool.Return(singleChar); + } + + return (endAddress - startAddress) / charSize; + } +} \ No newline at end of file diff --git a/src/Platforms/Echo.Platforms.AsmResolver/Emulation/ObjectHandle.cs b/src/Platforms/Echo.Platforms.AsmResolver/Emulation/ObjectHandle.cs index bd33d3b6..e5216309 100644 --- a/src/Platforms/Echo.Platforms.AsmResolver/Emulation/ObjectHandle.cs +++ b/src/Platforms/Echo.Platforms.AsmResolver/Emulation/ObjectHandle.cs @@ -10,7 +10,6 @@ namespace Echo.Platforms.AsmResolver.Emulation /// /// Represents an address to an object (including its object header) within a CIL virtual machine. /// - [DebuggerDisplay("{Address} ({Tag})")] public readonly struct ObjectHandle : IEquatable { /// @@ -51,16 +50,16 @@ public long Address public StructHandle Contents => new(Machine, Address + Machine.ValueFactory.ObjectHeaderSize); [DebuggerBrowsable(DebuggerBrowsableState.Never)] - internal string Tag + internal object? Tag { get { if (IsNull) - return "null"; + return null; try { - return GetObjectType().FullName; + return GetObjectType(); } catch { @@ -204,10 +203,6 @@ public void ReadArrayLength(BitVectorSpan buffer) /// Occurs when the array has an unknown length. public BitVector ReadArrayData() { - var arrayType = GetObjectType(); - if (arrayType is not SzArrayTypeSignature { BaseType: { } elementType }) - throw new ArgumentException("The object handle does not point to an array type"); - var length = Machine.ValueFactory.BitVectorPool.Rent(32, false); try { @@ -215,20 +210,38 @@ public BitVector ReadArrayData() ReadArrayLength(length); if (!length.AsSpan().IsFullyKnown) throw new ArgumentException("The array has an unknown length."); - - // Determine total space required to store the array data. - int elementSize = (int) Machine.ValueFactory.GetTypeValueMemoryLayout(elementType).Size; - var result = new BitVector(length.AsSpan().I32 * elementSize * 8, false); // Read the data. - ReadArrayData(result); - - return result; + return ReadArrayData(0, length.AsSpan().I32); } finally { Machine.ValueFactory.BitVectorPool.Return(length); - } + } + } + + /// + /// Interprets the handle as an array reference, and obtains all elements of the array as one continuous buffer + /// of bytes. + /// + /// The index to start reading from. + /// The number of elements to read. + /// The raw array data. + /// Occurs when the array has an unknown length. + public BitVector ReadArrayData(int startIndex, int length) + { + var arrayType = GetObjectType(); + if (arrayType is not SzArrayTypeSignature { BaseType: { } elementType }) + throw new ArgumentException("The object handle does not point to an array type"); + + // Determine total space required to store the array data. + int elementSize = (int) Machine.ValueFactory.GetTypeValueMemoryLayout(elementType).Size; + var result = new BitVector(length * elementSize * 8, false); + + // Read the data. + ReadArrayData(result, startIndex, elementType); + + return result; } /// @@ -242,6 +255,36 @@ public void ReadArrayData(BitVectorSpan buffer) Machine.Memory.Read(Address + Machine.ValueFactory.ArrayHeaderSize, buffer); } + /// + /// Interprets the handle as an array reference, and obtains all elements of the array as one continuous buffer + /// of bytes. + /// + /// The buffer to copy the array data into. + /// The start index to read from. + /// The raw array data. + public void ReadArrayData(BitVectorSpan buffer, int startIndex) + { + var arrayType = GetObjectType(); + if (arrayType is not SzArrayTypeSignature { BaseType: { } elementType }) + throw new ArgumentException("The object handle does not point to an array type"); + + ReadArrayData(buffer, startIndex, elementType); + } + + /// + /// Interprets the handle as an array reference, and obtains all elements of the array as one continuous buffer + /// of bytes. + /// + /// The buffer to copy the array data into. + /// The start index to read from. + /// The element type to assume. + /// The raw array data. + public void ReadArrayData(BitVectorSpan buffer, int startIndex, TypeSignature elementType) + { + int elementSize = (int) Machine.ValueFactory.GetTypeValueMemoryLayout(elementType).Size; + Machine.Memory.Read(Address + Machine.ValueFactory.ArrayHeaderSize + startIndex * elementSize, buffer); + } + /// /// Interprets the handle as an array reference, and writes elements to the array's data as one continuous buffer /// of bytes. @@ -257,7 +300,7 @@ public void WriteArrayData(BitVectorSpan buffer) /// of bytes. /// /// The buffer to copy the array data from. - public void WriteArrayData(byte[] buffer) + public void WriteArrayData(ReadOnlySpan buffer) { Machine.Memory.Write(Address + Machine.ValueFactory.ArrayHeaderSize, buffer); } @@ -362,6 +405,6 @@ public override int GetHashCode() } /// - public override string ToString() => $"0x{Address.ToString(Machine.Is32Bit ? "X8" : "X16")}"; + public override string ToString() => $"0x{Address.ToString(Machine.Is32Bit ? "X8" : "X16")} ({Tag})"; } } \ No newline at end of file diff --git a/test/Platforms/Echo.Platforms.AsmResolver.Tests/Echo.Platforms.AsmResolver.Tests.csproj b/test/Platforms/Echo.Platforms.AsmResolver.Tests/Echo.Platforms.AsmResolver.Tests.csproj index 1b4bc732..9795f218 100644 --- a/test/Platforms/Echo.Platforms.AsmResolver.Tests/Echo.Platforms.AsmResolver.Tests.csproj +++ b/test/Platforms/Echo.Platforms.AsmResolver.Tests/Echo.Platforms.AsmResolver.Tests.csproj @@ -8,6 +8,8 @@ true enable + + 12 diff --git a/test/Platforms/Echo.Platforms.AsmResolver.Tests/Emulation/Heap/ManagedObjectHeapTest.cs b/test/Platforms/Echo.Platforms.AsmResolver.Tests/Emulation/Heap/ManagedObjectHeapTest.cs index 93ea0cb6..87bd70d7 100644 --- a/test/Platforms/Echo.Platforms.AsmResolver.Tests/Emulation/Heap/ManagedObjectHeapTest.cs +++ b/test/Platforms/Echo.Platforms.AsmResolver.Tests/Emulation/Heap/ManagedObjectHeapTest.cs @@ -47,7 +47,7 @@ public unsafe void ValidateObjectSlicesAreCorrect(string value) { // Beautiful pointer abuse to read the raw data of the string. var rawObjectAddress = *(nint*) Unsafe.AsPointer(ref value); - byte[] data = new byte[sizeof(nint) + sizeof(int) + value.Length * sizeof(char)]; + byte[] data = new byte[sizeof(nint) + sizeof(int) + (value.Length+1) * sizeof(char)]; Marshal.Copy(rawObjectAddress, data, 0, data.Length); var objectSpan = new BitVector(data).AsSpan(); diff --git a/test/Platforms/Echo.Platforms.AsmResolver.Tests/Emulation/Invocation/StringAllocatorTest.cs b/test/Platforms/Echo.Platforms.AsmResolver.Tests/Emulation/Invocation/StringAllocatorTest.cs new file mode 100644 index 00000000..7a0a405f --- /dev/null +++ b/test/Platforms/Echo.Platforms.AsmResolver.Tests/Emulation/Invocation/StringAllocatorTest.cs @@ -0,0 +1,273 @@ +using System.Runtime.InteropServices; +using System.Text; +using System.Threading; +using AsmResolver.DotNet; +using AsmResolver.DotNet.Signatures.Types; +using Echo.Memory; +using Echo.Platforms.AsmResolver.Emulation; +using Echo.Platforms.AsmResolver.Emulation.Dispatch; +using Echo.Platforms.AsmResolver.Emulation.Invocation; +using Echo.Platforms.AsmResolver.Tests.Mock; +using Xunit; + +namespace Echo.Platforms.AsmResolver.Tests.Emulation.Invocation; + +public class StringAllocatorTest : IClassFixture +{ + private readonly CilExecutionContext _context; + private readonly TypeDefinition _stringType; + private readonly CorLibTypeFactory _corlibFactory; + + private readonly StringAllocator _allocator = new(); + + public StringAllocatorTest(MockModuleFixture fixture) + { + var machine = new CilVirtualMachine(fixture.MockModule, false); + var thread = machine.CreateThread(); + _context = new CilExecutionContext(thread, CancellationToken.None); + _context.Thread.CallStack.Push(fixture.MockModule.GetOrCreateModuleConstructor()); + + _corlibFactory = fixture.MockModule.CorLibTypeFactory; + _stringType = _corlibFactory.String.Type.Resolve()!; + } + + [Fact] + public void UsingCharArrayConstructorNull() + { + // Locate .ctor(char[]) + var ctor = _stringType.GetConstructor(_corlibFactory.Char.MakeSzArrayType())!; + + // Allocate using null pointer. + var result = _allocator.Allocate(_context, ctor, [_context.Machine.ValueFactory.CreateNull()]); + + // Verify. + Assert.Equal(AllocationResultType.FullyConstructed, result.ResultType); + Assert.Equal(0, result.Address!.AsObjectHandle(_context.Machine).Address); + } + + [Fact] + public void UsingCharArrayConstructorNonNull() + { + const string expectedString = "Hello, world!"; + + // Allocate a char[]. + char[] data = expectedString.ToCharArray(); + var array = _context.Machine.Heap + .AllocateSzArray(_corlibFactory.Char, data.Length, true) + .AsObjectHandle(_context.Machine); + + array.WriteArrayData(MemoryMarshal.Cast(data)); + + // Allocate string using .ctor(char[]) + var ctor = _stringType.GetConstructor(_corlibFactory.Char.MakeSzArrayType())!; + var result = _allocator.Allocate(_context, ctor, [ + _context.Machine.ValueFactory.CreateNativeInteger(array.Address) + ]); + + // Verify. + Assert.Equal(AllocationResultType.FullyConstructed, result.ResultType); + + var resultObject = result.Address!.AsObjectHandle(_context.Machine); + Assert.Equal(expectedString, Encoding.Unicode.GetString(resultObject.ReadStringData().Bits)); + Assert.Equal(data.Length, resultObject.ReadStringLength().AsSpan().I32); + } + + [Fact] + public void UsingCharArrayConstructorSized() + { + const string expectedString = "Hello, world!"; + const int startIndex = 2; + const int length = 6; + + // Allocate a char[]. + char[] data = expectedString.ToCharArray(); + var array = _context.Machine.Heap + .AllocateSzArray(_corlibFactory.Char, data.Length, true) + .AsObjectHandle(_context.Machine); + + array.WriteArrayData(MemoryMarshal.Cast(data)); + + // Allocate string using .ctor(char[], int32, int32) + var ctor = _stringType.GetConstructor( + _corlibFactory.Char.MakeSzArrayType(), + _corlibFactory.Int32, + _corlibFactory.Int32 + )!; + var result = _allocator.Allocate(_context, ctor, [ + _context.Machine.ValueFactory.CreateNativeInteger(array.Address), + new BitVector(startIndex), + new BitVector(length) + ]); + + // Verify. + Assert.Equal(AllocationResultType.FullyConstructed, result.ResultType); + + var resultObject = result.Address!.AsObjectHandle(_context.Machine); + Assert.Equal(expectedString[startIndex..(startIndex + length)], + Encoding.Unicode.GetString(resultObject.ReadStringData().Bits)); + Assert.Equal(length, resultObject.ReadStringLength().AsSpan().I32); + } + + [Fact] + public void UsingCharPointerConstructor() + { + const string expectedString = "Hello, world!"; + + // Allocate some flat memory. + char[] data = expectedString.ToCharArray(); + long address = _context.Machine.Heap.AllocateFlat((uint)(data.Length + 1) * sizeof(char), true); + _context.Machine.Memory.Write(address, MemoryMarshal.Cast(data)); + + // Allocate string using .ctor(char*) + var ctor = _stringType.GetConstructor(_corlibFactory.Char.MakePointerType())!; + var result = _allocator.Allocate(_context, ctor, [_context.Machine.ValueFactory.CreateNativeInteger(address)]); + + // Verify. + Assert.Equal(AllocationResultType.FullyConstructed, result.ResultType); + + var resultObject = result.Address!.AsObjectHandle(_context.Machine); + Assert.Equal(expectedString, Encoding.Unicode.GetString(resultObject.ReadStringData().Bits)); + Assert.Equal(data.Length, resultObject.ReadStringLength().AsSpan().I32); + } + + [Fact] + public void UsingCharPointerConstructorSized() + { + const string expectedString = "Hello, world!"; + const int startIndex = 2; + const int length = 6; + + // Allocate some flat memory. + char[] data = expectedString.ToCharArray(); + long address = _context.Machine.Heap.AllocateFlat((uint)(data.Length + 1) * sizeof(char), true); + _context.Machine.Memory.Write(address, MemoryMarshal.Cast(data)); + + // Allocate string using .ctor(char*, int32, int32) + var ctor = _stringType.GetConstructor( + _corlibFactory.Char.MakePointerType(), + _corlibFactory.Int32, + _corlibFactory.Int32 + )!; + var result = _allocator.Allocate(_context, ctor, [ + _context.Machine.ValueFactory.CreateNativeInteger(address), + new BitVector(startIndex), + new BitVector(length) + ]); + + // Verify. + Assert.Equal(AllocationResultType.FullyConstructed, result.ResultType); + + var resultObject = result.Address!.AsObjectHandle(_context.Machine); + Assert.Equal(expectedString[startIndex..(startIndex + length)], + Encoding.Unicode.GetString(resultObject.ReadStringData().Bits)); + Assert.Equal(length, resultObject.ReadStringLength().AsSpan().I32); + } + + [Fact] + public void UsingCharPointerConstructorWithUnknownBits() + { + // Allocate some flat memory with unknown bits. + long address = _context.Machine.Heap.AllocateFlat(20, false); + + // Attempt to allocate string using .ctor(char*) + var ctor = _stringType.GetConstructor(_corlibFactory.Char.MakePointerType())!; + Assert.ThrowsAny(() => + { + _allocator.Allocate(_context, ctor, [_context.Machine.ValueFactory.CreateNativeInteger(address)]); + }); + } + + [Fact] + public void UsingCharPointerConstructorWithUnknownBitsBounded() + { + // Allocate some flat memory with unknown bits. + long address = _context.Machine.Heap.AllocateFlat(20, false); + _context.Machine.Memory.Write(address, new BitVector( + [0x41, 0x00, 0x41, 0x00, 0x00, 0x00], + [0x0F, 0x0F, 0x0F, 0x0F, 0xFF, 0xFF] + )); + + // Allocate string using .ctor(char*) + var ctor = _stringType.GetConstructor(_corlibFactory.Char.MakePointerType())!; + var result = _allocator.Allocate(_context, ctor, [_context.Machine.ValueFactory.CreateNativeInteger(address)]); + + // Verify + Assert.Equal(AllocationResultType.FullyConstructed, result.ResultType); + + var resultObject = result.Address!.AsObjectHandle(_context.Machine); + Assert.Equal(2, resultObject.ReadStringLength().AsSpan().I32); + } + + [Fact] + public void UsingSBytePointerConstructor() + { + const string expectedString = "Hello, world!"; + + // Allocate some flat memory. + byte[] data = Encoding.ASCII.GetBytes(expectedString); + long address = _context.Machine.Heap.AllocateFlat((uint)(data.Length + 1), true); + _context.Machine.Memory.Write(address, data); + + // Allocate string using .ctor(sbyte*) + var ctor = _stringType.GetConstructor(_corlibFactory.SByte.MakePointerType())!; + var result = _allocator.Allocate(_context, ctor, [_context.Machine.ValueFactory.CreateNativeInteger(address)]); + + // Verify. + Assert.Equal(AllocationResultType.FullyConstructed, result.ResultType); + + var resultObject = result.Address!.AsObjectHandle(_context.Machine); + Assert.Equal(expectedString, Encoding.Unicode.GetString(resultObject.ReadStringData().Bits)); + Assert.Equal(data.Length, resultObject.ReadStringLength().AsSpan().I32); + } + + [Fact] + public void UsingSbytePointerConstructorSized() + { + const string expectedString = "Hello, world!"; + const int startIndex = 2; + const int length = 6; + + // Allocate some flat memory. + byte[] data = Encoding.ASCII.GetBytes(expectedString); + long address = _context.Machine.Heap.AllocateFlat((uint)(data.Length + 1), true); + _context.Machine.Memory.Write(address, data); + + // Allocate string using .ctor(sbyte*, int32, int32) + var ctor = _stringType.GetConstructor( + _corlibFactory.SByte.MakePointerType(), + _corlibFactory.Int32, + _corlibFactory.Int32 + )!; + var result = _allocator.Allocate(_context, ctor, [ + _context.Machine.ValueFactory.CreateNativeInteger(address), + new BitVector(startIndex), + new BitVector(length) + ]); + + // Verify. + Assert.Equal(AllocationResultType.FullyConstructed, result.ResultType); + + var resultObject = result.Address!.AsObjectHandle(_context.Machine); + Assert.Equal(expectedString[startIndex..(startIndex + length)], + Encoding.Unicode.GetString(resultObject.ReadStringData().Bits)); + Assert.Equal(length, resultObject.ReadStringLength().AsSpan().I32); + } + + [Fact] + public void UsingCharInt32Constructor() + { + // Allocate string using .ctor(char, int32) + var ctor = _stringType.GetConstructor(_corlibFactory.Char, _corlibFactory.Int32)!; + var result = _allocator.Allocate(_context, ctor, [ + new BitVector('A'), + new BitVector(30) + ]); + + // Verify. + Assert.Equal(AllocationResultType.FullyConstructed, result.ResultType); + + var resultObject = result.Address!.AsObjectHandle(_context.Machine); + Assert.Equal(new string('A', 30), Encoding.Unicode.GetString(resultObject.ReadStringData().Bits)); + Assert.Equal(30, resultObject.ReadStringLength().AsSpan().I32); + } +} \ No newline at end of file