Plattformsanrop (P/Invoke)

P/Invoke är en teknik som gör att du kan komma åt structs, återanrop och funktioner i ohanterade bibliotek från din hanterade kod. Merparten av P/Invoke-API:et finns i två namnområden: System och System.Runtime.InteropServices. Med de här två namnrymderna får du verktygen för att beskriva hur du vill kommunicera med den inbyggda komponenten.

Låt oss börja från det vanligaste exemplet och det är att anropa ohanterade funktioner i din hanterade kod. Nu ska vi visa en meddelanderuta från ett kommandoradsprogram:

using System;
using System.Runtime.InteropServices;

public partial class Program
{
    // Import user32.dll (containing the function we need) and define
    // the method corresponding to the native function.
    [LibraryImport("user32.dll", StringMarshalling = StringMarshalling.Utf16, SetLastError = true)]
    private static partial int MessageBox(IntPtr hWnd, string lpText, string lpCaption, uint uType);

    public static void Main(string[] args)
    {
        // Invoke the function as a regular managed method.
        MessageBox(IntPtr.Zero, "Command-line message box", "Attention!", 0);
    }
}

Föregående exempel är enkelt, men det visar vad som behövs för att anropa ohanterade funktioner från hanterad kod. Nu ska vi gå igenom exemplet:

  • Rad 2 visar using direktivet för namnområdet System.Runtime.InteropServices som innehåller alla objekt som behövs.
  • Rad 8 introducerar attributet LibraryImportAttribute . Det här attributet talar om för körningen att den ska läsa in den ohanterade binärfilen. Strängen som skickas är den ohanterade binärfilen som innehåller målfunktionen. Dessutom anger den kodning som ska användas för att ordna strängarna. Slutligen anger den att den här funktionen anropar SetLastError och att körningen ska samla in felkoden så att användaren kan hämta den via Marshal.GetLastPInvokeError().
  • Rad 9 är kruxet för P/Invoke-arbetet. Den definierar en hanterad metod som har exakt samma signatur som den ohanterade. Deklarationen LibraryImport använder attributet och nyckelordet partial för att instruera ett kompilatortillägg att generera kod som ska anropas till det ohanterade biblioteket.
    • I den genererade koden och före .NET 7 används den DllImport . Den här deklarationen använder nyckelordet extern för att indikera att det här är en extern metod, och när du anropar den bör körningen hitta den i den ohanterade binärfilen som anges i DllImport attributet.

Resten av exemplet anropar metoden på samma sätt som med andra hanterade metoder.

Exemplet är liknande för macOS. Namnet på biblioteket i LibraryImport attributet måste ändras eftersom macOS har ett annat schema för namngivning av dynamiska bibliotek. I följande exempel används getpid(2) funktionen för att hämta programmets process-ID och skriva ut det till konsolen:

using System;
using System.Runtime.InteropServices;

namespace PInvokeSamples
{
    public static partial class Program
    {
        // Import the libSystem shared library and define the method
        // corresponding to the native function.
        [LibraryImport("libSystem.dylib")]
        private static partial int getpid();

        public static void Main(string[] args)
        {
            // Invoke the function and get the process ID.
            int pid = getpid();
            Console.WriteLine(pid);
        }
    }
}

Det är också liknande på Linux. Funktionsnamnet är detsamma, eftersom getpid(2) är ett standard-POSIX-systemanrop.

using System;
using System.Runtime.InteropServices;

namespace PInvokeSamples
{
    public static partial class Program
    {
        // Import the libc shared library and define the method
        // corresponding to the native function.
        [LibraryImport("libc.so.6")]
        private static partial int getpid();

        public static void Main(string[] args)
        {
            // Invoke the function and get the process ID.
            int pid = getpid();
            Console.WriteLine(pid);
        }
    }
}

Anropa hanterad kod från ohanterad kod

Körningen gör att kommunikationen kan flöda i båda riktningarna, så att du kan anropa tillbaka till hanterad kod från inbyggda funktioner med hjälp av funktionspekare. Det närmaste en funktionspekare i hanterad kod är ett ombud, så det här är vad som används för att tillåta återanrop från inbyggd kod till hanterad kod.

Sättet att använda den här funktionen liknar den hanterade inbyggda processen som beskrevs tidigare. För ett givet återanrop definierar du ett ombud som matchar signaturen och skickar det till den externa metoden. Körningen tar hand om allt annat.

using System;
using System.Runtime.InteropServices;

namespace ConsoleApplication1
{
    public static partial class Program
    {
        // Define a delegate that corresponds to the unmanaged function.
        private delegate bool EnumWC(IntPtr hwnd, IntPtr lParam);

        // Import user32.dll (containing the function we need) and define
        // the method corresponding to the native function.
        [LibraryImport("user32.dll")]
        private static partial int EnumWindows(EnumWC lpEnumFunc, IntPtr lParam);

        // Define the implementation of the delegate; here, we simply output the window handle.
        private static bool OutputWindow(IntPtr hwnd, IntPtr lParam)
        {
            Console.WriteLine(hwnd.ToInt64());
            return true;
        }

