Vanara/PInvoke/Shared/InteropServices/SafeNativeArray.cs

299 lines
16 KiB
C#

using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using Vanara.Extensions;
namespace Vanara.InteropServices
{
/// <summary>
/// A safe unmanaged array of structures allocated on the global heap. Array size determined by allocated memory size divided by size of structure.
/// </summary>
/// <typeparam name="TElem">The type of the array elements.</typeparam>
public class SafeNativeArray<TElem> : SafeNativeArrayBase<TElem, HGlobalMemoryMethods> where TElem : struct
{
/// <summary>Initializes a new instance of the <see cref="SafeNativeArray{TElem}"/> class.</summary>
public SafeNativeArray() : base(0, 0) { }
/// <summary>Initializes a new instance of the <see cref="SafeNativeArray{TElem}"/> class from a copy of a managed TElem array.</summary>
/// <param name="array">The array of bytes to copy.</param>
/// <param name="headerSize">The number of bytes to allocate in front of the array allocation.</param>
public SafeNativeArray(TElem[] array, uint headerSize = 0) : base(array, headerSize) { }
/// <summary>Initializes a new instance of the <see cref="SafeNativeArray{TElem}"/> class.</summary>
/// <param name="elementCount">The element count. This value can be 0.</param>
/// <param name="headerSize">The number of bytes to allocate in front of the array allocation.</param>
public SafeNativeArray(int elementCount, uint headerSize = 0) : base((uint)(ElemSize * elementCount), headerSize) { }
/// <summary>Initializes a new instance of the <see cref="SafeNativeArray{TElem}"/> class.</summary>
/// <param name="ptr">The handle.</param>
/// <param name="size">The size of memory allocated to the handle, in bytes.</param>
/// <param name="ownsHandle">if set to <c>true</c> if this class is responsible for freeing the memory on disposal.</param>
public SafeNativeArray(IntPtr ptr, int size, bool ownsHandle) : base(ptr, (uint)size, ownsHandle) { }
/// <summary>Initializes a new instance of the <see cref="SafeNativeArray{TElem}"/> class.</summary>
/// <param name="ptr">The handle.</param>
/// <param name="size">The size of memory allocated to the handle, in bytes.</param>
/// <param name="ownsHandle">if set to <c>true</c> if this class is responsible for freeing the memory on disposal.</param>
/// <param name="headerSize">
/// The number of bytes allocated in front of the array allocation.
/// </param>
/// <param name="elementCount">Sets the exact number of elements in the array.</param>
/// <param name="readOnly">
/// If set to <c>true</c> the array is read-only and will throw an exception if an attempt is made to add, insert or remove elements.
/// </param>
public SafeNativeArray(IntPtr ptr, uint size, bool ownsHandle, uint headerSize, int elementCount, bool readOnly) :
base(ptr, size, ownsHandle, headerSize, elementCount, readOnly)
{
}
/// <summary>Creates a new instance of the <see cref="SafeNativeArray{TElem}"/> class using a byte count.</summary>
/// <param name="byteCount">The number of bytes to allocate for this new array.</param>
public static SafeNativeArray<TElem> FromByteCount(int byteCount)
{
if (byteCount % ElemSize != 0) throw new ArgumentException($"{nameof(byteCount)} parameter must be a multiple of the size of structure {nameof(TElem)}.");
return new SafeNativeArray<TElem>(byteCount / ElemSize);
}
}
/// <summary>A safe unmanaged array of structures. Array size determined by size of structure.</summary>
/// <typeparam name="TElem">The type of the array elements.</typeparam>
/// <typeparam name="TMem">The type of the memory allocation functions to use.</typeparam>
public class SafeNativeArrayBase<TElem, TMem> : SafeMemoryHandle<TMem>, IList<TElem> where TElem : struct where TMem : IMemoryMethods, new()
{
/// <summary>Gets the size of the element.</summary>
protected static readonly int ElemSize = Marshal.SizeOf(typeof(TElem));
/// <summary>Initializes a new instance of the <see cref="SafeNativeArrayBase{TElem, TMem}"/> class from a copy of a managed TElem array.</summary>
/// <param name="array">The array of <typeparamref name="TElem"/> with which to initialize the <see cref="SafeNativeArrayBase{TElem, TMem}"/>.</param>
/// <param name="headerSize">
/// The number of bytes to allocate in front of the array allocation. This may be used to write the element count or other values
/// using the <see cref="OnUpdateHeader"/> event.
/// </param>
public SafeNativeArrayBase(TElem[] array, uint headerSize = 0) : this((uint)GetRequiredSize(array?.Length ?? 0, headerSize), headerSize) => Elements = array;
/// <summary>Initializes a new instance of the <see cref="SafeNativeArrayBase{TElem, TMem}"/> class.</summary>
/// <param name="ptr">The handle.</param>
/// <param name="size">The size of memory allocated to the handle, in bytes.</param>
/// <param name="ownsHandle">if set to <c>true</c> if this class is responsible for freeing the memory on disposal.</param>
/// <param name="headerSize">
/// The number of bytes to allocate in front of the array allocation. This may be used to write the element count or other values
/// using the <see cref="OnUpdateHeader"/> event.
/// </param>
public SafeNativeArrayBase(IntPtr ptr, uint size, bool ownsHandle = true, uint headerSize = 0) : base(ptr, (int)size, ownsHandle) { HeaderSize = headerSize; Count = GetElemCountFromBytes(size, headerSize); OnUpdateHeader(); }
/// <summary>Initializes a new instance of the <see cref="SafeNativeArrayBase{TElem, TMem}"/> class.</summary>
/// <param name="ptr">The handle.</param>
/// <param name="size">The size of memory allocated to the handle, in bytes.</param>
/// <param name="ownsHandle">if set to <c>true</c> if this class is responsible for freeing the memory on disposal.</param>
/// <param name="headerSize">
/// The number of bytes to allocate in front of the array allocation. This may be used to write the element count or other values
/// using the <see cref="OnUpdateHeader"/> event.
/// </param>
/// <param name="elementCount">Sets the exact number of elements in the array.</param>
/// <param name="readOnly">
/// If set to <c>true</c> the array is read-only and will throw an exception if an attempt is made to add, insert or remove elements.
/// </param>
public SafeNativeArrayBase(IntPtr ptr, uint size, bool ownsHandle, uint headerSize, int elementCount, bool readOnly) :
base(ptr, (int)size, ownsHandle)
{
HeaderSize = headerSize;
Count = elementCount;
IsReadOnly = readOnly;
OnUpdateHeader();
}
/// <summary>Initializes a new instance of the <see cref="SafeNativeArray{TElem}"/> class.</summary>
/// <param name="byteCount">The number of bytes to allocate for this new array.</param>
/// <param name="headerSize">
/// The number of bytes to allocate in front of the array allocation. This may be used to write the element count or other values
/// using the <see cref="OnUpdateHeader"/> event.
/// </param>
protected SafeNativeArrayBase(uint byteCount, uint headerSize = 0) : base((int)byteCount) { HeaderSize = headerSize; Count = GetElemCountFromBytes(byteCount, headerSize); OnUpdateHeader(); }
/// <summary>Gets the number of elements contained in the <see cref="SafeNativeArray{TElem}"/>.</summary>
public virtual int Count { get; protected set; }
/// <summary>Gets the size, in bytes, of the header.</summary>
/// <value>The size of the header.</value>
public uint HeaderSize { get; }
/// <summary>Gets a value indicating whether the <see cref="T:System.Collections.Generic.ICollection`1"/> is read-only.</summary>
public bool IsReadOnly { get; } = false;
/// <summary>Gets or sets the elements.</summary>
/// <value>The elements of the array.</value>
protected virtual TElem[] Elements
{
get => handle.ToArray<TElem>(Count, (int)HeaderSize);
set
{
if (IsReadOnly) throw new InvalidOperationException("Array is read-only.");
var len = value?.Length ?? 0;
Count = len;
Size = GetRequiredSize(len, HeaderSize);
Zero();
handle.Write(value, (int)HeaderSize);
OnCountChanged();
OnUpdateHeader();
}
}
/// <summary>Called when the count has changed.</summary>
protected virtual void OnCountChanged() { }
/// <summary>Called when the header needs to be refreshed.</summary>
protected virtual void OnUpdateHeader() { }
/// <summary>Gets or sets the <typeparamref name="TElem"/> value at the specified index.</summary>
/// <value>The <typeparamref name="TElem"/> value.</value>
/// <param name="index">The index.</param>
/// <returns></returns>
/// <exception cref="ArgumentOutOfRangeException">index or index</exception>
public virtual TElem this[int index]
{
get => handle.ToStructure<TElem>(Size, ElemOffset(index));
set
{
if (IsReadOnly)
throw new InvalidOperationException("Array is read-only.");
handle.Write(value, ElemOffset(index), Size);
}
}
/// <summary>Adds an item to the <see cref="T:System.Collections.Generic.ICollection`1"/>.</summary>
/// <param name="item">The object to add to the <see cref="T:System.Collections.Generic.ICollection`1"/>.</param>
public void Add(TElem item) => Insert(Count, item);
/// <summary>Removes all items from the <see cref="T:System.Collections.Generic.ICollection`1"/>.</summary>
public virtual void Clear()
{
if (IsReadOnly)
throw new InvalidOperationException("Array is read-only.");
Size = (int)HeaderSize;
Count = 0;
OnCountChanged();
}
/// <summary>Determines whether this instance contains the object.</summary>
/// <param name="item">The object to locate in the <see cref="T:System.Collections.Generic.ICollection`1"/>.</param>
/// <returns>
/// true if <paramref name="item"/> is found in the <see cref="T:System.Collections.Generic.ICollection`1"/>; otherwise, false.
/// </returns>
public bool Contains(TElem item) => EnumElements().Contains(item);
/// <summary>
/// Copies the elements of the <see cref="T:System.Collections.Generic.ICollection`1"/> to an <see cref="T:System.Array"/>, starting
/// at a particular <see cref="T:System.Array"/> index.
/// </summary>
/// <param name="array">
/// The one-dimensional <see cref="T:System.Array"/> that is the destination of the elements copied from
/// <see cref="T:System.Collections.Generic.ICollection`1"/>. The <see cref="T:System.Array"/> must have zero-based indexing.
/// </param>
/// <param name="arrayIndex">The zero-based index in <paramref name="array"/> at which copying begins.</param>
public void CopyTo(TElem[] array, int arrayIndex) => Elements.CopyTo(array, arrayIndex);
/// <summary>Returns an enumerator that iterates through the collection.</summary>
/// <returns>A <see cref="IEnumerator{TElem}"/> that can be used to iterate through the collection.</returns>
public IEnumerator<TElem> GetEnumerator() => EnumElements().GetEnumerator();
/// <summary>Gets an array of pointers to each element in this native array.</summary>
/// <returns>An array of pointers to each element in this native array.</returns>
public IntPtr[] GetPointers() => Enumerable.Range(0, Count - 1).Select(i => PtrOfElem(i)).ToArray();
/// <summary>Gets a reference to the structure at a specified index.</summary>
/// <param name="index">The index.</param>
/// <returns>The reference to the <typeparamref name="TElem"/> value.</returns>
public ref TElem GetRefAt(int index) => ref handle.AsRef<TElem>(ElemOffset(index), Size);
/// <summary>Determines the index of a specific item in the <see cref="T:System.Collections.Generic.IList`1"/>.</summary>
/// <param name="item">The object to locate in the <see cref="T:System.Collections.Generic.IList`1"/>.</param>
/// <returns>The index of <paramref name="item"/> if found in the list; otherwise, -1.</returns>
public int IndexOf(TElem item) => Array.IndexOf(Elements, item);
/// <summary>Inserts an item to the <see cref="T:System.Collections.Generic.IList`1"/> at the specified index.</summary>
/// <param name="index">The zero-based index at which <paramref name="item"/> should be inserted.</param>
/// <param name="item">The object to insert into the <see cref="T:System.Collections.Generic.IList`1"/>.</param>
/// <exception cref="ArgumentOutOfRangeException">index</exception>
public virtual void Insert(int index, TElem item)
{
if (index < 0 || index > Count) throw new ArgumentOutOfRangeException(nameof(index));
if (IsReadOnly) throw new InvalidOperationException("Array is read-only.");
var newSz = GetRequiredSize(++Count, HeaderSize);
var newPtr = mm.AllocMem(newSz);
var insertPt = ElemOffset(index);
if (index > 0)
CopyTo(newPtr, 0, insertPt);
newPtr.Write(item, insertPt, newSz);
if (index < Count - 1)
CopyTo(newPtr.Offset(insertPt + ElemSize), insertPt, Size - insertPt);
mm.FreeMem(handle);
SetHandle(newPtr);
sz = newSz;
OnCountChanged();
}
/// <summary>Removes the first occurrence of a specific object from the <see cref="T:System.Collections.Generic.ICollection`1"/>.</summary>
/// <param name="item">The object to remove from the <see cref="T:System.Collections.Generic.ICollection`1"/>.</param>
/// <returns>
/// true if <paramref name="item"/> was successfully removed from the <see cref="T:System.Collections.Generic.ICollection`1"/>;
/// otherwise, false. This method also returns false if <paramref name="item"/> is not found in the original <see cref="T:System.Collections.Generic.ICollection`1"/>.
/// </returns>
public bool Remove(TElem item)
{
var idx = IndexOf(item);
if (idx == -1) return false;
RemoveAt(idx);
return true;
}
/// <summary>Removes the <see cref="T:System.Collections.Generic.IList`1"/> item at the specified index.</summary>
/// <param name="index">The zero-based index of the item to remove.</param>
/// <exception cref="ArgumentOutOfRangeException">index</exception>
public virtual void RemoveAt(int index)
{
if (IsReadOnly) throw new InvalidOperationException("Array is read-only.");
var rmvPt = ElemOffset(index);
Count--;
var newSz = GetRequiredSize(Count, HeaderSize);
CopyTo(handle.Offset(rmvPt), rmvPt + ElemSize, newSz - rmvPt);
Size -= ElemSize;
OnCountChanged();
}
/// <summary>Returns an enumerator that iterates through a collection.</summary>
/// <returns>An <see cref="IEnumerator"/> object that can be used to iterate through the collection.</returns>
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
/// <summary>Copies the values of this array to another native memory pointer.</summary>
/// <param name="ptr">The PTR.</param>
/// <param name="start">The start.</param>
/// <param name="length">The length.</param>
/// <exception cref="ArgumentOutOfRangeException"></exception>
protected void CopyTo(IntPtr ptr, int start = 0, int length = -1)
{
if (start > Size) throw new ArgumentOutOfRangeException();
if (length == -1) length = Size - start;
if (length + start > Size || length + start < 0) throw new ArgumentOutOfRangeException();
handle.CopyTo(start, ptr, length);
}
/// <summary>Enumerates the elements.</summary>
/// <returns>An enumeration of values from the pointer.</returns>
protected IEnumerable<TElem> EnumElements() => handle.ToIEnum<TElem>(Count, (int)HeaderSize);
private static int GetElemCountFromBytes(uint byteSize, uint headerSize)
{
if (headerSize > byteSize) throw new ArgumentOutOfRangeException(nameof(byteSize));
if ((byteSize - headerSize) % ElemSize != 0) throw new ArgumentOutOfRangeException(nameof(byteSize));
return (int)Convert.ChangeType((byteSize - headerSize) / ElemSize, typeof(int));
}
private static int GetRequiredSize(int elementCount, uint headerSize) => ElemSize * elementCount + (int)headerSize;
private int ElemOffset(int index) => index >= 0 && index < Count ? index * ElemSize + (int)HeaderSize : throw new ArgumentOutOfRangeException(nameof(index));
private IntPtr PtrOfElem(int index) => handle.Offset(ElemOffset(index));
}
}