Share via


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.
  • ✔️ 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 ä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ö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.

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:

❌ 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:

  1. Skapa en StringBuilder av den önskade kapaciteten (allokerar hanterad kapacitet) {1}.
  2. Åberopa:
    1. Allokerar en intern 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 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änder 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 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, , 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 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

INTE blittable:

  • bool

IBLAND blittable:

  • char

Typer med ibland blittable innehåll:

  • 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 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:

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.

Windows-datatyper

Datatypsintervall

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, 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 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 Reserved1aReserved1b. 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.