// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. using System; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; namespace CommunityToolkit.HighPerformance.Helpers; /// <summary> /// Helpers for working with <see cref="object"/> instances. /// </summary> public static class ObjectMarshal { /// <summary> /// Calculates the byte offset to a specific field within a given <see cref="object"/>. /// </summary> /// <typeparam name="T">The type of field being referenced.</typeparam> /// <param name="obj">The input <see cref="object"/> hosting the target field.</param> /// <param name="data">A reference to a target field of type <typeparamref name="T"/> within <paramref name="obj"/>.</param> /// <returns> /// The <see cref="IntPtr"/> value representing the offset to the target field from the start of the object data /// for the parameter <paramref name="obj"/>. The offset is in relation to the first usable byte after the method table. /// </returns> /// <remarks>The input parameters are not validated, and it's responsibility of the caller to ensure that /// the <paramref name="data"/> reference is actually pointing to a memory location within <paramref name="obj"/>. /// </remarks> [MethodImpl(MethodImplOptions.AggressiveInlining)] public static IntPtr DangerousGetObjectDataByteOffset<T>(object obj, ref T data) { RawObjectData? rawObj = Unsafe.As<RawObjectData>(obj)!; ref byte r0 = ref rawObj.Data; ref byte r1 = ref Unsafe.As<T, byte>(ref data); return Unsafe.ByteOffset(ref r0, ref r1); } /// <summary> /// Gets a <typeparamref name="T"/> reference to data within a given <see cref="object"/> at a specified offset. /// </summary> /// <typeparam name="T">The type of reference to retrieve.</typeparam> /// <param name="obj">The input <see cref="object"/> hosting the target field.</param> /// <param name="offset">The input byte offset for the <typeparamref name="T"/> reference to retrieve.</param> /// <returns>A <typeparamref name="T"/> reference at a specified offset within <paramref name="obj"/>.</returns> /// <remarks> /// None of the input arguments is validated, and it is responsibility of the caller to ensure they are valid. /// In particular, using an invalid offset might cause the retrieved reference to be misaligned with the /// desired data, which would break the type system. Or, if the offset causes the retrieved reference to point /// to a memory location outside of the input <see cref="object"/> instance, that might lead to runtime crashes. /// </remarks> [MethodImpl(MethodImplOptions.AggressiveInlining)] public static ref T DangerousGetObjectDataReferenceAt<T>(object obj, IntPtr offset) { RawObjectData? rawObj = Unsafe.As<RawObjectData>(obj)!; ref byte r0 = ref rawObj.Data; ref byte r1 = ref Unsafe.AddByteOffset(ref r0, offset); ref T r2 = ref Unsafe.As<byte, T>(ref r1); return ref r2; } // Description adapted from CoreCLR, see: // https://source.dot.net/#System.Private.CoreLib/src/System/Runtime/CompilerServices/RuntimeHelpers.CoreCLR.cs,301. // CLR objects are laid out in memory as follows: // [ sync block || pMethodTable || raw data .. ] // ^ ^ // | \-- ref Unsafe.As<RawObjectData>(owner).Data // \-- object // The reference to RawObjectData.Data points to the first data byte in the // target object, skipping over the sync block, method table and string length. // Even though the description above links to the CoreCLR source, this approach // can actually work on any .NET runtime, as it doesn't rely on a specific memory // layout. Even if some 3rd party .NET runtime had some additional fields in the // object header, before the field being referenced, the returned offset would still // be valid when used on instances of that particular type, as it's only being // used as a relative offset from the location pointed by the object reference. [StructLayout(LayoutKind.Explicit)] private sealed class RawObjectData { [FieldOffset(0)] public byte Data; } /// <summary> /// Tries to get a boxed <typeparamref name="T"/> value from an input <see cref="object"/> instance. /// </summary> /// <typeparam name="T">The type of value to try to unbox.</typeparam> /// <param name="obj">The input <see cref="object"/> instance to check.</param> /// <param name="value">The resulting <typeparamref name="T"/> value, if <paramref name="obj"/> was in fact a boxed <typeparamref name="T"/> value.</param> /// <returns><see langword="true"/> if a <typeparamref name="T"/> value was retrieved correctly, <see langword="false"/> otherwise.</returns> /// <remarks> /// This extension behaves just like the following method: /// <code> /// public static bool TryUnbox<T>(object obj, out T value) /// { /// if (obj is T) /// { /// value = (T)obj; /// /// return true; /// } /// /// value = default; /// /// return false; /// } /// </code> /// But in a more efficient way, and with the ability to also assign the unboxed value /// directly on an existing T variable, which is not possible with the code above. /// </remarks> [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool TryUnbox<T>(this object obj, out T value) where T : struct { if (obj.GetType() == typeof(T)) { value = Unsafe.Unbox<T>(obj); return true; } value = default; return false; } /// <summary> /// Unboxes a <typeparamref name="T"/> value from an input <see cref="object"/> instance. /// </summary> /// <typeparam name="T">The type of value to unbox.</typeparam> /// <param name="obj">The input <see cref="object"/> instance, representing a boxed <typeparamref name="T"/> value.</param> /// <returns>The <typeparamref name="T"/> value boxed in <paramref name="obj"/>.</returns> /// <exception cref="InvalidCastException">Thrown when <paramref name="obj"/> is not of type <typeparamref name="T"/>.</exception> [MethodImpl(MethodImplOptions.AggressiveInlining)] public static ref T DangerousUnbox<T>(object obj) where T : struct { return ref Unsafe.Unbox<T>(obj); } }