C#, Native Safe Buffer 구현
Native Safe Buffer를 구현하기 위해 ref struct 와 NativeMemory 클래스를 사용한다. ref struct는 주로 실제 메모리 주소(포인터)를 들고 있는 Span<T>을 안전하게 만들기 위해 존재하고 그 메모리를 안전하고 빠르게 다루는 규칙이라고 할 수 있다.
NativeMemory 클래스는 가비지 컬렉터(GC)의 관리를 받지 않는 네이티브 힙 메모리를 직접 할당하고 관리할 수 있게 해주는 도구인데, 기존의 Marshal.AllocHGlobal이나 stdole 등을 이용하던 방식보다 성능이 뛰어나고 C 언어의 malloc, free와 유사한 인터페이스를 제공한다.
다만 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}");
}
}
}