Skip to content

Instantly share code, notes, and snippets.

@lucasteles
Last active September 3, 2024 16:11
Show Gist options
  • Save lucasteles/13b4a2b4f44553f7873a45fd012ddf9a to your computer and use it in GitHub Desktop.
Save lucasteles/13b4a2b4f44553f7873a45fd012ddf9a to your computer and use it in GitHub Desktop.
Unmanaged Array type
using System.Buffers;
using System.Runtime.CompilerServices;
// ReSharper disable ParameterHidesMember
sealed unsafe class NativeMemoryManager<T> : MemoryManager<T>
where T : unmanaged
{
byte* pointer;
int length;
bool usingMemory;
internal NativeMemoryManager(byte* pointer, int length)
{
this.pointer = pointer;
this.length = length;
usingMemory = false;
}
protected override void Dispose(bool disposing) { }
public override Span<T> GetSpan()
{
usingMemory = true;
return new(pointer, length);
}
public override MemoryHandle Pin(int elementIndex = 0)
{
if ((uint)elementIndex >= (uint)length) ThrowHelpers.ThrowIndexOutOfRangeException();
return new(pointer + (elementIndex * Unsafe.SizeOf<T>()), default, this);
}
public override void Unpin() { }
public void EnableReuse() => usingMemory = false;
public void Reset(byte* pointer, int length)
{
if (usingMemory) throw new InvalidOperationException("Memory is in use");
this.pointer = pointer;
this.length = length;
}
}
using System.Globalization;
using System.Runtime.CompilerServices;
static class ThrowHelpers
{
public static void ThrowIfArgumentOutOfBounds(int argument,
int min = int.MinValue,
int max = int.MaxValue,
[CallerArgumentExpression(nameof(argument))]
string? paramName = null)
{
if (argument < min || argument > max)
throw new ArgumentOutOfRangeException(argument.ToString(CultureInfo.InvariantCulture), paramName);
}
public static void ThrowIfArgumentIsZeroOrLess(int argument,
[CallerArgumentExpression(nameof(argument))]
string? paramName = null)
{
if (argument <= 0)
throw new ArgumentOutOfRangeException(argument.ToString(CultureInfo.InvariantCulture), paramName);
}
public static void ThrowIfArgumentIsNegative(int argument,
[CallerArgumentExpression(nameof(argument))]
string? paramName = null)
{
if (argument < 0)
throw new ArgumentOutOfRangeException(argument.ToString(CultureInfo.InvariantCulture), paramName);
}
public static void ThrowIfTypeTooBigForStack<T>() where T : unmanaged
{
var size = Mem.SizeOf<T>();
if (size > Mem.MaxStackLimit)
throw new NetcodeException($"{typeof(T).Name} size too big for stack: {size}");
}
public static Exception StructMustNotHaveReferenceTypeMembers() =>
new ArgumentException("Input struct must not have reference type members");
public static void ThrowIfTypeIsReferenceOrContainsReferences<T>() where T : struct
{
if (Mem.IsReferenceOrContainsReferences<T>())
throw StructMustNotHaveReferenceTypeMembers();
}
public static void ThrowArgumentOutOfRangeException(string argName) =>
throw new ArgumentOutOfRangeException(argName);
#pragma warning disable S112
public static void ThrowIndexOutOfRangeException() => throw new IndexOutOfRangeException();
#pragma warning restore S112
}
using System.Text;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
/// <summary>
/// An disposable unmanaged array
/// </summary>
/// <typeparam name="T"></typeparam>
[StructLayout(LayoutKind.Sequential)]
[DebuggerDisplay("Length = {Length}")]
[DebuggerTypeProxy(typeof(UnmanagedArrayDebugView<>))]
[CollectionBuilder(typeof(UnmanagedArrayCollectionBuilder), nameof(UnmanagedArrayCollectionBuilder.Create))]
public readonly unsafe struct UnmanagedArray<T>
: IDisposable, IEquatable<UnmanagedArray<T>>, IReadOnlyList<T> where T : unmanaged
{
readonly bool memoryPressure;
/// <summary>
/// Returns an empty <see cref="UnmanagedArray{T}"/>.
/// </summary>
public static readonly UnmanagedArray<T> Empty = new();
readonly byte* buffer;
/// <summary>
/// Size of the array
/// </summary>
public int Length { get; }
/// <summary>
/// Initializes a new UnmanagedArray
/// </summary>
public UnmanagedArray()
{
Length = 0;
buffer = null;
}
/// <summary>
/// Initializes a new UnmanagedArray
/// </summary>
public UnmanagedArray(int size, bool zeroed = true, bool memoryPressure = false)
{
ThrowHelpers.ThrowIfArgumentIsNegative(size);
Length = size;
if (size is 0)
buffer = null;
else
buffer = zeroed
? (byte*)NativeMemory.AllocZeroed(checked((nuint)size), (nuint)Unsafe.SizeOf<T>())
: (byte*)NativeMemory.Alloc(checked((nuint)size), (nuint)Unsafe.SizeOf<T>());
this.memoryPressure = memoryPressure;
if (memoryPressure)
GC.AddMemoryPressure((long)ByteSize);
}
/// <summary>
/// Initializes a new UnmanagedArray with values from <paramref name="values"/>
/// </summary>
/// <param name="values"></param>
public UnmanagedArray(ReadOnlySpan<T> values) : this(values.Length)
{
if (!values.IsEmpty)
values.CopyTo(Span);
}
/// <inheritdoc />
public void Dispose()
{
if (!IsCreated || PointerAddress is 0)
return;
NativeMemory.Free(buffer);
if (memoryPressure)
GC.RemoveMemoryPressure((long)ByteSize);
}
nuint ByteSize => checked((nuint)Length) * (nuint)Unsafe.SizeOf<T>();
/// <summary>
/// Returns a value that indicates whether the current <see cref="UnmanagedArray{T}"/> is empty.
/// </summary>
public bool IsEmpty => Length is 0 || !IsCreated;
/// <summary>
/// Returns true if the array is initialized
/// </summary>
public bool IsCreated => buffer != null;
/// <summary>
/// Pointer address
/// </summary>
public nuint PointerAddress => (nuint)buffer;
/// <summary>
/// Returns a span for the current array
/// </summary>
public Span<T> Span => new(buffer, Length);
/// <summary>
/// Returns a readonly span for the current array
/// </summary>
public ReadOnlySpan<T> ReadOnlySpan => new(buffer, Length);
/// <summary>
/// Returns a memory for the current array
/// </summary>
public Memory<T> ToMemory() => ToMemory(0);
/// <summary>
/// Returns a memory for the current array starting at <paramref name="start"/>
/// </summary>
public Memory<T> ToMemory(long start)
{
if ((ulong)start > (ulong)Length) ThrowHelpers.ThrowArgumentOutOfRangeException(nameof(start));
return ToMemory(start, checked((int)(Length - start)));
}
/// <summary>
/// Returns a memory for the current array starting at <paramref name="start"/> with size <paramref name="length"/>
/// </summary>
public Memory<T> ToMemory(long start, int length)
{
if ((ulong)(start + length) > (ulong)(Length))
ThrowHelpers.ThrowArgumentOutOfRangeException(nameof(length));
return new NativeMemoryManager<T>(buffer + (start * Unsafe.SizeOf<T>()), length).Memory;
}
/// <inheritdoc cref="IEnumerable.GetEnumerator()"/>
public Enumerator GetEnumerator() => new(this);
/// <inheritdoc />
IEnumerator<T> IEnumerable<T>.GetEnumerator() => GetEnumerator();
/// <inheritdoc />
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
/// <summary>
/// Indicates whether the objects are equal to each another.
/// </summary>
public static bool Equals(in UnmanagedArray<T> left, in UnmanagedArray<T> right)
{
if (left.IsCreated != right.IsCreated)
return false;
if (left.buffer == right.buffer && left.Length == right.Length)
return true;
return left.Span.SequenceEqual(right);
}
/// <inheritdoc />
public bool Equals(UnmanagedArray<T> other) => Equals(in this, in other);
/// <inheritdoc/>
public override bool Equals(object? obj) => obj is UnmanagedArray<T> other && Equals(in this, in other);
int IReadOnlyCollection<T>.Count => Length;
T IReadOnlyList<T>.this[int index] => this[index];
/// <inheritdoc cref="Span" />
public static implicit operator Span<T>(in UnmanagedArray<T> array) => array.Span;
/// <inheritdoc cref="ReadOnlySpan" />
public static implicit operator ReadOnlySpan<T>(in UnmanagedArray<T> array) => array.ReadOnlySpan;
/// <inheritdoc cref="Equals(Backdash.Data.UnmanagedArray{T})" />
public static bool operator ==(in UnmanagedArray<T> left, in UnmanagedArray<T> right) => Equals(in left, in right);
/// <inheritdoc cref="Equals(Backdash.Data.UnmanagedArray{T})" />
public static bool operator !=(in UnmanagedArray<T> left, in UnmanagedArray<T> right) => !Equals(in left, in right);
/// <summary>
/// Forms a new Array for the slice out of the current span starting at a specified index for a specified length.
/// </summary>
/// <exception cref="ArgumentOutOfRangeException"></exception>
public Span<T> Slice(int start, int length)
{
if (start > Length || length > Length - start)
#pragma warning disable S3928
throw new ArgumentOutOfRangeException();
#pragma warning restore S3928
var startAddress = PointerAddress + (nuint)start;
return new(startAddress.ToPointer(), length);
}
/// <summary>
/// Returns a reference to specified element of the <see cref="EquatableArray{T}"/>.
/// </summary>
/// <param name="index">array index</param>
public ref T this[int index]
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get
{
if ((ulong)index >= (ulong)Length) ThrowHelpers.ThrowIndexOutOfRangeException();
var memoryIndex = index * Unsafe.SizeOf<T>();
return ref Unsafe.AsRef<T>(buffer + memoryIndex);
}
}
/// <summary>
/// Returns a reference to specified element at <paramref name="index"/> of the <see cref="EquatableArray{T}"/>.
/// </summary>
/// <param name="index">array index</param>
public ref T this[Index index]
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => ref Span[index];
}
/// <summary>
/// Returns a rage slice as a copy of the current <see cref="EquatableArray{T}"/>
/// </summary>
public Span<T> this[Range range]
{
get
{
var (offset, length) = range.GetOffsetAndLength(Length);
return Slice(offset, length);
}
}
/// <inheritdoc/>
public override int GetHashCode()
{
var value = MemoryMarshal.AsBytes(Span);
HashCode hash = new();
hash.AddBytes(value);
return hash.ToHashCode();
}
/// <inheritdoc/>
public override string ToString()
{
const string separator = ", ";
const char prefix = '[';
const char suffix = ']';
StringBuilder builder = new(Length * 2);
builder.Append(prefix);
for (var i = 0; i < Length; i++)
{
if (i > 0) builder.Append(separator);
if (this[i] is var value)
builder.Append(value);
}
builder.Append(suffix);
return builder.ToString();
}
/// <summary>
/// Returns a new array of the current <see cref="UnmanagedArray{T}"/>.
/// </summary>
public T[] ToArray() => Span.ToArray();
/// <summary>
/// Creates new stream over this unmanaged memory
/// </summary>
public Stream ToStream(long offset = 0, FileAccess fileAccess = FileAccess.Read)
{
if ((ulong)offset > (ulong)Length)
ThrowHelpers.ThrowArgumentOutOfRangeException(nameof(offset));
var size = (int)ByteSize;
return new UnmanagedMemoryStream(buffer + (offset * Unsafe.SizeOf<T>()), size, size, fileAccess);
}
/// <inheritdoc cref="MemoryExtensions.CopyTo{T}(T[], Span{T})"/>
public void CopyTo(Span<T> destination) => Span.CopyTo(destination);
/// <inheritdoc cref="MemoryExtensions.CopyTo{T}(T[], Memory{T})"/>
public void CopyTo(Memory<T> destination) => Span.CopyTo(destination.Span);
/// <summary>
/// Sets all elements in an array to the default value of each element type.
/// </summary>
public void Clear(bool nativeClear = false)
{
if (nativeClear)
NativeMemory.Clear(buffer, ByteSize);
else
Span.Clear();
}
/// <summary>
/// Sets a range of elements in the array to the default value of each element type.
/// </summary>
/// <param name="index"></param>
/// <param name="length"></param>
public void Clear(int index, int length) => Span.Slice(index, length).Clear();
/// <summary>
/// Sets a range of elements in an array to the default value of each element type.
/// </summary>
/// <param name="range">Slice to be clean</param>
public void Clear(Range range)
{
var (offset, length) = range.GetOffsetAndLength(Length);
Clear(offset, length);
}
/// <summary>
/// Assigns the given value of type T to each element of the specified array.
/// </summary>
public void Fill(T value) => Span.Fill(value);
/// <summary>
/// Assigns the given return value from delegate of type T to each element of the specified array.
/// </summary>
public void Fill(Func<T> value)
{
var values = Span;
for (var i = 0; i < values.Length; i++)
values[i] = value();
}
/// <inheritdoc cref="MemoryExtensions.Reverse{T}"/>
public void Reverse() => Span.Reverse();
/// <summary>
/// Create and allocates a new UnmanagedArray with same values
/// </summary>
public UnmanagedArray<T> Clone()
{
if (!IsCreated) return new();
UnmanagedArray<T> result = new(Length);
CopyTo(result);
return result;
}
/// <inheritdoc />
public struct Enumerator : IEnumerator<T>
{
readonly UnmanagedArray<T> array;
int index;
T current;
internal Enumerator(UnmanagedArray<T> array)
{
this.array = array;
index = 0;
current = default;
}
/// <inheritdoc />
public readonly void Dispose() { }
/// <inheritdoc />
public bool MoveNext()
{
if ((uint)index < (uint)array.Length)
{
current = array[index];
index++;
return true;
}
index = array.Length + 1;
current = default;
return false;
}
/// <inheritdoc />
public readonly T Current => current;
readonly object IEnumerator.Current
{
get
{
if (index == array.Length + 1)
throw new InvalidOperationException("Index out of range");
return Current;
}
}
void IEnumerator.Reset()
{
index = 0;
current = default;
}
}
}
/// <summary>
/// DebuggerTypeProxy for <see cref="UnmanagedArray{T}"/>
/// </summary>
sealed unsafe class UnmanagedArrayDebugView<T>(UnmanagedArray<T> array)
where T : unmanaged
{
public T[]? Items
{
get
{
if (!array.IsCreated)
return default;
var length = array.Length;
var dst = new T[length];
var handle = GCHandle.Alloc(dst, GCHandleType.Pinned);
var addr = handle.AddrOfPinnedObject();
Unsafe.CopyBlock((void*)addr, (void*)array.PointerAddress, (uint)(length * Unsafe.SizeOf<T>()));
handle.Free();
return dst;
}
}
}
/// <summary>
/// Initialization methods for instances of <see cref="EquatableArray{T}"/>.
/// </summary>
public static class UnmanagedArrayCollectionBuilder
{
/// <summary>
/// Produce an <see cref="UnmanagedArray{T}"/> of contents from specified elements.
/// </summary>
/// <typeparam name="T">The type of element in the array.</typeparam>
/// <param name="items">The elements to store in the array.</param>
/// <returns>An array containing the specified items.</returns>
public static UnmanagedArray<T> Create<T>(ReadOnlySpan<T> items) where T : unmanaged => new(items);
}
public class UnmanagedArrayTests
{
[Fact]
internal void ArrayInitializer()
{
using UnmanagedArray<int> arr = new(3);
arr[0] = 10;
arr[1] = 20;
arr[2] = 30;
arr.ToArray().Should().BeEquivalentTo([10, 20, 30]);
}
[Fact]
internal void ArrayValueInitializer()
{
using UnmanagedArray<int> arr = [10, 20, 30];
arr[0].Should().Be(10);
arr[1].Should().Be(20);
arr[2].Should().Be(30);
}
[PropertyTest]
public bool CloneCompareIntegers(int[] nums)
{
using UnmanagedArray<int> equatableArray = new(nums);
using var copy = equatableArray.Clone();
return equatableArray == copy;
}
[PropertyTest]
internal bool CloneCompareFrame(Frame[] frames)
{
using UnmanagedArray<Frame> equatableArray = new(frames);
using var copy = equatableArray.Clone();
return equatableArray == copy;
}
[PropertyTest]
internal bool ComparingIntHashCodes(int[] int1, int[] int2)
{
using UnmanagedArray<int> array1 = new(int1);
using UnmanagedArray<int> array2 = new(int2);
var cmp = array1 == array2;
var cmpHash = array1.GetHashCode() == array2.GetHashCode();
return cmp == cmpHash;
}
[PropertyTest]
internal bool ComparingComplexHashCodes(Frame[] frames1, Frame[] frames2)
{
using UnmanagedArray<Frame> array1 = new(frames1);
using UnmanagedArray<Frame> array2 = new(frames2);
var cmp = array1 == array2;
var cmpHash = array1.GetHashCode() == array2.GetHashCode();
return cmp == cmpHash;
}
[PropertyTest]
internal bool ChangeValue(NonEmptyArray<int> values, PositiveInt positiveInt)
{
using UnmanagedArray<int> array = new(values.Item);
var value = positiveInt.Item;
if (array[0] == value) value += 1;
var hashBefore = array.GetHashCode();
array[0] = value;
var hashAfter = array.GetHashCode();
return array[0] == value && hashBefore != hashAfter;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment