P/Invoke Grundlagen |
Geschrieben von: Kristian | ||||||||||||||||||||||||||
Freitag, den 05. Mai 2006 um 18:50 Uhr | ||||||||||||||||||||||||||
Seite 1 von 2
EinführungMicrosoft hat das .NET Framework mit einem reichhaltigen Satz an Werkzeugen und API's ausgerüstet um den Entwickler bei seiner produktiven Arbeit bestmöglich zu unterstützen. Doch das .NET Framework ist noch relativ jung und es gibt Funktionen die noch nicht in das Framework integriert wurden. Darüber hinaus existiert eine riesige Menge an alten Quellcodes die direkt in nativen Maschinencode übersetzt werden ohne jemals mit einer Laufzeitumgebung wie der CLR in Kontakt zu geraten. Aus diesen Gegebenheiten heraus resultiert ein scheinbares Problem. Firmen und Entwickler möchten einerseits die neuen Technologien des .NET Frameworks unmittelbar nutzen, andererseits können sie es sich nicht leisten ihre Produkte, Codes und Projekte von heute auf morgen in Richtung .NET zu portieren. Daneben gibt es aber auch viele Anwendungen die für bestimmte Prozessoren optimiert wurden und deren Portierung nach .NET mit einem erheblichen Aufwand verbunden wäre. Bevor wir uns ansehen wie das .NET Framework und C# mit diesem Problem umgehen, werfen wir einen Blick auf die CLR. Hintergründe zur CLRDie .NET Laufzeitumgebung, die Common Language Runtime, ist die virtuelle Maschine die für die Ausführung aller .NET Anwendungen verantwortlich ist. Bei der CLR handelt es sich um eine Microsoft spezifische Implementierung der CLI. Die CLR stellt das gesamte Programmiermodell bereit, das alle .NET Anwendungsarten nutzen. Sie beinhaltet an eigenen Komponenten Dateilader, Speichermanager (Garbage Collector), Sicherheitssystem, Threadpool und so weiter. Der integrierte JIT-Compiler kompiliert und verwaltet den managed Code - siehe auch Verwalteter Code hinter den Kulissen - die so genannte MSIL in den entsprechenden nativen Maschinencode, der vom Prozessor anschließend ausgeführt werden kann. Alle .NET Sprachen werden stets in diesen einheitlichen Zwischencode übersetzt. Auf diese Weise stellt die Laufzeitumgebung die semantische Interoperabilität zwischen den Sprachen sicher. Die automatische Speicherverwaltung, Verifizierung der Typsicherheit und Verwaltung des Thread-Pools garantieren das der Code in einer sicheren Ausführungs-Umgebung läuft. Im Gegensatz zu managed Code läuft unmanaged Code nicht unter dieser Laufzeitumgebung. Der Code wird unmittelbar in nativen Maschinencode übersetzt und ausgeführt. Komponenten wie COM Objekte und DLL's können auf diese Weise von den Vorteilen die eine Laufzeitumgebung, wie die CLR sie darstellt, nicht profitieren. Aus dem Prinzip des Zwischencodes der zunächst einmal von einer virtuellen Maschine ausgeführt werden muss ergeben sich aber auch Nachteile. Insbesondere die zwangsläufig schlechtere Ausführungsgeschwindigkeit beim Start der Anwendung verglichen mit nativen Anwendungen ist ein Handicap. Man sollte dies jedoch nicht pauschal verallgemeinern. Im Gegensatz zu nativ laufenden Code kann der JIT-Compiler den Zwischencode dynamisch anpassen. So kann er beispielsweise konkrete Aussagen zur Plattform treffen und den resultierenden CPU-Code explizit beeinflußen. Interoperabilität mit unmanaged CodeDieser Artikel zeigt die Details der Platform Invocation Services die vom .NET Framework zur Verfügung gestellt werden. Die Platform Invoke Facility agiert als eine Brücke zwischen managed und unmanaged Code. Ursprünglich sollte der Dienst nur den Zugriff auf die native Windows API ermöglichen, wurde später im Sinne der Entwickler insofern erweitert, als dass mit ihrer Hilfe nun auch Funktionen aus jeder beliebigen DLL aufgerufen werden können. Das .NET Framework stellt zwei Dienste für die Interoperabilität mit unmanaged Code zur Verfügung. Diese sind:
Das so genannte Marshaling (engl. ordnen, regeln) ist verantwortlich für den geregelten Austausch der Argumente (Integer, Strings, Arrays, Strukturen...) und Return Werte zwischen managed und unmanaged Code. Sowohl P/Invoke als auch COM Interop machen exzessiven Gebrauch vom Interoperabilität Marshaling um die Daten zwischen Aufrufer und Aufgerufenen auszutauschen. Der Interop Marshaler regelt dabei die Daten zwischen dem Common Language Runtime Heap und dem unverwalteten, dem unmanaged Heap. Interop Marshaling ist eine Laufzeit Aktivität die vom Marshaling Dienst (engl. Service) der Common Language Runtime durchgeführt wird. Die Methoden werden von der .NET Framework-Klasse Marshal zur Verfügung gestellt. P/InvokePlatform Invoke oder kurz P/Invoke ermöglicht es auf sehr einfache Art und Weise unverwaltete Funktionen die in nativen Dynamic Link Libraries implementiert sind aus der CLR heraus aufzurufen. P/Invoke erlaubt es Ihnen eine statische Methodendeklaration auf einen PE COFF Eintrittspunkt abzubilden der über LoadLibrary/GetProcAddress aufgelöst werden kann. P/Invoke verwendet eine verwaltete Methodendeklaration um den Stack Frame zu beschreiben, so wie beim Java Native Interface (JNI) und J/Direct, aber mit der Bedingung das der Funktionskörper von einer externen nativen DLL zur Verfügung gestellt wird. Wie auch immer, P/Invoke ist im Gegensatz zu JNI besonders nützlich um DLL's zu importieren die nicht mit der CLR geschrieben wurden. Sie markieren dazu einfach die statische Methodendeklaration mit dem Schlüsselwort static extern und verwenden die Attributklasse DllImport aus dem FCL Namensraum InteropServices um zu verdeutlichen das die Methode in einer externen nativen DLL definiert ist. Sobald es an der Zeit ist die Methode aufzurufen teilt das DllImport Attribut der CLR mit welche Argumente es an LoadLibrary und GetProcAddress übergeben muss. Das eingebaute C# Attribut DllImport ist einfach nur ein Alias für System.Runtime.InteropServices.DllImport. namespace System.Runtime.InteropServices { // Zeigt an ob die Attributmethode durch eine unmanaged // DLL als statischer Eintrittspunkt zur Verfügung gestellt wird [AttributeUsage(64, Inherited = false)] [ComVisible(true)] public sealed class DllImportAttribute : Attribute { public bool BestFitMapping; public CallingConvention CallingConvention; public CharSet CharSet; public string EntryPoint; public bool ExactSpelling; public bool PreserveSig; public bool SetLastError; public bool ThrowOnUnmappableChar; public DllImportAttribute(string dllName); public string Value { get; } } } Das DllImport Attribut nimmt unterschiedliche Parameter entgegen. Der Dateiname der DLL muss aber stets übergeben werden. Er wird von der Laufzeit benötigt um LoadLibrary aufzurufen noch bevor der eigentliche Methodenaufruf erfolgt. Bis der EntryPoint Parameter an DllImport übergeben wird ist der symbolische Name der Methode der String der für den Aufruf von GetProcAddress verwendet wird. In der kernel32.dll gibt es beispielsweise zwei Wege um die Funktion Sleep aufzurufen. Die erste Methode ist abhängig vom Namen der C# Funktion die mit dem Symbolnamen in der DLL übereinstimmt. Die zweite Methode ist hingegen abhängig vom EntryPoint Parameter. using System.Runtime.InteropServices; public class K32Wrapper { [DllImport("kernel32.dll")] public extern static void Sleep(uint msec); [DllImport("kernel32.dll", EntryPoint = "Sleep")] public extern static void Doze(uint msec); [DllImport("user32.dll")] public extern static uint MessageBox(int hwnd, String m, String c, uint flags); [DllImport("user32.dll", EntryPoint="MessageBoxW", ExactSpelling=true, CharSet=CharSet.Unicode)] public extern static uint UniBox(int hwnd, String m, String c, uint flags); } Ein weiterer Parameter der gesetzt werden muss ist CharSet sobald die Methode mit Strings arbeitet. Das bedeutet ob ANSI oder Unicode verwendet werden soll. Dies ist notwendig um zu kontrollieren wie der String Datentyp übersetzt wird damit der unmanaged Code anschließend mit diesem arbeiten kann. Der CharSet Parameter von DllImport erlaubt es entweder ANSI (CharSet.Ansi) oder Unicode (CharSet.Unicode) zu spezifizieren. Sie können dies auch über CharSet.Auto der Plattform überlassen, die je nachdem ob es sich um Windows NT oder um Windows 9x handelt automatisch den Zeichensatz festlegt. Diese Methode ähnelt dem TCHAR Datentyp, der in C/C++ Win32 verwendet wird um die eigene Anwendung Unicode verträglich zu gestalten. Jedoch mit dem Unterschied das der Zeichensatz und die verwendete API beim Laden bestimmt werden und nicht bei der Kompilierung. Dies hat den Vorteil das ein einmal kompiliertes .NET Programm theoretisch auf allen Windows Versionen ohne Probleme läuft. Um die Aufrufkonventionen und Zeichensätze anzuzeigen besitzt die Windows Plattform eine Reihe an so genannten Name Mangling Schemen. Das Name Mangling oder auch Name Decoration (Namens Dekoration) ist eine Technik um den Symbolnamen einer Funktion eindeutig im Maschinencode zu kennzeichnen. So ergibt sich in der Computertechnik teilweise ein Problem mit Namenskonflikten wie das folgende C++-Beispiel zeigt: int f (void) { return 1; } int f (int) { return 0; } void g (void) { int i = f(), j = f(0); } Bei der Übersetzung in eine C-Funktion die anschließend in einer DLL aufgerufen werden kann würde dies in einem Fehler resultieren, da in C Funktionen mit demselben Namen nicht gestattet sind. Hier kommt das Name Mangling ins Spiel. Der Compiler übersetzt den Code und generiert je nach Signatur einen individuellen Symbolnamen. Für das oben gezeigte Beispiel könnte dieser folgendermaßen aussehen. int __f_v (void) { return 1; } int __f_i (int) { return 0; } void __g_v (void) { int i = __f_v(), j = __f_i(0); } Tatsächlich implementiert der Compiler je nach Hersteller und Plattform seine eigenen Name Mangling Konventionen. In diesem Tutorial soll dies für uns aber nicht weiter relevant sein. Sobald der CharSet Parameter des DllImport Attributs auf Auto gesetzt wurde, besitzen die symbolischen Namen automatisch den Suffix W oder A, je nachdem ob der Unicode oder der ANSI Zeichensatz von der Laufzeit verwendet wird. Zusätzlich transformiert die Laufzeit das Symbol unter der Verwendung der stdcall Konvention (z.B. wird Sleep zu _Sleep@4) sofern der einfache Symbolname, also Sleep, nicht gefunden wurde. Mithilfe des Parameters ExactSpelling kann das Name Mangling unterdrückt werden. Schlußendlich, wenn Sie Win32-Funktionen aufrufen die COM ähnliche HRESULTs verwenden, haben Sie zwei Optionen. Standardmäßig behandelt P/Invoke das HRESULT als einen einfachen 32-Bit Integer der von der Funktion zurückgegeben wird und vom Programmierer selbst auf Fehler überprüft werden muss. Eine deutlich angenehmere Methode solch eine Funktion aufzurufen ist den Parameter PreserveSig=false an das DllImport Attribut zu übergeben. Dies verursacht das die P/Invoke Schicht den 32-Bit Integer als ein COM HRESULT behandelt und im Fehlerfall eine COMException auslöst. Da die meisten Methodenaufrufe mit P/Invoke jedoch keine HRESULTs zurückgeben ist PreserveSig standardmäßig auf true gesetzt und schützt die Signatur, so wie sie definiert wurde. In der nachfolgenden Übersicht sehen Sie alle Parameter der Attributklasse DllImport des .NET Frameworks 2.0 mitsamt kurzer Beschreibung:
Funktionen aus einer DLL nutzenManaged Code verwendet eine Code-Zugriffs-Sicherheit. Bevor auf eine Ressource zugegriffen wird oder anderweitige potentiell gefährliche Schritte durchgeführt werden überprüft die Laufzeit den Code. Mit der Einbeziehung von unmanaged Code verliert die CLR die Fähigkeit die Sicherheit der Umgebung zu gewährleisten. Konkret gesagt verlässt ihr Code bei Aufruf von unmanaged Code das Partial-trusted-Szenario und Sie geben die Typsicherheit im Programm auf. Die Laufzeit prüft ob bei allen Aufrufern im Aufrufstack die notwendige Sicherheitsstufe es erlaubt P/Invoke zu nutzen. Die entsprechenden Rechte müssen also auf der Plattform gegeben sein! Platform Invoke ist ein Dienst der es erlaubt beliebige unverwaltete Funktionen aus DLL's aufzurufen. Es lokalisiert und ruft eine exportierte Funktion auf und regelt (engl. marshals) die Argumente. Damit eine exportierte Funktion aufgerufen werden kann, müssen folgende Schritte abgearbeitet werden.
P/Invoke basiert auf Metadaten um die exportierte Funktion zu lokalisieren und ihre Argumente zur Laufzeit zu regeln. Die folgende Abbildung zeigt diesen Prozess. Sobald P/Invoke eine unmanaged Funktion aufruft, werden sequentiell folgende Aktionen durchgeführt:
Platform Invoke löst Exceptions (Ausnahmen) aus die von der unverwalteten Funktion an den verwalteten Aufrufer übergeben werden. |
||||||||||||||||||||||||||
Zuletzt aktualisiert am Freitag, den 04. Mai 2012 um 19:33 Uhr |
AUSWAHLMENÜ | ||||||||
|