原生互通性最佳做法Native interoperability best practices

.NET 提供您各種自訂原生互通性程式碼的方式。.NET gives you a variety of ways to customize your native interoperability code. 本文包含 Microsoft 的 .NET 小組在原生互通性上遵循的指導方針。This article includes the guidance that Microsoft's .NET teams follow for native interoperability.

一般方針General guidance

本節中的指導方針適用於所有互通案例。The guidance in this section applies to all interop scenarios.

  • ✔️ 請這樣做 為您的方法和參數使用與要呼叫之原生方法相同的命名和大小寫。✔️ DO use the same naming and capitalization for your methods and parameters as the native method you want to call.
  • ✔️ 請考慮 為常數值使用相同的命名和大小寫。✔️ CONSIDER using the same naming and capitalization for constant values.
  • ✔️ 請這樣做 使用與原生類型對應相近的 .NET 類型。✔️ DO use .NET types that map closest to the native type. 例如在 C# 中,當原生類型是 unsigned int 時就使用 uintFor example, in C#, use uint when the native type is unsigned int.
  • ✔️ 請這樣做 當您想要的行為與預設行為不同時,才使用 [In][Out] 屬性。✔️ DO only use [In] and [Out] attributes when the behavior you want differs from the default behavior.
  • ✔️ 請考慮 使用 System.Buffers.ArrayPool<T> 來建立您的原生陣列緩衝區集區。✔️ CONSIDER using System.Buffers.ArrayPool<T> to pool your native array buffers.
  • ✔️ 請考慮 將您的 P/Invoke 委派包裝在與您的原生程式庫有相同名稱和大小寫的類型中。✔️ CONSIDER wrapping your P/Invoke declarations in a class with the same name and capitalization as your native library.
    • 這樣可讓您的 [DllImport] 屬性使用 C# nameof 語言功能來傳入原生程式庫名稱,並確保您不會拼錯原生程式庫的名稱。This allows your [DllImport] attributes to use the C# nameof language feature to pass in the name of the native library and ensure that you didn't misspell the name of the native library.

DllImport 屬性設定DllImport attribute settings

設定Setting 預設Default 建議Recommendation 詳細資料Details
PreserveSig true 保留預設值keep default 當此項目明確設為 False 時,失敗的 HRESULT 傳回值會轉換成例外狀況 (結果為定義中的傳回值會變成 Null)。When this is explicitly set to false, failed HRESULT return values will be turned into exceptions (and the return value in the definition becomes null as a result).
SetLastError false 取決於 APIdepends on the API 如果 API 使用 GetLastError 並使用 Marshal.GetLastWin32Error 來取得值,請將此項目設為 True。Set this to true if the API uses GetLastError and use Marshal.GetLastWin32Error to get the value. 如果 API 設定的條件指出有錯誤,請先取得該錯誤再進行其他呼叫,以避免不小心複寫它。If the API sets a condition that says it has an error, get the error before making other calls to avoid inadvertently having it overwritten.
CharSet CharSet.None,會退而使用 CharSet.Ansi 行為CharSet.None, which falls back to CharSet.Ansi behavior 當定義中出現字串或字元時,請明確地使用 CharSet.UnicodeCharSet.AnsiExplicitly use CharSet.Unicode or CharSet.Ansi when strings or characters are present in the definition 此項目指定值為 false 時,字串的封送處理行為及 ExactSpelling 的作用。This specifies marshaling behavior of strings and what ExactSpelling does when false. 請留意到,CharSet.Ansi 在 Unix 上實際是 UTF8。Note that CharSet.Ansi is actually UTF8 on Unix. Windows「多數」時候是使用 Unicode,而 Unix 是使用 UTF8。Most of the time Windows uses Unicode while Unix uses UTF8. 請在字元集相關文件查看詳細資訊。See more information on the documentation on charsets.
ExactSpelling false true 將此項目設為 True 可獲得些微效能好處,因為執行階段不會根據 CharSet 的設定,查看名稱尾碼是 "A" 或 "W" 的替代函式 ("A" 是 CharSet.Ansi 而 "W" 是 CharSet.Unicode)。Set this to true and gain a slight perf benefit as the runtime will not look for alternate function names with either an "A" or "W" suffix depending on the value of the CharSet setting ("A" for CharSet.Ansi and "W" for CharSet.Unicode).

