Najlepsze rozwiązania dotyczące współdziałania natywnego
Platforma .NET udostępnia różne sposoby dostosowywania natywnego kodu współdziałania. Ten artykuł zawiera wskazówki, które są przestrzegane przez zespoły firmy Microsoft dla platformy .NET w celu zapewnienia współdziałania natywnego.
Wskazówki ogólne
Wskazówki zawarte w tej sekcji dotyczą wszystkich scenariuszy międzyoperacyjności.
- ✔️ Użyj polecenia
[LibraryImport]
, jeśli to możliwe, w przypadku określania wartości docelowej dla platformy .NET 7 lub nowszej.- Istnieją przypadki, w których użycie
[DllImport]
jest odpowiednie. Analizator kodu z identyfikatorem SYSLIB1054 informuje o tym, kiedy tak jest.
- Istnieją przypadki, w których użycie
- ✔️ Należy użyć tego samego nazewnictwa i wielkich liter dla metod i parametrów, co metoda natywna, którą chcesz wywołać.
- ✔️ ROZWAŻ użycie tego samego nazewnictwa i wielkości liter dla wartości stałych.
- ✔️ Do użyj typów platformy .NET, które mapują najbliżej typu natywnego. Na przykład w języku C#użyj wartości
uint
, gdy typem natywnym jestunsigned int
. - ✔️ Preferuj wyrażanie typów natywnych wyższego poziomu przy użyciu struktur platformy .NET, a nie klas.
- ✔️ Wolisz używać wskaźników funkcji, w przeciwieństwie do
Delegate
typów, podczas przekazywania wywołań zwrotnych do funkcji niezarządzanych w języku C#. - ✔️ Należy używać
[In]
atrybutów i[Out]
w parametrach tablicy. - ✔️ Należy używać
[In]
atrybutów i[Out]
tylko w innych typach, gdy zachowanie, którego chcesz użyć, różni się od domyślnego zachowania. - ✔️ ROZWAŻ użycie metody System.Buffers.ArrayPool<T> do pulowania macierzy natywnej.
- ✔️ ROZWAŻ zawijanie deklaracji P/Invoke w klasie o tej samej nazwie i wielkich literach co biblioteka natywna.
- Dzięki temu atrybuty
[LibraryImport]
lub[DllImport]
mogą używać funkcji języka C#nameof
do przekazania nazwy biblioteki natywnej i upewnić się, że nazwa biblioteki natywnej nie została pominięta.
- Dzięki temu atrybuty
- ✔️ Do obsługi służy
SafeHandle
do zarządzania okresem istnienia obiektów, które hermetyzują niezarządzane zasoby. Aby uzyskać więcej informacji, zobacz Oczyszczanie zasobów niezarządzanych. - ❌ UNIKAJ finalizatorów do zarządzania okresem istnienia obiektów, które hermetyzują niezarządzane zasoby. Aby uzyskać więcej informacji, zobacz Implementowanie metody Dispose.
Ustawienia atrybutu LibraryImport
Analizator kodu z identyfikatorem SYSLIB1054 ułatwia zapoznanie się z usługą LibraryImportAttribute
. W większości przypadków użycie metody wymaga jawnej LibraryImportAttribute
deklaracji, a nie polegania na ustawieniach domyślnych. Ten projekt jest zamierzony i pomaga uniknąć niezamierzonego zachowania w scenariuszach międzyoperacyjności.
Ustawienia atrybutu DllImport
Ustawienie | Wartość domyślna | Zalecenie | Szczegóły |
---|---|---|---|
PreserveSig | true |
Zachowaj wartość domyślną | Jeśli ta wartość jest jawnie ustawiona na wartość false, zwracane wartości HRESULT zostaną przekształcone w wyjątki (a wartość zwracana w definicji stanie się równa null w wyniku). |
SetLastError | false |
Zależy od interfejsu API | Ustaw wartość true, jeśli interfejs API używa metody GetLastError i użyj polecenia Marshal.GetLastWin32Error, aby uzyskać wartość. Jeśli interfejs API ustawia warunek informujący, że ma błąd, przed wykonaniem innych wywołań, aby uniknąć niezamierzonego zastąpienia. |
CharSet | Zdefiniowane przez kompilator (określone w dokumentacji zestawu znaków) | Jawne użycie CharSet.Unicode ciągów CharSet.Ansi lub znaków w definicji |
Określa zachowanie marshalling ciągów i co ExactSpelling robi, gdy false . Należy pamiętać, że CharSet.Ansi jest to rzeczywiście UTF8 w systemie Unix. W większości przypadków system Windows używa formatu Unicode, podczas gdy system Unix używa formatu UTF8. Zobacz więcej informacji na temat dokumentacji dotyczącej zestawów znaków. |
ExactSpelling | false |
true |
Ustaw wartość true i uzyskaj niewielką korzyść wydajności, ponieważ środowisko uruchomieniowe nie będzie szukać alternatywnych nazw funkcji z sufiksem "A" lub "W" w zależności od wartości CharSet ustawienia ("A" dla CharSet.Ansi i "W" dla CharSet.Unicode ). |
Parametry ciągu
Element A string
jest przypięty i używany bezpośrednio przez kod macierzysty (zamiast kopiowany) po przekazaniu przez wartość (nie ref
lub out
) i jedną z następujących wartości:
- LibraryImportAttribute.StringMarshalling parametr jest zdefiniowany jako Utf16.
- Argument jest jawnie oznaczony jako
[MarshalAs(UnmanagedType.LPWSTR)]
. - Parametr DllImportAttribute.CharSet ma wartość Unicode.
❌ NIE używaj [Out] string
parametrów. Parametry ciągu przekazywane przez wartość z atrybutem [Out]
mogą zdestabilizować środowisko uruchomieniowe, jeśli ciąg jest ciągiem internowany. Zobacz więcej informacji na temat interningu ciągów w dokumentacji programu String.Intern.
✔️ ROZWAŻ char[]
lub byte[]
tablice z kodu natywnego ArrayPool
, aby wypełnić bufor znaków. Wymaga to przekazania argumentu jako [Out]
.
Wskazówki specyficzne dla biblioteki DllImport
✔️ ROZWAŻ ustawienie CharSet
właściwości w programie , [DllImport]
aby środowisko uruchomieniowe znało oczekiwane kodowanie ciągów.
✔️ ROZWAŻ unikanie StringBuilder
parametrów. StringBuilder
Marshalling zawsze tworzy natywną kopię buforu. W związku z tym może to być niezwykle nieefektywne. Zapoznaj się z typowym scenariuszem wywoływania interfejsu API systemu Windows, który przyjmuje ciąg:
StringBuilder
Utwórz żądaną pojemność (przydziela pojemność zarządzaną). {1}- Wywołać:
- Przydziela bufor {2}macierzysty .
- Kopiuje zawartość, jeśli
[In]
(wartość domyślna parametruStringBuilder
). - Kopiuje bufor macierzysty do nowo przydzielonej tablicy zarządzanej, jeśli
[Out]
{3} (również wartość domyślna dlaStringBuilder
).
ToString()
przydziela kolejną tablicę {4}zarządzaną .
{4} To alokacje, aby pobrać ciąg z kodu natywnego. Najlepszym rozwiązaniem, aby ograniczyć ten problem, jest ponowne użycie StringBuilder
metody w innym wywołaniu, ale nadal zapisuje tylko jedną alokację. Znacznie lepiej używać i buforować bufor znaków z ArrayPool
klasy . Następnie możesz przejść do alokacji dla ToString()
kolejnych wywołań.
Innym problemem jest StringBuilder
to, że zawsze kopiuje bufor powrotny do pierwszej wartości null. Jeśli przekazany ciąg powrotny nie zostanie zakończony lub jest ciągiem z podwójnymi wartościami null, twój ciąg P/Invoke jest niepoprawny.
Jeśli używasz metody StringBuilder
, jedna ostatnia gotcha polega na tym, że pojemność nie zawiera ukrytej wartości null, która jest zawsze uwzględniana w międzyoperacie. Często zdarza się, że ludzie mają to złe, ponieważ większość interfejsów API chce mieć rozmiar buforu , w tym wartość null. Może to spowodować zmarnowanie/niepotrzebne alokacje. Ponadto ta funkcja gotcha uniemożliwia środowisku uruchomieniowemu optymalizowanie StringBuilder
marshallingu w celu zminimalizowania kopii.
Aby uzyskać więcej informacji na temat marshalingu ciągów, zobacz Domyślne marshalling dla ciągów i Dostosowywanie marshalling ciągów.
Windows Specific Dla
[Out]
ciągów CLR będzie domyślnie używanyCoTaskMemFree
do zwalniania ciągów lubSysStringFree
ciągów oznaczonych jakoUnmanagedType.BSTR
. W przypadku większości interfejsów API z buforem ciągu wyjściowego: przekazana liczba znaków musi zawierać wartość null. Jeśli zwrócona wartość jest mniejsza niż przekazana liczba znaków, wywołanie powiodło się, a wartość jest liczbą znaków bez końcowej wartości null. W przeciwnym razie liczba jest wymaganym rozmiarem buforu , w tym znakiem null.
- Przekaż wartość 5, pobierz 4: ciąg ma długość 4 znaków z końcową wartością null.
- Przekaż wartość 5, pobierz 6: ciąg ma długość 5 znaków, wymaga 6-znakowego buforu do przechowywania wartości null. Typy danych systemu Windows dla ciągów
Parametry logiczne i pola
Booleans są łatwe do bałaganu. Domyślnie platforma .NETjesta bool
jest do systemu Windows BOOL
, gdzie jest to wartość 4-bajtowa. _Bool
Jednak typy i bool
w językach C i C++ są pojedynczym bajtem. Może to prowadzić do trudności ze śledzeniem usterek, ponieważ połowa zwracanej wartości zostanie odrzucona, co potencjalnie spowoduje zmianę wyniku. Aby uzyskać więcej informacji na temat marshalingu wartości platformy .NET bool
do typów C lub C++ bool
, zobacz dokumentację dotyczącą dostosowywania marshalingu pól logicznych.
Identyfikatory guid
Identyfikatory GUID można używać bezpośrednio w podpisach. Wiele interfejsów API systemu Windows używa GUID&
aliasów typu, takich jak REFIID
. Gdy podpis metody zawiera parametr referencyjny, umieść ref
słowo kluczowe lub [MarshalAs(UnmanagedType.LPStruct)]
atrybut w deklaracji parametru GUID.
Identyfikator GUID | By-ref GUID |
---|---|
KNOWNFOLDERID |
REFKNOWNFOLDERID |
❌ Nie używaj [MarshalAs(UnmanagedType.LPStruct)]
żadnych elementów innych niż ref
parametry identyfikatora GUID.
Typy tabeli blittable
Typy blittable to typy, które mają tę samą reprezentację na poziomie bitów w zarządzanym i natywnym kodzie. W związku z tym nie muszą być konwertowane na inny format, który ma być sformatowany do i z kodu natywnego, a ponieważ poprawia to wydajność, powinny być preferowane. Niektóre typy nie są blittable, ale są znane z zawartości blittable. Te typy mają podobne optymalizacje jak typy blittable, gdy nie są zawarte w innym typie, ale nie są traktowane jako blittable, gdy w polach struktur lub w celach UnmanagedCallersOnlyAttribute
.
Typy blittable po włączeniu marshalingu środowiska uruchomieniowego
Typy blittable:
byte
,sbyte
, , ,int
single
ushort
uint
long
ulong
short
double
- struktury ze stałym układem, które mają tylko typy wartości w formie blittable dla pól wystąpień
- układ stały wymaga
[StructLayout(LayoutKind.Sequential)]
lub[StructLayout(LayoutKind.Explicit)]
- Struktury są
LayoutKind.Sequential
domyślnie
- układ stały wymaga
Typy z zawartością tabeli blittable:
- nienagnieżdżone, jednowymiarowe tablice typów pierwotnych w formie blittable (na przykład
int[]
) - klasy ze stałym układem, które mają tylko typy wartości w formie blittable dla pól wystąpień
- układ stały wymaga
[StructLayout(LayoutKind.Sequential)]
lub[StructLayout(LayoutKind.Explicit)]
- klasy są
LayoutKind.Auto
domyślnie
- układ stały wymaga
NOT blittable:
bool
CZASAMI blittable:
char
Typy z czasami zawartością tabeli blittable:
string
Gdy typy blittable są przekazywane przez odwołanie do in
, ref
lub out
, lub, gdy typy z zawartością blittable są przekazywane przez wartość, są po prostu przypięte przez marshaller zamiast być kopiowane do buforu pośredniego.
char
jest blittable w tablicy jednowymiarowej lub jeśli jest częścią typu, który zawiera go jawnie oznaczony za pomocą [StructLayout]
polecenia CharSet = CharSet.Unicode
.
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
public struct UnicodeCharStruct
{
public char c;
}
string
zawiera zawartość, jeśli nie jest zawarta w innym typie i jest przekazywana przez wartość (nie ref
lub out
) jako argument i jedną z następujących wartości:
- StringMarshalling parametr jest zdefiniowany jako Utf16.
- Argument jest jawnie oznaczony jako
[MarshalAs(UnmanagedType.LPWSTR)]
. - CharSet to Unicode.
Możesz sprawdzić, czy typ jest blittable lub zawiera zawartość tabeli blittable, próbując utworzyć przypięty GCHandle
element . Jeśli typ nie jest ciągiem lub uważanym za blittable, GCHandle.Alloc
zgłosi błąd ArgumentException
.
Typy tabeli Blittable, gdy marshalling środowiska uruchomieniowego jest wyłączony
Gdy marshalling środowiska uruchomieniowego jest wyłączony, reguły, dla których typów są blittable, są znacznie prostsze. Wszystkie typy, które są typami języka C# unmanaged
i nie mają żadnych pól oznaczonych jako [StructLayout(LayoutKind.Auto)]
blittable. Wszystkie typy, które nie są typami języka C# unmanaged
, nie są blittable. Koncepcja typów z zawartością tabeli blittable, takich jak tablice lub ciągi, nie ma zastosowania, gdy marshalling środowiska uruchomieniowego jest wyłączony. Każdy typ, który nie jest traktowany jako blittable przez wyżej wymienioną regułę, jest nieobsługiwany, gdy marshalling środowiska uruchomieniowego jest wyłączony.
Te reguły różnią się od wbudowanego systemu przede wszystkim w sytuacjach, w których bool
i char
są używane. Gdy marshalling jest wyłączony, bool
jest przekazywany jako wartość 1-bajtowa, a nie znormalizowany i char
zawsze jest przekazywany jako wartość 2-bajtowa. Po włączeniu bool
marshalingu środowiska uruchomieniowego można mapować na wartość 1, 2 lub 4 bajtów i zawsze jest znormalizowana, a char
następnie mapuje wartość na 1 lub 2 bajty w zależności od CharSet
wartości .
✔️ CZY sprawić, aby struktury można było rozbić, gdy jest to możliwe.
Aby uzyskać więcej informacji, zobacz:
Utrzymywanie aktywności obiektów zarządzanych
GC.KeepAlive()
zapewni, że obiekt pozostanie w zakresie do momentu trafienia metody KeepAlive.
HandleRef
umożliwia marshallerowi zachowanie obiektu żywego przez czas trwania wywołania P/Invoke. Można go użyć zamiast IntPtr
w podpisach metod. SafeHandle
skutecznie zastępuje tę klasę i zamiast tego należy jej używać.
GCHandle
umożliwia przypinanie zarządzanego obiektu i pobieranie do niego natywnego wskaźnika. Podstawowy wzorzec to:
GCHandle handle = GCHandle.Alloc(obj, GCHandleType.Pinned);
IntPtr ptr = handle.AddrOfPinnedObject();
handle.Free();
Przypinanie nie jest wartością domyślną dla elementu GCHandle
. Innym głównym wzorcem jest przekazywanie odwołania do obiektu zarządzanego za pomocą kodu natywnego i z powrotem do kodu zarządzanego, zwykle z wywołaniem zwrotnym. Oto wzorzec:
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();
Nie zapomnij, że należy jawnie zwolnić, GCHandle
aby uniknąć przecieków pamięci.
Typowe typy danych systemu Windows
Poniżej znajduje się lista typów danych, które są często używane w interfejsach API systemu Windows i których typów języka C# należy używać podczas wywoływania kodu systemu Windows.
Następujące typy są takie same jak w 32-bitowych i 64-bitowych systemach Windows, pomimo ich nazw.
Width | Windows | C# | Alternatywne rozwiązanie |
---|---|---|---|
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 |
Zobacz CLong i CULong . |
32 | LONG32 |
int |
|
32 | CLONG |
uint |
Zobacz CLong i CULong . |
32 | DWORD |
uint |
Zobacz CLong i CULong . |
32 | DWORD32 |
uint |
|
32 | UINT |
uint |
|
32 | UINT32 |
uint |
|
32 | ULONG |
uint |
Zobacz CLong i CULong . |
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 |
Następujące typy, będące wskaźnikami, są zgodne z szerokością platformy. W tym celu należy użyć polecenia IntPtr
/UIntPtr
.
Typy podpisanych wskaźników (użyj IntPtr ) |
Typy niepodpisanych wskaźników (użyj UIntPtr polecenia ) |
---|---|
HANDLE |
WPARAM |
HWND |
UINT_PTR |
HINSTANCE |
ULONG_PTR |
LPARAM |
SIZE_T |
LRESULT |
|
LONG_PTR |
|
INT_PTR |
System Windows PVOID
, który jest C void*
, może być marshalled jako IntPtr
lub UIntPtr
, ale preferuj void*
, jeśli to możliwe.
Wcześniej wbudowane typy obsługiwane
W przypadku usunięcia wbudowanej obsługi typu występują rzadkie wystąpienia.
UnmanagedType.IInspectable
Wbudowana obsługa UnmanagedType.HString
marshalingu została usunięta w wersji .NET 5. Należy ponownie skompilować pliki binarne, które używają tego typu marshalingu i które są przeznaczone dla poprzedniej platformy. Nadal jest możliwe przeprowadzanie marshalingu tego typu, ale należy przeprowadzić marshaling ręcznie, jak pokazano w poniższym przykładzie kodu. Ten kod będzie działać w przyszłości i jest również zgodny z poprzednimi strukturami.
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);
Zagadnienia dotyczące typów danych międzyplatformowych
Istnieją typy w języku C/C++, które mają szerokość geograficzną w sposobie ich definiowania. Podczas pisania międzyplatformowych międzyplatformowych przypadków mogą wystąpić różnice między platformami i mogą powodować problemy, jeśli nie są brane pod uwagę.
C/C++ long
C/C++ long
i C# long
nie muszą mieć tego samego rozmiaru.
Typ long
w języku C/C++ jest zdefiniowany tak, aby miał "co najmniej 32" bity. Oznacza to, że istnieje minimalna liczba wymaganych bitów, ale platformy mogą w razie potrzeby używać większej liczby bitów. W poniższej tabeli przedstawiono różnice w podanych bitach dla typu danych C/C++ long
między platformami.
Platforma | 32-bitowa | 64-bitowa |
---|---|---|
Windows | 32 | 32 |
macOS/*nix | 32 | 64 |
Natomiast język C# long
jest zawsze 64-bitowy. Z tego powodu najlepiej unikać używania języka C# long
do współdziałania z językiem C/C++ long
.
(Ten problem z językiem C/C++ long
nie istnieje dla języków C/C++ char
, short
, int
i long long
ponieważ są to odpowiednio 8, 16, 32 i 64 bity na wszystkich tych platformach).
W wersjach .NET 6 i nowszych użyj CLong
typów i CULong
do współdziałania z językami C/C++ long
i unsigned long
typami danych. Poniższy przykład dotyczy CLong
elementu , ale możesz użyć CULong
metody do abstrakcji unsigned long
w podobny sposób.
// Cross platform C function
// long Function(long a);
[DllImport("NativeLib")]
extern static CLong Function(CLong a);
// Usage
nint result = Function(new CLong(10)).Value;
W przypadku określania wartości docelowej dla platformy .NET 5 i starszych wersji należy zadeklarować oddzielne podpisy systemu Windows i innych niż Windows, aby rozwiązać ten 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);
}
Struktury
Zarządzane struktury są tworzone na stosie i nie są usuwane, dopóki metoda nie zostanie zwrócona. Następnie są one "przypięte" (nie zostaną przeniesione przez GC). Możesz również po prostu użyć adresu w niebezpiecznych blokach kodu, jeśli kod natywny nie będzie używać wskaźnika obok końca bieżącej metody.
Struktury blittable są o wiele bardziej wydajne, ponieważ mogą być po prostu używane bezpośrednio przez warstwę marshallingową. Spróbuj utworzyć struktury w formie usterek (na przykład unikaj bool
). Aby uzyskać więcej informacji, zobacz sekcję Typy usterek.
Jeśli struktura jest blittable, użyj sizeof()
zamiast Marshal.SizeOf<MyStruct>()
w celu uzyskania lepszej wydajności. Jak wspomniano powyżej, można sprawdzić, czy typ jest nie do usterki, próbując utworzyć przypięty GCHandle
element . Jeśli typ nie jest ciągiem lub uważanym za blittable, GCHandle.Alloc
zgłosi wartość ArgumentException
.
Wskaźniki do struktur w definicjach muszą być przekazywane przez ref
lub używane unsafe
i *
.
✔️ Dopasuj zarządzaną strukturę tak blisko, jak to możliwe do kształtu i nazw używanych w oficjalnej dokumentacji lub nagłówku platformy.
✔️ Aby zwiększyć wydajność, należy użyć języka C# sizeof()
zamiast Marshal.SizeOf<MyStruct>()
dla struktur blittable.
❌ UNIKAJ używania klas do wyrażania złożonych typów natywnych za pośrednictwem dziedziczenia.
❌ UNIKAJ używania System.Delegate
pól lub System.MulticastDelegate
do reprezentowania pól wskaźnika funkcji w strukturach.
Ponieważ System.Delegate i System.MulticastDelegate nie mają wymaganego podpisu, nie gwarantują, że delegat przekazany w będzie zgodny z podpisem oczekiwanym przez kod macierzysty. Ponadto w programach .NET Framework i .NET Core marshalling System.Delegate
struktury zawierającej element lub System.MulticastDelegate
z natywnej reprezentacji obiektu zarządzanego może zdestabilizować środowisko uruchomieniowe, jeśli wartość pola w reprezentacji natywnej nie jest wskaźnikiem funkcji, który opakowuje zarządzanego delegata. W wersjach .NET 5 i nowszych nie jest obsługiwane marshaling a System.Delegate
lub System.MulticastDelegate
pola z reprezentacji natywnej do obiektu zarządzanego. Użyj określonego typu delegata System.Delegate
zamiast lub System.MulticastDelegate
.
Stałe
Tablica podobna INT_PTR Reserved1[2]
musi być przesłana do dwóch IntPtr
pól i Reserved1a
Reserved1b
. Gdy macierz natywna jest typem pierwotnym, możemy użyć słowa kluczowego fixed
, aby napisać go nieco bardziej czysto. Na przykład SYSTEM_PROCESS_INFORMATION
wygląda następująco w nagłówku natywnym:
typedef struct _SYSTEM_PROCESS_INFORMATION {
ULONG NextEntryOffset;
ULONG NumberOfThreads;
BYTE Reserved1[48];
UNICODE_STRING ImageName;
...
} SYSTEM_PROCESS_INFORMATION
W języku C#możemy napisać go w następujący sposób:
internal unsafe struct SYSTEM_PROCESS_INFORMATION
{
internal uint NextEntryOffset;
internal uint NumberOfThreads;
private fixed byte Reserved1[48];
internal Interop.UNICODE_STRING ImageName;
...
}
Jednak istnieją pewne gotchas ze stałymi. Stałe typów niezwiązanych z blittable nie będą poprawnie rozsyłane, więc tablica w miejscu musi zostać rozszerzona na wiele pojedynczych pól. Ponadto w programach .NET Framework i .NET Core przed 3.0, jeśli struktura zawierająca stałe pole buforu jest zagnieżdżona w strukturze nielitowalnej, stałe pole buforu nie będzie poprawnie ustawiane na kod macierzysty.