.NET-Community-Toolkit/CommunityToolkit.HighPerfor.../Helpers/ObjectMarshal.cs

142 lines
6.6 KiB
C#

// 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&lt;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);
}
}