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;
+ }
+}