구조체 마샬링 사용자 지정

구조체에 대한 기본 마샬링 규칙이 필요한 것과 정확히 일치하지 않는 경우가 있습니다. .NET 런타임은 구조체의 레이아웃과 필드를 마샬링하는 방법을 사용자 지정할 수 있는 몇 가지 확장 지점을 제공합니다. 구조체 레이아웃 사용자 지정은 모든 시나리오에서 지원되지만, 필드 마샬링 사용자 지정은 런타임 마샬링이 사용되는 시나리오에서만 지원됩니다. 런타임 마샬링을 사용하지 않도록 설정하면 필드 마샬링을 수동으로 수행해야 합니다.

메모

이 문서에서는 원본 생성 interop에 대한 마샬링 사용자 지정을 다루지 않습니다. P/Invokes 또는 COM에 원본 생성 interop을 사용하는 경우 마샬링 사용자 지정을 참조하세요.

구조체 레이아웃 사용자 지정

.NET 메모리에 System.Runtime.InteropServices.StructLayoutAttribute 필드를 배치하는 방법을 사용자 지정할 수 있도록 특성과 System.Runtime.InteropServices.LayoutKind 열거형을 제공합니다. 다음 지침은 일반적인 문제를 방지하는 데 도움이 됩니다.

✔️ 가능하면 언제든지 사용하는 LayoutKind.Sequential 것이 좋습니다.

✔️ 네이티브 구조체에도 공용체와 같은 명시적 레이아웃이 있는 경우에만 마샬링에서 LayoutKind.Explicit를 사용하세요.

❌ 클래스를 사용하여 상속을 통해 복잡한 네이티브 형식을 표현하지 마세요.

❌ .NET Core 3.0 이전의 런타임을 대상으로 해야 하는 경우, Windows 이외 플랫폼에서 구조체를 마샬링할 때는 LayoutKind.Explicit 사용을 피하세요. 3.0 이전의 .NET Core 런타임은 Intel 또는 AMD 64비트 비 Windows 시스템의 네이티브 함수에 값별로 명시적 구조를 전달하는 것을 지원하지 않습니다. 그러나 런타임은 모든 플랫폼에서 참조로 명시적 구조 전달을 지원합니다.

고정 크기 버퍼 및 인라인 배열

구조체에서 고정 크기 버퍼를 정의해야 하는 경우 속성을 사용하여 StructLayoutAttribute.Size 추가 공간을 할당하지 마세요. 속성은 Size 형식의 관리되지 않는 레이아웃(호출 Marshal.StructureToPtr할 때 사용되는 레이아웃)을 제어하고 Blittable 형식의 관리형 레이아웃에만 영향을 줍니다. Blittable이 아닌 형식의 경우 런타임은 실제 필드를 기반으로 하는 공간만 할당하므로 추가 공간을 사용하려고 하면 메모리가 손상될 수 있습니다.

❌ 관리되는 형식에서 고정 크기 버퍼를 만드는 데 사용하지 StructLayoutAttribute.Size 마십시오.

