Native InteroperabilitätNative Interoperability

In diesem Dokument werden alle drei Möglichkeiten für „native Interoperabilität“ im Detail untersucht, die in .NET verfügbar sind.In this document, we will dive a little bit deeper into all three ways of doing "native interoperability" that are available using .NET.

Es gibt einige Gründe für das Aufrufen von nativem Code:There are a few of reasons why you would want to call into native code:

  • Im Lieferumfang von Betriebssystemen ist eine große Anzahl von APIs enthalten, die in den verwalteten Klassenbibliotheken nicht vorhanden sind.Operating Systems come with a large volume of APIs that are not present in the managed class libraries. Ein gutes Beispiel hierfür ist der Zugriff auf Hardware- oder Betriebssystem-Verwaltungsfunktionen.A prime example for this would be access to hardware or operating system management functions.
  • Die Kommunikation mit anderen Komponenten, die über ABIs im C-Format (native ABIs) verfügen oder diese erstellen können.Communicating with other components that have or can produce C-style ABIs (native ABIs). Hierzu gehört beispielsweise Java-Code, der über die Java Native Interface (JNI) verfügbar gemacht wird, oder eine beliebige andere verwaltete Sprache, die eine native Komponente erstellen kann.This covers, for example, Java code that is exposed via Java Native Interface (JNI) or any other managed language that could produce a native component.
  • Unter Windows werden für den Großteil der installierten Software (z.B. Microsoft Office-Suite) COM-Komponenten registriert, die ihre Programme darstellen und den Entwicklern deren Automatisierung oder Verwendung ermöglichen.On Windows, most of the software that gets installed, such as Microsoft Office suite, registers COM components that represent their programs and allow developers to automate them or use them. Auch hierfür ist native Interoperabilität erforderlich.This also requires native interoperability.

Natürlich deckt die obige Liste nicht alle möglichen Situationen und Szenarios ab, in denen ein Entwickler eine Schnittstelle mit nativen Komponenten benötigt.Of course, the list above does not cover all of the potential situations and scenarios in which the developer would want/like/need to interface with native components. Die .NET-Klassenbibliothek verwendet zum Beispiel die Unterstützung für native Interoperabilität, um eine große Anzahl ihrer APIs zu implementieren, z.B. die Unterstützung und Bearbeitung der Konsole, Dateisystemzugriff und mehr..NET class library, for instance, uses the native interoperability support to implement a fair number of its APIs, like console support and manipulation, file system access and others. Es ist jedoch unbedingt zu beachten, dass es eine Option gibt, wenn man sie benötigen sollte.However, it is important to note that there is an option, should one need it.

Hinweis

Die meisten Beispiele in diesem Dokument werden für alle drei unterstützten Plattformen für .NET Core (Windows, Linux und macOS) aufgeführt.Most of the examples in this document will be presented for all three supported platforms for .NET Core (Windows, Linux and macOS). Bei einigen kurzen und anschaulichen Beispielen wird allerdings nur ein Beispiel gezeigt, das Windows-Dateinamen und -Erweiterungen (d.h. „DLL“ für Bibliotheken) verwendet.However, for some short and illustrative examples, just one sample is shown that uses Windows filenames and extensions (that is, "dll" for libraries). Dies wurde nur der Einfachheit halber so gehandhabt und bedeutet nicht, dass diese Funktionen unter Linux oder macOS nicht verfügbar sind.This does not mean that those features are not available on Linux or macOS, it was done merely for convenience sake.

