Dela via


Bästa praxis för naturlig interoperabilitet

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.
  • ✔️ Använd samma namngivning och versalisering för dina metoder och parametrar som den inbyggda metod du vill anropa.
  • ✔️ ÖVERVÄG att använda samma namngivning och versalisering för konstanta värden.
  • ✔️ Använd .NET-typer som mappar närmast den ursprungliga typen. I C# använder du uint till exempel när den inbyggda typen är unsigned int.
  • ✔️ Föredrar att uttrycka interna typer på högre nivå med hjälp av .NET-structs i stället för klasser.
  • ✔️ Föredra att du använder funktionspekare, istället för Delegate typer, när du skickar återanrop till ohanterade funktioner i C#.
  • ✔️ ANVÄND [In] och [Out] attribut på 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 möjliggör att dina [LibraryImport]- eller [DllImport]-attribut kan använda C#-språkfunktionen för att ange namnet på det infödda biblioteket och säkerställa att du inte felstavade namnet på det infödda biblioteket.
  • ✔️ ANVÄND SafeHandle handtag 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.

Biblioteksimport-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 Detaljer
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 charset-dokumentationen) Använd uttryckligen CharSet.Unicode eller CharSet.Ansi när strängar eller tecken finns i definitionen ** Detta anger marshalling-beteendet för strängar och vad ExactSpelling 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 till "true" och få en liten prestandafördel, eftersom körtiden inte kommer att söka efter alternativa funktionsnamn med antingen suffixet "A" eller "W", beroende på värdet av CharSet-inställningen ("A" för CharSet.Ansi och "W" för CharSet.Unicode).

Strängparametrar

En string fästs och används direkt av inbyggd kod (snarare än att kopieras) när det överförs som värde (inte ref eller out) och något av följande:

❌ Använd inte [Out] string 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änginternering 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].

DllImport-specifik vägledning

✔️ ÖVERVÄG att ange egenskapen CharSet i [DllImport] så att programmet känner till den förväntade strängkodningen.

✔️ ÖVERVÄG att undvika StringBuilder parametrarna. StringBuilder marshalling skapar alltid en inhemsk buffertkopia. Som sådan kan det vara extremt ineffektivt. Ta det typiska scenariot med att anropa ett Windows-API som tar en sträng:

  1. Skapa en StringBuilder av den önskade kapaciteten (allokerar hanterad kapacitet) {1}.
  2. Åkalla:
    1. Allokerar en inbyggd buffert {2}.
    2. Kopierar innehållet om [In](standardvärdet för en StringBuilder parameter).
    3. Kopierar den interna bufferten till en nyligen allokerad hanterad matris om [Out]{3}(även standardvärdet för StringBuilder).
  3. ToString() allokerar ännu en hanterad matris {4}.

Det är {4} allokeringar för att få en sträng ur den ursprungliga 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 cachelagra 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 returnerade strängen inte är terminerad eller om den är två gånger null-terminerad, är din P/Invoke i bästa fall felaktig.

Om du faktiskt använder är en sista komplikation att kapaciteten StringBuilder inkluderar ett dolt null, vilket alltid tas hänsyn till vid interoperabilitet. Det är vanligt att människor gö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ängkoppling finns i Standardkoppling för strängar och Anpassning av strängkoppling.

Windows-specifika För [Out] strängar använder CLR CoTaskMemFree som standard för att frigöra strängar, eller SysStringFree för strängar som har markerats som UnmanagedType.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 det totala antalet den storlek på bufferten 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 värden är lätta att göra fel på. En .NET bool överförs som standard till ett Windows BOOL, där det är ett 4-bytevä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 marshalling av .NET-värden bool till C- eller C++-typer bool finns i dokumentationen om anpassning av marshalling för booleska fält.

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 (globalt unikt identifierare) 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 hanteras till och från naturlig kod, och bör de föredras eftersom detta förbättrar prestandan. 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, , singledouble
  • 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

Typer med innehåll som kan överföras direkt:

  • 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

INTE blittable:

  • bool

IBLAND blittable:

  • char

Typer med innehåll som ibland är blittable:

  • string

När blittable-typer skickas med referens med in, refeller 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 och uttryckligen är markerad med [StructLayout] med 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 som värde (inte ref eller out) som ett argument och något av följande:

Du kan se om en typ är blittable eller innehåller blittable-data genom att försöka skapa en låst GCHandle. Om typen inte är en sträng eller betraktas som överförbar, GCHandle.Alloc resulterar i 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#-typer är inte blittbara. 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 blivitbara enligt den ovan nämnda regeln 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 anropas.

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

Att fastnåla är inte standard för GCHandle. Det andra huvudmönstret är att skicka en referens till ett hanterat objekt genom inbyggd kod och tillbaka till hanterad kod, vanligtvis med en callback. 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.

Bredd 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 man överföras som antingen IntPtr eller UIntPtr, men föredra void* när det är möjligt.

Windows-datatyper

Datatypsintervall

Tidigare stödda inbyggda typer

Det finns sällsynta tillfällen när standardstöd för en typ tas bort.

Det inbyggda marskalksstödet för UnmanagedType.HString 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 plattformsövergripande datatyper

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 interoperera med C/C++ long.

(Det här problemet med C/C++ long finns inte för C/C++ char, short, intoch 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 "fästa" (det flyttas inte av GC). Du kan också helt enkelt ta adressen i osäkra kodblock om den inhemska 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.

Om structen är blittable använder du sizeof() i stället för Marshal.SizeOf<MyStruct>() för bättre prestanda. Som nämnts ovan kan du verifiera att typen är blittable genom att försöka skapa en häftad 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 paseras via ref eller använda 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.

❌ Lita inte på den interna representationen av strukturer som tillhandahålls av .NET-körningsbibliotek om det inte uttryckligen dokumenteras.

❌ 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 marshal av en struct som innehåller en System.Delegate eller System.MulticastDelegate från dess infödda representation till ett hanterat objekt destabilisera körningsmiljön om värdet för fältet i den infödda representationen inte är en funktionspekare som omsluter en hanterad delegering. 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 array som INT_PTR Reserved1[2] måste delas upp i två IntPtr fält, Reserved1a och 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 fallgropar 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. Dessutom, i .NET Framework och .NET Core före 3.0, om en struct som innehåller ett fast buffertfält är inbäddad i en icke-blittable struct, kommer det fasta buffertfältet inte att marshallas korrekt till inhemsk kod.