Bewährte Methoden für native Interoperabilität
.NET bietet verschiedene Möglichkeiten zum Anpassen Ihres Codes für die native Interoperabilität. Dieser Artikel bietet einen Leitfaden, den die .NET-Teams von Microsoft in Bezug auf die native Interoperabilität befolgen.
Allgemeine Anleitung
Die Anleitungen in diesem Abschnitt gelten für alle Interoperabilitätsszenarien.
- ✔️ Verwenden Sie
[LibraryImport]
, falls möglich, bei Verwendung von .NET 7+ .- Es gibt Fälle, in denen
[DllImport]
verwendet wird. Eine Codeanalyse mit ID SYSLIB1054 teilt Ihnen mit, wann dies der Fall ist.
- Es gibt Fälle, in denen
- ✔️ VERWENDEN Sie die gleichen Benennungen und die gleiche Groß- und Kleinschreibung für Ihre Methoden und Parameter wie die native Methode, die Sie aufrufen möchten.
- ✔️ ERWÄGEN Sie die Verwendung der gleichen Benennungen und der gleichen Groß- und Kleinschreibung für Werte von Konstanten.
- ✔️ VERWENDEN Sie .NET-Typen, die dem nativen Typ am ähnlichsten sind. Wenn z.B. der native Typ in C#
unsigned int
ist, verwenden Sieuint
. - ✔️ DRÜCKEN Sie native Typen auf höherer Ebene mit .NET-Strukturen statt Klassen aus.
- ✔️ Verwenden Sie lieber Funktionszeiger als
Delegate
-Typen, wenn Sie Rückrufe an nicht verwaltete Funktionen in C# übergeben. - ✔️ VERWENDEN Sie
[In]
- und[Out]
-Attribute für Arrayparameter. - ✔️ VERWENDEN Sie nur
[In]
- und[Out]
-Attribute für andere Typen, wenn sich das gewünschte Verhalten vom Standardverhalten unterscheidet. - ✔️ ERWÄGEN Sie die Verwendung von System.Buffers.ArrayPool<T>, um Ihre nativen Arraypuffer in einem Pool zusammenzufassen.
- ✔️ ERWÄGEN Sie eine Umschließung Ihrer P/Invoke-Deklarationen in einer Klasse mit dem gleichen Namen und der gleichen Groß- und Kleinschreibung wie in Ihrer nativen Bibliothek.
- Damit können Ihre
[LibraryImport]
- oder[DllImport]
-Attribute das C#nameof
-Sprachfeature verwenden, um den Namen der nativen Bibliothek zu übergeben, und so sicherstellen, dass der Name der nativen Bibliothek nicht falsch geschrieben wurde.
- Damit können Ihre
- ✔️ VERWENDEN Sie
SafeHandle
-Handles, um die Lebensdauer von Objekten zu verwalten, die nicht verwaltete Ressourcen kapseln. Weitere Informationen finden Sie unter Bereinigen von nicht verwalteten Ressourcen. - ❌ VERMEIDEN Sie Finalizer, um die Lebensdauer von Objekten zu verwalten, die nicht verwaltete Ressourcen kapseln. Weitere Informationen finden Sie unter Implementieren einer Dispose-Methode.
LibraryImport-Attributeinstellungen
Ein Codeanalysator mit ID SYSLIB1054 hilft Ihnen bei LibraryImportAttribute
. In den meisten Fällen erfordert die Verwendung von LibraryImportAttribute
eine explizite Deklaration, anstatt auf Standardeinstellungen zu vertrauen. Dieses Design ist beabsichtigt und trägt dazu bei, unbeabsichtigtes Verhalten in Interopszenarien zu vermeiden.
Attributeinstellungen für „DllImport“
Einstellung | Standard | Empfehlung | Details |
---|---|---|---|
PreserveSig | true |
Standard beibehalten | Wenn diese Einstellung explizit auf „false“ festgelegt wird, werden fehlerhafte HRESULT-Rückgabewerte zu Ausnahmen umgewandelt (und der Rückgabewert in der Definition wird dadurch NULL). |
SetLastError | false |
Abhängig von der API | Legen Sie diese Einstellung auf „true“ fest, wenn die API „GetLastError“ verwendet, und verwenden Sie „Marshal.GetLastWin32Error“, um den Wert abzurufen. Wenn die API eine Bedingung festlegt, die besagt, dass ein Fehler vorliegt, rufen Sie den Fehler ab, bevor Sie weitere Aufrufe senden, um ein versehentliches Überschreiben zu verhindern. |
CharSet | Vom Compiler definiert (angegeben in der Zeichensatzdokumentation) | Explizit CharSet.Unicode oder CharSet.Ansi verwenden, wenn in der Definition Zeichenfolgen oder Zeichen vorhanden sind |
Damit wird das Marshallingverhalten von Zeichenfolgen angegeben und festgelegt, was ExactSpelling bei false ausführt. Beachten Sie, dass CharSet.Ansi tatsächlich UTF8 oder Unix ist. In den meisten Fällen verwendet Windows Unicode, Unix verwendet UTF8. Weitere Informationen finden Sie in der Dokumentation zu Zeichensätzen. |
ExactSpelling | false |
true |
Legen Sie diesen Wert auf „true“ fest, um einen leichten Leistungsvorteil zu erzielen, da die Runtime nicht nach alternativen Funktionsnamen mit dem Suffix „A“ oder „W“ sucht, je nach Wert der Einstellung von CharSet („A“ für CharSet.Ansi und „W“ für CharSet.Unicode ). |
Zeichenfolgenparameter
Ein string
wird gepinnt und direkt von nativem Code verwendet (und nicht kopiert), wenn es als Wert (nicht ref
oder out
) übergeben wird und eine der folgenden Angaben enthält:
- LibraryImportAttribute.StringMarshalling ist als Utf16 definiert.
- Das Argument wird explizit als
[MarshalAs(UnmanagedType.LPWSTR)]
markiert. - DllImportAttribute.CharSet ist Unicode.
Parameter ❌NICHT verwenden[Out] string
. Zeichenfolgenparameter, die per Wert mit dem [Out]
-Attribut übergeben werden, können die Runtime destabilisieren, wenn die Zeichenfolge internalisiert ist. Weitere Informationen zum Internalisieren von Zeichenfolgen finden Sie in der Dokumentation zu String.Intern.
✔️ ERWÄGEN Sie die Verwendung von char[]
- oder byte[]
-Arrays aus einem ArrayPool
, wenn nativer Code zum Füllen eines Zeichenpuffers erwartet wird. Dazu muss das Argument als [Out]
übergeben werden.
DllImport-spezifische Anleitung
✔️ ERWÄGEN Sie, die Eigenschaft CharSet
in [DllImport]
festzulegen, damit die Runtime die erwartete Zeichenfolgencodierung kennt.
✔️ ERWÄGEN Sie die Vermeidung von StringBuilder
-Parametern. StringBuilder
-Marshalling erzeugt immer eine native Pufferkopie. Dies kann extrem ineffizient sein. Sehen Sie sich das folgende typische Szenario an, in dem eine Windows-API aufgerufen wird, die eine Zeichenfolge akzeptiert:
- Erstellen Sie einen
StringBuilder
mit der gewünschten Kapazität (ordnet die verwaltete Kapazität zu) {1}. - Aufrufen:
- Ordnet einen nativen Puffer zu {2}.
- Kopiert den Inhalt, wenn
[In]
(Standard für einenStringBuilder
-Parameter). - Kopiert den nativen Puffer in ein neu zugeordnetes verwaltetes Array, wenn
[Out]
{3} (ebenfalls Standard fürStringBuilder
).
ToString()
ordnet ein weiteres verwaltetes Array zu {4}.
Das sind {4} Zuordnungen, um eine Zeichenfolge aus nativem Code abzurufen. Die beste Möglichkeit, dies zu beschränken, besteht darin, den StringBuilder
in einem weiteren Aufruf wiederzuverwenden, damit wird aber dennoch nur eine Zuordnung eingespart. Es ist viel besser, einen Zeichenpuffer aus dem ArrayPool
zu verwenden und zwischenzuspeichern. Damit benötigen Sie in nachfolgenden Aufrufen nur noch die Zuordnung für ToString()
.
Ein weiteres Problem bei StringBuilder
ist, dass immer der Rückgabepuffer bis zum ersten NULL-Zeichen zurückkopiert wird. Wenn die zurückgegebene Zeichenfolge nicht beendet oder mit einem doppelten NULL-Zeichen beendet wird, ist „P/Invoke“ bestenfalls falsch.
Wenn Sie StringBuilder
tatsächlich verwenden, besteht eine weitere Besonderheit darin, dass die Kapazität kein verborgenes NULL-Zeichen umfasst, das bei der Interoperabilität immer berücksichtigt wird. Das wird häufig falsch gemacht, da die meisten APIs die Größe des Puffers einschließlich des NULL-Zeichens erwarten. Dies kann zu unnötigen bzw. verschwendeten Zuordnungen führen. Darüber hinaus verhindert diese Besonderheit, dass die Runtime das Marshallen von StringBuilder
optimiert, um die Erstellung von Kopien zu minimieren.
Weitere Informationen zum Marshalling von Zeichenfolgen finden Sie unter Standardmäßiges Marshalling für Zeichenfolgen und Anpassen des Zeichenfolgenmarshallings.
Windows-spezifisch: Bei
[Out]
-Zeichenfolgen verwendet die CLR (Common Language Runtime) standardmäßigCoTaskMemFree
, um Zeichenfolgen freizugeben, oderSysStringFree
bei Zeichenfolgen, die alsUnmanagedType.BSTR
gekennzeichnet sind. Bei den meisten APIs mit Puffer für Ausgabezeichenfolgen gilt Folgendes: Die übergebene Zeichenanzahl muss das NULL-Zeichen enthalten. Wenn der zurückgegebene Wert kleiner ist als die Zeichenanzahl, ist der Aufruf erfolgreich und der Wert ist die Anzahl der Zeichen ohne das nachgestellte NULL-Zeichen. Andernfalls ist die Anzahl die erforderliche Größe des Puffers einschließlich des NULL-Zeichens.
- 5 übergeben, 4 abrufen: Die Zeichenfolge ist 4 Zeichen lang und umfasst ein nachgestelltes NULL-Zeichen.
- 5 übergeben, 6 abrufen: Die Zeichenfolge ist 5 Zeichen lang und erfordert einen Puffer mit 6 Zeichen für das NULL-Zeichen. Windows-Datentypen für Zeichenfolgen
Boolesche Parameter und Felder
Bei booleschen Werten passieren leicht Fehler. Standardmäßig erfolgt für einen bool
-Wert von .NET ein Marshalling in einen BOOL
-Wert in Windows. Dort handelt es sich um einen 4 Byte langen Wert. Die Typen _Bool
und bool
in C und C++ sind jedoch Einzelbytewerte. Dies kann zu Bugs führen, die sich nur sehr schwer auffinden lassen, da der halbe Rückgabewert verworfen wird, sich das Ergebnis aber nur möglicherweise ändert. Weitere Informationen zum Marshallen von bool
-Werten aus .NET in bool
-Typen in C oder C++ finden Sie in der Dokumentation zum Anpassen des Marshallings von booleschen Feldern.
GUIDs
GUIDs können direkt in Signaturen verwendet werden. Viele Windows-APIs akzeptieren GUID&
-Typen wie REFIID
. Wenn die Methodensignatur einen Verweisparameter enthält, platzieren Sie entweder ein ref
Schlüsselwort (keyword) oder ein [MarshalAs(UnmanagedType.LPStruct)]
Attribut in der GUID-Parameterdeklaration.
GUID | By-ref-GUID |
---|---|
KNOWNFOLDERID |
REFKNOWNFOLDERID |
❌ Verwenden Sie NICHT [MarshalAs(UnmanagedType.LPStruct)]
für etwas anderes als für ref
-GUID-Parameter.
Für Blitting geeignete Typen
Für Blitting geeignete Typen sind Typen, die in verwaltetem und nativem Code die gleiche Darstellung auf Bitebene aufweisen. Als solche müssen sie nicht in ein anderes Format konvertiert werden, um ein Marshalling in den und aus dem nativen Code zu ermöglichen. Da dies die Leistung verbessert, sind diese Typen zu bevorzugen. Einige Typen sind für Blitting nicht geeignet, enthalten aber bekanntermaßen für Blitting geeignete Inhalte. Diese Typen haben ähnliche Optimierungen wie für Blitting geeignete Typen, wenn sie nicht in einem anderen Typ enthalten sind. Sie werden jedoch nicht als für Blitting geeignet angesehen, wenn sie sich in Feldern von Strukturen befinden oder für UnmanagedCallersOnlyAttribute
erforderlich sind.
Für Blitting geeignete Typen bei aktiviertem Laufzeitmarshalling
Für Blitting geeignete Typen:
byte
,sbyte
,short
,ushort
,int
,uint
,long
,ulong
,single
,double
- Strukturen mit festem Layout, die für Instanzfelder nur über für Blitting geeignete Typen verfügen
- Ein festes Layout erfordert
[StructLayout(LayoutKind.Sequential)]
oder[StructLayout(LayoutKind.Explicit)]
- Strukturen sind standardmäßig
LayoutKind.Sequential
- Ein festes Layout erfordert
Typen mit für Blitting geeigneten Inhalten:
- Nicht geschachtelte, eindimensionale Arrays aus für Blitting geeigneten primitiven Typen (z. B.
int[]
) - Klassen mit festem Layout, die für Instanzfelder nur über für Blitting geeignete Typen verfügen
- Ein festes Layout erfordert
[StructLayout(LayoutKind.Sequential)]
oder[StructLayout(LayoutKind.Explicit)]
- Klassen sind standardmäßig
LayoutKind.Auto
- Ein festes Layout erfordert
NICHT für Blitting geeignet:
bool
MANCHMAL für Blitting geeignet:
char
Typen mit MANCHMAL für Blitting geeigneten Inhalten:
string
Wenn für Blitting geeignete Typen per Verweis mit in
, ref
oder out
übergeben werden oder wenn Typen mit für Blitting geeigneten Inhalten per Wert übergeben werden, werden sie vom Marshaller einfach angepinnt, anstatt in einen Zwischenpuffer kopiert zu werden.
char
ist in einem eindimensionalen Array oder bei Verwendung in einem Typen für Blitting geeignet, der explizit mit [StructLayout]
mit CharSet = CharSet.Unicode
gekennzeichnet ist.
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
public struct UnicodeCharStruct
{
public char c;
}
string
enthält für Blitting geeignete Inhalte, wenn sie nicht in einem anderen Typ enthalten sind und als Argument und eine der folgenden Werte (nicht ref
oder out
) übergeben werden:
- StringMarshalling ist als Utf16 definiert.
- Das Argument wird explizit als
[MarshalAs(UnmanagedType.LPWSTR)]
markiert. - CharSet ist Unicode.
Sie können ermitteln, ob ein Typ für Blitting geeignet ist oder für Blitting geeignete Inhalte enthält, indem Sie versuchen, ein angepinntes GCHandle
zu erstellen. Wenn der Typ keine Zeichenfolge ist oder nicht als für Blitting geeignet betrachtet wird, löst GCHandle.Alloc
eine ArgumentException
aus.
Für Blitting geeignete Typen bei deaktiviertem Laufzeitmarshalling
Wenn das Laufzeitmarshalling deaktiviert ist, sind die Regeln dafür, welche Typen für Blitting geeignet sind, erheblich einfacher. Alle Typen, die C#-unmanaged
-Typen sind und keine Felder enthalten, die mit [StructLayout(LayoutKind.Auto)]
gekennzeichnet sind, sind für Blitting geeignet. Alle Typen, die keine unmanaged
-Typen in C# sind, sind nicht für Blitting geeignet. Das Konzept von Typen mit für Blitting geeigneten Inhalten, z. B. Arrays oder Zeichenfolgen, findet keine Anwendung, wenn das Laufzeitmarshalling deaktiviert ist. Ein Typ, der von der oben genannten Regel als nicht für Blitting geeignet betrachtet wird, wird bei deaktiviertem Laufzeitmarshalling nicht unterstützt.
Diese Regeln unterscheiden sich vom integrierten System hauptsächlich in Situationen, in denen bool
und char
verwendet werden. Wenn das Marshalling deaktiviert ist, wird bool
als 1-Byte-Wert übergeben und nicht normalisiert, und char
wird immer als 2-Byte-Wert übergeben. Wenn das Laufzeitmarshalling aktiviert ist, kann bool
einem 1-, 2- oder 4-Byte-Wert zugeordnet werden und wird immer normalisiert, und char
wird abhängig vom CharSet
entweder einem 1- oder 2-Byte-Wert zugeordnet.
✔️ LEGEN Sie Ihre Strukturen nach Möglichkeit als für Blitting geeignet fest.
Weitere Informationen finden Sie unter:
Beibehalten von verwalteten Objekten
GC.KeepAlive()
stellt sicher, dass ein Objekt im Gültigkeitsbereich bleibt, bis die KeepAlive-Methode erreicht wird.
HandleRef
ermöglicht es dem Marshaller, ein Objekt während der Dauer eines P/Invoke beizubehalten. Es kann statt IntPtr
in Methodensignaturen verwendet werden. SafeHandle
ersetzt diese Klasse und sollte stattdessen verwendet werden.
GCHandle
ermöglicht das Anheften eines verwalteten Objekts und Abrufen des nativen Zeigers auf das Objekt. Das grundlegende Muster sieht folgendermaßen aus:
GCHandle handle = GCHandle.Alloc(obj, GCHandleType.Pinned);
IntPtr ptr = handle.AddrOfPinnedObject();
handle.Free();
Anheften ist kein Standardvorgang für GCHandle
. Das andere wichtige Muster sieht so aus: Ein Verweis wird über nativen Code an ein verwaltetes Objekt übergeben und, in der Regel über einen Rückruf, wieder zurückgegeben. Hier sehen Sie das Muster:
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();
Denken Sie daran, dass GCHandle
explizit freigegeben werden muss, um Arbeitsspeicherverluste zu vermeiden.
Allgemeine Windows-Datentypen
Die folgende Liste enthält Datentypen, die in Windows-APIs häufig verwendet werden, sowie C#-Typen, die beim Aufrufen des Windows-Codes verwendet werden sollen.
Die folgenden Typen weisen trotz ihrer Namen die gleiche Größe wie 32- und 64-Bit-Typen für Windows auf.
Breite | Windows | C# | Alternative |
---|---|---|---|
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 |
Weitere Informationen finden Sie unter CLong und CULong . |
32 | LONG32 |
int |
|
32 | CLONG |
uint |
Weitere Informationen finden Sie unter CLong und CULong . |
32 | DWORD |
uint |
Weitere Informationen finden Sie unter CLong und CULong . |
32 | DWORD32 |
uint |
|
32 | UINT |
uint |
|
32 | UINT32 |
uint |
|
32 | ULONG |
uint |
Weitere Informationen finden Sie unter CLong und 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 |
Die folgenden Typen sind Zeiger und entsprechen der Breite der Plattform. Verwenden Sie IntPtr
/UIntPtr
für diese.
Signierte Zeigertypen (verwenden Sie IntPtr ). |
Nicht signierte Zeigertypen (verwenden Sie UIntPtr ). |
---|---|
HANDLE |
WPARAM |
HWND |
UINT_PTR |
HINSTANCE |
ULONG_PTR |
LPARAM |
SIZE_T |
LRESULT |
|
LONG_PTR |
|
INT_PTR |
Ein PVOID
-Typ in Windows, der void*
in C entspricht, kann als IntPtr
oder UIntPtr
gemarshallt werden. void*
sollte jedoch nach Möglichkeit bevorzugt werden.
Früher integrierte unterstützte Typen
Es gibt seltene Fälle, in denen die integrierte Unterstützung für einen Typ entfernt wird.
Die integrierte Unterstützung für das Marshalling für UnmanagedType.HString
und UnmanagedType.IInspectable
wurde im .NET 5-Release entfernt. Sie müssen Binärdateien, die diesen Marshalltyp und ein vorheriges Framework als Ziel verwenden, neu kompilieren. Es ist weiterhin möglich, diesen Typ zu marshallen, dies muss aber manuell erfolgen, wie im folgenden Codebeispiel gezeigt. Dieser Code wird in Zukunft funktionieren und ist auch mit früheren Frameworks kompatibel.
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);
Überlegungen zu plattformübergreifenden Datentypen
Es gibt Typen in C/C++, die etwas Spielraum bei der Definition aufweisen. Beim Schreiben plattformübergreifender Interoperabilität können Fälle auftreten, in denen sich Plattformen unterscheiden und die Probleme verursachen können, wenn sie nicht berücksichtigt werden.
C/C++ long
C/C++-long
und C#-long
weisen nicht unbedingt die gleiche Größe auf.
Der long
-Typ in C/C++ ist so definiert, dass er mindestens 32 Bits lang ist. Dies bedeutet, dass es eine Mindestanzahl erforderlicher Bits gibt, aber Plattformen können bei Bedarf mehr Bits verwenden. Die folgende Tabelle zeigt die Unterschiede in den bereitgestellten Bits für den C/C++-Datentyp long
auf verschiedenen Plattformen.
Plattform | 32-Bit | 64-Bit |
---|---|---|
Windows | 32 | 32 |
macOS/*nix | 32 | 64 |
Im Gegensatz dazu entspricht C#-long
immer 64 Bit. Aus diesem Grund empfiehlt es sich, die Verwendung von C#-long
für die Interoperabilität mit C/C++-long
zu vermeiden.
(Dieses Problem mit C/C++-long
ist für C/C++-char
, -short
, -int
und -long long
nicht vorhanden, da sie auf all diesen Plattformen 8, 16, 32 und 64 Bit aufweisen.)
Verwenden Sie in .NET 6 und höheren Versionen die Typen CLong
und CULong
für die Interoperabilität mit den C/C++-Datentypen long
und unsigned long
. Das folgende Beispiel gilt für CLong
, Sie können jedoch CULong
verwenden, um unsigned long
auf ähnliche Weise zu abstrahieren.
// Cross platform C function
// long Function(long a);
[DllImport("NativeLib")]
extern static CLong Function(CLong a);
// Usage
nint result = Function(new CLong(10)).Value;
Wenn Sie für .NET 5 und frühere Versionen programmieren, sollten Sie separate Windows- und Nicht-Windows-Signaturen deklarieren, um das Problem zu behandeln.
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);
}
Strukturen
Verwaltete Strukturen werden im Stapel erstellt und erst dann entfernt, wenn die Methode zurückgegeben wird. Per Definition werden sie „angeheftet“ (also von der Garbage Collection nicht verschoben). Sie können auch einfach die Adresse in unsicheren Codeblöcken verwenden, wenn der native Code den Zeiger nicht über das Ende der aktuellen Methode hinaus verwendet.
Für Blitting geeignete Strukturen sind wesentlich leistungsfähiger, da sie ganz einfach direkt von der Marshallingebene genutzt werden können. Versuchen Sie, Typen blitfähig zu machen (vermeiden Sie z.B. bool
). Weitere Informationen finden Sie im Abschnitt Für Blitting geeignete Typen.
Wenn eine Struktur für Blitting geeignet ist, verwenden Sie sizeof()
statt Marshal.SizeOf<MyStruct>()
, um eine bessere Leistung zu erzielen. Wie oben erwähnt, können Sie überprüfen, ob der Typ für Blitting geeignet ist, indem Sie versuchen, ein angeheftetes GCHandle
zu erstellen. Wenn der Typ keine Zeichenfolge ist oder nicht als für Blitting geeignet betrachtet wird, löst GCHandle.Alloc
eine ArgumentException
aus.
Zeiger auf Strukturen in Definitionen müssen entweder von ref
übergeben werden oder unsafe
und *
verwenden.
✔️ PASSEN Sie die verwaltete Struktur so eng wie möglich an die Form und die Namen an, die in der offiziellen Dokumentation zur Plattform oder im Header verwendet werden.
✔️ VERWENDEN Sie sizeof()
aus C# anstelle von Marshal.SizeOf<MyStruct>()
für Strukturen, die für Blitting geeignet sind, um die Leistung zu verbessern.
❌ VERMEIDEN Sie die Verwendung von Klassen, um komplexe native Typen durch Vererbung auszudrücken.
❌ VERMEIDEN Sie die Verwendung von System.Delegate
- oder System.MulticastDelegate
-Feldern, um Felder für Funktionszeiger in Strukturen darzustellen.
Da System.Delegate und System.MulticastDelegate nicht über eine erforderliche Signatur verfügen, garantieren sie nicht, dass der übergebene Delegat der Signatur entspricht, die vom nativen Code erwartet wird. Außerdem kann in .NET Framework und .NET Core das Marshalling einer Struktur mit einem System.Delegate
oder einen System.MulticastDelegate
von ihrer nativen Darstellung in ein verwaltetes Objekt die Runtime destabilisieren, wenn der Wert des Felds in der nativen Darstellung kein Funktionszeiger ist, der einen verwalteten Delegaten umschließt. In .NET 5 und höheren Versionen wird das Marshalling eines System.Delegate
- oder System.MulticastDelegate
-Felds von einer nativen Darstellung in ein verwaltetes Objekt nicht unterstützt. Verwenden Sie anstelle von System.Delegate
oder System.MulticastDelegate
einen bestimmten Delegattyp.
Puffer fester Größe
Ein Array wie INT_PTR Reserved1[2]
muss in zwei IntPtr
-Felder gemarshallt werden: Reserved1a
und Reserved1b
. Wenn das native Array ein primitiver Typ ist, können wir das Schlüsselwort fixed
verwenden, um es etwas übersichtlicher zu schreiben. SYSTEM_PROCESS_INFORMATION
sieht im nativen Header beispielsweise so aus:
typedef struct _SYSTEM_PROCESS_INFORMATION {
ULONG NextEntryOffset;
ULONG NumberOfThreads;
BYTE Reserved1[48];
UNICODE_STRING ImageName;
...
} SYSTEM_PROCESS_INFORMATION
In C# können wir es folgendermaßen schreiben:
internal unsafe struct SYSTEM_PROCESS_INFORMATION
{
internal uint NextEntryOffset;
internal uint NumberOfThreads;
private fixed byte Reserved1[48];
internal Interop.UNICODE_STRING ImageName;
...
}
Es gibt jedoch einige Besonderheiten bei festen Puffern. Bei festen Puffer aus nicht für Blitting geeigneten Typen wird das Marshalling nicht ordnungsgemäß ausgeführt, daher muss das vorhandene Array auf mehrere einzelne Felder erweitert werden. Darüber hinaus gilt für .NET Framework und .NET Core vor Version 3.0: Wenn eine Struktur, die ein festes Pufferfeld enthält, in einer nicht für Blitting geeigneten Struktur geschachtelt wird, erfolgt kein ordnungsgemäßes Marshalling des festen Pufferfelds zum nativen Code.