Skip to content

Instantly share code, notes, and snippets.

@lucasteles
Created May 21, 2026 20:13
Show Gist options
  • Select an option

  • Save lucasteles/403e8fd46c3d240ddf599901017faa61 to your computer and use it in GitHub Desktop.

Select an option

Save lucasteles/403e8fd46c3d240ddf599901017faa61 to your computer and use it in GitHub Desktop.
C# mutable NativeArray
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);
}
#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