복잡한 배열 및 컬렉션 마샬링
복잡한 배열 및 컬렉션 마샬링
.NET 애플리케이션에서 다차원 배열이나 컬렉션과 같은 복잡한 데이터 구조를 네이티브 코드로 전달할 때, 마샬링 과정에서 적절한 변환과 메모리 관리가 필요합니다. 복잡한 배열과 컬렉션은 단순한 데이터 타입에 비해 메모리 배치와 데이터 전송에 더 많은 주의가 필요하며, 특히 다차원 배열이나 가변 크기 배열의 경우 마샬링이 복잡해질 수 있습니다.
일차원 배열 마샬링
일차원 배열은 .NET에서 기본적으로 지원하는 배열 형태이며, 네이티브 코드로 쉽게 마샬링할 수 있습니다. 배열의 요소가 Blittable 타입인 경우, 배열 자체가 Blittable로 간주되므로 마샬링 과정에서 별도의 데이터 변환이 필요하지 않습니다.
C++ 코드
// NativeLibrary.cpp
extern "C" __declspec(dllexport) void PrintArray(const int* array, int length) {
for (int i = 0; i < length; i++) {
printf("%d ", array[i]);
}
printf("\n");
}
.NET 코드
using System;
using System.Runtime.InteropServices;
class Program
{
[DllImport("NativeLibrary.dll", CallingConvention = CallingConvention.Cdecl)]
public static extern void PrintArray(int[] array, int length);
static void Main()
{
int[] numbers = { 1, 2, 3, 4, 5 };
PrintArray(numbers, numbers.Length);
}
}
이 예제에서는 C#의 int[] 배열이 네이티브 C++ 함수의 int* 포인터로 마샬링됩니다. 배열의 각 요소가 Blittable 타입이므로, 추가적인 데이터 변환 없이 배열 전체가 네이티브 코드로 전달됩니다.
다차원 배열 마샬링
다차원 배열은 일차원 배열과 달리 메모리 레이아웃이 복잡합니다. 특히 .NET에서 지원하는 다차원 배열은 연속된 메모리 블록으로 저장되지 않을 수 있으며, 이에 따라 네이티브 코드로 마샬링할 때 추가적인 처리 과정이 필요할 수 있습니다.
다음은 2차원 배열을 마샬링하는 예시입니다.
C++ 코드
// NativeLibrary.cpp
extern "C" __declspec(dllexport) void PrintMatrix(const int* matrix, int rows, int cols) {
for (int i = 0; i < rows; i++) {
for (int j = 0; j < cols; j++) {
printf("%d ", matrix[i * cols + j]);
}
printf("\n");
}
}
.NET 코드
using System;
using System.Runtime.InteropServices;
class Program
{
[DllImport("NativeLibrary.dll", CallingConvention = CallingConvention.Cdecl)]
public static extern void PrintMatrix(int[] matrix, int rows, int cols);
static void Main()
{
int[,] matrix = { { 1, 2 }, { 3, 4 }, { 5, 6 } };
int[] flatMatrix = new int[matrix.Length];
Buffer.BlockCopy(matrix, 0, flatMatrix, 0, matrix.Length * sizeof(int));
PrintMatrix(flatMatrix, matrix.GetLength(0), matrix.GetLength(1));
}
}
이 예제에서는 C++에서 배열을 포인터로 처리하지만, .NET의 2차원 배열은 연속된 메모리 블록으로 저장되지 않기 때문에 Buffer.BlockCopy 메서드를 사용해 2차원 배열을 1차원 배열로 변환한 후, C++ 함수로 마샬링합니다.
가변 크기 배열과 동적 할당 문제
가변 크기 배열은 배열의 크기가 런타임에 결정되며, 네이티브 코드로 마샬링할 때는 크기를 명시적으로 전달해야 합니다. 또한, 동적으로 할당된 배열의 메모리 관리는 주의해야 하며, 메모리 누수를 방지하기 위해 네이티브 코드에서 메모리 해제를 확실하게 처리해야 합니다.
다음은 가변 크기 배열을 C++ 코드로 전달하는 예시입니다.
C++ 코드
// NativeLibrary.cpp
extern "C" __declspec(dllexport) void SumArray(const int* array, int length, int* result) {
*result = 0;
for (int i = 0; i < length; i++) {
*result += array[i];
}
}
.NET 코드
using System;
using System.Runtime.InteropServices;
class Program
{
[DllImport("NativeLibrary.dll", CallingConvention = CallingConvention.Cdecl)]
public static extern void SumArray(int[] array, int length, out int result);
static void Main()
{
int[] numbers = { 1, 2, 3, 4, 5 };
SumArray(numbers, numbers.Length, out int sum);
Console.WriteLine($"Sum of array: {sum}");
}
}
이 예제에서는 배열의 크기를 네이티브 코드로 전달하고, 결과값을 out
매개변수를 통해 반환받습니다. 배열 크기를 명시적으로 전달하여, 가변 크기 배열의 크기를 네이티브 코드에서 처리할 수 있도록 합니다.
컬렉션 마샬링
.NET의 컬렉션(List<T>
, Dictionary<K,V>
, HashSet<T>
등)은 배열과는 다르게 동작하므로, 네이티브 코드로 마샬링할 때는 일반적으로 배열로 변환한 후 전달해야 합니다. 컬렉션은 참조 타입이기 때문에 메모리 레이아웃이 .NET과 네이티브 간에 호환되지 않으며, 마샬링 시 메모리 복사가 필요합니다.
예시: List<T>
마샬링
다음 예제는 .NET의 **List<int>
**를 배열로 변환하여 네이티브 코드로 전달하는 방식입니다.
.NET 코드
using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
class Program
{
[DllImport("NativeLibrary.dll", CallingConvention = CallingConvention.Cdecl)]
public static extern void PrintArray(int[] array, int length);
static void Main()
{
List<int> numbers = new List<int> { 1, 2, 3, 4, 5 };
PrintArray(numbers.ToArray(), numbers.Count);
}
}
이 예제에서는 **List<int>
**를 배열로 변환한 후 네이티브 코드로 전달합니다. 컬렉션은 참조 타입이기 때문에, 배열로 변환하여 Blittable 타입으로 처리할 수 있도록 합니다.
결론
복잡한 배열과 컬렉션을 네이티브 코드로 마샬링할 때는 배열의 메모리 레이아웃과 크기, 그리고 데이터 구조의 변환 과정을 신중히 고려해야 합니다. 일차원 배열은 비교적 쉽게 마샬링할 수 있지만, 다차원 배열과 컬렉션의 경우 추가적인 변환 작업이 필요합니다. 배열을 평탄화하거나 컬렉션을 배열로 변환하는 방식으로 효율적으로 마샬링할 수 있으며, 가변 크기 배열의 경우에는 메모리 관리에 주의해야 합니다.
다음 글에서는 포인터와 핸들 마샬링에 대해 다루며, 포인터와 핸들을 효과적으로 마샬링하는 방법을 설명하겠습니다.