Typelibs fest im Griff

Veröffentlicht: 19. Jun 2000 | Aktualisiert: 08. Nov 2004

Von Torsten Zimmermann

COM hat für uns Visual Basic-Programmierer eine ganze Reihe von Vorteilen gebracht. Visual Basic bis zur Version 3 war noch jene Entwicklungsumgebung, die man zwar relativ gut erweitern konnte (wir erinnern uns an VBX-Steuerelemente), aber in ihrer Struktur doch an gewisse Grenzen gestoßen war. Die Lösung kam dann zum richtigen Zeitpunkt, und sie hieß COM. Insofern war der lange Zeitraum zwischen Version 3 und 4 durchaus verständlich - schließlich musste Visual Basic von Grund auf neu entwickelt werden. Seit der Version 4 ist nun COM das Fundament von VB, und es ist sicherlich nicht übertrieben zu sagen: Wer Visual Basic verstehen will, muss COM verstehen.

* * *

Auf dieser Seite

COM-Schnittstellen beschreiben COM-Schnittstellen beschreiben
entry statt Declare entry statt Declare
Typelibs aus ODL-Dateien erzeugen Typelibs aus ODL-Dateien erzeugen
VB-Fehlerbehandlung für Typelib-Funktionen VB-Fehlerbehandlung für Typelib-Funktionen
Funktionsparameter als Rückgabewert definieren Funktionsparameter als Rückgabewert definieren
Ausblick Ausblick

Diesen Artikel können Sie hier lesen dank freundlicher Unterstützung der Zeitschrift:

bpro

Überblick
Thema
Typelibs sind ein wesentlicher Bestandteil von COM. Sie definieren und dokumentieren die Dienstleistungen von ActiveX-Komponenten.
VB-Entwickler können Typelibs aber auch dazu benutzen, ihre Projekte transparenter zu machen, indem sie Definitionen für Konstanten, Typen und DLL-Funktionen in eigene Typelibs auslagern.
Der Bericht führt in den Aufbau von Typelibs sowie ihre Erzeugung ein.
Technik
Definitionen in Typelibs erzeugen und nutzen; VB-Fehlerbehandlung im Zusammenspiel mit DLL-Funktionen verstehen.
Voraussetzungen
mktyplib.exe
VB-Versionen: VB5, VB6

Wenn man sich die Gesamtheit der Microsoft-Technologien der letzten fünf Jahre anschaut, wird schnell klar, dass das heutige VB die konsequente Umsetzung dieser Technologien ist. An dem Paradigma jedoch, dass VB den Entwickler von den Schwierigkeiten der Windows-Programmierung fernhalten soll, hat sich nichts geändert. Auch mit den Feinheiten der COM-Programmierung hat ein VB-Entwickler in der Regel nichts zu tun.

Warum dann aber einen Artikel über eine der vielen Interna von COM? Die Antwort ist einfach und galt schon immer für Visual Basic: VB ist eine sehr geradlinige Sprache, und Sie können tolle Dinge damit tun - doch eben diese Geradlinigkeit hat als Preis einen Verlust an Kontrolle über Interna. Sie können einige Dinge, die für die Programmierung in C und C++ selbstverständlich sind, nicht realisieren. Bislang stellte das Windows-API den alleinigen Fundus dar, aus dem VB-Entwickler zur Erweiterung ihrer Anwendungen schöpfen konnten (wenn man hier einmal von den zahlreichen Tools absieht). Da VB nun aber vollständig auf COM basiert, liegt es nahe, eben diese COM-Technologien zu nutzen, um alles aus VB herauszuholen. Ich möchte daher mit diesem Artikel eine Reihe von Beiträgen beginnen, die sich intensiv mit dem Thema Visual Basic und COM auseinandersetzt.

COM-Schnittstellen beschreiben

In der Welt der Objekte ist es oft notwendig, jedem Objekt eine Beschreibung seiner selbst (welche Eigenschaften und Methoden besitzt es) mit auf den Weg zu geben. Das ist natürlich kein Konzept von Microsoft, sondern gilt z.B. auch für CORBA-Objekte. Microsoft hat im Zusammenhang mit COM lediglich die existierende Beschreibungssprache IDL (Interface Definition Language) erweitert und das Ergebnis dann ODL (Object Definition Language) genannt. Mithilfe dieser Beschreibungssprache können Objekte oder, um genauer zu sein, COM-Schnittstellen beschrieben werden. Sie lässt sich aber, wie wir noch sehen werden, auch für andere Dinge nutzen. Ein sehr praktisches Resultat solcher Beschreibungen zeigen uns der VB-Objektbrowser und Intellisense. Beide nutzen COM-Schnittstellenbeschreibungen, um uns Klartextinformationen zu den Eigenschaften, Methoden und Konstanten von Komponenten und Klassen zu geben.