字串參數String parameters

當 CharSet 是 Unicode 時或引數明確標示為 「且」[MarshalAs(UnmanagedType.LPWSTR)] 該字串是以傳值方式傳遞時 (不是 refout),則該字串會被固定並由機器碼直接使用 (而不是複製)。When the CharSet is Unicode or the argument is explicitly marked as [MarshalAs(UnmanagedType.LPWSTR)] and the string is passed by value (not ref or out), the string will be pinned and used directly by native code (rather than copied).

請記得將 [DllImport] 標示為 Charset.Unicode,除非您明確地想要對字串進行 ANSI 處理。Remember to mark the [DllImport] as Charset.Unicode unless you explicitly want ANSI treatment of your strings.

❌ 請勿使用 [Out] string 參數。❌ DO NOT use [Out] string parameters. 使用 [Out] 屬性以傳值方式傳遞的字串參數,可能會使執行階段不穩定 (如果該字串是暫留字串)。String parameters passed by value with the [Out] attribute can destabilize the runtime if the string is an interned string. 請在 String.Intern 的文件中查看字串暫留的詳細資訊。See more information about string interning in the documentation for String.Intern.

❌ 避免 StringBuilder 參數。❌ AVOID StringBuilder parameters. StringBuilder 封送處理「一律」會建立原生緩衝區複本。StringBuilder marshaling always creates a native buffer copy. 因此,這麼做可能非常沒有效率。As such, it can be extremely inefficient. 請考慮呼叫接受字串之 Windows API 的典型案例:Take the typical scenario of calling a Windows API that takes a string:

  1. 建立所需容量的 SB (配置受控容量) {1}Create a SB of the desired capacity (allocates managed capacity) {1}
  2. 叫用Invoke
    1. 配置原生緩衝區 {2}Allocates a native buffer {2}
    2. 若為 ,則複製內容 (StringBuilder 參數的預設值)[In] Copies the contents if [In] (the default for a StringBuilder parameter)
    3. 若為 [Out] ,則將原生緩衝區複製到新配置的受控陣列中 {3} (也是 StringBuilder 的預設值) Copies the native buffer into a newly allocated managed array if [Out] {3} (also the default for StringBuilder)
  3. ToString() 配置另一個受控陣列 {4}ToString() allocates yet another managed array {4}

這是 {4} 用來從原生程式碼中取得字串的配置。That is {4} allocations to get a string out of native code. 要限制此情況最好的方式是在其他呼叫中重複使用 StringBuilder,但這樣仍然只儲存 1 配置。The best you can do to limit this is to reuse the StringBuilder in another call but this still only saves 1 allocation. 這樣比較好使用及從 ArrayPool 快取字元緩衝區,而且在後續的呼叫您可以直接取得 ToString() 的配置。It's much better to use and cache a character buffer from ArrayPool- you can then get down to just the allocation for the ToString() on subsequent calls.

StringBuilder 的另一個問題是它一律會將傳回緩衝區備份複製到第一個 Null。The other issue with StringBuilder is that it always copies the return buffer back up to the first null. 如果傳回的字串沒有中止,或者它是雙重 Null 結尾的字串,則您 P/Invoke 最佳的狀態會是不正確。If the passed back string isn't terminated or is a double-null-terminated string, your P/Invoke is incorrect at best.

如果您「確實」使用 StringBuilder,最後一個陷阱是該容量 包含隱藏的 Null (封送一律會計算)。If you do use StringBuilder, one last gotcha is that the capacity does not include a hidden null, which is always accounted for in interop. 這經常被誤解,因為大部分 API 都想要緩衝區「包含」Null。It's common for people to get this wrong as most APIs want the size of the buffer including the null. 這會導致浪費/不必要的配置。This can result in wasted/unnecessary allocations. 此外,此陷阱會防止執行階段最佳化 StringBuilder 封送處理以減少複本。Additionally, this gotcha prevents the runtime from optimizing StringBuilder marshaling to minimize copies.

✔️ 請考慮 使用來自 ArrayPoolchar[]✔️ CONSIDER using char[]s from an ArrayPool.

如需字串封送處理的詳細資訊,請參閱字串的預設封送處理自訂字串封送處理For more information on string marshaling, see Default Marshaling for Strings and Customizing string marshaling.

Windows 特定[Out]若為字串,CLR 預設會使用這些字串 CoTaskMemFree 來釋放字串 SysStringFree ,或針對標記為的字串使用 UnmanagedType.BSTRWindows Specific For [Out] strings the CLR will use CoTaskMemFree by default to free strings or SysStringFree for strings that are marked as UnmanagedType.BSTR. 針對具有輸出字串緩衝區的大部分 api: 傳入的字元計數必須包含 null。For most APIs with an output string buffer: The passed in character count must include the null. 如果傳回值小於呼叫接收的傳入字元計數,則該值是「不含」尾端 Null 的字元數目。If the returned value is less than the passed in character count the call has succeeded and the value is the number of characters without the trailing null. 否則,該計數為緩衝區「包含」Null 字元所需的大小。Otherwise the count is the required size of the buffer including the null character.

  • 傳入5,get 4:字串的長度為4個字元,結尾為 null。Pass in 5, get 4: The string is 4 characters long with a trailing null.
  • 傳入5,get 6:字串長度為5個字元,需要6個字元的緩衝區來保存 null。Pass in 5, get 6: The string is 5 characters long, need a 6 character buffer to hold the null. 字串的 Windows 資料類型Windows Data Types for Strings

布林值參數和欄位Boolean parameters and fields

布林值很容易弄錯。Booleans are easy to mess up. 根據預設,.NET bool 會封送到 Windows BOOL (4 個位元組的值)。By default, a .NET bool is marshaled to a Windows BOOL, where it's a 4-byte value. 不過,C 和 C++ 中的 _Boolbool 是「單一」位元組。However, the _Bool, and bool types in C and C++ are a single byte. 這可能導致難以追蹤的錯誤 (bug),因為傳回值會有一半被捨棄,這有可能「潛在地」變更結果。This can lead to hard to track down bugs as half the return value will be discarded, which will only potentially change the result. 如需將 .NET bool 值封送至 C 或 C++ bool 類型的詳細資訊,請參閱自訂布林欄位封送處理文件。For more for information on marshaling .NET bool values to C or C++ bool types, see the documentation on customizing boolean field marshaling.

GUIDGUIDs

GUID 可直接在特徵標記中使用。GUIDs are usable directly in signatures. 許多 Windows API 都接受 GUID& 類型的別名,如 REFIIDMany Windows APIs take GUID& type aliases like REFIID. 當以傳址方式傳遞時,可以透過 ref[MarshalAs(UnmanagedType.LPStruct)] 屬性來傳遞它們。When passed by ref, they can either be passed by ref or with the [MarshalAs(UnmanagedType.LPStruct)] attribute.

GUIDGUID 傳址 GUIDBy-ref GUID
KNOWNFOLDERID REFKNOWNFOLDERID

❌ 請勿使用 [MarshalAs(UnmanagedType.LPStruct)] GUID 參數以外的任何其他作業 ref❌ DO NOT Use [MarshalAs(UnmanagedType.LPStruct)] for anything other than ref GUID parameters.

Blittable 類型Blittable types

Blittable 類型是受控碼和機器碼有相同位元層級表示的類型。Blittable types are types that have the same bit-level representation in managed and native code. 由於它們不需要為了從機器碼來回封送而轉換成其他格式,因此它們可以改善效能,建議您使用它們。As such they do not need to be converted to another format to be marshaled to and from native code, and as this improves performance they should be preferred. 某些類型並不是可複製的,但已知會包含可內容的內容。Some types are not blittable but are known to contain blittable contents. 當這些型別不包含在其他類型中時,這些型別會有類似的優化,但在結構的欄位或的用途時,不會將其視為可為我的類型 UnmanagedCallersOnlyAttributeThese types have similar optimizations as blittable types when they are not contained in another type, but are not considered blittable when in fields of structs or for the purposes of UnmanagedCallersOnlyAttribute.

Blittable 類型:Blittable types:

  • byte, sbyte, short, ushort, int, uint, long, ulong, single, doublebyte, sbyte, short, ushort, int, uint, long, ulong, single, double
  • 具有固定配置的結構,這些結構只有實例欄位的可類別型實值型別structs with fixed layout that only have blittable value types for instance fields
    • 固定配置需要 [StructLayout(LayoutKind.Sequential)][StructLayout(LayoutKind.Explicit)]fixed layout requires [StructLayout(LayoutKind.Sequential)] or [StructLayout(LayoutKind.Explicit)]
    • 結構 LayoutKind.Sequential 預設為structs are LayoutKind.Sequential by default

具有可直接執行內容的類型:Types with blittable contents:

  • 非嵌套的一維一維的一維陣列 (例如, int[]) non-nested, one-dimensional arrays of blittable primitive types (for example, int[])
  • 具有固定配置的類別,這些類別只有實例欄位的可設定為可設定為可設定類型classes with fixed layout that only have blittable value types for instance fields
    • 固定配置需要 [StructLayout(LayoutKind.Sequential)][StructLayout(LayoutKind.Explicit)]fixed layout requires [StructLayout(LayoutKind.Sequential)] or [StructLayout(LayoutKind.Explicit)]
    • 類別 LayoutKind.Auto 預設為classes are LayoutKind.Auto by default

非 Blittable:NOT blittable:

  • bool

有時 Blittable:SOMETIMES blittable:

  • char

具有有時可直接執行內容的類型:Types with SOMETIMES blittable contents:

  • string

當以傳址方式以傳址方式傳遞型別的型別時, in ref 或以 out 傳值方式傳遞具有可陣列內容的型別時,它們只會由封送處理來釘選,而不會複製到中繼緩衝區。When blittable types are passed by reference with in, ref, or out, or when types with blittable contents are passed by value, they're simply pinned by the marshaller instead of being copied to an intermediate buffer.

char 在一維陣列中時, 當它所屬的類型明確地以 CharSet = CharSet.Unicode 標示為 [StructLayout] 時,它是 Blittable 的。char is blittable in a one-dimensional array or if it's part of a type that contains it's explicitly marked with [StructLayout] with CharSet = CharSet.Unicode.

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

string 如果未包含在其他類型中,且正在傳遞做為 [MarshalAs(UnmanagedType.LPWStr)] 或已設定的引數,則包含可集中傳送的內容 [DllImport] CharSet = CharSet.Unicodestring contains blittable contents if it isn't contained in another type and it's being passed as an argument that is marked with [MarshalAs(UnmanagedType.LPWStr)] or the [DllImport] has CharSet = CharSet.Unicode set.

您可以藉由嘗試建立釘選的方式,查看型別是否為可直接進行的,或包含可進行的內容 GCHandleYou can see if a type is blittable or contains blittable contents by attempting to create a pinned GCHandle. 如果該類型不是字串或被視為 Blittable,則 GCHandle.Alloc 會擲回 ArgumentExceptionIf the type isn't a string or considered blittable, GCHandle.Alloc will throw an ArgumentException.

✔️ 請這樣做 盡可能讓您的結構是 Blittable 的。✔️ DO make your structures blittable when possible.

如需詳細資訊,請參閱For more information, see:

讓受控物件保持運作Keeping managed objects alive

GC.KeepAlive() 會確保物件在範圍保持運作,直到叫用 KeepAlive 方法。GC.KeepAlive() will ensure an object stays in scope until the KeepAlive method is hit.

HandleRef 允許封送處理器在 P/Invoke 期間讓物件保持在作用中狀態。HandleRef allows the marshaller to keep an object alive for the duration of a P/Invoke. 可以使用它,而不使用方法特徵標記中的 IntPtrIt can be used instead of IntPtr in method signatures. 應改為使用 SafeHandle,它可有效地取代此類別。SafeHandle effectively replaces this class and should be used instead.

GCHandle 允許釘選 managed 物件,並取得其原生指標。GCHandle allows pinning a managed object and getting the native pointer to it. 基本模式為:The basic pattern is:

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

固定不是 GCHandle 的預設值。Pinning isn't the default for GCHandle. 其他主要模式是透過機器碼將參考傳遞至受控物件,然後再回到受控碼 (通常是使用回呼)。The other major pattern is for passing a reference to a managed object through native code and back to managed code, usually with a callback. 以下是模式:Here is the pattern:

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,以避免記憶體流失。Don't forget that GCHandle needs to be explicitly freed to avoid memory leaks.

常見的 Windows 資料類型Common Windows data types

以下是常用於 Windows API 中的資料類型清單,以及在呼叫至 Windows 程式碼內時要使用的 C# 類型。Here is a list of data types commonly used in Windows APIs and which C# types to use when calling into the Windows code.

下列類型在 32 位元和 64 位元 Windows 上的大小相同 (除了其名稱)。The following types are the same size on 32-bit and 64-bit Windows, despite their names.

寬度Width WindowsWindows C#C# 替代函式Alternative
3232 BOOL int bool
88 BOOLEAN byte [MarshalAs(UnmanagedType.U1)] bool
88 BYTE byte
88 CHAR sbyte
88 UCHAR byte
1616 SHORT short
1616 CSHORT short
1616 USHORT ushort
1616 WORD ushort
1616 ATOM ushort
3232 INT int
3232 LONG int 請參閱 CLongCULong See CLong and CULong.
3232 ULONG uint 請參閱 CLongCULong See CLong and CULong.
3232 DWORD uint
6464 QWORD long
6464 LARGE_INTEGER long
6464 LONGLONG long
6464 ULONGLONG ulong
6464 ULARGE_INTEGER ulong
3232 HRESULT int
3232 NTSTATUS int

下列類型 (為指標) 確實按照平台的寬度。The following types, being pointers, do follow the width of the platform. IntPtr/UIntPtr 用於這些。Use IntPtr/UIntPtr for these.

帶正負號指標類型 (使用 IntPtr)Signed Pointer Types (use IntPtr) 不帶正負號指標類型 (使用 UIntPtr)Unsigned Pointer Types (use UIntPtr)
HANDLE WPARAM
HWND UINT_PTR
HINSTANCE ULONG_PTR
LPARAM SIZE_T
LRESULT
LONG_PTR
INT_PTR

Windows PVOID 是 C void*,可以作為 IntPtrUIntPtr 來封送,但建議盡可能使用 void*A Windows PVOID which is a C void* can be marshaled as either IntPtr or UIntPtr, but prefer void* when possible.

Windows 資料類型Windows Data Types

資料類型範圍Data Type Ranges

先前內建支援的類型Formerly built-in supported types

移除型別的內建支援時,有很罕見的情況。There are rare instances when built-in support for a type is removed.

UnmanagedType.HString已在 .net 5 版本中移除內建封送處理支援。The UnmanagedType.HString built-in marshal support was removed in the .NET 5 release. 您必須重新編譯使用此封送處理類型的二進位檔,並以先前的架構為目標。You must recompile binaries that use this marshalling type and that target a previous framework. 您仍然可以封送處理這種類型,但您必須以手動方式封送處理,如下列程式碼範例所示。It's still possible to marshal this type, but you must marshal it manually, as the following code example shows. 這段程式碼會繼續運作,而且也與先前的架構相容。This code will work moving forward and is also compatible with previous frameworks.

static class HSTRING
{
    public static IntPtr FromString(string s)
    {
        Marshal.ThrowExceptionForHR(WindowsCreateString(s, s.Length, out IntPtr h));
        return h;
    }

    public static void Delete(IntPtr s)
    {
        Marshal.ThrowExceptionForHR(WindowsDeleteString(s));
    }

    [DllImport("api-ms-win-core-winrt-string-l1-1-0.dll", CallingConvention = CallingConvention.StdCall, ExactSpelling = true)]
    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", CallingConvention = CallingConvention.StdCall, ExactSpelling = true)]
    private static extern int WindowsDeleteString(IntPtr hstring);
}

// Usage example
IntPtr hstring = HSTRING.FromString("HSTRING from .NET to WinRT API");
try
{
    // Pass hstring to WinRT or Win32 API.
}
finally
{
    HSTRING.Delete(hstring);
}

跨平臺資料類型考慮Cross-platform data type considerations

C/c + + 語言中的類型有其定義方式的緯度。There are types in the C/C++ language that have latitude in how they are defined. 撰寫跨平臺 interop 時,可能會在平臺不同的情況下引發案例,如果未考慮則可能會造成問題。When writing cross-platform interop, cases can arise where platforms differ and can cause issues if not considered.

C/C + + longC/C++ long

C/c + + long 和 c # long 不是相同的類型。C/C++ long and C# long are not the same types. 使用 c # 與 long c/c + + 互通性 long 幾乎不正確。Using C# long to interop with C/C++ long is almost never correct.

longC/c + + 中的類型定義為具有「至少 32 」位。The long type in C/C++ is defined to have "at least 32" bits. 這表示有最少的必要位,但平臺可以視需要選擇使用更多位數。This means there is a minimum number of required bits, but platforms can choose to use more bits if desired. 下表說明平臺之間 C/c + + 資料類型所提供的位差異 longThe following table illustrates the differences in provided bits for the C/C++ long data type between platforms.

平台Platform 32 位元32-bit 64 位元64-bit
WindowsWindows 3232 3232
macOS/ * nixmacOS/*nix 3232 6464

當原生函式已定義為 long 在所有平臺上使用時,這些差異可能會讓撰寫跨平臺 P/invoke 變得困難。These differences can make authoring cross-platform P/Invokes difficult when the native function is defined to use long on all platforms.

在 .NET 6 和更新版本中,使用 CLongCULong 類型來與 C/c + + longunsigned long 資料類型進行 interop。In .NET 6 and later versions, use the CLong and CULong types for interop with C/C++ long and unsigned long data types. 下列範例適用于 CLong ,但您可以用 CULong unsigned long 類似的方式使用進行抽象。The following example is for CLong, but you can use CULong to abstract unsigned long in a similar way.

// 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 簽章來處理問題。When targeting .NET 5 and earlier versions, you should declare separate Windows and non-Windows signatures to handle the problem.

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);
}

結構Structs

受控結構是在堆疊上建立的,直到方法傳回才會將它移除。Managed structs are created on the stack and aren't removed until the method returns. 根據定義,他們是「固定的」(不會被 GC 移除)。By definition then, they are "pinned" (it won't get moved by the GC). 如果機器碼不使用目前方法結尾所傳遞的指標,您也可以直接接受不安全程式碼區塊中的位址。You can also simply take the address in unsafe code blocks if native code won't use the pointer past the end of the current method.

Blittable 結構效能更好,因為封送處理層可以直接使用它們。Blittable structs are much more performant as they can simply be used directly by the marshaling layer. 請嘗試讓結構是 Blittable 的 (例如,避免bool)。Try to make structs blittable (for example, avoid bool). 如需詳細資訊,請參閱 Blittable 類型一節。For more information, see the Blittable Types section.

「如果」結構是 Blittable 的,為了獲得更好的效能,請使用 sizeof() 而不使用 Marshal.SizeOf<MyStruct>()If the struct is blittable, use sizeof() instead of Marshal.SizeOf<MyStruct>() for better performance. 如先前所述,您可以藉由嘗試建立固定的 GCHandle,以驗證類型是否為 Blittable 的。As mentioned above, you can validate that the type is blittable by attempting to create a pinned GCHandle. 如果該類型不是字串或被視為 Blittable,則 GCHandle.Alloc 會擲回 ArgumentExceptionIf the type is not a string or considered blittable, GCHandle.Alloc will throw an ArgumentException.

在定義中,結構的指標必須以 ref 傳遞,或者使用 unsafe*Pointers to structs in definitions must either be passed by ref or use unsafe and *.

✔️ 請這樣做 盡可能近似地對應受控結構與官方平台文件或標頭中所使用的圖形和名稱。✔️ DO match the managed struct as closely as possible to the shape and names that are used in the official platform documentation or header.

✔️ 請這樣做 針對 Blittable 結構使用 C# sizeof(),而不使用 Marshal.SizeOf<MyStruct>(),以改善效能。✔️ DO use the C# sizeof() instead of Marshal.SizeOf<MyStruct>() for blittable structures to improve performance.

❌ 避免使用 System.DelegateSystem.MulticastDelegate 欄位來表示結構中的函式指標欄位。❌ AVOID using System.Delegate or System.MulticastDelegate fields to represent function pointer fields in structures.

由於 System.DelegateSystem.MulticastDelegate 沒有必要的簽章,因此不保證傳入的委派將符合原生程式碼所預期的簽章。Since System.Delegate and System.MulticastDelegate don't have a required signature, they don't guarantee that the delegate passed in will match the signature the native code expects. 此外,在 .NET Framework 和 .NET Core 中, System.Delegate System.MulticastDelegate 如果原生標記法中的欄位值不是包裝 managed 委派的函式指標,則將包含或的結構從原生表示封送處理至 managed 物件時,可能會使執行時間不穩定。Additionally, in .NET Framework and .NET Core, marshaling a struct containing a System.Delegate or System.MulticastDelegate from its native representation to a managed object can destabilize the runtime if the value of the field in the native representation isn't a function pointer that wraps a managed delegate. 在 .NET 5 和更新版本中, System.Delegate System.MulticastDelegate 不支援將原生標記法中的或欄位封送處理至 managed 物件。In .NET 5 and later versions, marshaling a System.Delegate or System.MulticastDelegate field from a native representation to a managed object is not supported. 使用特定的委派類型,而不是 System.DelegateSystem.MulticastDelegateUse a specific delegate type instead of System.Delegate or System.MulticastDelegate.

固定緩衝區Fixed Buffers

INT_PTR Reserved1[2] 之類的陣列必須封送至兩個 IntPtr 欄位:Reserved1aReserved1bAn array like INT_PTR Reserved1[2] has to be marshaled to two IntPtr fields, Reserved1a and Reserved1b. 當原生陣列是基本類型時,我們可以使用 fixed 關鍵字來將它撰寫得更簡潔一點。When the native array is a primitive type, we can use the fixed keyword to write it a little more cleanly. 例如,在原生標頭中 SYSTEM_PROCESS_INFORMATION 看起來像這樣:For example, SYSTEM_PROCESS_INFORMATION looks like this in the native header:

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

在 C# 中,我們可以將它撰寫像這樣:In C#, we can write it like this:

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

不過,固定的緩衝區有一些陷阱。However, there are some gotchas with fixed buffers. 非 Blittable 類型的固定緩衝區不會正確地進行封送,因此需要將就地陣列展開成多個個別欄位。Fixed buffers of non-blittable types won't be correctly marshaled, so the in-place array needs to be expanded out to multiple individual fields. 此外,在 .NET Framework 和 .NET Core 3.0 之前,如果包含固定緩衝區欄位的結構是呈巢狀包含在非 Blittable 結構中,則該固定緩衝區欄位不會正確地封送至機器碼。Additionally, in .NET Framework and .NET Core before 3.0, if a struct containing a fixed buffer field is nested within a non-blittable struct, the fixed buffer field won't be correctly marshaled to native code.