        public static void Main(string[] args)
        {
            // Invoke the method; note the delegate as a first parameter.
            EnumWindows(OutputWindow, IntPtr.Zero);
        }
    }
}

Innan du går igenom exemplet är det bra att granska signaturerna för de ohanterade funktioner som du behöver arbeta med. Funktionen som ska anropas för att räkna upp alla fönster har följande signatur: BOOL EnumWindows (WNDENUMPROC lpEnumFunc, LPARAM lParam);

Den första parametern är ett återanrop. Återanropet har följande signatur: BOOL CALLBACK EnumWindowsProc (HWND hwnd, LPARAM lParam);

Nu ska vi gå igenom exemplet:

  • Rad 9 i exemplet definierar ett ombud som matchar motringningens signatur från ohanterad kod. Observera hur typerna LPARAM och HWND representeras med hjälp av IntPtr den hanterade koden.
  • Raderna #13 och #14 introducerar EnumWindows funktionen från user32.dll-biblioteket.
  • Raderna #17–20 implementerar ombudet. I det här enkla exemplet vill vi bara mata ut handtaget till konsolen.
  • Slutligen anropas och skickas den externa metoden i rad 24 i ombudet.

Linux- och macOS-exemplen visas nedan. För dem använder vi funktionen ftw som finns i libcC-biblioteket. Den här funktionen används för att bläddra i kataloghierarkier och den tar en pekare till en funktion som en av dess parametrar. Den nämnda funktionen har följande signatur: int (*fn) (const char *fpath, const struct stat *sb, int typeflag).

using System;
using System.Runtime.InteropServices;

namespace PInvokeSamples
{
    public static partial class Program
    {
        // Define a delegate that has the same signature as the native function.
        private delegate int DirClbk(string fName, ref Stat stat, int typeFlag);

        // Import the libc and define the method to represent the native function.
        [LibraryImport("libc.so.6", StringMarshalling = StringMarshalling.Utf16)]
        private static partial int ftw(string dirpath, DirClbk cl, int descriptors);

        // Implement the above DirClbk delegate;
        // this one just prints out the filename that is passed to it.
        private static int DisplayEntry(string fName, ref Stat stat, int typeFlag)
        {
            Console.WriteLine(fName);
            return 0;
        }

        public static void Main(string[] args)
        {
            // Call the native function.
            // Note the second parameter which represents the delegate (callback).
            ftw(".", DisplayEntry, 10);
        }
    }

    // The native callback takes a pointer to a struct. This type
    // represents that struct in managed code.
    [StructLayout(LayoutKind.Sequential)]
    public struct Stat
    {
        public uint DeviceID;
        public uint InodeNumber;
        public uint Mode;
        public uint HardLinks;
        public uint UserID;
        public uint GroupID;
        public uint SpecialDeviceID;
        public ulong Size;
        public ulong BlockSize;
        public uint Blocks;
        public long TimeLastAccess;
        public long TimeLastModification;
        public long TimeLastStatusChange;
    }
}

macOS-exemplet använder samma funktion, och den enda skillnaden är argumentet till LibraryImport attributet, eftersom macOS behåller libc på en annan plats.

using System;
using System.Runtime.InteropServices;

namespace PInvokeSamples
{
    public static partial class Program
    {
        // Define a delegate that has the same signature as the native function.
        private delegate int DirClbk(string fName, ref Stat stat, int typeFlag);

        // Import the libc and define the method to represent the native function.
        [LibraryImport("libSystem.dylib", StringMarshalling = StringMarshalling.Utf16)]
        private static partial int ftw(string dirpath, DirClbk cl, int descriptors);

        // Implement the above DirClbk delegate;
        // this one just prints out the filename that is passed to it.
        private static int DisplayEntry(string fName, ref Stat stat, int typeFlag)
        {
            Console.WriteLine(fName);
            return 0;
        }

        public static void Main(string[] args)
        {
            // Call the native function.
            // Note the second parameter which represents the delegate (callback).
            ftw(".", DisplayEntry, 10);
        }
    }

    // The native callback takes a pointer to a struct. This type
    // represents that struct in managed code.
    [StructLayout(LayoutKind.Sequential)]
    public struct Stat
    {
        public uint DeviceID;
        public uint InodeNumber;
        public uint Mode;
        public uint HardLinks;
        public uint UserID;
        public uint GroupID;
        public uint SpecialDeviceID;
        public ulong Size;
        public ulong BlockSize;
        public uint Blocks;
        public long TimeLastAccess;
        public long TimeLastModification;
        public long TimeLastStatusChange;
    }
}

Båda de föregående exemplen beror på parametrar, och i båda fallen anges parametrarna som hanterade typer. Runtime gör det "rätta" och bearbetar dessa till dess motsvarigheter på andra sidan. Lär dig mer om hur typer är ordnade till intern kod på vår sida om typ marshalling.

Fler resurser