Hauptseite >Tips zu VB5/6 >  VB mit PowerBASIC erweitern
 
VB schirmt uns von vielen systemnahen und damit "gefährlichen" Operationen ab. Daraus resultiert die hohe Produktivität der Arbeit mit dieser Sprache. Beispielsweise ist es mit reinem VB nahezu unmöglich, Pufferüberläufe oder "Memory Leaks" zu produzieren oder gar zu ungültigen Speicheradressen zu springen. Damit ist die Sprache das optimale RAD-Tool, obwohl sie ihre Vergangenheit als Interpreter-Sprache längst hinter sich hat und seit Version 5 echten Maschinencode erzeugt.

Aber alles hat seinen Preis. Die hohe Sicherheit bei gleichzeitig hoher Produktivität wird erkauft durch den Verzicht auf Zeigeroperationen, Aufrufe von Funktions-Adressen, Inline-Assembler und dergleichen mehr - diese Dinge sind in VB nicht oder nur sehr eingeschränkt möglich. Das ist in aller Regel nicht weiter schlimm, in der Praxis werden Sie so etwas nur sehr selten benötigen.

Aber manchmal kommt man eben doch in die Situation. Die Lösung besteht dann darin, in einer anderen Sprache eine Standard-DLL zu schreiben, in der Sie das betreffende Problem lösen. Diese DLL können Sie dann problemlos von VB aus aufrufen, sofern die DLL ihre Funktionen nach der StdCall-Aufrufkonvention exportiert.

Beim Stichwort "andere Sprache" werden nun die meisten gleich an den "Alleskönner" C/C++ denken, aber hier ist die gute Nachricht: es gibt durchaus BASIC-Dialekte, die dafür ebenfalls bestens geeignet sind.

Der Compiler der Firma PowerBASIC Inc. ist eine hocheffiziente und sehr flexibel einsetzbare Weiterentwicklung des alten TurboBASIC. Mit ihm lassen sich native Windows-DLLs, Windows-GUI- und Windows-Konsolen-Anwendungen erzeugen, die von keinerlei Laufzeit-Komponenten abhängig sind. Mit Zeiger-Arithmetik, Aufruf von Funktions-Adressen (CALL DWORD) bis hin zu Inline-Assembler für besonders zeitkritische Routinen bietet die Sprache alles, was man für systemnahe Zugriffe benötigt. Und: die Zusammenarbeit mit VB ist völlig unproblematisch. Die grösste Hürde am Anfang besteht darin, zu wissen, wie Funktionsdeklarationen auf beiden Seiten aussehen müssen, damit VB und PB miteinander kommunizieren können; daher soll im Folgenden mit ein paar einfachen Beispielen gezeigt werden, wie es funktioniert. Um den PB-Code übersetzen zu können, benötigen Sie den Compiler PowerBASIC für Windows (Version 7 oder höher, Bezug in Deutschland über diese Adresse), der Preis ist mit derzeit 149,00 € (Stand Sept. 2008) angesichts des Funktionsumfangs geradezu ein Schnäppchen.


PowerBASIC-Code:

#COMPILE DLL "pbtest1.dll" 'DLL erzeugen 
#DIM ALL      'entspricht "Option Explicit" in VB 

#INCLUDE "Win32API.inc"  'enthält API-Deklarationen

'Prototyp für GlobalMemoryStatusEx()
DECLARE FUNCTION Proto_GlobalMemoryStatusEx (BYREF lpBuffer AS MEMORYSTATUSEX) AS LONG
'*****************************************************
FUNCTION AddOne (BYVAL l AS LONG) EXPORT AS LONG
'Übergebenen Wert + 1 zurückgeben
INCR l
FUNCTION = l
END FUNCTION
'*****************************************************
SUB AddOneInPlace (BYREF l AS LONG) EXPORT
'Wert um 1 erhöht zurückgeben
INCR l
END SUB
'*****************************************************
FUNCTION UpperCase (BYREF s AS ASCIIZ) EXPORT AS STRING
'Argument in Grossbuchstaben übersetzen
FUNCTION = UCASE$(s)
END FUNCTION
'*****************************************************
FUNCTION IsZero (BYVAL l AS LONG) EXPORT AS INTEGER
'prüfen, ob 0
  IF l = 0 THEN
    FUNCTION = -1  'True
  ELSE
    FUNCTION = 0
    'diese Zuweisung ist eigentlich nicht notwendig, da
    'Funktionen und Variablen wie in VB mit 0 bzw. ""
    'vorbelegt werden
  END IF

'oder einfach so:
'   FUNCTION = (l = 0)

'oder spasseshalber in Assembler:
' ! MOV eax, l
' ! XOR bx, bx
' ! CMP eax, 0
' ! JNE isnot0
' ! DEC bx
' isnot0:
' ! MOV FUNCTION, bx