Plattformaufruf (P/Invoke)Platform Invoke (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.P/Invoke is a technology that allows you to access structs, callbacks and functions in unmanaged libraries from your managed code. Der Großteil der P/Invoke-API ist in zwei Namespaces enthalten: System und System.Runtime.InteropServices.Most of the P/Invoke API is contained in two namespaces: System and System.Runtime.InteropServices. Über diese beiden Namespaces können Sie auf die Attribute zugreifen, die beschreiben, wie Sie mit der nativen Komponente kommunizieren möchten.Using these two namespaces will allow you access to the attributes that describe how you want to communicate with the native component.

Beginnen wir mit dem gängigsten Beispiel, dem Aufruf nicht verwalteter Funktionen im Ihrem verwalteten Code.Let’s start from the most common example, and that is calling unmanaged functions in your managed code. Wir zeigen ein Meldungsfeld aus einer Befehlszeilenanwendung:Let’s show a message box from a command-line application:

using System.Runtime.InteropServices;

public class Program {

    // Import user32.dll (containing the function we need) and define
    // the method corresponding to the native function.
    [DllImport("user32.dll")]
    public static extern int MessageBox(IntPtr hWnd, String text, String caption, int options);

    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.The example above is pretty simple, but it does show off what is needed to invoke unmanaged functions from managed code. Gehen wir das Beispiel schrittweise durch:Let’s step through the example:

  • Zeile 1 zeigt die using-Anweisung für System.Runtime.InteropServices. Dies ist der Namespace, der alle benötigten Elemente enthält.Line #1 shows the using statement for the System.Runtime.InteropServices which is the namespace that holds all of the items we need.
  • In Zeile 5 wird das DllImport-Attribut eingeführt.Line #5 introduces the DllImport attribute. Dieses Attribut ist äußerst wichtig, da es der Runtime mitteilt, dass die nicht verwaltete DLL geladen werden soll.This attribute is crucial, as it tells the runtime that it should load the unmanaged DLL. Dabei handelt es sich um die DLL, in der der Aufruf erfolgen soll.This is the DLL into which we wish to invoke.
  • Zeile 6 ist der Kern der P/Invoke-Funktion.Line #6 is the crux of the P/Invoke work. Sie definiert eine verwaltete Methode, die genau dieselbe Signatur aufweist wie die nicht verwaltete Methode.It defines a managed method that has the exact same signature as the unmanaged one. Wie Sie sehen, enthält die Deklaration ein neues Schlüsselwort, extern. Dieses teilt der Runtime mit, dass es sich um eine externe Methode handelt und dass die Runtime die Methode bei Aufruf in der im DllImport-Attribut angegebenen DLL findet.The declaration has a new keyword that you can notice, extern, which tells the runtime this is an external method, and that when you invoke it, the runtime should find it in the DLL specified in DllImport attribute.

Der Rest des Beispiels besteht nur aus dem Aufruf der Methode, der wie bei jeder anderen verwalteten Methode erfolgt.The rest of the example is just invoking the method as you would any other managed method.

Das Beispiel ist für macOS ähnlich.The sample is similar for macOS. Eine Sache, die geändert werden muss, ist natürlich der Name der Bibliothek im DllImport-Attribut, da macOS ein anderes Schema für die Benennung dynamischer Bibliotheken verwendet.One thing that needs to change is, of course, the name of the library in the DllImport attribute, as macOS has a different scheme of naming dynamic libraries. Das Beispiel unten verwendet die getpid(2)-Funktion, um die Prozess-ID der Anwendung abzurufen und in der Konsole auszugeben.The sample below uses the getpid(2) function to get the process ID of the application and print it out to the console.

using System;
using System.Runtime.InteropServices;

namespace PInvokeSamples {
    public static class Program {

        // Import the libc and define the method corresponding to the native function.
        [DllImport("libSystem.dylib")]
        private static extern int getpid();

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

Unter Linux verhält es sich natürlich ähnlich.It is similar on Linux, of course. Der Funktionsname ist identisch, da es sich bei getpid(2) um einen POSIX-Systemaufruf handelt.The function name is same, since getpid(2) is POSIX system call.

using System;
using System.Runtime.InteropServices;

namespace PInvokeSamples {
    public static class Program {

        // Import the libc and define the method corresponding to the native function.
        [DllImport("libc.so.6")]
        private static extern 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 ausInvoking managed code from unmanaged code

Selbstverständlich ermöglicht die Runtime die Kommunikation in beide Richtungen, sodass Sie verwaltete Artefakte über Funktionszeiger von nativen Funktionen aus aufrufen können.Of course, the runtime allows communication to flow both ways which enables you to call into managed artifacts from native functions, using function pointers. 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.The closest thing to a function pointer in managed code is a delegate, so this is what is used to allow callbacks from native code into managed code.

Dieses Feature wird ähnlich dem oben beschriebenen Verfahren von verwaltetem zu nativem Code verwendet.The way to use this feature is similar to managed to native process described above. Für einen bestimmten Rückruf definieren Sie einen Delegaten, der der Signatur entspricht, und übergeben Sie ihn an die externe Methode.For a given callback, you define a delegate that matches the signature, and pass that into the external method. Die Runtime übernimmt alles Weitere.The runtime will take care of everything else.

using System;
using System.Runtime.InteropServices;

namespace ConsoleApplication1 {

    class Program {

        // Define a delegate that corresponds to the unmanaged function.
        delegate bool EnumWC(IntPtr hwnd, IntPtr lParam);

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

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

        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.Before we walk through our example, it is good to go over the signatures of the unmanaged functions we need to work with. Die Funktion, die wir zum Auflisten aller Fenster aufrufen möchten, weist folgende Signatur auf: BOOL EnumWindows (WNDENUMPROC lpEnumFunc, LPARAM lParam);The function we want to call to enumerate all of the windows has the following signature: BOOL EnumWindows (WNDENUMPROC lpEnumFunc, LPARAM lParam);

Der erste Parameter ist ein Rückruf.The first parameter is a callback. Dieser Rückruf besitzt die folgende Signatur: BOOL CALLBACK EnumWindowsProc (HWND hwnd, LPARAM lParam);The said callback has the following signature: BOOL CALLBACK EnumWindowsProc (HWND hwnd, LPARAM lParam);

In diesem Sinn betrachten wir das Beispiel:With this in mind, let’s walk through the example:

  • Zeile 8 im Beispiel definiert einen Delegaten, der der Signatur des Rückrufs von nicht verwaltetem Code aus entspricht.Line #8 in the example defines a delegate that matches the signature of the callback from unmanaged code. Beachten Sie, wie die Typen LPARAM und HWND über IntPtr im verwalteten Code dargestellt werden.Notice how the LPARAM and HWND types are represented using IntPtr in the managed code.
  • In den Zeilen 10 und 11 wird die EnumWindows-Funktion über die user32.dll-Bibliothek eingeführt.Lines #10 and #11 introduce the EnumWindows function from the user32.dll library.
  • In den Zeilen 13 bis 16 wird der Delegat implementiert.Lines #13 - 16 implement the delegate. In diesem einfachen Beispiel möchten wir nur das Handle an die Konsole ausgeben.For this simple example, we just want to output the handle to the console.
  • Abschließend wird in Zeile 19 die externe Methode aufgerufen und an den Delegaten übergeben.Finally, in line #19 we invoke the external method and pass in the delegate.

Die Beispiele für Linux und macOS werden im Folgenden angezeigt.The Linux and macOS examples are shown below. Hierfür verwenden wir die ftw-Funktion, die in libc zu finden ist, der C-Bibliothek.For them, we use the ftw function that can be found in libc, the C library. Anhand dieser Funktion werden die Verzeichnishierarchien durchlaufen. Sie verwendet einen Zeiger auf eine Funktion als einen ihrer Parameter.This function is used to traverse directory hierarchies and it takes a pointer to a function as one of its parameters. Diese Funktion besitzt die folgende Signatur: int (*fn) (const char *fpath, const struct stat *sb, int typeflag).The said function has the following signature: int (*fn) (const char *fpath, const struct stat *sb, int typeflag).

using System;
using System.Runtime.InteropServices;

namespace PInvokeSamples {
    public static class Program {

            // Define a delegate that has the same signature as the native function.
            delegate int DirClbk(string fName, StatClass stat, int typeFlag);

            // Import the libc and define the method to represent the native function.
            [DllImport("libc.so.6")]
            static extern 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.
            static int DisplayEntry(string fName, StatClass 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. The below class
    // represents that struct in managed code. You can find more information
    // about this in the section on marshalling below.
    [StructLayout(LayoutKind.Sequential)]
    public class StatClass {
            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 DllImport-Attribut, da macOS libc an einer anderen Stelle speichert.macOS example uses the same function, and the only difference is the argument to the DllImport attribute, as macOS keeps libc in a different place.

using System;
using System.Runtime.InteropServices;

namespace PInvokeSamples {
        public static class Program {

                // Define a delegate that has the same signature as the native function.
                delegate int DirClbk(string fName, StatClass stat, int typeFlag);

                // Import the libc and define the method to represent the native function.
                [DllImport("libSystem.dylib")]
                static extern 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.
                static int DisplayEntry(string fName, StatClass 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. The below class
        // represents that struct in managed code. You can find more information
        // about this in the section on marshalling below.
        [StructLayout(LayoutKind.Sequential)]
        public class StatClass {
                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.Both of the above examples depend on parameters, and in both cases, the parameters are given as managed types. Die Runtime reagiert demgemäß und verarbeitet diese in ihren Entsprechungen auf der anderen Seite.Runtime does the "right thing" and processes these into its equivalents on the other side. Da dieser Vorgang für das Schreiben von hochwertigem nativem Interopcode entscheidend ist, sehen wir uns an, was beim Marshallen der Typen durch die Runtime geschieht.Since this process is really important to writing quality native interop code, let’s take a look at what happens when the runtime marshals the types.

Marshallen von TypenType marshalling

Das Marshallen bezeichnet den Vorgang zum Umwandeln von Typen, wenn diese die verwaltete Grenze zu nativem Code und umgekehrt überschreiten.Marshalling is the process of transforming types when they need to cross the managed boundary into native and vice versa.

Das Marshallen ist erforderlich, weil sich die Typen in verwaltetem und nicht verwaltetem Code unterscheiden.The reason marshalling is needed is because the types in the managed and unmanaged code are different. In verwaltetem Code verwenden Sie z.B. einen String-Typ, während Zeichenfolgen im nicht verwalteten Bereich Unicode (Breitzeichen), Nicht-Unicode, mit NULL endend, ASCII usw. sein können. Standardmäßig versucht das P/Invoke-Subsystem, die richtige Aktion basierend auf dem Standardverhalten durchzuführen, das in MSDN beschrieben wird.In managed code, for instance, you have a String, while in the unmanaged world strings can be Unicode ("wide"), non-Unicode, null-terminated, ASCII, etc. By default, the P/Invoke subsystem will try to do the Right Thing based on the default behavior which you can see on MSDN. In Situationen, in denen Sie eine zusätzliche Kontrolle benötigen, können Sie jedoch das MarshalAs-Attribut verwenden, um anzugeben, welcher Typ auf der nicht verwalteten Seite erwartet wird.However, for those situations where you need extra control, you can employ the MarshalAs attribute to specify what is the expected type on the unmanaged side. Wenn die Zeichenfolge beispielsweise als nicht mit Null endende ANSI-Zeichenfolge gesendet werden soll, können wir dies folgendermaßen erreichen:For instance, if we want the string to be sent as a null-terminated ANSI string, we could do it like this:

[DllImport("somenativelibrary.dll")]
static extern int MethodA([MarshalAs(UnmanagedType.LPStr)] string parameter);

Marshallen von Klassen und StrukturenMarshalling classes and structs

Ein weiterer Aspekt des Marshallens von Typen ist die Übergabe einer Struktur an eine nicht verwaltete Methode.Another aspect of type marshalling is how to pass in a struct to an unmanaged method. Einige der nicht verwalteten Methoden erfordern beispielsweise eine Struktur als Parameter.For instance, some of the unmanaged methods require a struct as a parameter. In diesen Fällen müssen wir eine entsprechende Struktur oder eine Klasse im verwalteten Bereich erstellen, um sie als Parameter zu verwenden.In these cases, we need to create a corresponding struct or a class in managed part of the world to use it as a parameter. Allerdings reicht das Definieren der Klasse nicht aus, wir müssen dem Marshaller außerdem mitteilen, wie Felder in der Klasse der nicht verwalteten Struktur zuzuordnen sind.However, just defining the class is not enough, we also need to instruct the marshaler how to map fields in the class to the unmanaged struct. An dieser Stelle komm das StructLayout-Attribut ins Spiel.This is where the StructLayout attribute comes into play.

[DllImport("kernel32.dll")]
static extern void GetSystemTime(SystemTime systemTime);

[StructLayout(LayoutKind.Sequential)]
class SystemTime {
    public ushort Year;
    public ushort Month;
    public ushort DayOfWeek;
    public ushort Day;
    public ushort Hour;
    public ushort Minute;
    public ushort Second;
    public ushort Milsecond;
}

public static void Main(string[] args) {
    SystemTime st = new SystemTime();
    GetSystemTime(st);
    Console.WriteLine(st.Year);
}

Das obige Beispiel zeigt ein einfaches Beispiel für einen Aufruf in der GetSystemTime()-Funktion.The example above shows off a simple example of calling into GetSystemTime() function. Der interessante Teil befindet sich in Zeile 4.The interesting bit is on line 4. Das Attribut gibt an, dass die Felder der Klasse sequenziell der Struktur auf der anderen (nicht verwalteten) Seite zugeordnet werden sollen.The attribute specifies that the fields of the class should be mapped sequentially to the struct on the other (unmanaged) side. Dies bedeutet, dass die Benennung der Felder nicht wichtig ist, sondern nur deren Reihenfolge, da diese der nicht verwalteten Struktur entsprechen muss, wie unten gezeigt:This means that the naming of the fields is not important, only their order is important, as it needs to correspond to the unmanaged struct, shown below:

typedef struct _SYSTEMTIME {
  WORD wYear;
  WORD wMonth;
  WORD wDayOfWeek;
  WORD wDay;
  WORD wHour;
  WORD wMinute;
  WORD wSecond;
  WORD wMilliseconds;
} SYSTEMTIME, *PSYSTEMTIME*;

Das Linux- und macOS-Beispiel hierfür haben wir bereits im vorherigen Beispiel gesehen.We already saw the Linux and macOS example for this in the previous example. Es wird unten noch einmal aufgeführt.It is shown again below.

[StructLayout(LayoutKind.Sequential)]
public class StatClass {
        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;
}

Die StatClass-Klasse stellt eine Struktur dar, die vom stat-Systemaufruf auf UNIX-Systemen zurückgegeben wird.The StatClass class represents a structure that is returned by the stat system call on UNIX systems. Sie stellt Informationen über eine bestimmte Datei dar.It represents information about a given file. Die oben gezeigte Klasse ist die stat-Strukturdarstellung in verwaltetem Code.The class above is the stat struct representation in managed code. Wiederum müssen die Felder in der Klasse derselben Reihenfolge entsprechen wie die native Struktur. (Sie finden diese über Manpages in Ihrer bevorzugten UNIX-Implementierung.) Zudem müssen Sie denselben zugrunde liegenden Typ aufweisen.Again, the fields in the class have to be in the same order as the native struct (you can find these by perusing man pages on your favorite UNIX implementation) and they have to be of the same underlying type.

Weitere RessourcenMore resources