Da VB mit reinen ODL-Dateien aber noch ziemlich wenig anfangen kann (ODL-Dateien sind reine Textdateien), ist es notwendig, sie in ein Format zu bringen, das von VB leicht verstanden wird. Dieses Format nennt sich Typelib. Eine Typelib ist eine als Binärdatei kompilierte ODL-Datei. Allgemein stellen Typelibs das standardisierte Navigationssystem in der Welt der COM-Objekte dar; sie liefern die Informationen, die für den Zugriff auf Objekte notwendig sind. Auch jede mit Visual Basic erzeugte ActiveX-DLL oder -EXE-Datei enthält eine solche Typelib. Diese wird, ohne dass Sie als Entwickler etwas dafür tun müssten, als Ressource automatisch erzeugt und eingebunden und enthält dann Informationen über die Klassen, die Ihre Anwendung publiziert.

 

entry statt Declare

Nach soviel Theorie möchte ich nun die manuelle Erzeugung von Typelibs erläutern. Dafür werden wir uns mit dem Aufbau von ODL-Dateien beschäftigen und dann sehen, wie man sie in Typelibs umwandelt.

Wie bei jeder Programmiersprache folgt auch die Erstellung von ODL-Dateien einer genau definierten Syntax. Da es sich um reine Textdateien handelt, können diese Dateien mit jedem beliebigen Texteditor bearbeitet werden (Visual C++ bietet aber durchaus auch komfortablere Wege dafür).

Grundschema einer ODL-Datei

Der Inhalt von ODL-Dateien folgt einem festen Schema. An erster Stelle steht immer die Bibliothek (die Bezeichnung Typelib als Abkürzung für Type Library ist also nicht aus der Luft gegriffen):

[<i>Liste_der_Attribute</i>]  
library <i>Name_der_Library</i> { 
... 
}

(Kursiv gesetzte Bezeichnungen in Angaben zum allgemeinen Aufbau einer ODL-Datei müssen in der Realität ersetzt werden.)

library ist ein Schlüsselwort, das unbedingt angegeben werden muss. So lautet auch der Name, der dann etwa im Objektbrowser von Visual Basic erscheint.

Die ID einer Typelib

Unter ODL kommt den Attributen eines Schlüsselwortes eine wesentliche Bedeutung zu. Sie beschreiben die Verwendung des sich an das Schlüsselwort anschließenden Bereichs. Einige von ihnen sind optional, andere müssen in jedem Fall angegeben werden. Bei dem Schlüsselwort library muss z.B. unbedingt eine ID mithilfe des Schlüsselworts uuid definiert werden, damit die Typelib eindeutig identifiziert werden kann. Diese ID ist eine GUID, die Sie mit guidgen.exe erstellen können. Optional ist hier die Beschreibung mit dem Schlüsselwort helpstring. Da dies aber eine Möglichkeit ist, der Bibliothek eine aussagekräftige Beschreibung mit auf den Weg zu geben, sollten Sie davon Gebrauch machen.

Attribute sind voneinander durch Komma zu trennen. Der Wert wird in Klammern eingeschlossen wie ein Funktionsparameter an sie übergeben. Und interessanterweise werden Attribute den zu modifizierenden Daten vorangestellt:

Der erste Schritt zu einer Typelib sieht dann so aus:

[uuid(C54F1500-A1B8-11d2-840D-0000B45A4682), helpstring("BasicPro Typelib")] 
library BasicPro { 
... 
};

In dem Bereich, der vom Schlüsselwort library eingeklammert ist, können Sie jetzt alles definieren, was Sie in der Typelib unterbringen wollen: Konstanten, Datentypen, Funktionen usw. Ich beginne mit den einfachen Dingen, um dann zu den komplexeren fortzuschreiten.

Einfache eigene Datentypen definieren

Wenn Sie ein Freund eigener Datentypen sind, dann werden Ihnen die Typelibs gefallen, denn Sie können sich einen eigenen Datentyp auf der Grundlage eines bestehenden definieren:

typedef [public] long HWND; 
typedef [public] long HDC;

Bindet man eine Typelib mit diesen Definitionen in ein VB-Projekt ein, können Variablen mit den neuen Datentypen HWND und HDC definiert werden.