END FUNCTION
'*****************************************************
SUB SwapLongs (BYREF l1 AS LONG, BYREF l2 AS LONG) EXPORT
'zwei Long-Werte vertauschen
SWAP l1, l2
END SUB
'*****************************************************
SUB SwapStrings (BYVAL s1 AS STRING PTR, BYVAL s2 AS STRING PTR) EXPORT
'zwei Strings vertauschen
SWAP @s1, @s2
END SUB
'*****************************************************
FUNCTION SystemDirectory() EXPORT AS STRING
'Systemverzeichnis ermitteln
LOCAL l AS DWORD, s AS STRING
l = GetSystemDirectory(BYVAL 0&, 0)
s = SPACE$(l)
l = GetSystemDirectory(BYVAL STRPTR(s), l)
FUNCTION = LEFT$(s,l)
END FUNCTION
'*****************************************************
FUNCTION SHR(BYVAL l AS LONG, BYVAL offs AS LONG) EXPORT AS LONG
'shift right
SHIFT RIGHT l, offs
FUNCTION = l
END FUNCTION
'*****************************************************
FUNCTION GetInstalledRamSize ALIAS "GIRS" () EXPORT AS LONG
'Grösse des installierten Hauptspeichers ermitteln
LOCAL hModule, pGlobalMemoryStatusEx AS DWORD
LOCAL mInfo AS MEMORYSTATUS, mInfoEx AS MEMORYSTATUSEX
LOCAL l AS LONG, q AS QUAD
hModule = GetModuleHandle("KERNEL32.DLL")
IF ISTRUE hModule THEN
  'Das Laden der KERNEL32.DLL war schon mal erfolgreich; alles andere wäre
  'allerdings auch eine Katastrophe ... ;-)
  'Spannend wird's jetzt:
  pGlobalMemoryStatusEx = GetProcAddress(hModule, "GlobalMemoryStatusEx")
  IF ISTRUE pGlobalMemoryStatusEx THEN
    'Die vorliegende Kernel-Version enthält die Funktion GlobalMemoryStatusEx()
    mInfoEx.dwLength = SIZEOF(mInfoEx)
    'dynamischer Aufruf von GlobalMemoryStatusEx()
    CALL DWORD pGlobalMemoryStatusEx USING Proto_GlobalMemoryStatusEx(mInfoEx) TO l
    IF l THEN
      'Aufruf war erfolgreich
      q = mInfoEx.ullTotalPhys
      SHIFT RIGHT q, 10  '= \ 1024 (in Kilobyte umrechnen)
      FUNCTION = q
      EXIT FUNCTION 'Funktion verlassen
    END IF
  END IF
END IF
'Notausgang für ältere Systeme: Aufruf der alten Methode
mInfo.dwLength = SIZEOF(mInfo)
GlobalMemoryStatus mInfo
SHIFT RIGHT mInfo.dwTotalPhys, 10 '= \ 1024 (in Kilobyte umrechnen)
FUNCTION = mInfo.dwTotalPhys
END FUNCTION
'*****************************************************
' DLL-Einsprungpunkt (wird für jede DLL benötigt)
FUNCTION LIBMAIN (BYVAL hInstance   AS LONG, _
                  BYVAL fwdReason   AS LONG, _
                  BYVAL lpvReserved AS LONG) AS LONG

    SELECT CASE fwdReason

    CASE %DLL_PROCESS_ATTACH
        FUNCTION = 1
    CASE %DLL_PROCESS_DETACH
        FUNCTION = 1
    CASE %DLL_THREAD_ATTACH
        FUNCTION = 1
    CASE %DLL_THREAD_DETACH
        FUNCTION = 1
    END SELECT

END FUNCTION
            
Und hier der VB-Code:

Option Explicit

'Deklaration der DLL-Funktionen
Declare Function AddOne Lib "pbtest1.dll" Alias "ADDONE" (ByVal l As Long) As Long
Declare Sub AddOneInPlace Lib "pbtest1.dll" Alias "ADDONEINPLACE" (ByRef l As Long)
Declare Function UpperCase Lib "pbtest1.dll" Alias "UPPERCASE" (ByVal s As String) As String
Declare Function IsZero Lib "pbtest1.dll" Alias "ISZERO" (ByVal l As Long) As Boolean
Declare Sub SwapLongs Lib "pbtest1.dll" Alias "SWAPLONGS" (ByRef l1 As Long, ByRef l2 As Long)
Declare Sub SwapStrings Lib "pbtest1.dll" Alias "SWAPSTRINGS" (ByRef s1 As String, ByRef s2 As String)
Declare Function SystemDirectory Lib "pbtest1.dll" Alias "SYSTEMDIRECTORY" () As String
Declare Function GetInstalledRamSize Lib "pbtest1.dll" Alias "GIRS" () As Long
Declare Function ShiftRight Lib "pbtest1.dll" Alias "SHR" (ByVal l As Long, ByVal offs As Long) As Long

