Native Safe Buffer를 구현하기 위해 ref structNativeMemory 클래스를 사용한다. ref struct는 주로 실제 메모리 주소(포인터)를 들고 있는 Span<T>을 안전하게 만들기 위해 존재하고 그 메모리를 안전하고 빠르게 다루는 규칙이라고 할 수 있다.

NativeMemory 클래스는 가비지 컬렉터(GC)의 관리를 받지 않는 네이티브 힙 메모리를 직접 할당하고 관리할 수 있게 해주는 도구인데, 기존의 Marshal.AllocHGlobal이나 stdole 등을 이용하던 방식보다 성능이 뛰어나고 C 언어의 mallocfree와 유사한 인터페이스를 제공한다.

다만 GC가 메모리를 치워주지 않으므로 Free호출이 필수이며, 잘못된 주소에 접근하면 프로그램이 즉시 Access Violation 될 수 있다.

메모리 할당 주체
  • 배열로부터 가져올 때: Span<byte> span = new byte[1024]; : 이것은 결국 Managed 배열이고. GC가 관리하며, 대용량일 경우 GC 부하(LOH 등)가 발생한다.
  • 스택으로부터 가져올 때: Span<byte> span = stackalloc byte[1024]; : GC 부하가 없고 빠르지만, 크기 제한이 엄격하며 보통 1MB가 넘어가면 StackOverflowException 발생
  • 네이티브 메모리로부터 가져올 때: NativeMemory.Alloc(...count...) : 가장 빠르고 크기 제한도 없지만, 수동으로 해제(Free) 하지 않으면 메모리 누수(Memory Leak)가 발생

stackalloc으로 만든 Span은 그 함수가 끝나면 사라지지만 네이티브 메모리로 만든 NativeSafeBuffer는 (비록 ref struct라 제약은 있지만) Dispose를 호출하기 전까지는 메모리가 안정적으로 유지된다.

비교 항목 new byte[] (Span) stackalloc (Span) NativeSafeBuffer
관리 주체 가비지 컬렉터 (GC) 스택 (Stack) OS (Native)
해제 시점 GC가 한가할 때 함수 종료 시 즉시 Dispose 호출 시 즉시
크기 제한 힙 메모리만큼 매우 작음 (1MB) RAM 용량만큼
안전성 매우 안전함 빠르지만 위험함 수동 해제 필수 (using)
NativeSafeBuffer.cs
using System;  
using System.Runtime.InteropServices;  
  
namespace ConsoleProject;  
  
public unsafe ref struct NativeSafeBuffer<T> where T : unmanaged  
{  
    private T* mPtr;  
    public int Length { get; }  
    public readonly int ByteCount => Length * sizeof(T);  
  
    public NativeSafeBuffer(int count)  
    {  
        ArgumentOutOfRangeException.ThrowIfNegativeOrZero(count);  
        Length = count;  
        mPtr = (T*)NativeMemory.Alloc((nuint)(count * sizeof(T)));  
    }  
  
    public readonly ref T this[int index]  
    {  
        get  
        {  
            if (mPtr == null || (uint)index >= (uint)Length)  
            {  
                throw new IndexOutOfRangeException();  
            }  
  
            return ref mPtr[index];  
        }  
    }  
  
    public readonly Span<T> AsSpan() => mPtr == null ? Span<T>.Empty : new Span<T>(mPtr, Length);  
  
    public void Dispose()  
    {  
        // Console.WriteLine("해제(Dispose)");  
        if (mPtr == null)  
        {  
            return;  
        }  
  
        NativeMemory.Free(mPtr);  
        mPtr = null;  
    }  
}
Program.cs
using System;  
using System.Text;  
  
namespace ConsoleProject;  
  
internal class Program  
{  
    private static void Main()  
    {  
        try  
        {  
            using NativeSafeBuffer<byte> byteBuffer = new(1024);  
            const string message = "Hello, Native Memory!";  
            int written = Encoding.UTF8.GetBytes(message, byteBuffer.AsSpan());  
            byteBuffer[0] = (byte)'h';  
            string result = Encoding.UTF8.GetString(byteBuffer.AsSpan()[..written]);  
            Console.WriteLine(result); // H -> h  
            Console.WriteLine($"[byte 버퍼] 요소 개수: {byteBuffer.Length}, 할당된 총 바이트: {byteBuffer.ByteCount} bytes");  
              
            Console.WriteLine("---------------------------");  
  
            using NativeSafeBuffer<int> intBuffer = new(10);  
            intBuffer[0] = 123;  
            intBuffer[9] = 999;  
            Console.WriteLine($"Numbers: {intBuffer[0]}, {intBuffer[9]}");  
            Console.WriteLine($"[Integer 버퍼] 요소 개수: {intBuffer.Length}, 할당된 총 바이트: {intBuffer.ByteCount} bytes");  
              
            Console.WriteLine("---------------------------");  
  
            using NativeSafeBuffer<char> charBuffer = new(1024);  
            const string message_char = "안녕하세요 C# 네이티브!";  
  
            if (!message_char.AsSpan().TryCopyTo(charBuffer.AsSpan()))  
            {  
                Console.WriteLine("버퍼가 부족하여 복사를 중단했습니다.");  
                return;  
            }  
  
            charBuffer[message_char.Length - 1] = '?'; // ! -> ?  
  
            ReadOnlySpan<char> resultSpan = charBuffer.AsSpan()[..message_char.Length];  
            Console.WriteLine(resultSpan.ToString());  
            Console.WriteLine($"Charsize: {resultSpan.Length}");  
  
            for (int i = 0; i < 10; i++)  
            {  
                charBuffer[i] = (char)('A' + i);  
            }  
  
            Console.WriteLine(charBuffer.AsSpan()[..10].ToString());  
            Console.WriteLine($"[Char 버퍼] 요소 개수: {charBuffer.Length}, 할당된 총 바이트: {charBuffer.ByteCount} bytes");  
            Console.WriteLine("---------------------------");  
        }  
        catch (Exception ex)  
        {  
            Console.WriteLine($"오류 발생: {ex.Message}");  
        }  
    }  
}