✔️ 최신 .NET(C# 12 이상)에서 고정 크기 버퍼를 정의하는 데 사용합니다System.Runtime.CompilerServices.InlineArrayAttribute.

다음 예제에서는 메모리 손상으로 이어질 수 있는 잘못된 방법을 보여 줍니다.

// ❌ DON'T: This is dangerous and can corrupt memory
[StructLayout(LayoutKind.Explicit, Size = 100)]
class Bad100ByteBuffer
{
    [FieldOffset(0)]
    private byte b;

    unsafe void UseBuffer()
    {
        fixed (byte* p = &b)
        {
            // Writing beyond the first byte corrupts memory
        }
    }
}

대신 적절한 크기의 버퍼를 만드는 데 사용합니다 InlineArrayAttribute .

// ✔️ DO: Use InlineArrayAttribute for fixed-size buffers
class Good100ByteBuffer
{
    [InlineArray(100)]
    private struct Buffer100
    {
        private byte _element0;
    }

    private Buffer100 buffer;

    unsafe void UseBuffer()
    {
        fixed (byte* p = &buffer[0])
        {
            // The buffer is guaranteed to be 100 bytes
        }
    }
}

인라인 배열에 대한 자세한 내용은 C# 언어 참조를 참조하세요.

부울 필드 마샬링 사용자 지정

네이티브 코드에는 다양한 불리언 표현 방식이 있습니다. Windows에서만 해도 부울 값을 나타내는 세 가지 방법이 있습니다. 런타임은 구조체의 네이티브 정의를 알지 못하므로, 기껏해야 부울 값을 어떻게 마샬링할지 추측할 수 있을 뿐입니다. .NET 런타임은 부울 필드를 마샬링하는 방법을 지정할 수 있는 수단을 제공합니다. 다음 예제에서는 .NET bool을 여러 네이티브 Boolean 형식으로 마샬링하는 방법을 보여 줍니다.

부울 값은 다음 예제와 같이 기본적으로 네이티브 4바이트 Win32 BOOL 값으로 마샬링됩니다.

public struct WinBool
{
    public bool b;
}
struct WinBool
{
    public BOOL b;
};

명시적이 되도록 하려면 값을 사용하여 UnmanagedType.Bool 위와 동일한 동작을 가져올 수 있습니다.

public struct WinBool
{
    [MarshalAs(UnmanagedType.Bool)]
    public bool b;
}
struct WinBool
{
    public BOOL b;
};

UnmanagedType.U1 아래 값 또는 UnmanagedType.I1 값을 사용하여 런타임에 필드를 1 바이트 네이티브 b 형식으로 마샬링 bool 하도록 지시할 수 있습니다.

public struct CBool
{
    [MarshalAs(UnmanagedType.U1)]
    public bool b;
}
struct CBool
{
    public bool b;
};

Windows에서는 UnmanagedType.VariantBool 값을 사용하여 런타임에 부울 값을 2바이트 VARIANT_BOOL 값으로 마샬링하도록 지정할 수 있습니다.

public struct VariantBool
{
    [MarshalAs(UnmanagedType.VariantBool)]
    public bool b;
}
struct VariantBool
{
    public VARIANT_BOOL b;
};

메모

VARIANT_BOOLVARIANT_TRUE = -1VARIANT_FALSE = 0라는 점에서 대부분의 bool 형식과 다릅니다. 또한 같지 VARIANT_TRUE 않은 모든 값은 false로 간주됩니다.

배열 필드 마샬링 사용자 지정

.NET 배열 마샬링을 사용자 지정하는 몇 가지 방법도 포함되어 있습니다.

기본적으로 .NET 배열을 연속 요소 목록에 대한 포인터로 마샬링합니다.

public struct DefaultArray
{
    public int[] values;
}
struct DefaultArray
{
    int32_t* values;
};

COM API와 상호 작용하는 경우 배열을 개체로 SAFEARRAY* 마샬링해야 할 수 있습니다. System.Runtime.InteropServices.MarshalAsAttribute 값과 UnmanagedType.SafeArray 값을 사용하여 런타임에 배열을 SAFEARRAY*로 마샬링하도록 지시할 수 있습니다:

public struct SafeArrayExample
{
    [MarshalAs(UnmanagedType.SafeArray)]
    public int[] values;
}
struct SafeArrayExample
{
    SAFEARRAY* values;
};

SAFEARRAY에 있는 요소 유형을 사용자 지정해야 하는 경우 MarshalAsAttribute.SafeArraySubTypeMarshalAsAttribute.SafeArrayUserDefinedSubType 필드를 사용하여 SAFEARRAY의 정확한 요소 유형을 사용자 지정할 수 있습니다.

배열을 현재 위치에서 마샬링해야 하는 경우 이 값을 사용하여 UnmanagedType.ByValArray 마샬러에 배열을 현재 위치로 마샬링하도록 지시할 수 있습니다. 이 마샬링을 사용하는 경우 런타임이 구조체의 공간을 올바르게 할당할 MarshalAsAttribute.SizeConst 수 있도록 배열의 요소 수에 대한 값을 필드에 제공해야 합니다.

public struct InPlaceArray
{
    [MarshalAs(UnmanagedType.ByValArray, SizeConst = 4)]
    public int[] values;
}
struct InPlaceArray
{
    int values[4];
};

메모

.NET 가변 길이 배열 필드를 C99 유연한 배열 멤버로 마샬링하는 것을 지원하지 않습니다.

문자열 필드 마샬링 사용자 지정

또한 .NET 문자열 필드를 마샬링하기 위한 다양한 사용자 지정을 제공합니다.

기본적으로 .NET 문자열을 null로 끝나는 문자열에 대한 포인터로 마샬링합니다. 인코딩은 에 있는 필드의 StructLayoutAttribute.CharSet 값에 System.Runtime.InteropServices.StructLayoutAttribute따라 달라집니다. 특성이 지정되지 않은 경우 인코딩은 기본적으로 ANSI 인코딩으로 설정됩니다.

[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)]
public struct DefaultString
{
    public string str;
}
struct DefaultString
{
    char* str;
};
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
public struct DefaultString
{
    public string str;
}
struct DefaultString
{
    char16_t* str; // Could also be wchar_t* on Windows.
};

다른 필드에 대해 다른 인코딩을 사용해야 하거나 구조체 정의에서 더 명시적인 것을 선호하는 경우 특성에 UnmanagedType.LPStrUnmanagedType.LPWStr 값 또는 System.Runtime.InteropServices.MarshalAsAttribute 값을 사용할 수 있습니다.

public struct AnsiString
{
    [MarshalAs(UnmanagedType.LPStr)]
    public string str;
}
struct AnsiString
{
    char* str;
};
public struct UnicodeString
{
    [MarshalAs(UnmanagedType.LPWStr)]
    public string str;
}
struct UnicodeString
{
    char16_t* str; // Could also be wchar_t* on Windows.
};

UTF-8 인코딩을 사용해 문자열을 마샬링하려는 경우, MarshalAsAttribute에서 UnmanagedType.LPUTF8Str 값을 사용할 수 있습니다.

public struct UTF8String
{
    [MarshalAs(UnmanagedType.LPUTF8Str)]
    public string str;
}
struct UTF8String
{
    char* str;
};

메모

사용 UnmanagedType.LPUTF8Str 하려면 .NET Framework 4.7 이상 버전 또는 .NET Core 1.1 이상 버전이 필요합니다. .NET Standard 2.0에서는 사용할 수 없습니다.

COM API를 사용하는 경우 문자열 BSTR을 로 마샬링해야 할 수 있습니다. UnmanagedType.BStr 값을 사용하여 문자열을 BSTR로 마샬링할 수 있습니다.

public struct BString
{
    [MarshalAs(UnmanagedType.BStr)]
    public string str;
}
struct BString
{
    BSTR str;
};

WinRT 기반 API를 사용하는 경우 문자열을 HSTRING로 마샬링해야 할 수 있습니다. UnmanagedType.HString 값을 사용하여 문자열을 HSTRING로 마샬링할 수 있습니다. HSTRING 마샬링은 기본 제공 WinRT 지원을 사용하는 런타임에서만 지원됩니다. winRT 지원은 .NET 5에서 제거되었으므로 HSTRING .NET 5 이상에서는 마샬링이 지원되지 않습니다.

public struct HString
{
    [MarshalAs(UnmanagedType.HString)]
    public string str;
}
struct BString
{
    HSTRING str;
};

API에서 구조체 내에 문자열을 직접 전달해야 하는 경우 UnmanagedType.ByValTStr 값을 사용할 수 있습니다. ByValTStr에 의해 마샬링된 문자열의 인코딩은 CharSet 특성에 의해 결정된다는 점에 유의하세요. 또한 문자열 길이가 필드에 전달되어야 합니다 MarshalAsAttribute.SizeConst .

[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)]
public struct DefaultString
{
    [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 4)]
    public string str;
}
struct DefaultString
{
    char str[4];
};
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
public struct DefaultString
{
    [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 4)]
    public string str;
}
struct DefaultString
{
    char16_t str[4]; // Could also be wchar_t[4] on Windows.
};

10진수 필드 마샬링 사용자 지정하기

Windows 작업하는 경우 네이티브 CY 또는 CURRENCY 구조를 사용하는 일부 API가 발생할 수 있습니다. 기본적으로 .NET decimal 형식은 네이티브 DECIMAL 구조체로 마샬링됩니다. 그러나 UnmanagedType.Currency 값을 사용하여 MarshalAsAttribute를 사용하면 마샬러가 decimal 값을 기본 CY 값으로 변환하도록 지정할 수 있습니다.

public struct Currency
{
    [MarshalAs(UnmanagedType.Currency)]
    public decimal dec;
}
struct Currency
{
    CY dec;
};

노조

공용체는 같은 메모리 공간을 공유하면서 서로 다른 형식의 데이터를 저장할 수 있는 데이터 형식입니다. C 언어의 일반적인 데이터 형식입니다. .NET에서는 LayoutKind.Explicit를 사용하여 유니언을 표현할 수 있습니다. .NET에서 공용체를 정의할 때는 구조체를 사용하는 것이 좋습니다. 클래스를 사용하면 레이아웃 문제가 발생하고 예측할 수 없는 동작이 발생할 수 있습니다.

struct device1_config
{
    void* a;
    void* b;
    void* c;
};
struct device2_config
{
    int32_t a;
    int32_t b;
};
struct config
{
    int32_t type;

    union
    {
        device1_config dev1;
        device2_config dev2;
    };
};
public unsafe struct Device1Config
{
    void* a;
    void* b;
    void* c;
}

public struct Device2Config
{
    int a;
    int b;
}

public struct Config
{
    public int Type;

    public _Union Anonymous;

    [StructLayout(LayoutKind.Explicit)]
    public struct _Union
    {
        [FieldOffset(0)]
        public Device1Config Dev1;

        [FieldOffset(0)]
        public Device2Config Dev2;
    }
}

마샬링 System.Object

Windows에서는 object 형식의 필드를 네이티브 코드로 마샬링할 수 있습니다. 다음 세 가지 유형 중 하나로 이러한 필드를 마샬링할 수 있습니다.

기본적으로 object 형식의 필드는 해당 객체를 래핑하는 IUnknown*로 마샬링됩니다.

public struct ObjectDefault
{
    public object obj;
}
struct ObjectDefault
{
    IUnknown* obj;
};

개체 필드를 IDispatch*로 마샬링하려면 UnmanagedType.IDispatch 값을 사용하여 MarshalAsAttribute를 추가합니다.

public struct ObjectDispatch
{
    [MarshalAs(UnmanagedType.IDispatch)]
    public object obj;
}
struct ObjectDispatch
{
    IDispatch* obj;
};

이를 VARIANT로 마샬링하려면 UnmanagedType.Struct 값으로 MarshalAsAttribute를 추가하세요.

public struct ObjectVariant
{
    [MarshalAs(UnmanagedType.Struct)]
    public object obj;
}
struct ObjectVariant
{
    VARIANT obj;
};

다음 표에서는 obj 필드의 다양한 런타임 형식이 VARIANT에 저장되는 다양한 형식에 어떻게 매핑되는지 설명합니다.

.NET 형식 VARIANT 형식
byte VT_UI1
sbyte VT_I1
short VT_I2
ushort VT_UI2
int VT_I4
uint VT_UI4
long VT_I8
ulong VT_UI8
float VT_R4
double VT_R8
char VT_UI2
string VT_BSTR
System.Runtime.InteropServices.BStrWrapper VT_BSTR
object VT_DISPATCH
System.Runtime.InteropServices.UnknownWrapper VT_UNKNOWN
System.Runtime.InteropServices.DispatchWrapper VT_DISPATCH
System.Reflection.Missing VT_ERROR
(object)null VT_EMPTY
bool VT_BOOL
System.DateTime VT_DATE
decimal VT_DECIMAL
System.Runtime.InteropServices.CurrencyWrapper VT_CURRENCY
System.DBNull VT_NULL