diff --git a/WhiteMagic/MarshalCache.cs b/WhiteMagic/MarshalCache.cs new file mode 100644 index 0000000..0b34953 --- /dev/null +++ b/WhiteMagic/MarshalCache.cs @@ -0,0 +1,69 @@ +using System.Reflection; +using System.Runtime.InteropServices; + +namespace WhiteMagic; + +/// +/// Computes and caches marshal-related metadata for type +/// exactly once. and +/// branch on these cached flags to decide between blittable Span/MemoryMarshal +/// paths and the fallback marshal path. +/// +/// The type to cache metadata for. +public static class MarshalCache +{ + /// The unmanaged size of in bytes. + public static readonly int Size; + + /// The unmanaged size of as an unsigned integer. + public static readonly uint SizeU; + + /// + /// when has at least one field + /// decorated with , meaning it cannot be copied + /// via a simple pointer dereference. + /// + public static readonly bool TypeRequiresMarshal; + + /// when is . + public static readonly bool IsIntPtr; + + /// The underlying type code of . + public static readonly TypeCode TypeCode; + + /// + /// The effective type that the marshaler uses. For an enum this is the underlying + /// integer type; for all other types it is itself. + /// + public static readonly Type RealType; + + static MarshalCache() + { + TypeCode = Type.GetTypeCode(typeof(T)); + + if (typeof(T) == typeof(bool)) + { + Size = 1; + RealType = typeof(T); + } + else if (typeof(T).IsEnum) + { + Type underlying = typeof(T).GetEnumUnderlyingType(); + Size = Marshal.SizeOf(underlying); + RealType = underlying; + TypeCode = Type.GetTypeCode(underlying); + } + else + { + Size = Marshal.SizeOf(typeof(T)); + RealType = typeof(T); + } + + SizeU = (uint)Size; + IsIntPtr = RealType == typeof(IntPtr); + + TypeRequiresMarshal = + RealType.GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic) + .Any(f => f.GetCustomAttributes(typeof(MarshalAsAttribute), true).Length != 0); + } +} diff --git a/WhiteMagicTest/MarshalCacheTests.cs b/WhiteMagicTest/MarshalCacheTests.cs new file mode 100644 index 0000000..6511eac --- /dev/null +++ b/WhiteMagicTest/MarshalCacheTests.cs @@ -0,0 +1,111 @@ +using System.Runtime.InteropServices; +using WhiteMagic; + +namespace WhiteMagicTest; + +/// +/// Tests for : blittable size, marshal-required flag, +/// IsIntPtr, and computed-once behavior. +/// +public class MarshalCacheTests +{ + [Fact] + public void Size_for_int_is_4() + { + Assert.Equal(4, MarshalCache.Size); + } + + [Fact] + public void Size_for_byte_is_1() + { + Assert.Equal(1, MarshalCache.Size); + } + + [Fact] + public void Size_for_IntPtr_matches_native_pointer_size() + { + Assert.Equal(IntPtr.Size, MarshalCache.Size); + } + + [Fact] + public void Size_for_bool_is_1() + { + Assert.Equal(1, MarshalCache.Size); + } + + [Fact] + public void Size_for_enum_matches_underlying_type() + { + Assert.Equal(4, MarshalCache.Size); + } + + [Fact] + public void Size_for_blittable_struct_is_accurate() + { + Assert.Equal(8, MarshalCache.Size); + } + + [Fact] + public void TypeRequiresMarshal_is_false_for_blittable_types() + { + Assert.False(MarshalCache.TypeRequiresMarshal); + Assert.False(MarshalCache.TypeRequiresMarshal); + Assert.False(MarshalCache.TypeRequiresMarshal); + } + + [Fact] + public void TypeRequiresMarshal_is_true_for_types_with_MarshalAs_field() + { + Assert.True(MarshalCache.TypeRequiresMarshal); + } + + [Fact] + public void IsIntPtr_is_true_for_IntPtr() + { + Assert.True(MarshalCache.IsIntPtr); + } + + [Fact] + public void IsIntPtr_is_false_for_non_IntPtr_types() + { + Assert.False(MarshalCache.IsIntPtr); + Assert.False(MarshalCache.IsIntPtr); + Assert.False(MarshalCache.IsIntPtr); + } + + [Fact] + public void All_properties_are_computed_once_and_cached() + { + int size1 = MarshalCache.Size; + bool marshal1 = MarshalCache.TypeRequiresMarshal; + bool intPtr1 = MarshalCache.IsIntPtr; + + int size2 = MarshalCache.Size; + bool marshal2 = MarshalCache.TypeRequiresMarshal; + bool intPtr2 = MarshalCache.IsIntPtr; + + Assert.Equal(size1, size2); + Assert.Equal(marshal1, marshal2); + Assert.Equal(intPtr1, intPtr2); + } + + [Fact] + public void SizeU_matches_Size_as_uint() + { + Assert.Equal((uint)MarshalCache.Size, MarshalCache.SizeU); + } + + [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; + } +}