구조체 및 클래스 마샬링
.NET에서 구조체와 클래스는 복잡한 데이터 타입을 표현하는 데 자주 사용됩니다. 하지만 .NET의 매니지드 코드에서 네이티브 코드로 데이터를 전송할 때, 두 코드 간의 메모리 레이아웃이 다를 수 있어, 이를 올바르게 처리하기 위해 마샬링이 필요합니다.
구조체 마샬링
구조체는 Blittable 타입일 경우 특별한 변환 없이 마샬링이 가능하지만, Non-Blittable 타입을 포함하거나 복잡한 구조를 가진다면 마샬링 과정에서 메모리 변환이 필요할 수 있습니다.
간단한 구조체 마샬링
다음은 Blittable 타입으로만 구성된 간단한 구조체를 마샬링하는 예입니다.
C++ 코드
// NativeLibrary.cpp
struct Point {
int x;
int y;
};
extern "C" __declspec(dllexport) Point CreatePoint(int x, int y) {
Point p;
p.x = x;
p.y = y;
return p;
}
.NET 코드
using System;
using System.Runtime.InteropServices;
[StructLayout(LayoutKind.Sequential)]
public struct Point
{
public int x;
public int y;
}
class Program
{
[DllImport("NativeLibrary.dll", CallingConvention = CallingConvention.Cdecl)]
public static extern Point CreatePoint(int x, int y);
static void Main()
{
Point p = CreatePoint(5, 10);
Console.WriteLine($"Point: ({p.x}, {p.y})");
}
}
위 예제에서는 구조체 Point
가 Blittable 타입으로 구성되어 있기 때문에 .NET과 C++ 간에 특별한 변환 없이 바로 마샬링할 수 있습니다. 이 구조체는 메모리 레이아웃이 동일하므로 [StructLayout(LayoutKind.Sequential)]
특성을 사용하여 동일한 순서로 메모리에 배치됩니다.
중첩된 구조체 마샬링
구조체 안에 또 다른 구조체가 포함된 경우, 중첩된 구조체를 마샬링할 때도 메모리 배치에 주의해야 합니다. 모든 중첩된 구조체가 Blittable 타입으로만 이루어져 있다면 특별한 문제는 없지만, Non-Blittable 타입이 포함되면 마샬링 과정에서 추가적인 변환이 필요할 수 있습니다.
C++ 코드
// NativeLibrary.cpp
struct Point {
int x;
int y;
};
struct Rectangle {
Point topLeft;
Point bottomRight;
};
extern "C" __declspec(dllexport) Rectangle CreateRectangle(int x1, int y1, int x2, int y2) {
Rectangle rect;
rect.topLeft.x = x1;
rect.topLeft.y = y1;
rect.bottomRight.x = x2;
rect.bottomRight.y = y2;
return rect;
}
.NET 코드
using System;
using System.Runtime.InteropServices;
[StructLayout(LayoutKind.Sequential)]
public struct Point
{
public int x;
public int y;
}
[StructLayout(LayoutKind.Sequential)]
public struct Rectangle
{
public Point topLeft;
public Point bottomRight;
}
class Program
{
[DllImport("NativeLibrary.dll", CallingConvention = CallingConvention.Cdecl)]
public static extern Rectangle CreateRectangle(int x1, int y1, int x2, int y2);
static void Main()
{
Rectangle rect = CreateRectangle(0, 0, 10, 10);
Console.WriteLine($"Rectangle: Top Left ({rect.topLeft.x}, {rect.topLeft.y}), Bottom Right ({rect.bottomRight.x}, {rect.bottomRight.y})");
}
}
이 예제에서는 중첩된 구조체를 마샬링하는 방식입니다. Rectangle 구조체가 Point 구조체로 구성되어 있으며, .NET과 네이티브 코드 간의 메모리 레이아웃을 일치시키기 위해 [StructLayout(LayoutKind.Sequential)]
특성을 사용합니다.
클래스 마샬링
클래스는 구조체와 달리 참조 타입이기 때문에, .NET과 네이티브 코드 간에 추가적인 마샬링 처리가 필요합니다. 클래스는 기본적으로 Non-Blittable 타입으로 간주되며, 마샬링 시 메모리 복사와 변환이 이루어집니다.
클래스 마샬링의 기본 개념
클래스는 참조 타입이기 때문에, .NET에서 언매니지드 코드로 전달할 때 객체의 메모리 주소가 전달됩니다. 이때, 클래스의 내부 필드가 Non-Blittable 타입일 경우 각각의 필드를 변환해야 하며, 이러한 변환 작업은 성능에 영향을 줄 수 있습니다.
C++ 코드
// NativeLibrary.cpp
struct Circle {
double radius;
double Area() {
return 3.14159 * radius * radius;
}
};
extern "C" __declspec(dllexport) Circle* CreateCircle(double radius) {
Circle* circle = new Circle();
circle->radius = radius;
return circle;
}
extern "C" __declspec(dllexport) double GetCircleArea(Circle* circle) {
return circle->Area();
}
extern "C" __declspec(dllexport) void DeleteCircle(Circle* circle) {
delete circle;
}
.NET 코드
using System;
using System.Runtime.InteropServices;
[StructLayout(LayoutKind.Sequential)]
public class Circle
{
public double radius;
}
class Program
{
[DllImport("NativeLibrary.dll", CallingConvention = CallingConvention.Cdecl)]
public static extern IntPtr CreateCircle(double radius);
[DllImport("NativeLibrary.dll", CallingConvention = CallingConvention.Cdecl)]
public static extern double GetCircleArea(IntPtr circle);
[DllImport("NativeLibrary.dll", CallingConvention = CallingConvention.Cdecl)]
public static extern void DeleteCircle(IntPtr circle);
static void Main()
{
IntPtr circlePtr = CreateCircle(5.0);
double area = GetCircleArea(circlePtr);
Console.WriteLine($"Circle Area: {area}");
DeleteCircle(circlePtr);
}
}
이 예제에서는 C++에서 객체를 생성하고, .NET에서 해당 객체의 메서드를 호출하여 작업을 수행하는 방식입니다. 클래스는 참조 타입이므로 IntPtr을 사용하여 객체의 포인터를 전달하고, 메모리 해제를 위해 DeleteCircle을 호출합니다.
필드 순서와 StructLayout
구조체나 클래스에서 필드의 순서는 매우 중요합니다. [StructLayout]
특성을 통해 필드가 메모리에 배치되는 순서를 제어할 수 있으며, 이를 통해 .NET과 네이티브 코드 간의 메모리 레이아웃을 일치시킬 수 있습니다.
필드 순서 지정 예
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)]
public struct Person
{
public int age;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 50)]
public string name;
}
이 예에서는 [MarshalAs]
특성을 사용하여 문자열의 크기를 지정하고, 메모리 배치 순서를 Sequential로 지정하여 .NET과 네이티브 코드 간의 메모리 호환성을 유지합니다.
결론
구조체와 클래스는 .NET과 네이티브 코드 간의 데이터를 전달할 때 자주 사용되는 타입입니다. 구조체는 Blittable 타입일 경우 성능이 뛰어나지만, Non-Blittable 타입이나 중첩된 구조체는 마샬링 과정에서 추가적인 처리가 필요합니다. 클래스는 참조 타입이므로 포인터를 통해 전달되며, 마샬링 시 메모리 변환과 복사가 이루어집니다.
다음 글에서는 복잡한 배열 및 컬렉션 마샬링에 대해 다루며, 배열과 컬렉션을 효율적으로 마샬링하는 방법을 설명하겠습니다.