Files
whitemagic/WhiteMagic/MarshalCache.cs
T
2026-07-21 22:30:10 +02:00

78 lines
3.4 KiB
C#

using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace WhiteMagic;
/// <summary>
/// Computes and caches the byte size and marshalling decision for type
/// <typeparamref name="T"/> exactly once. <see cref="MemoryBase.Read{T}"/> and
/// <see cref="MemoryBase.Write{T}"/> branch on <see cref="TypeRequiresMarshal"/>
/// to decide between the blittable <c>Span</c> /
/// <see cref="System.Runtime.InteropServices.MemoryMarshal"/> path and the
/// <see cref="Marshal.PtrToStructure"/> path.
/// </summary>
/// <typeparam name="T">The type to cache metadata for.</typeparam>
public static class MarshalCache<T>
{
/// <summary>
/// The byte size of <typeparamref name="T"/>. For the blittable path this is
/// the managed layout size <see cref="Unsafe.SizeOf{T}"/> — the width that
/// <see cref="MemoryMarshal.Read{T}"/> / <see cref="MemoryMarshal.Write{T}"/>
/// actually consume. For primitive-sized types (<see cref="bool"/>, <see cref="char"/>,
/// and the underlying of enums) the size matches the CLR primitive width.
/// </summary>
public static readonly int Size;
/// <summary>
/// <see langword="true"/> when <typeparamref name="T"/> cannot be copied through
/// the blittable <see cref="System.Runtime.InteropServices.MemoryMarshal"/> path
/// and must fall back to <see cref="Marshal.PtrToStructure"/> /
/// <see cref="Marshal.StructureToPtr"/>. This is the case when a top-level field
/// carries <see cref="MarshalAsAttribute"/>, or when <typeparamref name="T"/>
/// contains a managed reference
/// (<see cref="RuntimeHelpers.IsReferenceOrContainsReferences{T}"/>).
/// </summary>
/// <remarks>
/// The <see cref="MarshalAsAttribute"/> check inspects only top-level fields; a
/// <see cref="MarshalAsAttribute"/> on a field of a nested struct is not detected.
/// Reference-containing nested structs are still caught, because the reference
/// check propagates through nested value types.
/// </remarks>
public static readonly bool TypeRequiresMarshal;
static MarshalCache()
{
if (typeof(T) == typeof(bool))
{
Size = 1;
}
else if (typeof(T) == typeof(char))
{
// Marshal.SizeOf<char> reports 1 (ANSI char), but the blittable
// MemoryMarshal path reads/writes a char as a 2-byte UTF-16 code unit.
// Use the managed layout width so Size matches what the reader actually uses.
Size = 2;
}
else if (typeof(T).IsEnum)
{
Size = Marshal.SizeOf(typeof(T).GetEnumUnderlyingType());
}
else
{
// The blittable path goes through MemoryMarshal, which uses the CLR managed
// layout. Use Unsafe.SizeOf<T> so Size agrees with that layout —
// Marshal.SizeOf<T> can disagree when a struct contains a `bool` field
// (unmanaged width 4 vs managed width 1).
Size = Unsafe.SizeOf<T>();
}
bool hasMarshalAsField =
typeof(T).GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)
.Any(f => f.GetCustomAttributes(typeof(MarshalAsAttribute), true).Length != 0);
TypeRequiresMarshal =
hasMarshalAsField || RuntimeHelpers.IsReferenceOrContainsReferences<T>();
}
}