Interessant war das vor allem während der Umstellung von 16 Bit auf 32 Bit für die Deklarationen kompatibler Datentypen. Sie mussten dann lediglich eine Typelib austauschen, um aus einem 16 Bit-HWND ein 32 Bit-HWND zu machen.

Unter 32 Bit -Visual Basic hat das ein wenig an Bedeutung verloren, es sei denn, Sie bevorzugen aus Gründen der besseren Dokumentation die Deklaration einer Variable nach ihrer inhaltlichen Bedeutung. Dabei ist aber zu beachten, dass Sie damit die Typenüberprüfung von Visual Basic nicht überlisten können. Intern ist der Datentyp HWND immer noch ein Long. Folgender Programmcode ist also zulässig:

Dim var1 As Long 
Dim var2 As HWND 
var1 = var2 
var2 = var1

Eigene strukturierte Typen
Bereits etwas komplexer ist die Definition von Strukturen bzw. UDTs. Dazu zunächst ein Beispiel:

typedef struct tagRECT 
{ 
long left; 
long top; 
long right; 
long bottom; 
} RECT;

Die Definition von Strukturen entspricht im Wesentlichen der C-Syntax. Details zu den verwendbaren Grunddatentypen entnehmen Sie bitte Kasten 1.

Datentypen in Typelibs

Die üblichen Schwierigkeiten bei der Umsetzung von Datentypen, die primär für die Verwendung in C und C++ ausgerichtet sind, werden durch die Verwendung von Typelibs nicht beendet. Nicht alle grundsätzlich in Typelibs zu verwendenen Datentypen können mit VB auch genutzt werden. Hinzu kommt, dass es auch noch Unterschiede zwischen den einzelnen Visual Basic-Versionen gibt. Hier eine Liste der Datentypen, die Sie verwenden sollten, um auf der sicheren Seite zu sein:

Typelib

Visual Basic

Boolean

Long

Char

Byte

Double

Double

Long

Long

Short

Integer

LPSTR

String

Currency

Currency

Date

Date

Variant

Variant

Void *

As Any (Zeiger auf untypisierte Daten)

BSTR

String

HRESULT

Long

Kasten 1: Visual Basic-Datentypen in Typelibs

Problematisch sind vor allem jene Datentypen, mit denen Zeichenketten definiert werden. Während ein LPSTR bei der Deklaration von Funktionsaufrufen keine Probleme bereitet - dieser wird von VB richtig in einen null-terminierten String umgewandelt - kann ein LPSTR in Strukturen nicht verwendet werden. Erfordert eine Struktur die Übergabe einer Zeichenkette, haben Sie zwei Alternativen.

Die erste, etwas einfachere, ist die Definition eines Long statt eines LPSTR, dem Sie dann in Visual Basic einen Wert über die undokumentierte Funktion StrPtr (liefert die Long-Adresse des ersten Zeichens des Wertes einer Stringvariablen) zuordnen. Diese Variante sollten Sie favorisieren, wenn es um Zeichenketten variabler Länge geht.

Die alternative Methode besteht in der Definition eines Bytefeldes, dem Sie dann in Visual Basic jedes Zeichen extra zuordnen. Das ist ohne Zweifel der aufwendigere Weg, den Sie jedoch bei Zeichenketten mit fixer Länge gehen sollten.

Ein weiterer wichtiger Nebeneffekt ist der Aufruf von Funktionen, die einen BSTR anstatt eines LPSTR erwarten. Ist eine solche Funktion innerhalb von Visual Basic deklariert, können Sie die automatische Typenumwandlung UNICODE nach ANSI und zurück nicht umgehen und werden ein sonderbares Ergebnis erhalten. Erst wenn diese Funktion in einer Typelib definiert ist und damit VB bekannt gemacht wurde, dass es sich um einen BSTR anstatt eines LPSTR handelt, funktioniert der Aufruf der Funktion fehlerfrei.

Ist eine Struktur einmal in einer Typelib definiert, kann sie auch in Visual Basic verwendet werden:

Dim mystruct as RECT 
mystruct.left = 1234

Konstantendefinitionen

Für die Definition von Konstanten stehen Ihnen zwei Varianten zur Verfügung. Die erste ist die Definition über Aufzählungen und entspricht den mit Enum in VB definierten.

Im folgenden Beispiel werden z.B. die Konstanten vereinbart, die den Fensterstil einer Combobox beschreiben:

typedef enum { 
CBS_SIMPLE = 0x0001, 
CBS_DROPDOWN = 0x0002, 
CBS_DROPDOWNLIST = 0x0003, 
CBS_OWNERDRAWFIXED = 0x0010, 
CBS_OWNERDRAWVARIABLE = 0x0020, 
CBS_AUTOHSCROLL = 0x0040, 
CBS_OEMCONVERT = 0x0080, 
CBS_SORT = 0x0100, 
CBS_HASSTRINGS = 0x0200, 
CBS_NOINTEGRALHEIGHT = 0x0400, 
CBS_DISABLENOSCROLL = 0x0800, 
CBS_UPPERCASE = 0x2000, 
CBS_LOWERCASE = 0x4000 
} ComboboxStyleConstants;

Auch das Verhalten solcher Aufzählungen in der Entwicklungsumgebung ist dem der VB-Typen enum gleich. Es kann also ein Datentyp auf dieser Basis deklariert werden - Intellisense bietet Ihnen dann später eine Liste zur Auswahl an, wenn Sie z.B. eine Zuweisung an eine Variable von diesem Typ schreiben.

Eine andere Möglichkeit, Konstanten zu definieren, sind Bereiche, die mit dem Schlüsselwort module eingeleitet werden. Module dienen zwar hauptsächlich dazu, Funktionen zu deklarieren, doch bevor ich darauf zu sprechen komme, hier ein Beispiel für die Definition einiger Konstanten:

[dllname("advapi32.dll")] 
module ShellAPI { 
const long HKEY_CLASSES_ROOT = 0x80000000; 
const long HKEY_CURRENT_USER = 0x80000001; 
const long HKEY_LOCAL_MACHINE = 0x80000002; 
};

Die auf diese Art definierten Konstanten stehen Ihnen allerdings nicht über Intellisense zur Verfügung. Das ist verständlich, denn sie definieren ja keinen neuen Typ, wie es oben bei typedef enum der Fall ist. Dort wird ComboboxStyleConstants als Datentyp mit einer festgelegten Menge diskreter Werte deklariert.

Funktionsdeklarationen

Den mit Abstand umfangreichsten Teil bei der Erstellung einer ODL-Datei nimmt die Deklaration von Funktionen ein, die aus einer DLL exportiert werden. An dieser Stelle scheiden sich auch die Geister, was die Verwendung von Typelibs angeht. Für Puristen hört an dieser Stelle jedoch der Spaß auf, denn eine einmal erstellte Typelib ist ein statisches Gebilde. Sie können natürlich Dinge im ODL-Quellcode verändern und dann mit dem Typelib-Compiler eine neue Typelib erstellen. Das ist jedoch mit einigem Aufwand verbunden, was der Arbeitsweise vieler Entwickler, die intensiv mit dem Windows-API arbeiten, widerspricht. Ich halte diese Argumente aber für unbegründet. Wie wir noch sehen werden, haben Sie bei der Deklaration von Funktionen in einer Typelib die gleiche Flexibilität wie bei der Deklaration innerhalb von Visual Basic und sogar noch ein bisschen mehr. Fast jede Funktion aus einer DLL kann in einer Typelib abgebildet werden, und falls das nicht der Fall ist, sollten Sie vielleicht auch die Hände von der Funktion lassen. Diese hält sich dann nämlich nicht an die Standards. Durch Alias-Techniken können Sie eine Funktion auch entsprechend der Verwendung ihrer Parameter umbenennen, so dass Sie spätestens dadurch die volle Brandbreite der Möglichkeiten erreichen. Nun aber zur Praxis:

Funktionen werden, wie bereits angedeutet, innerhalb eines Moduls definiert. Das Schlüsselwort module verfügt wieder über Attribute, von denen das wichtigste der Name der DLL ist, aus dem die Funktion exportiert wird. Schematisch sieht das so aus:

[dllname("<i>Name_der_DLL</i>")] module <i>Name_des_Moduls</i> { 
[entry("<i>Name_der_Funktion_in_der_DLL</i>")] <i>Typ_des_Rückgabewertes 
Aufrufkonvention Aufrufname_der_Funktion</i> (<i>Parameter</i>); 
};

Ein praktisches Beispiel ist dann folgende Deklaration:

[dllname("user32.dll")] module BPAPIUser { 
[entry("GetSysColor")] long stdcall GetSysColor ([in] long ColorIndex); 
};

Hier zu gibt es natürlich einiges zu sagen. Der Name eines Moduls kann frei gewählt werden, sollte sich aber soweit möglich von der Benennung der Standardmodule in Visual Basic unterscheiden.

Die obige Funktion GetSysColor wird aus user32.dll exportiert, die auch im Attribut dllname angegeben ist.

Das Attribut entry einer Funktion ist ihr Name, unter dem sie aus der DLL exportiert wird. Hier ist rein theoretisch auch die Angabe eines Index möglich. Da dies jedoch unter Win32 im Gegensatz zu Win16 eher untypisch ist, sollten Sie davon keinen Gebrauch machen.

Ein weiteres Attribut, das Sie an dieser Stelle angeben können, ist helpstring. Damit können Sie der Funktion eine kleine Beschreibung mit auf den Weg geben, die dann auch im Objektbrowser von Visual Basic sichtbar ist.

Nach den Attributen folgen der Rückgabedatentyp und die Aufrufkonvention. Unter VB5 war es noch möglich, pascal als Aufrufkonvention anzugeben. VB6 ist in dieser Hinsicht strenger, so dass Sie sich auf stdcall beschränken sollten. Die Aufrufkonvention macht eine Aussage darüber, in welcher Reihenfolge die Funktionsparameter auf dem Stack übergeben werden - sicherlich eine Frage, die für die normale VB-Programmierung von untergeordneter Bedeutung ist, aber an der Schnittstelle zu DLLs wichtig wird.

Der Aufrufkonvention schließt sich der Name der Funktion an, unter dem sie in Visual Basic aufgerufen wird. Danach folgen ihre Parameter.

Funktionsparameter

Den Parametern möchte ich besondere Aufmerksamkeit widmen, weil sie das A und O einer guten Deklaration ausmachen.

Es gibt grundsätzlich zwei Parameterarten: in-Parameter entsprechen einer Übergabe als Wert (ByVal), und out-Parameter entsprechen einer Übergabe als Referenz (ByRef).

In letzterem Fall wird also immer die Adresse des Parameterwertes statt des Parameterwertes selbst übergeben. Eine Kombination beider Parameterarten ist möglich. Dazu ein Beispiel:

[entry("SendMessageA")] long stdcall SendMessage ([in] HWND hWND, [in] long 
Message, [in] long wParam, [in, out] void * lParam);

Der letzte Parameter, lParam, wird hier so definiert, dass er einem As Any (void *) ohne ByVal ([in, out]) enspricht.

Ein reiner in-Parameter würde ein ByVal bedingen, und ein reiner out-Parameter erzwänge die Übergabe als Referenz. Die Definition void * steht für eine typlose Übergabe. Durch die Kombination zweier Parameterarten mit einem void-Typ können Sie also praktisch alles übergeben.

Dabei gibt es jedoch eine kleine Falle, in die man sehr schnell tappt (siehe auch Kasten 1), und die stellt der VB-Datentyp String dar. Durch die Deklaration einer Funktion in einer Typelib wird die automatische Typisierung, die Visual Basic beim Aufruf von externen Funktionen vornimmt, ausgeschaltet. Das ist ein wichtiger und beachtenswerter Punkt. Visual Basic hält sich also sehr streng an die Deklaration, was im Prinzip auch im Interesse des Entwicklers ist. Bei der Verwendung von Zeichenketten ist das aber etwas problematisch, weil durch die Deklaration innerhalb von VB implizit eine Typisierung vorgenommen wird, auf die viele Aufrufe aufbauen. Beim Aufruf einer in einer Typelib deklarierten Funktion muss dies jedoch explizit definiert werden. So würde in obiger Deklaration eine Zeichenkette nicht automatisch null-terminiert. Entweder Sie fügen das 0-Zeichen selbst hinzu und übergeben einen Zeiger auf die Zeichenkette oder Sie rufen die Funktion explizit mit ByVal auf. Alternativ können Sie auch der Typelib eine neue Deklaration speziell für die Übergabe von Zeichenketten hinzufügen:

[entry("SendMessageA")] long stdcall SendMessageStr ([in] HWND hWND, [in] long 
Message, [in] long wParam, [in] LPSTR lParam);

Dadurch, dass die Typelibs deutlich statischer sind als die Deklaration einer Funktion innerhalb von Visual Basic, ist diese Form durchaus legitim. Wenn Sie viel mit API-Aufrufen arbeiten, machen Sie es sicherlich in VB auch nicht anders und legen für unterschiedliche Parameterkonstellationen spezifische Aufrufvarianten mittels einer Alias-Deklaration an. Sie könnten auch in einer Typelib für jeden möglichen Fall eine dem Original verwandte Deklaration schreiben und damit die gleiche Flexibilität erreichen, wie sie auch mit Visual Basic möglich ist.

Da out-Parameter immer einen Wert zurückliefern, müssen sie auch immer als Zeiger definiert werden. Als out-Parameter kann jeder Datentyp verwendet werden, auch Strukturen. Auch dazu ein kleines Beispiel:

[entry("GetWindowRect")] long stdcall GetWindowRect ([in] HWND hwnd, [in, out] 
RECT * lpRect);

Semantisch entspricht diese Deklaration genau der, wie sie in Visual Basic vorgenommen wird. Der erste Parameter ist ein Long, der hier mittels eines typedef in HWND umgewandelt wurde (s.o.) und der zweite ist ein Zeiger auf eine RECT-Struktur. Diese wird dann nach dem Aufruf mit den gefüllten Werten zurückgeliefert.

Eigene Datentypen in Funktionsdefinitionen nutzen

Durch die relativ einfache Umsetzbarkeit von Deklarationen in Typelibs können Sie zahlreiche Vorteile der COM-Technologien nutzen. Dazu zählt z.B. Intellisense. Das bei Intellisense verwendete Verfahren beruht ebenfalls auf der Nutzung von Typinformationen, die entweder von Visual Basic selbst (bei Klassen, Formularen usw.) oder aber aus externen Typelibs abgerufen werden. Dieser Umstand kann genutzt werden, um die Verwendung von API-Aufrufen so einfach zu machen wie den Aufruf einer Methode, die innerhalb von Visual Basic definiert wurde. Weiter oben hatte ich bereits ein Beispiel für die Deklaration von Konstanten und Aufzählungen gezeigt. Hier ist ein weiteres:

typedef enum { 
SW_HIDE = 0, 
SW_SHOWNORMAL = 1, 
SW_NORMAL = 1, 
SW_SHOWMINIMIZED = 2, 
SW_SHOWMAXIMIZED = 3, 
SW_MAXIMIZE = 3, 
SW_SHOWNOACTIVATE = 4, 
SW_SHOW = 5, 
SW_MINIMIZE = 6, 
SW_SHOWMINNOACTIVE = 7, 
SW_SHOWNA = 8, 
SW_RESTORE = 9, 
SW_SHOWDEFAULT = 10, 
SW_MAX = 10 
} ShowWindowConstants;

Da es sich bei ShowWindowConstants nun um einen neuen Datentyp handelt, kann dieser auch bei der Deklaration von Funktionen verwendet werden:

[entry("ShowWindow")] long stdcall ShowWindow ([in] HWND hwnd, [in] 
ShowWindowConstants nCmdShow);

Wenn Sie die Funktion ShowWindow in Visual Basic verwenden wollen und den zweiten Parameter eingeben, wird Ihnen dank Intellisense eine Liste der möglichen Parameter angeboten. Für alle, die die Hälfte ihres bisherigen Entwicklerlebens mit der Suche nach den richtigen Parametern beim Aufruf von API-Funktionen verbracht haben, wird das eine wahre Freude sein.

 

Typelibs aus ODL-Dateien erzeugen

Nachdem die Arbeiten an der ODL-Datei, welche die Deklaration von Funktionen, Strukturen und Konstanten beinhaltet, abgeschlossen sind, muss aus ihr noch eine Typelib erzeugt werden. Ich verwende dafür den Typelib-Compiler mktyplib.exe, der auch Visual Studio beiliegt. Sein Aufruf kann folgendermaßen aussehen:

mktyplib.exe /nologo /WIN32 /tlb bptyplib.tlb bptyplib.odl

Es empfiehlt sich, dafür eine Batchdatei zu verwenden (siehe BasicPro Heft-CD 1/99; bitte passen Sie darin jedoch den Pfad zum Typelib-Compiler an).

Der Parameter /nologo erspart Ihnen die Ausgabe von Copyright-Informationen und /win32 definiert, dass die Typelib für die Win32-Plattform erzeugt werden soll. Das ist wichtig, wenn Sie eine Typelib parallel für Win16 und Win32 entwickeln.

Der nächste Parameter /tlb gefolgt von einem Dateinamen gibt den Namen der zu erzeugenden Typelib und der ODL-Quelldatei an.

Eine Liste aller Parameter von mktyplib.exe kann wie üblich mit /? erzeugt werden.

 

VB-Fehlerbehandlung für Typelib-Funktionen

Damit ist die grundsätzliche Vorgehensweise bei der Deklaration von Funktionen in Typelibs erläutert. Typelibs sind aber auch eine hervorragende Möglichkeit, um die Arbeit der Visual Basic-VM (Virtual Machine) zu erkunden.

Dazu ein Beispiel: Es beschäftigt sich mit der Rückgabe von Fehlerwerten aus einer DLL an ein Visual Basic-Programm. Unter COM ist es üblich, den Rückgabewert einer Funktion als Datentyp HRESULT zu definieren. Dieser Datentyp, ein 32 Bit-Wert, ist nicht nur einfach eine Zahl, die man vielleicht mit einer Fehlernummer vergleichen könnte, sondern ein Wert, der einen Fehlercode kodiert beinhaltet. Um die Funktionsweise dieses Datentyps zu erläutern sowie dessen Interpretation in Visual Basic, habe ich folgende kleine C-Funktion geschrieben (siehe Heft-CD), die nicht viel tut, außer einen ByRef-Parameter zu verändern und einen HRESULT-Wert zurückzuliefern:

HRESULT APIENTRY DoSomething(bool WithError, long * i) 
{ 
*i = 10; 
if (!WithError) 
return S_OK; 
else 
return E_NOTIMPL; 
}

Würde man diese Funktion in Visual Basic direkt verwenden wollen, so müsste die Deklaration folgendermaßen aussehen:

Declare Function DoSomething Lib "bpdll.dll" (ByVal WithError as Long, 
i As Long) as Long

Auf den ersten Parameter WithError gehe ich in Kürze ein - Sie sollten ihn vorerst ignorieren.

Der Parameter i wird als Referenz übergeben und der Rückgabewert muss, aufgrund der 32 Bit-Breite von HRESULT, als Long definiert werden. Damit erschöpfen sich bereits die Möglichkeiten von Visual Basic.

Anders jedoch, wenn man Typelibs zur Deklaration dieser Funktion verwendet. Zunächst noch einmal die Deklaration, wie sie der in Visual Basic entspricht:

[dllname("BPDLL.DLL")] 
module BasicPro { 
[entry("DoSomething")] long _stdcall DoSomething ([in] bool 
WithError, [out] long * i); 
};

Auch hier wird i als out-Parameter definiert, was einer Übergabe als Referenz entspricht. Da sich jedoch der Datentyp HRESULT in einer Typelib definieren lässt, könnte die Deklaration auch anders aussehen:

[entry("DoSomething")] HRESULT _stdcall DoSomethingWithError ([in] bool 
WithError, [out] long * i);

Interessanterweise wird die Information über den Rückgabewert, den die Funktion aus der DLL liefert, von der Visual Basic-Laufzeitumgebung ausgewertet und in eine so genannte Exception, eine Ausnahmebehandlung, umgewandelt, die dann als Fehler im Visual Basic-Programm ankommt. Dadurch, dass die Funktion aus der DLL einen Wert ungleich S_OK zurückliefert, der das korrekte Arbeiten signalisieren würde, nimmt die VM an, dass ein Fehler aufgetreten ist und handelt danach.

Jetzt wird auch der Sinn des Parameters WithError deutlich, der lediglich dazu dient, einen Fehler von der Funktion auslösen zu lassen. Der entsprechende Aufruf in Visual Basic lautet:

On Error Resume Next 
Dim i As Long 
DoSomethingWithError True, i 
If Err.Number <> 0 Then 
... 
End If

Das ist eine sehr simple Möglichkeit, Fehlerinformationen aus einer DLL heraus weiterzureichen. Sie basiert allein auf der Deklaration COM-spezifischer Datentypen für Visual Basic. Für HRESULT sind zahlreiche vordefinierte Werte festgelegt, auf die ich mich an dieser Stelle auch beschränken möchte. Liefert die Funktion DoSomethingWithError nicht S_OK zurück, sondern stattdessen den vordefinierten HRESULT-Wert E_NOTIMPL, macht die VM daraus den Fehler 455-Objekt unterstützt diese Aktion nicht, der natürlich unter VB per On Error abgefangen werden muss.

Wichtig ist, dass der Rückgabewert als Datentyp HRESULT definiert wird, weil andernfalls Visual Basic nicht erkennt, dass damit ein Fehlercode zurückgegeben werden soll!

 

Funktionsparameter als Rückgabewert definieren

