Freigeben über


Plattformaufruf (P/Invoke)

P/Invoke ist eine Technologie, die Ihnen den Zugriff auf Strukturen, Rückrufe und Funktionen in nicht verwalteten Bibliotheken von verwaltetem Code aus ermöglicht. Der Großteil der P/Invoke-API ist in zwei Namespaces enthalten: System und System.Runtime.InteropServices. Über diese beiden Namespaces können Sie auf die Tools zugreifen, die beschreiben, wie Sie mit der nativen Komponente kommunizieren möchten.

Beginnen wir mit dem gängigsten Beispiel, dem Aufruf nicht verwalteter Funktionen in Ihrem verwalteten Code. Wir zeigen ein Meldungsfeld aus einer Befehlszeilenanwendung:

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

Das obige Beispiel ist recht einfach, zeigt jedoch, was zum Aufrufen nicht verwalteter Funktionen von verwaltetem Code aus erforderlich ist. Gehen wir das Beispiel schrittweise durch:

  • Zeile 2 zeigt die using-Direktive für den System.Runtime.InteropServices-Namespace, der alle benötigten Elemente enthält.
  • In Zeile 8 wird das LibraryImportAttribute-Attribut eingeführt. Dieses Attribut teilt der Laufzeit mit, dass die nicht verwaltete Binärdatei geladen werden soll. Die übergebene Zeichenfolge ist die nicht verwaltete Binärdatei, die die Zielfunktion enthält. Darüber hinaus gibt sie die Codierung an, die zum Marshallen der Zeichenfolgen verwendet werden soll. Schließlich gibt sie an, dass diese Funktion SetLastError aufruft und dass die Laufzeit diesen Fehlercode abfangen muss, damit der Benutzer ihn über Marshal.GetLastPInvokeError() abrufen kann.
  • Zeile 9 ist der Kern der P/Invoke-Funktion. Sie definiert eine verwaltete Methode, die genau dieselbe Signatur aufweist wie die nicht verwaltete Methode. Die Deklaration verwendet das Attribut LibraryImport und das Schlüsselwort partial , um eine Compilererweiterung anzuweisen, code zum Aufrufen in die nicht verwaltete Bibliothek zu generieren.
    • Innerhalb des generierten Codes und vor .NET 7 wird die DllImport verwendet. Diese Deklaration verwendet das Schlüsselwort extern, um anzugeben, dass es sich bei der Laufzeit um eine externe Methode handelt. Wenn Sie sie aufrufen, sollte die Laufzeit sie in der im Attribut DllImport angegebenen nicht verwalteten Binärdatei finden.

Der Rest des Beispiels besteht aus dem Aufruf der Methode, der wie bei jeder anderen verwalteten Methode erfolgt.

Das Beispiel ist für macOS ähnlich. Der Name der Bibliothek im LibraryImport-Attribut muss geändert werden, da macOS ein anderes Schema für die Benennung dynamischer Bibliotheken verwendet. Das folgende Beispiel verwendet die getpid(2)-Funktion, um die Prozess-ID der Anwendung abzurufen und in der Konsole auszugeben.

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

Bei Linux ist es ähnlich. Der Funktionsname ist identisch, da es sich bei getpid(2) um einen standardmäßigen POSIX-Systemaufruf handelt.

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

Aufrufen von verwaltetem Code von nicht verwaltetem Code aus

Die Runtime ermöglicht den Kommunikationsfluss in beide Richtungen, sodass Sie verwalteten Code mithilfe von Funktionszeigern aus nativen Funktionen zurückrufen können. Einem Funktionszeiger in verwaltetem Code kommt ein Delegat am nächsten. Dieser wird verwendet, um Rückrufe von nativem Code an verwalteten Code zu ermöglichen.

Dieses Feature wird ähnlich dem oben beschriebenen Verfahren von verwaltetem zu nativem Code verwendet. Für einen bestimmten Rückruf definieren Sie einen Delegaten, der der Signatur entspricht, und übergeben Sie ihn an die externe Methode. Die Runtime übernimmt alles Weitere.

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

Bevor wir unser Beispiel durchgehen, sollten wir die Signaturen der nicht verwalteten Funktionen betrachten, mit denen wir arbeiten müssen. Die Funktion, die wir zum Auflisten aller Fenster aufrufen möchten, weist folgende Signatur auf: BOOL EnumWindows (WNDENUMPROC lpEnumFunc, LPARAM lParam);

Der erste Parameter ist ein Rückruf. Dieser Rückruf besitzt die folgende Signatur: BOOL CALLBACK EnumWindowsProc (HWND hwnd, LPARAM lParam);

Schauen wir uns jetzt das Beispiel an:

  • Zeile 9 im Beispiel definiert einen Delegaten, der der Signatur des Rückrufs von nicht verwaltetem Code aus entspricht. Beachten Sie, wie die Typen LPARAM und HWND über IntPtr im verwalteten Code dargestellt werden.
  • In den Zeilen 13 und 14 wird die EnumWindows-Funktion über die user32.dll-Bibliothek eingeführt.
  • In den Zeilen 17 bis 20 wird der Delegat implementiert. In diesem einfachen Beispiel möchten wir nur das Handle an die Konsole ausgeben.
  • Abschließend wird in Zeile 24 die externe Methode aufgerufen und an den Delegaten übergeben.

Die Beispiele für Linux und macOS werden im Folgenden angezeigt. Hierfür verwenden wir die ftw-Funktion, die in libc zu finden ist, der C-Bibliothek. Anhand dieser Funktion werden die Verzeichnishierarchien durchlaufen. Sie verwendet einen Zeiger auf eine Funktion als einen ihrer Parameter. Diese Funktion besitzt die folgende 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;
    }
}

Im macOS-Beispiel wird die gleiche Funktion verwendet. Der einzige Unterschied ist das Argument für das LibraryImport-Attribut, da macOS libc an einer anderen Stelle speichert.

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;
    }
}

Beide oben aufgeführten Beispiele hängen von Parametern ab, und in beiden Fällen werden die Parameter als verwaltete Typen angegeben. Die Runtime reagiert demgemäß und verarbeitet diese in ihren Entsprechungen auf der anderen Seite. Weitere Informationen dazu, wie Typen in nativen Code auf unserer Seite gemarshallt werden, finden Sie unter Marshalling von Typen.

Weitere Ressourcen