다음을 통해 공유


기본 상호 운용성 모범 사례

.NET은 네이티브 interop 코드를 사용자 지정할 수 있는 다양한 방법을 제공합니다. 이 문서에는 Microsoft .NET 팀이 네이티브 상호 운용성을 위해 따르는 지침이 포함되어 있습니다.

일반 지침

이 섹션의 지침은 모든 interop 시나리오에 적용됩니다.

  • ✔️ .NET 7+를 대상으로 하는 경우 가능하면 [LibraryImport]를 사용합니다.
    • [DllImport]를 사용하는 것이 적절한 경우가 있습니다. ID가 SYSLIB1054인 코드 분석기는 이러한 경우를 알려 줍니다.
  • ✔️ 호출하려는 네이티브 메서드와 동일한 명명 및 대문자 표시를 메서드와 매개 변수에 사용합니다.
  • ✔️ 상수 값에 동일한 명명 및 대문자 표시를 사용하는 것이 좋습니다.
  • ✔️ 네이티브 형식에 가장 가깝게 매핑되는 .NET 형식을 사용합니다. 예를 들어 C#에서 네이티브 형식이 unsigned int인 경우 uint를 사용합니다.
  • ✔️ .NET 구조체를 사용하여 클래스보다 더 높은 수준의 네이티브 형식을 표현해 보세요.
  • ✔️ C#에서 관리되지 않는 함수에 콜백을 전달할 때 Delegate 형식이 아닌 함수 포인터를 사용하는 것이 좋습니다.
  • ✔️ 배열 매개 변수에 [In][Out] 특성을 사용하세요.
  • ✔️ 원하는 동작이 기본 동작과 다른 경우에만 다른 형식에 [In][Out] 특성을 사용하세요.
  • ✔ 네이티브 배열 버퍼를 풀하려는 경우 System.Buffers.ArrayPool<T>을 사용하는 것이 좋습니다.
  • ✔️ 네이티브 라이브러리와 동일한 이름 및 대문자 표시를 사용하여 P/Invoke 선언을 클래스에 래핑하는 것이 좋습니다.
    • 이렇게 하면 [LibraryImport] 또는 [DllImport] 특성이 C# nameof 언어 기능을 사용하여 네이티브 라이브러리의 이름을 전달하고 네이티브 라이브러리 이름의 철자가 잘못 입력되지 않았는지 확인할 수 있습니다.

LibraryImport 특성 설정

ID가 SYSLIB1054인 코드 분석기는 LibraryImportAttribute를 안내하는 데 도움이 됩니다. 대부분의 경우 LibraryImportAttribute를 사용하려면 기본 설정에 의존하지 않고 명시적인 선언이 필요합니다. 이 디자인은 의도적이며 Interop 시나리오에서 의도하지 않은 동작을 방지하는 데 도움이 됩니다.

DllImport 특성 설정

설정 기본값 권장 세부 정보
PreserveSig true 기본값 유지 이 옵션을 명시적으로 false로 설정하면 실패한 HRESULT 반환 값이 예외로 바뀝니다(그 결과로 정의의 반환 값은 Null이 됨).
SetLastError false API에 따라 다름 API에서 GetLastError를 사용하는 경우 이 옵션을 true로 설정하고, Marshal.GetLastWin32Error를 사용하여 값을 가져옵니다. API에서 오류가 있음을 나타내는 조건을 설정하는 경우 실수로 덮어쓰지 않도록 다른 호출을 수행하기 전에 오류를 가져옵니다.
CharSet 컴파일러 정의(문자 집합 설명서에 지정) 정의에 문자열 또는 문자가 있는 경우 명시적으로 CharSet.Unicode 또는 CharSet.Ansi 사용 문자열의 마샬링 동작과 false인 경우 ExactSpelling에서 수행하는 작업을 지정합니다. CharSet.Ansi는 Unix에서 실제로 UTF8입니다. 대부분의 경우 Windows에서는 유니코드, Unix에서는 UTF8이 사용됩니다. 자세한 내용은 문자 집합 문서를 참조하세요.
ExactSpelling false true 이 옵션을 true로 설정하면 CharSet 설정 값(CharSet.Ansi는 “A”, CharSet.Unicode는 “W”)에 따라 “A” 또는 “W” 접미사가 있는 대체 함수 이름을 런타임이 찾지 않으므로 성능이 약간 향상됩니다.

문자열 매개 변수

string은 값(ref 또는 out 아님) 및 다음 중 하나로 전달될 때 네이티브 코드에 의해 직접 고정되고 사용됩니다(복사되는 것이 아님).

[Out] string 매개 변수를 사용하지 마세요. [Out] 특성을 사용하여 값으로 전달된 문자열 매개 변수는 문자열이 인턴 지정된 문자열인 경우 런타임을 불안정하게 만들 수 있습니다. 문자열 인터닝에 대한 자세한 내용은 String.Intern 문서를 참조하세요.

✔️ 네이티브 코드가 문자 버퍼를 채울 것으로 예상되는 경우 ArrayPoolchar[] 또는 byte[] 배열을 사용해 보세요. 이를 위해서는 인수를 [Out]으로 전달해야 합니다.

DllImport 관련 지침

✔️ 런타임이 예상되는 문자열 인코딩을 알 수 있도록 [DllImport]에서 CharSet 속성을 설정해 보세요.

✔️ StringBuilder 매개 변수를 피하는 것이 좋습니다. StringBuilder 마샬링은 ‘항상’ 네이티브 버퍼 복사본을 만듭니다. 따라서 매우 비효율적일 수 있습니다. 문자열을 사용하는 Windows API를 호출하는 일반적인 시나리오를 살펴보겠습니다.

  1. 원하는 용량(관리 용량 할당) {1}StringBuilder를 만듭니다.
  2. 호출:
    1. 네이티브 버퍼 {2}를 할당합니다.
    2. [In](StringBuilder 매개 변수의 기본값)인 경우 콘텐츠를 복사합니다.
    3. [Out]{3}(StringBuilder의 기본값)인 경우 새로 할당된 관리형 배열에 네이티브 버퍼를 복사합니다.
  3. ToString()이 다른 관리형 배열 {4}을 할당합니다.

이는 네이티브 코드에서 문자열을 가져오기 위한 {4} 할당입니다. 이를 제한하는 가장 좋은 방법은 다른 호출에서 StringBuilder를 재사용하는 것이지만, 여전히 1개 할당만 저장됩니다. ArrayPool의 문자 버퍼를 사용하고 캐시하는 것이 훨씬 좋습니다. 그런 다음 후속 호출에서 ToString()에 대한 할당만 확인할 수 있습니다.

StringBuilder의 다른 문제는 항상 반환 버퍼 백업을 첫 번째 Null에 복사하는 것입니다. 다시 전달된 문자열이 종료되지 않았거나 이중 Null 종료 문자열인 경우 P/Invoke가 올바르지 않습니다.

StringBuilder를 사용하는 경우 유의할 마지막 문제는 interop에 대해 항상 고려되는 숨겨진 Null이 용량에 포함되지 않는다는 것입니다. 대부분의 API가 Null을 ‘포함’하는 버퍼 크기를 원하기 때문에 이 동작이 잘못 파악되는 경우가 많습니다. 이로 인해 불필요한 할당이 발생할 수 있습니다. 또한 이 문제로 인해 런타임이 복사본 최소화를 위해 StringBuilder 마샬링을 최적화할 수 없게 됩니다.

문자열 마샬링에 대한 자세한 내용은 문자열의 기본 마샬링문자열 마샬링 사용자 지정을 참조하세요.

Windows 특정 - [Out] 문자열에 대해 CLR에서는 기본적으로 CoTaskMemFree를 사용하여 문자열을 해제하거나, UnmanagedType.BSTR로 표시된 문자열에 SysStringFree를 사용합니다. 출력 문자열 버퍼가 있는 대부분의 API: 전달된 문자 수에는 null이 포함되어야 합니다. 반환된 값이 전달된 문자 수보다 작으면 호출이 성공하고 값은 후행 Null이 ‘없는’ 문자 수가 됩니다. 그렇지 않으면 개수는 Null 문자를 ‘포함’하는 필수 버퍼 크기입니다.

  • 5 문자 전달, 4 문자 가져오기: 문자열은 4 문자 길이이고 뒤에 null이 있습니다.
  • 5 문자 전달, 6 문자 가져오기: 문자열의 길이는 5 문자이므로 null을 보유하려면 6 문자 버퍼가 필요합니다. 문자열에 대한 Windows 데이터 형식

부울 매개 변수 및 필드

부울은 문제가 발생하기 쉽습니다. 기본적으로 .NET bool은 4바이트 값인 Windows BOOL로 마샬링됩니다. 그러나 C 및 C++의 _Boolbool 형식은 ‘1’바이트입니다. 이로 인해 반환 값의 절반이 버려지고 ‘잠재적’으로 결과가 변경될 수 있기 때문에 버그를 추적하기 어려울 수 있습니다. .NET bool 값을 C 또는 C++ bool 형식으로 마샬링하는 방법에 대한 자세한 내용은 부울 필드 마샬링 사용자 지정 문서를 참조하세요.

GUIDs

GUID는 시그니처에 직접 사용할 수 있습니다. 많은 Windows API는 REFIID와 같은 GUID& 형식 별칭을 사용합니다. 메서드 서명에 참조 매개 변수가 포함된 경우 GUID 매개 변수 선언에 ref 키워드 또는 [MarshalAs(UnmanagedType.LPStruct)] 특성을 배치합니다.

GUID by-ref GUID
KNOWNFOLDERID REFKNOWNFOLDERID

ref GUID 매개 변수 이외의 다른 항목에는 [MarshalAs(UnmanagedType.LPStruct)]를 사용하지 마세요.

blittable 형식

blittable 형식은 관리 코드와 네이티브 코드에 동일한 비트 수준 표현이 있는 형식입니다. 따라서 네이티브 코드로(에서) 마샬링하기 위해 다른 형식으로 변환하지 않아도 되며, 이로 인해 성능이 향상되기 때문에 이 형식을 사용하는 것이 좋습니다. 일부 형식은 blittable이 아니지만 blittable 콘텐츠를 포함하는 것으로 알려져 있습니다. 이러한 형식은 다른 형식에 포함되지 않은 경우 blittable 형식과 유사한 최적화를 갖지만 구조체 필드에 있거나 UnmanagedCallersOnlyAttribute의 목적으로 blittable로 간주되지 않습니다.

런타임 마샬링이 사용하도록 설정된 경우 blittable 형식

blittable 형식:

  • byte, sbyte, short, ushort, int, uint, long, ulong, single, double
  • 인스턴스 필드에 대해 blittable 값 형식만 있는 고정 레이아웃이 있는 구조체
    • 고정 레이아웃에는 [StructLayout(LayoutKind.Sequential)] 또는 [StructLayout(LayoutKind.Explicit)]이 필요함
    • 구조체는 기본적으로 LayoutKind.Sequential입니다.

blittable 콘텐츠가 있는 형식:

  • blittable 기본 형식의 중첩되지 않은 1차원 배열(예: int[])
  • 인스턴스 필드에 대해 blittable 값 형식만 있는 고정 레이아웃이 있는 클래스
    • 고정 레이아웃에는 [StructLayout(LayoutKind.Sequential)] 또는 [StructLayout(LayoutKind.Explicit)]이 필요함
    • 기본적으로 클래스는 LayoutKind.Auto

비 blittable:

  • bool

때때로 blittable:

  • char

SOMETIMES blittable 콘텐츠가 있는 형식:

  • string

blittable 형식이 in, ref 또는 out을 사용하여 참조로 전달되거나 blittable 콘텐츠가 있는 형식이 값으로 전달되는 경우 중간 버퍼에 복사되지 않고 마샬러에 의해 단순히 고정됩니다.

char는 1차원 배열의 blittable이거나, 포함하는 형식의 일부인 경우 [StructLayout]CharSet = CharSet.Unicode로 명시적으로 표시됩니다.

[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
public struct UnicodeCharStruct
{
    public char c;
}

string은 다른 형식으로 포함되지 않고 값(ref 또는 out 아님)에 의해 인수와 다음 중 하나로 전달되는 경우 blittable 콘텐츠를 포함합니다.

  • StringMarshallingUtf16으로 정의됩니다.
  • 인수는 명시적으로 [MarshalAs(UnmanagedType.LPWSTR)]로 표시됩니다.
  • CharSet는 유니코드입니다.

고정된 GCHandle을 만들어 형식이 blittable인지 또는 blittable 콘텐츠를 포함하는지 확인할 수 있습니다. 형식이 문자열이 아니거나 blittable로 간주되지 않는 경우 GCHandle.Alloc에서 ArgumentException이 throw됩니다.

런타임 마샬링이 사용하지 않도록 설정된 경우 Blittable 형식

런타임 마샬링이 사용하지 않도록 설정되면 형식이 blittable 규칙이 훨씬 더 간단해집니다. C# unmanaged 형식이고 [StructLayout(LayoutKind.Auto)]로 표시된 필드가 없는 모든 형식은 blittable입니다. C# unmanaged 형식이 아닌 모든 형식은 blittable이 아닙니다. 배열이나 문자열과 같은 blittable 콘텐츠가 있는 형식의 개념은 런타임 마샬링이 사용하지 않도록 설정된 경우 적용되지 않습니다. 앞서 언급한 규칙에 따라 blittable로 간주되지 않는 모든 형식은 런타임 마샬링이 사용하지 않도록 설정되면 지원되지 않습니다.

이러한 규칙은 주로 boolchar가 사용되는 상황에서 기본 제공 시스템과 다릅니다. 마샬링이 사용하지 않도록 설정되면 bool은 1바이트 값으로 전달되고 정규화되지 않으며 char는 항상 2바이트 값으로 전달됩니다. 런타임 마샬링이 사용하도록 설정되면 bool은 1, 2 또는 4바이트 값으로 매핑될 수 있으며 항상 정규화되며, charCharSet에 따라 1바이트 또는 2바이트 값으로 매핑됩니다.

✔️ 가능한 경우 구조체를 blittable로 설정합니다.

자세한 내용은 다음을 참조하세요.

관리형 개체를 활성 상태로 유지

GC.KeepAlive()는 KeepAlive 메서드가 적중될 때까지 개체가 범위 내에 유지되도록 합니다.

HandleRef를 사용하면 마샬러가 P/Invoke 기간에 개체를 활성 상태로 유지할 수 있습니다. 메서드 시그니처에 IntPtr 대신 사용할 수 있습니다. SafeHandle은 이 클래스를 효과적으로 대체하며 대신 사용되어야 합니다.

GCHandle을 사용하면 관리형 개체를 고정하고 개체에 대한 네이티브 포인터를 가져올 수 있습니다. 기본 패턴은 다음과 같습니다.

GCHandle handle = GCHandle.Alloc(obj, GCHandleType.Pinned);
IntPtr ptr = handle.AddrOfPinnedObject();
handle.Free();

고정은 GCHandle의 기본값이 아닙니다. 다른 주요 패턴은 네이티브 코드를 통해 관리형 개체에 대한 참조를 전달하고, 일반적으로 콜백을 사용하여 관리 코드에 다시 전달하기 위한 것입니다. 패턴은 다음과 같습니다.

GCHandle handle = GCHandle.Alloc(obj);
SomeNativeEnumerator(callbackDelegate, GCHandle.ToIntPtr(handle));

// In the callback
GCHandle handle = GCHandle.FromIntPtr(param);
object managedObject = handle.Target;

// After the last callback
handle.Free();

메모리 누수를 방지하려면 GCHandle을 명시적으로 해제해야 합니다.

일반적인 Windows 데이터 형식

Windows API에서 일반적으로 사용되는 데이터 형식과 Windows 코드를 호출할 때 사용할 C# 형식 목록은 다음과 같습니다.

다음 형식은 이름과 관계없이 32비트 및 64비트 Windows에서 동일한 크기입니다.

Width Windows C# 대체
32 BOOL int bool
8 BOOLEAN byte [MarshalAs(UnmanagedType.U1)] bool
8 BYTE byte
8 UCHAR byte
8 UINT8 byte
8 CCHAR byte
8 CHAR sbyte
8 CHAR sbyte
8 INT8 sbyte
16 CSHORT short
16 INT16 short
16 SHORT short
16 ATOM ushort
16 UINT16 ushort
16 USHORT ushort
16 WORD ushort
32 INT int
32 INT32 int
32 LONG int CLongCULong를 확인합니다.
32 LONG32 int
32 CLONG uint CLongCULong를 확인합니다.
32 DWORD uint CLongCULong를 확인합니다.
32 DWORD32 uint
32 UINT uint
32 UINT32 uint
32 ULONG uint CLongCULong를 확인합니다.
32 ULONG32 uint
64 INT64 long
64 LARGE_INTEGER long
64 LONG64 long
64 LONGLONG long
64 QWORD long
64 DWORD64 ulong
64 UINT64 ulong
64 ULONG64 ulong
64 ULONGLONG ulong
64 ULARGE_INTEGER ulong
32 HRESULT int
32 NTSTATUS int

다음 형식은 포인터로, 플랫폼의 너비를 따릅니다. 이러한 형식에는 IntPtr/UIntPtr을 사용합니다.

서명된 포인터 형식(IntPtr 사용) 서명되지 않은 포인터 형식(UIntPtr 사용)
HANDLE WPARAM
HWND UINT_PTR
HINSTANCE ULONG_PTR
LPARAM SIZE_T
LRESULT
LONG_PTR
INT_PTR

C void*인 Windows PVOIDIntPtr 또는 UIntPtr로 마샬링할 수 있지만, 가능한 경우 void*를 사용하는 것이 좋습니다.

Windows 데이터 형식

데이터 형식 범위

이전에는 기본 제공 지원 형식

형식에 대한 기본 제공 지원이 제거되는 경우는 거의 없습니다.

UnmanagedType.HStringUnmanagedType.IInspectable 기본 제공 마샬 지원은 .NET 5 릴리스에서 제거되었습니다. 이 마샬링 형식을 사용하고 이전 프레임워크를 대상으로 하는 이진 파일을 다시 컴파일해야 합니다. 이 형식을 마샬링하는 것도 가능하지만 다음 코드 예와 같이 수동으로 마샬링해야 합니다. 이 코드는 앞으로 작동하며 이전 프레임워크와도 호환됩니다.

public sealed class HStringMarshaler : ICustomMarshaler
{
    public static readonly HStringMarshaler Instance = new HStringMarshaler();

    public static ICustomMarshaler GetInstance(string _) => Instance;

    public void CleanUpManagedData(object ManagedObj) { }

    public void CleanUpNativeData(IntPtr pNativeData)
    {
        if (pNativeData != IntPtr.Zero)
        {
            Marshal.ThrowExceptionForHR(WindowsDeleteString(pNativeData));
        }
    }

    public int GetNativeDataSize() => -1;

    public IntPtr MarshalManagedToNative(object ManagedObj)
    {
        if (ManagedObj is null)
            return IntPtr.Zero;

        var str = (string)ManagedObj;
        Marshal.ThrowExceptionForHR(WindowsCreateString(str, str.Length, out var ptr));
        return ptr;
    }

    public object MarshalNativeToManaged(IntPtr pNativeData)
    {
        if (pNativeData == IntPtr.Zero)
            return null;

        var ptr = WindowsGetStringRawBuffer(pNativeData, out var length);
        if (ptr == IntPtr.Zero)
            return null;

        if (length == 0)
            return string.Empty;

        return Marshal.PtrToStringUni(ptr, length);
    }

    [DllImport("api-ms-win-core-winrt-string-l1-1-0.dll")]
    [DefaultDllImportSearchPaths(DllImportSearchPath.System32)]
    private static extern int WindowsCreateString([MarshalAs(UnmanagedType.LPWStr)] string sourceString, int length, out IntPtr hstring);

    [DllImport("api-ms-win-core-winrt-string-l1-1-0.dll")]
    [DefaultDllImportSearchPaths(DllImportSearchPath.System32)]
    private static extern int WindowsDeleteString(IntPtr hstring);

    [DllImport("api-ms-win-core-winrt-string-l1-1-0.dll")]
    [DefaultDllImportSearchPaths(DllImportSearchPath.System32)]
    private static extern IntPtr WindowsGetStringRawBuffer(IntPtr hstring, out int length);
}

// Example usage:
[DllImport("api-ms-win-core-winrt-l1-1-0.dll", PreserveSig = true)]
internal static extern int RoGetActivationFactory(
    /*[MarshalAs(UnmanagedType.HString)]*/[MarshalAs(UnmanagedType.CustomMarshaler, MarshalTypeRef = typeof(HStringMarshaler))] string activatableClassId,
    [In] ref Guid iid,
    [Out, MarshalAs(UnmanagedType.IUnknown)] out object factory);

플랫폼 간 데이터 형식 고려 사항

C/C++ 언어에는 정의 방법에 대한 위도가 있는 형식이 있습니다. 플랫폼 간 interop을 작성할 때 플랫폼이 다른 경우가 발생할 수 있으며 고려하지 않으면 문제가 발생할 수 있습니다.

C/C++ long

C/C++ long 및 C# long의 크기가 반드시 동일할 필요는 없습니다.

C/C++의 long 형식은 "최소 32" 비트를 갖도록 정의됩니다. 즉, 필요한 최소 비트 수가 있지만 플랫폼은 원할 경우 더 많은 비트를 사용하도록 선택할 수 있습니다. 다음 표에서는 플랫폼 간 C/C++ long 데이터 형식에 대해 제공되는 비트의 차이점을 보여 줍니다.

플랫폼 32비트 64비트
Windows 32 32
macOS/*nix 32 64

이와 대조적으로 C# long은 항상 64비트입니다. 이러한 이유로 C/C++ long과 상호 운용하기 위해 C# long을 사용하지 않는 것이 가장 좋습니다.

(C/C++ long의 이 문제는 C/C++ char, short, intlong long이 이러한 모든 플랫폼에서 각각 8, 16, 32 및 64비트이므로 존재하지 않습니다.)

.NET 6 이상 버전에서는 C/C++ longunsigned long 데이터 형식과의 상호 운용을 위해 CLongCULong 형식을 사용합니다. 다음 예에서는 CLong에 대한 것이지만 비슷한 방식으로 CULong을 사용하여 unsigned long을 추상화할 수 있습니다.

// Cross platform C function
// long Function(long a);
[DllImport("NativeLib")]
extern static CLong Function(CLong a);

// Usage
nint result = Function(new CLong(10)).Value;

.NET 5 및 이전 버전을 대상으로 하는 경우 문제를 처리하기 위해 별도의 Windows 및 비 Windows 서명을 선언해야 합니다.

static readonly bool IsWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows);

// Cross platform C function
// long Function(long a);

[DllImport("NativeLib", EntryPoint = "Function")]
extern static int FunctionWindows(int a);

[DllImport("NativeLib", EntryPoint = "Function")]
extern static nint FunctionUnix(nint a);

// Usage
nint result;
if (IsWindows)
{
    result = FunctionWindows(10);
}
else
{
    result = FunctionUnix(10);
}

구조체

관리형 구조체는 스택에 생성되며, 메서드가 반환될 때까지 제거되지 않습니다. 정의상, “고정”되며 GC에 의해 이동되지 않습니다. 네이티브 코드에서 현재 메서드의 끝을 지나 포인터를 사용하지 않는 경우에도 안전하지 않은 코드 블록의 주소를 사용하면 됩니다.

blittable 구조체는 마샬링 계층에서 직접 사용할 수 있으므로 성능이 훨씬 더 뛰어납니다. 구조체를 blittable로 설정합니다(예: bool 사용 안 함). 자세한 내용은 blittable 형식 섹션을 참조하세요.

구조체가 blittable인 경우 성능 향상을 위해 Marshal.SizeOf<MyStruct>() 대신 sizeof()를 사용합니다. 위에서 언급한 대로, 고정 GCHandle 만들기를 시도하여 형식이 blittable인지 확인할 수 있습니다. 형식이 문자열이 아니거나 blittable로 간주되지 않는 경우 GCHandle.Alloc에서 ArgumentException이 throw됩니다.

정의의 구조체 포인터는 ref에 의해 전달되거나 unsafe*를 사용해야 합니다.

✔️ 공식 플랫폼 문서 또는 헤더에 사용되는 모양 및 이름에 최대한 가깝게 관리형 구조체를 일치시킵니다.

✔️ 성능 개선을 위해 blittable 구조체에 Marshal.SizeOf<MyStruct>() 대신 C# sizeof()를 사용합니다.

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

❌ 구조체에 함수 포인터 필드를 표시하기 위해 System.Delegate 또는 System.MulticastDelegate 필드를 사용하지 마세요.

System.DelegateSystem.MulticastDelegate에는 필수 서명이 없으므로 전달된 대리자가 네이티브 코드에 필요한 시그니처와 일치하는 것을 보장하지 않습니다. 또한 .NET Framework 및 .NET Core에서 네이티브 표현의 필드 값이 관리되는 대리자를 래핑하는 함수 포인터가 아닌 경우 System.Delegate 또는 System.MulticastDelegate를 포함하는 구조체를 관리 개체로 마샬링하면 런타임이 불안정해질 수 있습니다. .NET 5 이상 버전에서는 System.Delegate 또는 System.MulticastDelegate 필드를 네이티브 표현에서 관리 개체로 마샬링할 수 없습니다. System.Delegate 또는 System.MulticastDelegate 대신 특정 대리자 형식을 사용합니다.

고정 버퍼

INT_PTR Reserved1[2]와 같은 배열은 두 개의 IntPtr 필드(Reserved1aReserved1b)로 마샬링되어야 합니다. 네이티브 배열이 기본 형식인 경우 fixed 키워드를 사용하여 보다 명확하게 작성할 수 있습니다. 예를 들어 SYSTEM_PROCESS_INFORMATION은 네이티브 헤더에서 다음과 같습니다.

typedef struct _SYSTEM_PROCESS_INFORMATION {
    ULONG NextEntryOffset;
    ULONG NumberOfThreads;
    BYTE Reserved1[48];
    UNICODE_STRING ImageName;
...
} SYSTEM_PROCESS_INFORMATION

C#에서는 다음과 같이 작성할 수 있습니다.

internal unsafe struct SYSTEM_PROCESS_INFORMATION
{
    internal uint NextEntryOffset;
    internal uint NumberOfThreads;
    private fixed byte Reserved1[48];
    internal Interop.UNICODE_STRING ImageName;
    ...
}

그러나 고정 버퍼에는 몇 가지 문제가 있습니다. 비 blittable 형식의 고정 버퍼는 올바르게 마샬링되지 않으므로, 현재 위치 배열을 여러 개의 개별 필드로 확장해야 합니다. 또한 .NET Framework 및 .NET Core 3.0 이전 버전에서 고정 버퍼 필드가 포함된 구조체를 비 blittable 구조체 내에 중첩하면 고정 버퍼 필드는 네이티브 코드로 올바르게 마샬링되지 않습니다.