x64 call stub was ABI-broken: fixed 0x20 frame left rsp misaligned at the inner call (callee entry rsp ≡ 0, ABI requires ≡ 8) and, for 5+ args, wrote stack args over the return address. Compute frame K ≡ 8 (mod 16), K ≥ 0x20 + 8*stackArgs, so the callee sees a 16-aligned stack and stack args land above the shadow window. Load register args as full 64-bit imm64 (was imm32, which truncated pointers > 4 GiB). BuildCallStub now takes nuint[]; x86 range- checks each arg against uint.MaxValue instead of silently truncating. MarshalCache conflated managed and unmanaged width in one Size field: the blittable path needs Unsafe.SizeOf<T> (bool = 1) while the marshal path needs Marshal.SizeOf<T> (inline ByValTStr/ByValArray expand past the managed pointer). Add MarshalSize; MemoryBase picks per TypeRequiresMarshal at all four IO sites. Prevents PtrToStructure/StructureToPtr from over-reading/overwriting the pinned scratch buffer (heap corruption on write). Extract shared RPM/WPM into RpmHelper: honor partial reads (dead Array.Resize removed), consistent write-return semantics; InProcessReader now guards MainModule like ExternalReader. Tests: x64 frame-alignment property + inline-marshal round-trip added (both fail against the pre-fix code); existing x64 byte-expectation tests updated to the new frame. Build clean, 100/100 pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
170 lines
5.5 KiB
C#
170 lines
5.5 KiB
C#
using System.Runtime.CompilerServices;
|
|
using System.Runtime.InteropServices;
|
|
using WhiteMagic;
|
|
|
|
namespace WhiteMagicTest;
|
|
|
|
/// <summary>
|
|
/// Tests for <see cref="MarshalCache{T}"/>: blittable size, marshal-required flag,
|
|
/// the separate MarshalSize field, and computed-once behavior.
|
|
/// </summary>
|
|
public class MarshalCacheTests
|
|
{
|
|
[Fact]
|
|
public void Size_for_int_is_4()
|
|
{
|
|
Assert.Equal(4, MarshalCache<int>.Size);
|
|
}
|
|
|
|
[Fact]
|
|
public void Size_for_byte_is_1()
|
|
{
|
|
Assert.Equal(1, MarshalCache<byte>.Size);
|
|
}
|
|
|
|
[Fact]
|
|
public void Size_for_IntPtr_matches_native_pointer_size()
|
|
{
|
|
Assert.Equal(IntPtr.Size, MarshalCache<IntPtr>.Size);
|
|
}
|
|
|
|
[Fact]
|
|
public void Size_for_bool_is_1()
|
|
{
|
|
Assert.Equal(1, MarshalCache<bool>.Size);
|
|
}
|
|
|
|
[Fact]
|
|
public void Size_for_enum_matches_underlying_type()
|
|
{
|
|
Assert.Equal(4, MarshalCache<DayOfWeek>.Size);
|
|
}
|
|
|
|
[Fact]
|
|
public void Size_for_blittable_struct_is_accurate()
|
|
{
|
|
Assert.Equal(8, MarshalCache<BlittableStruct>.Size);
|
|
}
|
|
|
|
[Fact]
|
|
public void Size_for_struct_with_bool_is_managed_layout_width()
|
|
{
|
|
// Regression for the Marshal vs. Unsafe size disagreement on a struct
|
|
// whose only field is `bool`: Marshal reports 4 (Win32 BOOL default
|
|
// marshaling), but the blittable path (MemoryMarshal.Read<T>) actually
|
|
// lays out a bool as 1 byte. MarshalCache.Size must match managed width.
|
|
Assert.Equal(Unsafe.SizeOf<SingleBoolStruct>(), MarshalCache<SingleBoolStruct>.Size);
|
|
Assert.Equal(1, MarshalCache<SingleBoolStruct>.Size);
|
|
}
|
|
|
|
[Fact]
|
|
public void Size_for_struct_with_bools_in_sequence_matches_managed_layout()
|
|
{
|
|
// Sequential struct { bool, bool } — managed width is 2, Marshal width is 8
|
|
// (two BOOLs). The blittable path uses 1 byte per bool, so Size must equal
|
|
// the managed width.
|
|
Assert.Equal(Unsafe.SizeOf<SequentialBoolStruct>(), MarshalCache<SequentialBoolStruct>.Size);
|
|
Assert.Equal(2, MarshalCache<SequentialBoolStruct>.Size);
|
|
}
|
|
|
|
[Fact]
|
|
public void TypeRequiresMarshal_is_false_for_blittable_types()
|
|
{
|
|
Assert.False(MarshalCache<int>.TypeRequiresMarshal);
|
|
Assert.False(MarshalCache<byte>.TypeRequiresMarshal);
|
|
Assert.False(MarshalCache<IntPtr>.TypeRequiresMarshal);
|
|
}
|
|
|
|
[Fact]
|
|
public void TypeRequiresMarshal_is_true_for_types_with_MarshalAs_field()
|
|
{
|
|
Assert.True(MarshalCache<MarshalAsStruct>.TypeRequiresMarshal);
|
|
}
|
|
|
|
[Fact]
|
|
public void TypeRequiresMarshal_is_true_for_reference_containing_types()
|
|
{
|
|
Assert.True(MarshalCache<InlineStrStruct>.TypeRequiresMarshal);
|
|
}
|
|
|
|
[Fact]
|
|
public void Inline_struct_with_MarshalAs_has_separate_MarshalSize()
|
|
{
|
|
// Regression for the marshal-path size bug. A struct with an inline
|
|
// ByValTStr field has mismatched managed and unmanaged widths: the managed
|
|
// width is just the pointer reference (8 bytes); the marshal unroller
|
|
// expands it into a 16-WCHAR inline buffer (32 bytes). MarshalCache.Size
|
|
// must match what the blittable path uses; MarshalCache.MarshalSize must
|
|
// match what the marshal path uses.
|
|
Assert.True(MarshalCache<InlineStrStruct>.TypeRequiresMarshal);
|
|
int expectedManaged = Unsafe.SizeOf<InlineStrStruct>(); // 8 (ptr)
|
|
int expectedMarshal = Marshal.SizeOf<InlineStrStruct>(); // 32 (16 WCHAR)
|
|
Assert.Equal(expectedManaged, MarshalCache<InlineStrStruct>.Size);
|
|
Assert.Equal(expectedMarshal, MarshalCache<InlineStrStruct>.MarshalSize);
|
|
Assert.NotEqual(MarshalCache<InlineStrStruct>.Size,
|
|
MarshalCache<InlineStrStruct>.MarshalSize);
|
|
}
|
|
|
|
[Fact]
|
|
public void MarshalSize_equals_Size_for_blittable_types()
|
|
{
|
|
// No interop expansion is needed when the type is blittable; both widths
|
|
// coincide.
|
|
Assert.Equal(MarshalCache<int>.Size, MarshalCache<int>.MarshalSize);
|
|
Assert.Equal(MarshalCache<IntPtr>.Size, MarshalCache<IntPtr>.MarshalSize);
|
|
Assert.Equal(MarshalCache<BlittableStruct>.Size, MarshalCache<BlittableStruct>.MarshalSize);
|
|
}
|
|
|
|
[Fact]
|
|
public void Properties_are_computed_once_and_cached()
|
|
{
|
|
int size1 = MarshalCache<int>.Size;
|
|
bool marshal1 = MarshalCache<int>.TypeRequiresMarshal;
|
|
|
|
int size2 = MarshalCache<int>.Size;
|
|
bool marshal2 = MarshalCache<int>.TypeRequiresMarshal;
|
|
|
|
Assert.Equal(size1, size2);
|
|
Assert.Equal(marshal1, marshal2);
|
|
}
|
|
|
|
[StructLayout(LayoutKind.Sequential)]
|
|
private struct BlittableStruct
|
|
{
|
|
public int X;
|
|
public int Y;
|
|
}
|
|
|
|
[StructLayout(LayoutKind.Sequential)]
|
|
private struct MarshalAsStruct
|
|
{
|
|
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 16)]
|
|
public byte[] Data;
|
|
}
|
|
|
|
[StructLayout(LayoutKind.Sequential)]
|
|
private struct SingleBoolStruct
|
|
{
|
|
public bool Flag;
|
|
}
|
|
|
|
[StructLayout(LayoutKind.Sequential)]
|
|
private struct SequentialBoolStruct
|
|
{
|
|
public bool A;
|
|
public bool B;
|
|
}
|
|
|
|
/// <summary>
|
|
/// A struct whose marshal layout carries an inline character buffer but
|
|
/// whose CLR managed layout is just a reference pointer. The canonical way
|
|
/// to exercise the marshal-vs-managed width split.
|
|
/// </summary>
|
|
[StructLayout(LayoutKind.Sequential)]
|
|
public struct InlineStrStruct
|
|
{
|
|
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 16)]
|
|
public string Name;
|
|
}
|
|
}
|