18.2 API-Aufrufe mit PInvoke
Mit seinen vielen Klassen und Methoden bietet uns das .NET Framework eine fast unbegrenzte Funktionalität. Dennoch lassen sich nicht alle Probleme damit lösen, insbesondere wenn es um spezielle Aufgaben des Betriebssystems geht. Daher steht Ihnen ein Mechanismus zur Verfügung, um externe Technologien einzusetzen. Diese Technik wird als PInvoke bezeichnet, die Abkürzung für Platform Invocation Services.
PInvoke ermöglicht den Aufruf von Funktionen unverwalteter DLL-Dateien, zu denen auch die der Win32-API gehören. Dabei steht ein Attribut im Blickpunkt, das im Namensraum System.Runtime.InteropServices definiert ist: das Attribut DllImportAttribute. Die Syntax wurde bereits in Abschnitt 3.4.3, »Externe Funktionen«, beschrieben. Hier geht es um dessen Einsatz.
Bevor wir uns damit etwas näher beschäftigen, hier ein kleines, einfaches Beispiel. In der Systemdatei user32.dll ist eine Funktion MessageBox zu finden, die genauso wie die gleichnamige Klasse im .NET Framework ein Meldungsfenster anzeigt. Wollen wir das Meldungsfenster aus der Win32-API nutzen, können wir den folgenden Code in einer Form schreiben:
Imports System.Runtime.InteropServices Public Class Form1 : Inherits Form <DllImport("user32.dll")> _ Public Shared Function MessageBox(HWnd As Integer, _ text As String, caption As String, type As Integer) As Integer End Function ' alternativ: ' Declare Auto Function MessageBox Lib "user32.dll" (hWnd As Integer, _ ' text As String, caption As String, type As Integer ) As Integer Private Sub button1_Click(sender As Object, e As EventArgs) MessageBox(0, "Win32-API-Meldungsfenster", "Hallo...", 0) End Sub End Class
Das Attribut DllImport kann nur mit einer Methode verknüpft werden, wobei dem Konstruktor der Name der DLL-Datei übergeben wird, in dem sich die Funktion befindet. Befindet sich die Datei in einem Verzeichnis, das in der Systemvariablen Path eingetragen ist, brauchen Sie den Pfad zu der Datei nicht anzugeben, wie in unserem Beispiel.
Hinter dem Attribut erfolgt die Deklaration der externen Methode, die immer mit Shared an die Klasse gebunden ist. Der Grund ist recht einfach: Eine Funktion aus einer externen DLL-Datei ist niemals eine Instanzmethode, sondern steht immer global zur Verfügung.
18.2.1 Das Attribut DllImport
Nicht alle Methoden einer externen Bibliothek sind so einfach aufzurufen wie die der MessageBox. Bei komplexeren Funktionen kommen unter Umständen auch benannte Parameter des Attributs ins Spiel, die der Tabelle 18.5 zu entnehmen sind.
Eigenschaft | Beschreibung | |
BestFitMapping |
Gibt an, ob die Konvertierung von Unicode nach ANSI (notwendig für Windows 98 und ME) optimiert ist (Standard True). |
|
CallingConvention |
Art der Stack-Entleerung nach Aufrufen. Beim Standardwert CallingConvention.Winapi macht das das Betriebssystem. |
E |
CharSet |
Art der verwendeten Zeichenketten (1 oder 2 Byte je Buchstabe). Standard ist Charset.Ansi. |
E |
EntryPoint |
Name oder Nummer der externen Funktion. Muss nicht mit dem Methodennamen in Visual Basic übereinstimmen. |
|
ExactSpelling |
API-Funktionen mit angehängtem »A« unterstützen kein Unicode (z. B. Windows 98), ein »W« signalisiert Unicodeunterstützung. Beim Standardwert False wird automatisch ein Buchstabe in Abhängigkeit von CharSet angehängt. |
|
PreserveSig |
Der Standardwert True reicht einen ganzzahliger Rückgabewert HRESULT unverändert durch. Bei False löst ein HRESULT-Wert ungleich 0 in eine Ausnahme aus. Auf alle anderen Rückgabetypen hat die Eigenschaft keinen Einfluss. |
|
SetLastError |
Mit dem Nichtstandardwert True wird ein Fehler in der API-Funktion gespeichert und kann mit GetLastWin32Error der Klasse Marshal ausgelesen werden. Eine API-Funktion signalisiert einen Fehler durch die Rückgabe von False. |
|
ThrowOnUnmappableChar |
Ist BestFittingMapping=True und misslingt eine Umwandlung, wird beim Nichtstandardwert True eine Ausnahme ausgelöst. |
Exemplarisch zeigt das folgende Codefragment den Import der API-Funktion MessageBox unter dem Alias Meldungsfenster. Der Wert des Attributs EntryPoint spezifiziert den Originalnamen der Funktion innerhalb der DLL.
<DllImport("user32.dll", EntryPoint="MessageBox")> _ Public Shared Function Meldungsfenster(HWnd As Integer, _ text As String, caption As String, type As Integer) As Integer End Function Private Sub button1_Click(sender As Object, e As EventArgs) Meldungsfenster(0, "Win32-API-Meldungsfenster", "Hallo...", 0) End Sub
18.2.2 Datentypen von API-Funktionen
Bei nahezu jedem Aufruf einer API-Funktion müssen Sie Datentypen angeben. So einfach sich das im ersten Moment anhört, so schwierig ist dabei oft die Umsetzung. Da die meisten Funktionen in den APIs in C/C++ geschrieben sind, finden viele Datentypen keine direkte Entsprechung in Visual Basic (oder, um es allgemeiner auszudrücken, im .NET Framework). Geben Sie aber einen falschen Typ an, kommt es zu einem Laufzeitfehler.
Das Umsetzen eines VB-Datentyps in einen API-Datentyp wird als Marshalling bezeichnet. Im .NET Framework kann mit dem Attribut MarshalAs das Umsetzen gesteuert werden. Das Attribut wird bei dem Parameter angegeben, der einer API-Funktion übergeben wird. Wahrscheinlich am häufigsten kommt ein positionaler Parameter zum Einsatz, der in der Enumeration UnmanagedType definiert ist. Funktionen, die beispielsweise einen der C++-Datentypen LPStr (nullterminierter ASCII-String) oder LPWStr (nullterminierter Unicode-String) erwarten, kann mit MarshalAs ein VB-String übergeben werden.
Im folgenden Beispiel wird mit der Funktion GetDiskFreeSpaceEx aus der Bibliothek kernel32.dll das frei verfügbare und gesamte Speichervolumen eines Laufwerks ermittelt. Die Originaldefinition lautet:
BOOL GetDiskFreeSpaceEx { LPCTSTR lpDirectoryName, PULARGE_INTEGER lpFreeBytesAvailable, PULARGE_INTEGER lpTotalNumberOfBytes, PULARGE_INTEGER lpTotalNumberOfFreeBytes };
Dem ersten Parameter wird eine Zeichenfolge übergeben, die das zu untersuchende Laufwerk angibt. In allen anderen Parametern werden von der Funktion die Ergebnisse an den Aufrufer übermittelt. Im zweiten Parameter handelt es sich um die nutzbaren Bytes, die vom aufrufenden Thread genutzt werden können, im dritten die Größe des angegebenen Laufwerks und im vierten um das Gesamtvolumen der unbelegten Bytes.
Damit die Übergabe des Laufwerks als String korrekt gemarshallt werden kann, wird dem ersten Argument das MarshalAs-Attribut vorangestellt; alle anderen Argumente sind vom ganzzahligen Typ Long.
Der Rückgabewert der API-Funktion ist vom Typ BOOL, der 4 Byte groß ist und eine von 0 abweichende Zahl liefert, wenn der Aufruf erfolgreich war. Entgegengenommen wird der Rückgabewert in einem Integer, er lässt sich aber auch direkt in einer Bedingung auswerten.
'...\Programmiertechniken\PInvoke\Plattenplatz.vb |
Imports System.Runtime.InteropServices Namespace PInvoke Module Plattenplatz <DllImport("Kernel32.dll")> _ Private Function GetDiskFreeSpaceEx( _ <MarshalAs(UnmanagedType.LPStr)> ByVal lpDirectoryName As String, _ ByRef lpFreeBytesAvailable As Long, _ ByRef lpTotalNumberOfBytes As Long, _ ByRef lpTotalNumberOfFreeBytes As Long) As Integer End Function Sub Druck() Dim disk As String = "C:\" Dim verfügbar, total, frei If 0 <> GetDiskFreeSpaceEx(disk, verfügbar, total, frei) Then Console.WriteLine("Freie Bytes (User) = {0}", verfügbar) Console.WriteLine("Verfügbare Bytes (User) = {0}", total) Console.WriteLine("Freie Bytes (total) = {0}", frei) End If Console.ReadLine() End Sub End Module End Namespace
Das Umsetzen der Datentypen ist ein sehr großer Themenkreis, auf den wir hier nicht tiefer eingehen wollen. Für weitergehende Informationen sei auf die Online-Dokumentation verwiesen.
Zum Abschluss dieses Abschnitts soll noch ein weiteres Beispiel einer API-Funktion gezeigt werden, mit der es möglich ist, eine Wave-Datei in einer .NET-Anwendung abzuspielen. Hierbei handelt es sich um die Funktion sndPlaySound der Bibliothek winmm.dll.
BOOL sndPlaySound { LPCSTR lpszSound, UINT fuSound };
Der erste Parameter erwartet die Angabe einer Wave-Datei, dem zweiten werden Konstanten übergeben, mit denen das Verhalten der Funktion gesteuert wird. Die Auswertung des zweiten Parameters erfolgt bitweise. Mit SND_ASYNC lässt sich dabei festlegen, dass der Sound asynchron abgespielt wird, SND_LOOP sagt aus, dass der Sound so lange gespielt wird, bis die Funktion erneut aufgerufen wird. Sollte die angegebene Wave-Datei nicht gefunden werden, wird per Voreinstellung der Standardsound abgespielt. Mit der Konstanten SND_NODEFAULT lässt sich das abstellen.
Eine Hürde im Zusammenhang mit API-Funktionen ist es häufig, die Werte der Konstanten zu ermitteln, da diese in den Dokumentationen nicht mit angegeben werden. Oftmals genügt ein Blick in die C/C++-Headerdateien, um einen Wert in Erfahrung zu bringen. Ganzzahlige Konstanten versuche ich immer in einer Enumeration unterzubringen, um nur definierte Werte zu erlauben.
'...\Programmiertechniken\PInvoke\Plattenplatz.vb |
Imports System.Runtime.InteropServices Namespace PInvoke Module Wave <DllImport("winmm.dll")> _ Private Function sndPlaySound(ByVal lpszSound As String, _ ByVal fuSound As Integer) As Integer End Function <Flags()> Private Enum Sound SND_ASYNC = &H1 ' asynchron abspielen SND_NODEFAULT = &H2 ' Standardsound bei Problemen SND_LOOP = &H8 ' Wiederholung bis zum erneuten Aufruf End Enum Sub Ausgabe() Dim flags As Integer = 0 Console.Write("Asynchrones Abspielen? (j/n)") If "j" = Console.ReadLine() Then flags = flags Or Sound.SND_ASYNC Console.Write("Wiederholtes Abspielen? (j/n)") If "j" = Console.ReadLine() Then flags = flags Or Sound.SND_LOOP Console.Write("Standardsound abspielen? (j/n)") If "n" = Console.ReadLine() Then flags = flags Or Sound.SND_NODEFAULT ' Datei abspielen sndPlaySound("ringin.wav", flags) Console.ReadLine() End Sub End Module End Namespace
Ihre Meinung
Wie hat Ihnen das Openbook gefallen? Wir freuen uns immer über Ihre Rückmeldung. Schreiben Sie uns gerne Ihr Feedback als E-Mail an kommunikation@rheinwerk-verlag.de.