Portierung leichtgemacht

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


Aus: ST-Computer 06 / 1997, Seite 30

Links

Copyright-Bestimmungen: siehe Über diese Seite