using System.Collections.Concurrent;
using System.Runtime.InteropServices;
using System.Text;
namespace Sandbox;
///
/// A growable block of native memory with automatic pooling and safe disposal.
/// Used internally for high-performance scenarios where managed arrays are too slow.
/// Memory is allocated from the unmanaged heap and will not be moved by the GC.
///
internal unsafe class NativeMemoryBlock : IDisposable
{
// Pool of reusable blocks to reduce allocation overhead
static ConcurrentQueue SharedPool = new();
///
/// Gets the pointer to the allocated native memory.
///
public void* Pointer { get; private set; }
///
/// Gets the current size of the allocated buffer in bytes.
///
public int Size { get; private set; }
///
/// Tracks whether this block is currently in the shared pool.
///
private bool InPool;
///
/// Creates a new native memory block with the specified initial size.
///
/// Initial size in bytes
public NativeMemoryBlock( int initialSize )
{
Pointer = NativeMemory.Alloc( (uint)initialSize );
Size = initialSize;
}
///
/// Gets a block from the pool or creates a new one.
/// Blocks are automatically grown to the requested size if needed.
///
/// Minimum size needed in bytes
/// A ready-to-use memory block
public static NativeMemoryBlock GetOrCreatePooled( int initialSize )
{
if ( initialSize <= 0 ) initialSize = 512;
if ( SharedPool.TryDequeue( out var pooled ) )
{
pooled.InPool = false;
pooled.Grow( initialSize );
return pooled;
}
return new NativeMemoryBlock( initialSize );
}
///
/// Finalizer ensures native memory is freed even if Dispose is not called.
///
~NativeMemoryBlock()
{
Dispose();
}
///
/// Disposes the block, either returning it to the pool or freeing the memory.
/// Small blocks (<64KB) are pooled for reuse. Larger blocks are freed immediately.
///
public void Dispose()
{
if ( InPool )
return;
if ( Pointer is null )
return;
// Pool blocks smaller than 64KB (up to 8 in the pool)
// Larger blocks aren't pooled to avoid memory bloat
if ( Size < 1024 * 64 && SharedPool.Count < 8 )
{
SharedPool.Enqueue( this );
InPool = true;
return;
}
// Free memory for large blocks or when pool is full
if ( Pointer is not null )
{
NativeMemory.Free( Pointer );
Pointer = null;
}
}
///
/// Grows the buffer to at least the specified size.
/// The actual size may be larger due to exponential growth strategy (doubling).
///
/// Minimum required size in bytes
/// If the block has been disposed
public void Grow( int totalSize )
{
if ( Pointer is null )
throw new ObjectDisposedException( nameof( NativeMemoryBlock ) );
if ( Size >= totalSize )
return;
// Double size until we reach the required size
while ( Size < totalSize )
{
Size *= 2;
}
Pointer = NativeMemory.Realloc( Pointer, (uint)Size );
}
///
/// Interprets the memory as a null-terminated UTF-8 string and converts it to a C# string.
///
/// The decoded string, or empty string if no null terminator found
public string ToNullTerminatedUtf8String()
{
// Find null terminator
int len = 0;
for ( int i = 0; i < Size; i++ )
{
if ( ((byte*)Pointer)[i] == 0 )
{
len = i;
break;
}
}
if ( len == 0 || len >= Size )
return string.Empty;
return System.Text.Encoding.UTF8.GetString( (byte*)Pointer, len );
}
}