Ein Konzept zum modularen, rechnerunabhÀngigen Programmieren
Im Rahmen der Entwicklung eines gröĂeren Softwarepaketes stand ich kĂŒrzlich vor dem Problem, ein Modul zu entwerfen, welches mit dem Benutzer ĂŒber Tastatur und Bildschirm kommunizieren, gleichzeitig aber möglichst unabhĂ€ngig vom Betriebssystem sein sollte. Einen Weg, dies zu realisieren, möchte ich im Folgenden vorstellen.
Die BetriebssystemunabhĂ€ngigkeit, auch PortabilitĂ€t genannt, war von groĂem Interesse, da das Modul auf möglichst vielen verschiedenen Plattformen lauffĂ€hig sein sollte. Leider ist es nun so, daĂ man bei der Programmentwicklung auf betriebssystemabhĂ€ngige Funktionen angewiesen ist, sobald man Bildschirmausgaben tĂ€tigt, die ĂŒber die Textausgabe (âprintf') hinausgehen oder die Tastatur abfragt und sich dabei nicht auf die Standardeingabe (âget...â oder âscanf') beschrĂ€nken möchte oder kann. Man kommt also kaum gĂ€nzlich daran vorbei, auch Bibliotheken des Betriebssystems zu nutzen. Daher ist es sinnvoll, sich Gedanken darĂŒber zu machen, wie man den Quellcode so strukturieren kann, daĂ sich die Anpassungen an andere Systeme in Grenzen halten.
Modularisierung
Viele verstehen unter Modularisierung, den Quelltext in verschiedenen Quelltextdateien unterzubringen, die dann thematisch zusammengehörige Funktionen beherbergen. Das sorgt zwar schon fĂŒr eine Struktur, doch wenn die Anzahl der Quelltextdateien ein gewisses MaĂ ĂŒberschreitet, wird es trotzdem unĂŒbersichtlich. Deshalb fasse ich mehrere Quelltextdateien zu Bibliotheken zusammen, die ĂŒber eine einfach zu handhabende Schnittstelle zu benutzen sind.
Zum Beispiel könnte man eine Bibliothek schreiben, die es dem Programmierer mit einigen wenigen Funktionsaufrufen erlaubt, ein Rechteck per Tastatursteuerung ĂŒber den Bildschirm laufen zu lassen. Genau dies leistet MOVERECT (Listings 5 - 11). NatĂŒrlich ist dies nur ein vereinfachtes Beispiel. Die Aufgabe der Bibliothek könnte beliebig komplexer sein. Doch beschĂ€ftigen wir uns erst einmal mit der PortabilitĂ€t.
PortabilitÀt
Ziel meines Konzeptes ist es, die einzelnen Bibliotheken betriebssystemunabhĂ€ngig zu halten. Alle betriebssystemabhĂ€ngigen Funktionen sollen âvon auĂen hineingereicht" werden, also von dem Programm zur VerfĂŒgung gestellt werden, das die Bibliothek benutzt. Dies hat den enormen Vorteil, daĂ die Bibliothek von einer Portierung völlig unberĂŒhrt bleibt und nur das Rahmenprogramm abgeĂ€ndert werden muĂ.
Die Bibliothek greift also auf Funktionen zu, die ihr von auĂen zugĂ€nglich gemacht werden. Um dies zu realisieren, hat jede Bibliothek einen sogenannten HOOKTABLE (hook, engl. Haken). Das ist nicht mehr als eine Struktur, die mehrere Funktionspointer, also Zeiger auf Funktionen, enthĂ€lt. Genau eine Instanz dieser Struktur ist als globale Variable definiert. Im HOOKTABLE findet die Bibliothek Zeiger auf all die Funktionen, die ihr von auĂen zur VerfĂŒgung gestellt werden. Diese Zeiger mĂŒssen natĂŒrlich erst einmal initialisiert werden.
Das funktioniert mit der sogenannten Bibliotheksinitialisierung, in unserem Beispiel wĂ€re dies die Funktion âMinit-MOVERECTâ. Dieser Funktion ĂŒbergibt man einige Funktionspointer, die sich die Bibliothek dann im HOOKTABLE merkt. Wenn es jetzt bei Aufrufen von weiteren Funktionen dieser Bibliothek dazu kommt, daĂ Funktionen von auĂen benutzt werden sollen, so schaut die Bibliothek in den HOOKTABLE und fĂŒhrt dann die Funktion aus, auf die der entsprechende Funktionspointer zeigt. Das ist dann eine Funktion des Rahmenprogramms. Sinnvollerweise wird man im HOOKTABLE Zeiger fĂŒr alle die Funktionen einrichten, die betriebssystemabhĂ€ngig sind, da diese dann vom Rahmenprogramm hineingereicht werden können. Somit taucht in der Bibliothek selbst kein Aufruf von Betriebssystemfunktionen auf.
Zum Beispiel erwartet âMinitMOVERECT' einen Zeiger namens âDrawRectâ. Dieser Zeiger sollte sinnvollerweise auf eine Funktion zeigen, die ein Rechteck malt. Eine solche Funktion sei im Rahmenprogramm (hier Listings 1-4) definiert, in unserem Beispiel wĂ€re dies die Funktion âFRDrawRectâ. Man ĂŒbergibt einen Zeiger auf diese Funktion an die Funktion âMinitMOVERECT. Wenn nun im weiteren Verlauf des Programms eine andere Funktion von MOVERECT aufgerufen wird, die evtl, ein Rechteck malen muĂ, so wird diese Funktion im HOOKTABLE nachsehen, einen Zeiger auf die im Rahmenprogramm definierte Funktion âFRDrawRectâ finden und diese ausfĂŒhren.
SYS
Wollte man nun das Rahmenprogramm auf ein anderes System portieren, so mĂŒĂte man nur andere Funktionen bei der Bibliotheksinitialisierung angeben. Das erfordert aber bei jeder Portierung eine AbĂ€nderung des Quellcodes des Rahmenprogramms, was recht unpraktisch ist, wenn man, wie ich, auf zwei Systemen gleichzeitig entwickelt. Aus diesem Grunde definiere ich mir einfach zwei verschiedene Funktionen, die das gleiche machen, den gleichen Prototypen haben, aber fĂŒr verschiedene Betriebssysteme ausgerichtet sind und sorge dafĂŒr, daĂ je nach System nur eine - und zwar die richtige - compiliert wird.
Der Aufbau des fertigen Programmes
Die andere wird vom Compiler einfach ignoriert. Da beide den gleichen Prototypen haben, erspare ich mir damit eine Anpassung der Parameterliste bei der Bibliotheksinitialisierung. Die ĂŒbergebenen Funktionen heiĂen immer gleich, es steckt nur je nach System eine andere dahinter. Alle Definitionen dieser verschiedenen Funktionen werden in einer weiteren Quelltextdatei des Rahmenprogramms untergebracht, nĂ€mlich in SYS.C.
Als Beispiel soll wieder âFRDrawRectâ dienen. Wie in SYS.C (Listing 4) zu sehen ist, ist die Definition von 'FR-DrawRectâ eingeschlossen in eine Compiler-if-Anweisung mit einem else-Zweig. Wenn ich nun SYS.C unter TOS compiliere, so ist das Makro â_TOS_â definiert, und der Compiler bearbeitet den if-Zweig, compiliert also das âFRDrawRectâ, das VDI-Ausgaben macht. Sollte ich SYS.C aber unter DOS auf einem anderen Compiler compilieren, so ist '_TOS_â nicht definiert, der Compiler wĂŒrde also den else-Zweig bearbeiten, der in unserem Beispiel noch leer ist. In diesem else-Zweig wĂŒrde man nun auch eine Funktion âFRDrawRectâ definieren, die aber DOS-Bildschirmausgaben macht. Der Compiler wird dabei nicht einen Fehler âdoubly defined Symbolâ oder Ă€hnliches ausgeben, da er ja nur einen Zweig der if-else-Konstruktion bearbeitet. Wichtig ist, daĂ die Prototypen fĂŒr die VDI- und DOS-Funktion gleich sind. Deshalb stehen sie im Header SYS.H auch nicht in einer Compiler-if-Anweisung.
Die Konstanten-Makros hingegen sind wieder in eine if-Anweisung eingeklammert. Sie stellen bestimmte Tastatureingaben dar und sind deshalb betriebssystemabhĂ€ngig. Man kann auch hier den else-Zweig durch entsprechende Neudefinitionen der Makros, die fĂŒr TOS schon aufgefĂŒhrt sind, fĂŒr ein anderes System, z.B. DOS, ergĂ€nzen. Genauso verhĂ€lt es sich mit den â#includeâ-Anweisungen. Je nach System werden andere Header eingebunden, daher sind auch diese Anweisungen in eine Compiler-if-else-Konstruktion eingebettet.
Arbeitsweise
ZusammengefaĂt funktioniert die ganze Konstruktion wie in Bild 1 dargestellt. Das Hauptprogramm in FRAME.C, das nun gĂ€nzlich betriebssystemunabhĂ€ngig ist, ruft die Bibliotheksinitialisierung auf und teilt der Bibliothek mittels der Parameterliste mit, wo sie bestimmte Funktionen finden kann. Diese Funktionen stammen alle aus der Datei SYS.C. Die Bibliothek kann jetzt jederzeit auf Funktionen von SYS.C zugreifen, und zwar je nach System auf die passende.
Das hat den Effekt, daĂ das Programm ohne AbĂ€nderungen auf zwei Systemen compilierbar ist und lĂ€uft, wobei die Bibliothek MOVERECT noch nicht einmal neu compiliert werden muĂ, da sie systemunabhĂ€ngig ist. Sie kann als Object-File dazugelinkt werden.
Falls Sie die Listings abtippen sollten und noch Einsteiger sind, hier noch ein paar Worte zum konkreten Vorgehen: Sichern Sie die Quellcodes unter den Namen wie angegeben. Compilieren und linken Sie dann das Projekt MOVERECT. Evtl, mĂŒssen Sie in der Projektdatei den Destination-Pfad an Ihre Konfiguration anpassen. Sorgen Sie dafĂŒr, daĂ das Ergebnis des Linkvorgangs in den Ordner der Bibliotheken (meist LIB) geschrieben wird, und kopieren Sie den Header MOVERECT.H in den Ordner der Header (meist INCLUDE). Compilieren und linken Sie nun das Projekt FRAME und es mĂŒĂte ein lauffĂ€higes FRAME.TOS entstehen.
AufgepaĂt!
Im Zusammenhang mit der Entwicklung dieses Konzepts stieĂ ich bei der Portierung auf DOS leider auch auf Probleme. Zum Beispiel erwartete der DOS-Compiler eine Leerzeile nach der letzten Anweisung im Programmtext. Das ist weiter nicht schlimm, aber ein echtes Problem war, daĂ DOS nur ein 16-Bit-System ist. Das wirkt sich zum Beispiel auf das Reservieren von Speicherplatz aus. Man kann hier nicht einfach 1 MB alloziieren, sondern höchstens 65 KB! Mit einem Trick lĂ€Ăt sich diese EinschrĂ€nkung umgehen, nĂ€mlich mit dem sogenannten â__huge'-Pointern. NĂ€heres dazu entnehmen Sie der Online-Hilfe bzw. den HandbĂŒchern Ihres Compilers.
Fazit
Wie gesagt, dies ist natĂŒrlich nur ein kleines Beispiel. In der Praxis sind bei meiner Arbeit schon weitaus umfangreichere Bibliotheken entstanden. Dazu habe ich Rahmenprogramme entwickelt, die dem hier abgedruckten sehr Ă€hneln. Diese Rahmenprogramme kann ich dank der Compileranweisungen in SYS.C jederzeit auf verschiedenen Systemen compilieren, linken und starten. AuĂerdem wirken sich Ănderungen in den Bibliotheken sofort auf die Programmversionen aller Systeme aus, da von allen Versionen dieselbe Bibliothek benutzt wird. Ich brauche nicht fĂŒr jede Version die Ănderungen an der Bibliothek wiederholen.
Dieses Konzept ist also eine durchaus praktische Sache und hat sich in der Praxis bewÀhrt.
Anmerkung:
Die hier veröffentlichten Listings erhalten Sie ĂŒber unsere Spezial-Diskette Nummer 6/97, unser Internet-Angebot oder ĂŒber die Mailbox.
Listings zu "Portierung leichtgemacht"
Listing 1:
; Die Projektdatei von FRAME
FRAME.TOS
.L[-G -L -Y -F]
.C[-C -Y -T -J -Z -M -R -K -P]
=
PCSTART.O
FRAME.C
SYS.C
MOVERECT.O
PCSTDLIB.LIB
PCEXTLIB.LIB
PCTOSLIB.LIB
PCGEMLIB.LIB
PCFLTLIB.LIB
Llsting 2:
/************************************/
/* Das Rahmenprogramm zu MOVERECT */
/************************************/
#include <moverect.h>
#include "sys.h"
/* Hauptprogramm */
int main( void )
{
if( !MInitMOVERECT( FRInitGraphicOutput,
FRDrawRect,
FRExitGraphicOutput,
FRGetCommand, 10, 10 ) )
return( -1 );
if( !MInitMove() )
return( -1 );
MDoMove();
MExitMove();
return( 0 );
}
Listing 3:
/************************************/
/* Das Rahmenprogramm zu MOVERECT */
/************************************/
#ifndef __SYS
#define _SYS
int FRInitGraphicOutput( int *scr_w, int *scr_h );
void FRExitGraphicOutput ( void );
void FRDrawRect( int x, int y, int w, int h );
int FRGetCommand( void );
#ifdef __TOS__
#define KEY_ESCAPE 27
#define KEY_CRSUP 72
#define KEY_CRSDOWN 80
#define KEY_CRSLEFT 75
#define KEY_CRSRIGHT 77
#else
#endif
#endif
Listing 4:
/************************************/
/* Das Rahmenprogramm zu MOVERECT */
/************************************/
#include <moverect.h>
#include "sys.h"
#ifdef ___TOS__
#include <aes.h>
#include <ext.h>
#include <vdi.h>
#else
#endif
int v_handle;
#ifdef __TOS__
int FRInitGraphicOutput( int *scr_w, int *scr_h )
{
int i;
int work_in[11],
work out[57],
dummy;
/* Handle holen */
v_handle = graf_handle{ &dummy, &dummy, &dummy, &dummy );
for( i = 0; i < 10; work_in[i++] = 1 );
work_in[10] = 2;
/* Und Workstation öffnen */
v_opnvwk{ work_in, &v_handle, work_out );
/* Falls es nicht geklappt hat, abbrechen */
if ( !v_handle )
return( 0 );
*scr_w = work_out[0] + 1;
*scr_h = work_out[1] + 1;
return( 1 );
}
void FRExitGraphicOutput( void )
{
v_clsvwk( v_handle );
}
void FRDrawRect( int x, int y, int w, int h )
{
int pxy[4];
pxy[0] = x;
pxy[1] = y;
pxy[2] = x + w - 1;
pxy[3] = y + h - 1;
vswr_mode ( v_handle, MD_XOR );
vsf_interior{ v_handle, FIS_HOLLOW );
v_bar{ v_handle, pxy );
}
int FRGetCommand( void )
{
switch( getch() )
{
case KEY_ESCAPE:
return( M_QUIT );
case KEY_CRSUP:
return( M_RECTUP );
case KEY_CRSDOWN:
return( M_RECTDOWN );
case KEY CRSLEFT:
return ( M_RECTLEFT );
case KEY_CRSRIGHT:
return( M_RECTRIGHT );
default:
return ( 0 );
}
}
#else
#endif
Listing 5:
-
; Die Projektdatei von MOVERECT
F:\EWS\PUREC\LIB\MOVERECT.O
.L[-G -L -Y -J -F -V]
.C[-C -Y -T -J -Z -M -R -K -P]
=
M_MAIN.C
M_DRAW.C
Listing 6:
/******************************************/
/* Der Header zu MOVERECT */
/******************************************/
#ifndef __MOVERECT
#define __MOVERECT
#define M_QUIT 1
#define M_RECTUP 2
#define M_RECTDOWN 4
#define M_RECTLEFT 8
#define M_RECTRIGHT 16
/* Das Modul initialisieren */
int MInitMOVERECT
{ int (*InitGraphicOutput)
(int *scr_wf int *scr_h),
void (*DrawRect)
(int x, int y, int w, int n),
void (*ExitGraphicOutput)
(void),
int (*GetCommand)
(void),
int rect_w, int rect_h );
/* Das Bewegen eines Rechtecks initialisieren */
int MInitMove( void );
/* Das Rechteck bewegen */
void MDoMove( void );
/* Das Bewegen eines Rechtecks abmelden */
void MExitMove( void );
#endif
Listing 7:
/******************************************/
/* Der Header zu MOVERECT */
/******************************************/
#ifndef __MOVERECT
#define __MOVERECT
#define M_QUIT 1
#define M_RECTUP 2
#define M_RECTDOWN 4
#define M_RECTLEFT 8
#define M_RECTRIGHT 16
/* Das Modul initialisieren */
int MInitMOVERECT
{ int (*InitGraphicOutput)
(int *scr_wf int *scr_h),
void (*DrawRect)
(int x, int y, int w, int n),
void (*ExitGraphicOutput)
(void),
int (*GetCommand)
(void),
int rect_w, int rect_h );
/* Das Bewegen eines Rechtecks initialisieren */
int MInitMove( void );
/* Das Rechteck bewegen */
void MDoMove( void );
/* Das Bewegen eines Rechtecks abmelden */
void MExitMove( void );
#endif
Listing 8:
/**********************************************/
/* Die Strukturdefinitionen von MOVERECT */
/**********************************************/
#ifndef __M_STRUCT
#define __M_STRUCT
typedef struct
{
int (*InitGraphicOutput)
(int *scr_w, int *scr_h);
void (*DrawRect)
(int x, int y, int w, int h);
void (*ExitGraphicOutput)(void);
int (*GetCommand)(void);
} M_HOOKTABLE;
typedef struct
{
int scr_w, scr_h;
int x, y, w, h;
} M_GLOBALS;
#endif
Listing 9:
/*****************************/
/* Das Modul MOVERECT */
/*****************************/
#ifndef __M_DRAW
#define __M_DRAW
/* Ein Rechteck malen */
void MDrawRect( void );
#endif