Created
May 21, 2026 20:13
-
-
Save lucasteles/403e8fd46c3d240ddf599901017faa61 to your computer and use it in GitHub Desktop.
C# mutable NativeArray
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| using System.Collections; | |
| using System.Diagnostics; | |
| using System.Runtime.CompilerServices; | |
| using System.Runtime.InteropServices; | |
| using System.Text; | |
| namespace FGScript.Data; | |
| using MImpl = MethodImplAttribute; | |
| using MO = MethodImplOptions; | |
| [DebuggerDisplay("Count = {Count}")] | |
| [DebuggerTypeProxy(typeof(NativeArray<>.DebugView))] | |
| [CollectionBuilder(typeof(NativeArrayCollectionBuilder), nameof(NativeArrayCollectionBuilder.Create))] | |
| public sealed unsafe class NativeArray<T> : IDisposable, IReadOnlyList<T>, IList<T> where T : unmanaged | |
| { | |
| T* buffer; | |
| int capacity; | |
| bool disposed; | |
| readonly int itemSize; | |
| public NativeArray(int capacity = 16, bool zeroed = false) | |
| { | |
| this.capacity = capacity; | |
| itemSize = Unsafe.SizeOf<T>(); | |
| var bufferSize = (nuint)(capacity * itemSize); | |
| buffer = zeroed ? (T*)NativeMemory.AllocZeroed(bufferSize) : (T*)NativeMemory.Alloc(bufferSize); | |
| Count = 0; | |
| } | |
| public NativeArray(ReadOnlySpan<T> values) : this(values.Length) | |
| { | |
| Span<T> capacitySpan = new(buffer, capacity); | |
| values.CopyTo(capacitySpan); | |
| Count = values.Length; | |
| } | |
| ~NativeArray() => DisposeInternal(); | |
| public void Dispose() | |
| { | |
| DisposeInternal(); | |
| GC.SuppressFinalize(this); | |
| } | |
| void DisposeInternal() | |
| { | |
| if (Interlocked.Exchange(ref disposed, true)) return; | |
| NativeMemory.Free(buffer); | |
| buffer = null; | |
| } | |
| public int Count { get; private set; } | |
| public bool IsReadOnly => false; | |
| public bool IsEmpty => Count is 0; | |
| public ref T this[int i] | |
| { | |
| [MImpl(MO.AggressiveInlining)] | |
| get => ref buffer[i]; | |
| } | |
| T IReadOnlyList<T>.this[int index] => buffer[index]; | |
| public ref T this[Index index] | |
| { | |
| [MImpl(MO.AggressiveInlining)] | |
| get => ref buffer[index.GetOffset(Count)]; | |
| } | |
| public Span<T> this[Range range] | |
| { | |
| [MImpl(MO.AggressiveInlining)] | |
| get => AsSpan(range); | |
| } | |
| T IList<T>.this[int index] { get => buffer[index]; set => buffer[index] = value; } | |
| [MImpl(MO.AggressiveInlining)] | |
| void Resize(int size) | |
| { | |
| capacity = size; | |
| buffer = (T*)NativeMemory.Realloc(buffer, (nuint)(itemSize * capacity)); | |
| } | |
| [MImpl(MO.AggressiveInlining)] | |
| void EnsureCapacity(int size) | |
| { | |
| if (capacity >= size) return; | |
| var next = capacity; | |
| while (next < size) next *= 2; | |
| Resize(next); | |
| } | |
| public void Add(T item) | |
| { | |
| if (Count >= capacity) Resize(capacity * 2); | |
| buffer[Count] = item; | |
| Count += 1; | |
| } | |
| public void AddRange(params ReadOnlySpan<T> values) | |
| { | |
| var totalSize = Count + values.Length; | |
| EnsureCapacity(totalSize); | |
| var dest = buffer + Count; | |
| fixed (T* source = values) | |
| Unsafe.CopyBlock(dest, source, (uint)(itemSize * values.Length)); | |
| Count += values.Length; | |
| } | |
| public void AddReversed(params ReadOnlySpan<T> values) | |
| { | |
| var current = Count; | |
| AddRange(values); | |
| Span<T> span = new(buffer + current, Count - current); | |
| span.Reverse(); | |
| } | |
| public void Insert(int index, T item) | |
| { | |
| ArgumentOutOfRangeException.ThrowIfNegative(index); | |
| ArgumentOutOfRangeException.ThrowIfGreaterThan(index, Count); | |
| if (Count >= capacity) Resize(capacity * 2); | |
| var copyCount = Count - index; | |
| NativeMemory.Copy(buffer + index, buffer + index + 1, (nuint)(itemSize * copyCount)); | |
| this[index] = item; | |
| Count += 1; | |
| } | |
| public void Insert(Index index, T item) => Insert(index.GetOffset(Count), item); | |
| public int IndexOf(T item) => IndexOf(item, null); | |
| public int IndexOf(T item, IEqualityComparer<T>? comparer) => AsSpan().IndexOf(item, comparer); | |
| public bool Contains(T item) => Contains(item, null); | |
| public bool Contains(T item, IEqualityComparer<T>? comparer) => !IsEmpty && IndexOf(item, comparer) >= 0; | |
| public T Pop() => | |
| Count is 0 | |
| ? throw new InvalidOperationException("Array is empty: No items to pop.") | |
| : buffer[Count--]; | |
| public bool TryPop(out T item) | |
| { | |
| if (Count > 0) | |
| { | |
| item = buffer[Count - 1]; | |
| Count -= 1; | |
| return true; | |
| } | |
| item = default; | |
| return false; | |
| } | |
| public ref T Peek() | |
| { | |
| if (Count is 0) | |
| throw new InvalidOperationException("Array is empty: No items to peek."); | |
| return ref buffer[Count - 1]; | |
| } | |
| public bool TryPeek(out T item) | |
| { | |
| if (Count > 0) | |
| { | |
| item = buffer[Count - 1]; | |
| return true; | |
| } | |
| item = default; | |
| return false; | |
| } | |
| public bool Remove(T item) => Remove(item, EqualityComparer<T>.Default); | |
| public bool Remove(T item, IEqualityComparer<T> comparer) | |
| { | |
| var found = false; | |
| for (var i = 0; i < Count; i += 1) | |
| { | |
| if (!comparer.Equals(buffer[i], item)) | |
| continue; | |
| RemoveAt(i); | |
| Count -= 1; | |
| found = true; | |
| } | |
| return found; | |
| } | |
| public void RemoveAt(int index) | |
| { | |
| ArgumentOutOfRangeException.ThrowIfNegative(index); | |
| ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(index, Count); | |
| var border = Count - 1; | |
| if (index < border) | |
| { | |
| var copyIndex = index + 1; | |
| var copyCount = Count - copyIndex; | |
| NativeMemory.Copy(buffer + copyIndex, buffer + index, (nuint)(itemSize * copyCount)); | |
| } | |
| Count = border; | |
| } | |
| public void RemoveAt(int index, int length) | |
| { | |
| ArgumentOutOfRangeException.ThrowIfNegative(index); | |
| ArgumentOutOfRangeException.ThrowIfNegative(length); | |
| ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(index + length, Count); | |
| if (length is 0) return; | |
| var border = Count - length; | |
| if (index < border) | |
| { | |
| var copyIndex = index + length; | |
| var copyCount = Count - copyIndex; | |
| NativeMemory.Copy(buffer + copyIndex, buffer + index, (nuint)(itemSize * copyCount)); | |
| } | |
| Count = border; | |
| } | |
| public void RemoveAt(Index index) => RemoveAt(index.GetOffset(Count)); | |
| public void RemoveAt(Range range, bool inclusive = false) | |
| { | |
| var (offset, length) = range.GetOffsetAndLength(Count); | |
| if (inclusive) length++; | |
| RemoveAt(offset, length); | |
| } | |
| nuint CurrentBufferSize | |
| { | |
| [MImpl(MO.AggressiveInlining)] | |
| get => checked((nuint)Count) * (nuint)itemSize; | |
| } | |
| nuint TotalBufferSize | |
| { | |
| [MImpl(MO.AggressiveInlining)] | |
| get => checked((nuint)capacity) * (nuint)itemSize; | |
| } | |
| public void Clear() => Count = 0; | |
| public void ResetData() => NativeMemory.Clear(buffer, TotalBufferSize); | |
| public void CopyTo(NativeArray<T> other) | |
| { | |
| if (Count >= other.capacity) | |
| other.Resize(Count); | |
| NativeMemory.Copy(buffer, other.buffer, CurrentBufferSize); | |
| other.Count = Count; | |
| } | |
| public void CopyTo(Span<T> other) => AsSpan().CopyTo(other); | |
| public void CopyTo(T[] array, int arrayIndex) | |
| { | |
| ArgumentNullException.ThrowIfNull(array); | |
| CopyTo(array.AsSpan(arrayIndex)); | |
| } | |
| public void CopyTo(void* other) => Unsafe.CopyBlock(other, buffer, (uint)CurrentBufferSize); | |
| public void CopyFrom(ReadOnlySpan<T> values) => values.CopyTo(GetResetSpan(values.Length)); | |
| public T[] ToArray() | |
| { | |
| var result = new T[Count]; | |
| CopyTo(result); | |
| return result; | |
| } | |
| public Span<T> GetResetSpan(int size) | |
| { | |
| ArgumentOutOfRangeException.ThrowIfGreaterThan(size, capacity); | |
| EnsureCapacity(size); | |
| Count = size; | |
| return AsSpan(); | |
| } | |
| [MImpl(MO.AggressiveInlining)] | |
| public Span<T> AsSpan() => new(buffer, Count); | |
| [MImpl(MO.AggressiveInlining)] | |
| public Span<T> AsSpan(int start, int length) | |
| { | |
| ArgumentOutOfRangeException.ThrowIfNegative(start); | |
| ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(length, Count - start); | |
| return new(buffer + (nuint)start, length); | |
| } | |
| [MImpl(MO.AggressiveInlining)] | |
| public Span<T> AsSpan(Range range) | |
| { | |
| var (offset, length) = range.GetOffsetAndLength(Count); | |
| return AsSpan(offset, length); | |
| } | |
| [MImpl(MO.AggressiveInlining)] | |
| public static implicit operator Span<T>(NativeArray<T> array) => array.AsSpan(); | |
| [MImpl(MO.AggressiveInlining)] | |
| public static implicit operator ReadOnlySpan<T>(NativeArray<T> array) => array.AsSpan(); | |
| /// <inheritdoc/> | |
| public override string ToString() | |
| { | |
| const string separator = ", "; | |
| const char prefix = '['; | |
| const char suffix = ']'; | |
| StringBuilder builder = new(Count * 2); | |
| builder.Append(prefix); | |
| for (var i = 0; i < Count; i++) | |
| { | |
| if (i > 0) builder.Append(separator); | |
| if (this[i] is var value) | |
| builder.Append(value); | |
| } | |
| builder.Append(suffix); | |
| return builder.ToString(); | |
| } | |
| public Enumerator GetEnumerator() => new(this); | |
| /// <inheritdoc /> | |
| IEnumerator<T> IEnumerable<T>.GetEnumerator() => GetEnumerator(); | |
| /// <inheritdoc /> | |
| IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); | |
| public struct Enumerator : IEnumerator<T> | |
| { | |
| readonly NativeArray<T> values; | |
| int index; | |
| [MImpl(MO.AggressiveInlining)] | |
| internal Enumerator(NativeArray<T> items) | |
| { | |
| values = items; | |
| index = -1; | |
| } | |
| /// <inheritdoc /> | |
| public readonly void Dispose() { } | |
| /// <inheritdoc /> | |
| [MImpl(MO.AggressiveInlining)] | |
| public bool MoveNext() | |
| { | |
| var next = index + 1; | |
| if (next >= values.Count) return false; | |
| index = next; | |
| return true; | |
| } | |
| /// <inheritdoc /> | |
| public readonly T Current | |
| { | |
| [MImpl(MO.AggressiveInlining)] | |
| get => values[index]; | |
| } | |
| readonly object IEnumerator.Current => Current; | |
| void IEnumerator.Reset() => index = -1; | |
| } | |
| sealed class DebugView | |
| { | |
| #pragma warning disable IDE0290,S1144 | |
| [DebuggerBrowsable(DebuggerBrowsableState.Never)] | |
| readonly NativeArray<T>? array; | |
| // ReSharper disable once ConvertToPrimaryConstructor | |
| public DebugView(NativeArray<T>? array) => this.array = array; | |
| public T[]? Items | |
| { | |
| get | |
| { | |
| if (array is null) return null; | |
| if (array.IsEmpty) return []; | |
| var result = new T[array.Count]; | |
| var handle = GCHandle.Alloc(result, GCHandleType.Pinned); | |
| var addr = handle.AddrOfPinnedObject(); | |
| array.CopyTo((void*)addr); | |
| handle.Free(); | |
| return result; | |
| } | |
| } | |
| } | |
| } | |
| public static class NativeArrayCollectionBuilder | |
| { | |
| public static NativeArray<T> Create<T>(ReadOnlySpan<T> items) where T : unmanaged => new(items); | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #pragma warning disable IDE0028 | |
| namespace FGScript.Core.Tests.Data; | |
| public class NativeArrayTests | |
| { | |
| [Test] | |
| public void ShouldRemoveAtIndex() | |
| { | |
| using NativeArray<int> sut = [1, 2, 3, 4, 5]; | |
| sut.RemoveAt(2); | |
| sut.ToArray().Should().Equal(1, 2, 4, 5); | |
| } | |
| [Test] | |
| public void ShouldRemoveAtRange() | |
| { | |
| using NativeArray<int> sut = [1, 2, 3, 4, 5, 6]; | |
| sut.RemoveAt(1..4); | |
| sut.ToArray().Should().Equal(1, 5, 6); | |
| } | |
| [Test] | |
| public void ShouldAddRange() | |
| { | |
| using NativeArray<int> sut = new(2); | |
| sut.Add(10); | |
| sut.AddRange(20, 30, 40, 50); | |
| sut.ToArray().Should().Equal(10, 20, 30, 40, 50); | |
| } | |
| [Test] | |
| public void ShouldAddReversed() | |
| { | |
| using NativeArray<int> sut = new(2); | |
| sut.Add(50); | |
| sut.AddReversed(10, 20, 30, 40); | |
| sut.ToArray().Should().Equal(50, 40, 30, 20, 10); | |
| } | |
| [Test] | |
| public void ShouldBeEnumerated() | |
| { | |
| using NativeArray<int> sut = [10, 20, 30, 40, 50]; | |
| List<int> temp = new(sut.Count); | |
| temp.Insert(0, 10); | |
| foreach (var item in sut) | |
| temp.Add(item); | |
| temp.Should().Equal(10, 20, 30, 40, 50); | |
| } | |
| [Test] | |
| public void ShouldInsert() | |
| { | |
| using NativeArray<int> sut = [10, 20, 40, 50]; | |
| sut.Insert(2, 30); | |
| sut.ToArray().Should().Equal(10, 20, 30, 40, 50); | |
| } | |
| [Test] | |
| public void ShouldInsertBack() | |
| { | |
| using NativeArray<int> sut = [10, 20, 30, 50]; | |
| sut.Insert(^1, 40); | |
| sut.ToArray().Should().Equal(10, 20, 30, 40, 50); | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment