Metodtips för intern samverkan
Med .NET kan du anpassa din interna samverkanskod på olika sätt. Den här artikeln innehåller de riktlinjer som Microsofts .NET-team följer för intern samverkan.
Allmän vägledning
Vägledningen i det här avsnittet gäller för alla interop-scenarier.
- ✔️ Använd
[LibraryImport]
, om möjligt, när du riktar in dig på .NET 7+.- Det finns fall då det är lämpligt att använda
[DllImport]
. En kodanalysator med ID SYSLIB1054 anger när så är fallet.
- Det finns fall då det är lämpligt att använda
- ✔️ Använd samma namngivning och versaler för dina metoder och parametrar som den interna metod som du vill anropa.
- ✔️ ÖVERVÄG att använda samma namngivning och versaler för konstanta värden.
- ✔️ Använd .NET-typer som mappar närmast den inbyggda typen. I C# använder du
uint
till exempel när den inbyggda typen ärunsigned int
. - ✔️ Föredrar att uttrycka interna typer på högre nivå med hjälp av .NET-structs i stället för klasser.
- ✔️ Föredrar att använda funktionspekare, till
Delegate
skillnad från typer, när du skickar återanrop till ohanterade funktioner i C#. - ✔️
[In]
ANVÄND och[Out]
attribut för matrisparametrar. - ✔️ Använd endast
[In]
attribut och[Out]
på andra typer när det beteende du vill ha skiljer sig från standardbeteendet. - ✔️ ÖVERVÄG att använda System.Buffers.ArrayPool<T> för att poola dina interna matrisbuffertar.
- ✔️ ÖVERVÄG att omsluta dina P/Invoke-deklarationer i en klass med samma namn och versaler som ditt interna bibliotek.
- Detta gör att dina
[LibraryImport]
attribut kan[DllImport]
använda C#nameof
-språkfunktionen för att skicka in namnet på det interna biblioteket och se till att du inte felstavade namnet på det interna biblioteket.
- Detta gör att dina
- ✔️ ANVÄND
SafeHandle
referenser för att hantera livslängden för objekt som kapslar in ohanterade resurser. Mer information finns i Rensa ohanterade resurser. - ❌ UNDVIK slutförare för att hantera livslängden för objekt som kapslar in ohanterade resurser. Mer information finns i Implementera en avyttringsmetod.
BibliotekImportera attributinställningar
En kodanalyserare med ID SYSLIB1054 hjälper dig med LibraryImportAttribute
. I de flesta fall kräver användningen av LibraryImportAttribute
en explicit deklaration i stället för att förlita sig på standardinställningar. Den här designen är avsiktlig och hjälper till att undvika oavsiktligt beteende i interop-scenarier.
DllImportera attributinställningar
Inställning | Standardvärde | Rekommendation | Details |
---|---|---|---|
PreserveSig | true |
Behåll standard | När detta uttryckligen anges till falskt omvandlas misslyckade HRESULT-returvärden till undantag (och returvärdet i definitionen blir null som ett resultat). |
SetLastError | false |
Beror på API:et | Ange detta till sant om API:et använder GetLastError och använder Marshal.GetLastWin32Error för att hämta värdet. Om API:et anger ett villkor som säger att det har ett fel hämtar du felet innan du gör andra anrop för att undvika att det skrivs över av misstag. |
CharSet | Kompilatordefinierad (anges i charsetdokumentationen) | CharSet.Unicode Använd eller CharSet.Ansi när strängar eller tecken finns i definitionen |
Detta anger marshallingbeteendet för strängar och vad ExactSpelling som gör när false . Observera att det CharSet.Ansi faktiskt är UTF8 på Unix. För det mesta använder Windows Unicode medan Unix använder UTF8. Mer information om teckenuppsättningar finns i dokumentationen. |
ExactSpelling | false |
true |
Ställ in detta på sant och få en liten perf-fördel eftersom körningen inte söker efter alternativa funktionsnamn med antingen suffixet "A" eller "W" beroende på värdet CharSet för inställningen ("A" för CharSet.Ansi och "W" för CharSet.Unicode ). |
Strängparametrar
A string
fästs och används direkt av inbyggd kod (i stället för att kopieras) när det skickas av värdet (inte ref
eller out
) och något av följande:
- LibraryImportAttribute.StringMarshalling definieras som Utf16.
- Argumentet markeras uttryckligen som
[MarshalAs(UnmanagedType.LPWSTR)]
. - DllImportAttribute.CharSet är Unicode.
❌ Använd [Out] string
inte parametrar. Strängparametrar som skickas av värde med [Out]
attributet kan destabilisera körningen om strängen är en intern sträng. Se mer information om strängpraktik i dokumentationen för String.Intern.
✔️ CONSIDER char[]
eller byte[]
matriser från en ArrayPool
när inbyggd kod förväntas fylla en teckenbuffert. Detta kräver att argumentet skickas som [Out]
.
DllImportspecifik vägledning
✔️ ÖVERVÄG att CharSet
ange egenskapen i [DllImport]
så att körningen känner till den förväntade strängkodningen.
✔️ ÖVERVÄG att undvika StringBuilder
parametrar. StringBuilder
marshalling skapar alltid en intern buffertkopia. Som sådan kan det vara extremt ineffektivt. Ta det typiska scenariot med att anropa ett Windows-API som tar en sträng:
- Skapa en
StringBuilder
av den önskade kapaciteten (allokerar hanterad kapacitet) {1}. - Åkalla:
- Allokerar en intern buffert {2}.
- Kopierar innehållet om
[In]
(standardvärdet för enStringBuilder
parameter). - Kopierar den interna bufferten till en nyligen allokerad hanterad matris om
[Out]
{3} (även standardvärdet förStringBuilder
).
ToString()
allokerar ännu en hanterad matris {4}.
Det är {4} allokeringar för att få en sträng ur den interna koden. Det bästa du kan göra för att begränsa detta är att återanvända StringBuilder
i ett annat anrop, men det sparar fortfarande bara en allokering. Det är mycket bättre att använda och cachelagrat en teckenbuffert från ArrayPool
. Du kan sedan bara komma ner till allokeringen för efterföljande ToString()
anrop.
Det andra problemet med StringBuilder
är att den alltid kopierar returbufferten tillbaka till den första null-värdet. Om den skickade bakåtsträngen inte avslutas eller om den är en dubbel-null-avslutad sträng är din P/Invoke i bästa fall felaktig.
Om du använder StringBuilder
är en sista gotcha att kapaciteten inte innehåller en dold null, som alltid redovisas i interop. Det är vanligt att personer får det här fel eftersom de flesta API:er vill ha storleken på bufferten , inklusive null. Detta kan resultera i bortkastade/onödiga allokeringar. Dessutom förhindrar den här gotcha körningen från att StringBuilder
optimera marshalling för att minimera kopior.
Mer information om sträng marshalling finns i Standard marshalling för strängar och Anpassa sträng marshalling.
Windows-specifika för
[Out]
strängar som CLR använderCoTaskMemFree
som standard för att frigöra strängar ellerSysStringFree
för strängar som har markerats somUnmanagedType.BSTR
. För de flesta API:er med en utdatasträngsbuffert: Antalet skickade tecken måste innehålla null. Om det returnerade värdet är mindre än antalet skickade tecken har anropet lyckats och värdet är antalet tecken utan avslutande null. Annars är antalet buffertens storlek som krävs, inklusive null-tecknet.
- Skicka in 5, hämta 4: Strängen är 4 tecken lång med en avslutande null.
- Skicka in 5, hämta 6: Strängen är 5 tecken lång, behöver en buffert på 6 tecken för att innehålla null-värdet. Windows-datatyper för strängar
Booleska parametrar och fält
Booleska är lätta att förstöra. Som standard är en .NET-kodad bool
till ett Windows BOOL
, där det är ett 4-bytesvärde. Typerna _Bool
, och bool
i C och C++ är dock en enda byte. Detta kan leda till att det är svårt att spåra buggar eftersom hälften av returvärdet tas bort, vilket bara kan ändra resultatet. Mer information om hur du sorterar .NET-värden bool
till C- eller C++ bool
-typer finns i dokumentationen om hur du anpassar boolesk fält marshalling.
Guid
GUID:er kan användas direkt i signaturer. Många Windows-API:er använder GUID&
typalias som REFIID
. När metodsignaturen innehåller en referensparameter placerar du antingen ett ref
nyckelord eller ett [MarshalAs(UnmanagedType.LPStruct)]
attribut i GUID-parameterdeklarationen.
GUID | GUID med referens |
---|---|
KNOWNFOLDERID |
REFKNOWNFOLDERID |
❌ Använd [MarshalAs(UnmanagedType.LPStruct)]
inte för något annat än ref
GUID-parametrar.
Blittable-typer
Blittable-typer är typer som har samma representation på bitnivå i hanterad och intern kod. Därför behöver de inte konverteras till ett annat format som ska ordnas till och från inbyggd kod, och eftersom detta förbättrar prestandan bör de föredras. Vissa typer är inte blittable men är kända för att innehålla blittable-innehåll. Dessa typer har liknande optimeringar som blittable-typer när de inte finns i en annan typ, men inte betraktas som blittable när de finns i fält med structs eller i syfte att UnmanagedCallersOnlyAttribute
.
Blittable-typer när runtime-marshalling är aktiverat
Blittable-typer:
byte
,sbyte
,short
,ushort
,int
,uint
, ,long
,ulong
, ,single
double
- structs med fast layout som bara har blittable-värdetyper för instansfält
- fast layout kräver
[StructLayout(LayoutKind.Sequential)]
eller[StructLayout(LayoutKind.Explicit)]
- structs är
LayoutKind.Sequential
som standard
- fast layout kräver
Typer med blittable-innehåll:
- icke-kapslade, endimensionella matriser med blittable primitiva typer (till exempel
int[]
) - Klasser med fast layout som bara har blittable-värdetyper för instansfält
- fast layout kräver
[StructLayout(LayoutKind.Sequential)]
eller[StructLayout(LayoutKind.Explicit)]
- Klasserna är
LayoutKind.Auto
som standard
- fast layout kräver
INTE blittable:
bool
IBLAND blittable:
char
Typer med ibland blittable innehåll:
string
När blittable-typer skickas med referens med in
, ref
eller out
, eller när typer med blittable-innehåll skickas med värde, fästs de helt enkelt av marshallern i stället för att kopieras till en mellanliggande buffert.
char
är blittable i en endimensionell matris eller om den är en del av en typ som innehåller den uttryckligen markerad med [StructLayout]
.CharSet = CharSet.Unicode
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
public struct UnicodeCharStruct
{
public char c;
}
string
innehåller blittable-innehåll om det inte finns i en annan typ och skickas av värdet (inte ref
eller out
) som ett argument och något av följande:
- StringMarshalling definieras som Utf16.
- Argumentet markeras uttryckligen som
[MarshalAs(UnmanagedType.LPWSTR)]
. - CharSet är Unicode.
Du kan se om en typ är blittable eller innehåller blittable-innehåll genom att försöka skapa en fäst GCHandle
. Om typen inte är en sträng eller betraktas som blittable, GCHandle.Alloc
genererar en ArgumentException
.
Blittable-typer när runtime-marshalling inaktiveras
När runtime-marshalling inaktiveras är reglerna för vilka typer som är blittable betydligt enklare. Alla typer som är C#- unmanaged
typer och inte har några fält som är markerade med [StructLayout(LayoutKind.Auto)]
är blittable. Alla typer som inte är C#- unmanaged
typer är inte blittable. Begreppet typer med blittable-innehåll, till exempel matriser eller strängar, gäller inte när runtime-marshalling inaktiveras. Alla typer som inte anses blittable av ovan nämnda regel stöds inte när runtime-marshalling inaktiveras.
Dessa regler skiljer sig från det inbyggda systemet främst i situationer där bool
och char
används. När marshalling är inaktiverad bool
skickas som ett 1 byte-värde och normaliseras inte och char
skickas alltid som ett 2-bytesvärde. När runtime-marshalling är aktiverat bool
kan mappa till ett värde på 1, 2 eller 4 byte och normaliseras alltid och char
mappas till antingen ett värde på 1 eller 2 byte beroende på CharSet
.
✔️ Gör dina strukturer blittable när det är möjligt.
Mer information finns i:
Hålla hanterade objekt vid liv
GC.KeepAlive()
ser till att ett objekt förblir i omfånget tills metoden KeepAlive har träffats.
HandleRef
tillåter marshallern att hålla ett objekt vid liv under en P/Invoke-varaktighet. Den kan användas i stället för IntPtr
i metodsignaturer. SafeHandle
ersätter den här klassen effektivt och bör användas i stället.
GCHandle
tillåter att du fäster ett hanterat objekt och hämtar den inbyggda pekaren till det. Det grundläggande mönstret är:
GCHandle handle = GCHandle.Alloc(obj, GCHandleType.Pinned);
IntPtr ptr = handle.AddrOfPinnedObject();
handle.Free();
Fäst är inte standard för GCHandle
. Det andra huvudmönstret är att skicka en referens till ett hanterat objekt via intern kod och tillbaka till hanterad kod, vanligtvis med motringning. Här är mönstret:
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();
Glöm inte att GCHandle
det uttryckligen måste frigöras för att undvika minnesläckor.
Vanliga Windows-datatyper
Här är en lista över datatyper som ofta används i Windows-API:er och vilka C#-typer som ska användas vid anrop till Windows-koden.
Följande typer har samma storlek på 32-bitars och 64-bitars Windows, trots deras namn.
Width | Windows | C# | Alternativ |
---|---|---|---|
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 |
Se CLong och CULong . |
32 | LONG32 |
int |
|
32 | CLONG |
uint |
Se CLong och CULong . |
32 | DWORD |
uint |
Se CLong och CULong . |
32 | DWORD32 |
uint |
|
32 | UINT |
uint |
|
32 | UINT32 |
uint |
|
32 | ULONG |
uint |
Se CLong och 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 |
Följande typer, som pekare, följer plattformens bredd. Använd IntPtr
/UIntPtr
för dessa.
Signerade pekartyper (använd IntPtr ) |
Osignerade pekartyper (använd UIntPtr ) |
---|---|
HANDLE |
WPARAM |
HWND |
UINT_PTR |
HINSTANCE |
ULONG_PTR |
LPARAM |
SIZE_T |
LRESULT |
|
LONG_PTR |
|
INT_PTR |
En Windows PVOID
, som är en C void*
, kan vara marshalled som antingen IntPtr
eller UIntPtr
, men föredrar void*
när det är möjligt.
Tidigare inbyggda typer som stöds
Det finns sällsynta instanser när inbyggt stöd för en typ tas bort.
Det UnmanagedType.HString
inbyggda marskalksstödet och UnmanagedType.IInspectable
har tagits bort i .NET 5-versionen. Du måste kompilera om binärfiler som använder den här marshallingtypen och som är avsedda för ett tidigare ramverk. Det går fortfarande att konvertera den här typen, men du måste konvertera den manuellt, vilket visas i följande kodexempel. Den här koden fungerar framåt och är även kompatibel med tidigare ramverk.
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);
Överväganden för plattformsoberoende datatyp
Det finns typer i C/C++-språket som har latitud i hur de definieras. När du skriver plattformsoberoende interop kan det uppstå fall där plattformar skiljer sig åt och kan orsaka problem om de inte beaktas.
C/C++ long
C/C++ long
och C# long
är inte nödvändigtvis samma storlek.
Typen long
i C/C++ definieras för att ha "minst 32" bitar. Det innebär att det finns ett minsta antal nödvändiga bitar, men plattformar kan välja att använda fler bitar om så önskas. I följande tabell visas skillnaderna i angivna bitar för C/C++ long
-datatypen mellan plattformar.
Plattform | 32-bitars | 64-bitars |
---|---|---|
Windows | 32 | 32 |
macOS/*nix | 32 | 64 |
C# long
är däremot alltid 64 bitar. Därför är det bäst att undvika att använda C# long
för att interop med C/C++ long
.
(Det här problemet med C/C++ long
finns inte för C/C++ char
, short
, int
och long long
eftersom de är 8, 16, 32 respektive 64 bitar på alla dessa plattformar.)
I .NET 6 och senare versioner använder du typerna CLong
och CULong
för interop med C/C++ long
och unsigned long
datatyper. Följande exempel är för CLong
, men du kan använda CULong
för att abstrahera unsigned long
på ett liknande sätt.
// Cross platform C function
// long Function(long a);
[DllImport("NativeLib")]
extern static CLong Function(CLong a);
// Usage
nint result = Function(new CLong(10)).Value;
När du riktar in dig på .NET 5 och tidigare versioner bör du deklarera separata Windows- och icke-Windows-signaturer för att hantera problemet.
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);
}
Strukturer
Hanterade structs skapas i stacken och tas inte bort förrän metoden returnerar. Per definition är de sedan "fästa" (det flyttas inte av GC). Du kan också helt enkelt ta adressen i osäkra kodblock om den interna koden inte använder pekaren förbi slutet av den aktuella metoden.
Blittable structs är mycket mer högpresterande eftersom de helt enkelt kan användas direkt av marshallingskiktet. Försök att göra structs blittable (till exempel undvika bool
). Mer information finns i avsnittet Blittable Types (Blittable Types).
Om structen är blittable använder du sizeof()
i stället Marshal.SizeOf<MyStruct>()
för för bättre prestanda. Som nämnts ovan kan du verifiera att typen är blittable genom att försöka skapa en fäst GCHandle
. Om typen inte är en sträng eller betraktas som blittable, GCHandle.Alloc
genererar en ArgumentException
.
Pekare till structs i definitioner måste antingen skickas av ref
eller användas unsafe
och *
.
✔️ Matcha den hanterade structen så nära formen och namnen som används i den officiella plattformsdokumentationen eller rubriken.
✔️ Använd C# sizeof()
i stället Marshal.SizeOf<MyStruct>()
för för blittable-strukturer för att förbättra prestanda.
❌ UNDVIK att använda klasser för att uttrycka komplexa inbyggda typer genom arv.
❌ UNDVIK att använda System.Delegate
fält eller System.MulticastDelegate
för att representera funktionspekarfält i strukturer.
Eftersom System.Delegate och System.MulticastDelegate inte har en obligatorisk signatur garanterar de inte att ombudet som skickas in matchar signaturen som den interna koden förväntar sig. I .NET Framework och .NET Core kan dessutom marshalling av en struct som innehåller en System.Delegate
eller System.MulticastDelegate
från dess interna representation till ett hanterat objekt destabilisera körningen om värdet för fältet i den interna representationen inte är en funktionspekare som omsluter ett hanterat ombud. I .NET 5 och senare versioner stöds inte marshalling av ett System.Delegate
eller System.MulticastDelegate
ett fält från en intern representation till ett hanterat objekt. Använd en specifik ombudstyp System.Delegate
i stället för eller System.MulticastDelegate
.
Fasta buffertar
En matris som INT_PTR Reserved1[2]
måste vara uppdelade i två IntPtr
fält och Reserved1a
Reserved1b
. När den interna matrisen är en primitiv typ kan vi använda nyckelordet fixed
för att skriva det lite mer rent. Det ser till exempel SYSTEM_PROCESS_INFORMATION
ut så här i den interna rubriken:
typedef struct _SYSTEM_PROCESS_INFORMATION {
ULONG NextEntryOffset;
ULONG NumberOfThreads;
BYTE Reserved1[48];
UNICODE_STRING ImageName;
...
} SYSTEM_PROCESS_INFORMATION
I C# kan vi skriva det så här:
internal unsafe struct SYSTEM_PROCESS_INFORMATION
{
internal uint NextEntryOffset;
internal uint NumberOfThreads;
private fixed byte Reserved1[48];
internal Interop.UNICODE_STRING ImageName;
...
}
Det finns dock vissa gotchas med fasta buffertar. Fasta buffertar av icke-blittable-typer kommer inte att vara korrekt ordnade, så matrisen på plats måste utökas till flera enskilda fält. Om en struct som innehåller ett fast buffertfält är kapslad inom en icke-blittable struct i .NET Framework och .NET Core före 3.0 kommer det fasta buffertfältet dessutom inte att vara korrekt uppdelat till intern kod.