Was soll man aber tun, wenn eine Funktion einen "echten" Rückgabewert hat und man gleichzeitig die VB-Fehlerbehandlung nutzen möchte? Es ist bei der Deklaration von Funktionen in einer Typelib möglich, einen Parameter als Funktionsresultat zu definieren. Man deformiert also praktisch die Deklaration in der ODL-Datei und fügt einen out-Parameter an, der nicht an die Funktion übergeben wird, sondern nur zur Rückgabe des Resultats dient.

Der Hintergrund dafür ist, dass Programmiersprachen mit Laufzeitmodul den Rückgabewert über Fehlerinformationen vor dem Entwickler verbergen und stattdessen einen direkten Zugriff auf einen Parameter ermöglichen sollen. Da es unter COM den Begriff Property, so wie Visual Basic-Programmierer ihn kennen, nicht gibt, wird diese Vorgehensweise auch dazu verwendet, um aus einer Methode das zu machen, was allgemein als Objekt-Eigenschaft bekannt ist. Wenn Sie sich einmal die Typdefinitionen eines mit ATL entwickelten Controls ansehen, werden Sie genau diesen Punkt beobachten können. Wir können dieses Verfahren aber auch dazu verwenden, den Aufruf einer Funktion zu vereinfachen. So könnte die Funktion DoSomethingSimple in einer Typelib folgendermaßen deklariert werden:

[entry("DoSomething")] HRESULT stdcall DoSomethingSimple ([in] bool WithError, 
[out, retval] long * i);

Bitte beachten Sie das Attribut retval für den letzten Parameter. An dieser Stelle wird wieder die Visual Basic-VM aktiv, die aus dem Parameter i einen Rückgabewert für den Aufruf aus Visual Basic heraus macht. Dieser sieht dann so aus:

i = DoSomethingSimple(False)

Es ist selbstverständlich, dass nur ein Parameter in einer Deklaration mit diesem Attribut versehen werden kann. Zur Erinnerung: Es handelt sich auf der DLL-Seite immer noch um die gleiche Funktion. Inhaltlich sind also alle Aufrufe gleich; sie unterschieden sich lediglich in der Interpretation von Parametern und Rückgabewert in VB.

Der Übersichtlichkeit wegen habe ich die Funktion in der Typelib entsprechend ihrer Verwendung umbenannt. Der Aufruf

i = DoSomethingSimple(True)

würde also wieder einen Fehler auslösen.

Vorsicht mit Rückgabewerten bei der Definition von Windows-API-Funktionen

Wir könnten jetzt vielleicht der Versuchung unterliegen, das Windows-API entsprechend auf die Möglichkeit einer Re-Deklaration und damit einer Vereinfachung des Aufrufs der API-Funktionen hin zu untersuchen. Rein theoretisch ist das möglich. Dabei ist jedoch zu beachten, dass Rückgabeparameter bei einem Aufruf nicht sicher initialisiert werden. Zwar belegt VB sie z.Z. noch mit Standardwerten, das könnte sich aber in zukünftigen Versionen ändern. Da diese Parameter durch die Definition als Rückgabewert als reine out-Parameter betrachtet werden, können Sie sie bei einem Aufruf nicht mit Werten vorbelegen. Viele API-Funktionen erwarten aber gerade das. So sind viele out-Parameter auch gleichzeitig in-Parameter, denen beispielsweise die Größe einer Zeichenkette übergeben wird. Wären solche Parameter als Rückgabewert definiert, könnten Sie dem Parameter nichts mit auf dem Weg geben, was ein Fehlschlagen des Aufrufes zur Folge hätte. Die Verwendung des Attributs retval eignet sich also vor allem für selbstgeschriebene DLL-Funktionen.

 

Ausblick

Die Verwendung von Typelibs bietet eine elegante Variante, die Möglichkeiten von Visual Basic sehr viel feiner als normalerweise üblich auszureizen und zu erweitern. Die hier vorgestellten Techniken stellen aber nur einen ersten Einstieg in die Verwendung von Typelibs dar.

In einer der nächsten Ausgaben werde ich das Thema wieder aufgreifen und mich mit der Frage beschäftigen, wie Sie mit der Hilfe von Typelibs auf solche Objekte bzw. Schnittstellen aus der COM- und Windows-Welt zugreifen können, die Ihnen normalerweise nicht zur Verfügung stehen.

Bis dahin empfehle ich Ihnen, mit mktyplib.exe und ODL-Dateien etwas "zu spielen". Sie werden bestimmt einen Weg entdecken, wie Sie sich den Zugriff auf häufig genutzte Windows-DLLs erleichtern können.