'Test der DLL-Funktionen
Sub Main()
Dim l1 As Long, l2 As Long
Dim s1 As String, s2 As String
l1 = AddOne(41)
Debug.Assert l1 = 42
AddOneInPlace l1
Debug.Assert l1 = 43
s1 = UpperCase("hällo wörld")
Debug.Assert s1 = "HÄLLO WÖRLD"
Debug.Assert IsZero(l2) = True
l2 = 55
Debug.Assert IsZero(l2) = False
SwapLongs l1, l2
Debug.Assert l1 = 55
Debug.Assert l2 = 43
s2 = CStr(l1)
SwapStrings s1, s2
Debug.Assert s1 = "55"
Debug.Assert s2 = "HÄLLO WÖRLD"
Debug.Assert ShiftRight(16, 1) = 8
Debug.Print "Systemverzeichnis: " & SystemDirectory()
Debug.Print "Installierter Hauptspeicher: " & GetInstalledRamSize() & " KB"
End Sub
            

Wie Sie sehen, machen die Funktionen nichts Besonderes, das in VB nicht auch zu bewerkstelligen wäre (von SHR() und GetInstalledRamSize() mal abgesehen), und nutzen die Möglichkeiten von PB bei Weitem nicht aus. Die Beispiele zeigen lediglich, wie die grundlegenden Datentypen zwischen VB und PB hin- und hergereicht werden.

Wenn Sie erst einmal gesehen haben, wie einfach das geht, werden Sie sicher bald von ganz alleine neugierig, wie Sie Ihre VB-Programme mit Hilfe von PowerBASIC erweitern können, und vielfältige Möglichkeiten entdecken.

Für mich fängt das schon damit an, dass ich grundlegende Funktionalitäten, die ihre Stabilität längst bewiesen haben, gerne in eine DLL auslagere, damit sie mich beim Debuggen nicht weiter stören, wenn ich mal wieder vergessen habe, die Shift- zusammen mit der F8-Taste zu drücken.

Weitere Einsatzbereiche sind die oben erwähnten Situationen, in denen es die Sicherheitsmechanismen von VB schwer oder unmöglich machen, bestimmte Funktionen auszuführen. Dazu gehört auch der Aufruf von Funktionen in DLLs, die eine andere als die StdCall-Aufrufkonvention voraussetzen.

Auch bei sehr zeitkritischen Routinen kommt eine Auslagerung in Betracht, da man in PB vielfältige Optimierungsmöglichkeiten hat. Beachten Sie jedoch, dass VB bei der Übergabe von String-Parametern an eine native Windows-DLL den Unicode-String (2 Byte pro Zeichen) erst in einen ANSI-String (1 Byte pro Zeichen) umwandelt, was natürlich etwas Zeit kostet; das oben gezeigte UpperCase-Beispiel brächte also sicherlich keinen Performance-Vorteil, da der übergebene String in der DLL nicht sonderlich intensiv bearbeitet wird.

Last but not least bietet PB umfangreiche Unterstützung für Threading, so dass Sie auch dieses Feature in Ihren VB-Programmen nutzen können - ohne dass Sie auf ActiveX-Programme angewiesen sind, die auf dem Zielsystem registriert sein müssen.

Das obige Beispiel zeigt, wie man mit PowerBASIC native Windows-DLLs erzeugt - das sind diejenigen, die Sie in VB über Declare-Statements nutzbar machen und die nicht registriert sein müssen (und auch nicht sein können). Mit VB können Sie solche DLLs nicht erzeugen. Daneben gibt es noch COM-DLLs, aus denen Sie im Normalfall Instanzen von COM-Klassen erzeugen können, wenn Sie in Ihrem VB-Projekt einen Verweis auf die betreffende Bibliothek gesetzt haben - dazu muss die DLL allerdings registriert sein. Solche (und nur solche) DLLs können mit VB erzeugt werden. Seit Version 9.0 kann PowerBASIC ebenfalls COM-DLLs erzeugen, wobei diese DLLs gleichzeitig nativ Funktionen exportieren können. Somit können Sie aus PB-DLLs COM-Objekte erzeugen, ohne dass die DLL registriert sein muss. Wie dies geht, werde ich demnächst in einem weiteren Artikel zeigen.

Abschliessend noch der Hinweis: Mit PowerBASIC verlassen wir in vielerlei Hinsicht die "schützende Hülle" von VB; Sie sollten ihren PB-Code daher besonders ausführlich testen.
Hauptseite >  Tips zu VB5/6 >  diese Seite