Die Speicherzuordnung in RPG-Programmen wird, OPM (Original Program Model) lässt grüßen, vom Compiler zur Übersetzungszeit vorgenommen. Das hat den Vorteil, dass der Speicherbedarf bereits zur Übersetzungszeit bekannt ist und dass vom Compiler Regeln abgeprüft werden können, wie der Speicher später verwendet werden soll. Für den Programmierer hat dies allerdings zur Folge, dass keine dynamischen Arrays und Mehrfach-Datenstrukturen möglich sind – von verschachtelten Strukturen vorher nicht festgelegter Tiefe ganz zu schweigen.

Grenzen des Wachstums

Seitdem RPG dem erlauchten Kreis der ILE- (Integrated Language Environment-) Welt beigetreten ist, kann man auf die entsprechenden C-APIs zurückgreifen und es sind sogar eigene RPG-Operationen und so genannte BIFs (Built in Functions) zur dynamischen Speicherverwaltung hinzugekommen. Ehe man nun froh zu Werke geht, empfiehlt es sich jedoch, sich mit den Erfahrungen vertraut zu machen, die C-Programmierer schon seit den ersten Tagen von C mit Pointern und Speicherzuordnung zur Laufzeit gemacht haben.

Wenn man sich zur Laufzeit Speicher zuordnen kann, geht das auch in einer Endlosschleife. Dies hat dann zur Folge, dass das Programm proportional zur Laufzeit in seinem Speicherbedarf wächst und das solange, bis die Laufzeitumgebung dem ein Ende bereitet oder ihr selber die Luft ausgeht. So manches Windows-Programm, das nicht mehr zu reagieren scheint, leidet an dieser Krankheit.
Um dieser Luftknappheit vorzubeugen, kann und soll man in Programmen, in denen man Speicher dynamisch anfordert, Speicher auch wieder freigeben. Ob hiervon Gebrauch gemacht wird, obliegt allerdings dem Programmierer – wem sonst. Die Freigabe von Speicher stellt allerdings nunmehr wieder selber eine Fehlerquelle dar; der Versuch, auf nicht mehr zugeordneten Speicher zuzugreifen, führt ins Nirwana und damit zu Laufzeitfehlern.

Zur Laufzeit angeforderter Speicher wird über Pointer verwaltet und im Programm angesprochen. Mit diesen Pointern wird nun gerechnet und es werden Zuweisungen an Pointer-Variablen vorgenommen. Programmfehler in diesen Bereichen können nun wieder zu Problemen der Laufzeit-Umgebung führen. So mancher Blue Screen geht auf das Konto von Fehlern dieser Art.
Mit der Verwendung dieser Techniken der C-Umgebungen kommt jetzt auch eine neue Art von Fehlern und erhöhter Destabilisierung der Ablauf-Umgebungen in die geruhsame Welt der RPG-Programme.

Ein feiner Unterschied

Java gilt als stabiler und gutmütiger als C. Das resultiert ganz wesentlich daraus, dass die Entwickler von Java bei C abgeschrieben haben und zum wesentlichen Teil selber C-Programmierer waren. Bei der Entwicklung von Java wurde versucht, die Instabilitäten von C-Programmen – soweit es geht – zu vermeiden und die Ursachen zu beseitigen. Der Weg des Verzichts auf Speicherzuordnung zur Laufzeit, den RPG zunächst ging, schied aus. Gerade objektorientierte Programme brauchen eine flexible Speicherverwaltung, da Objekte sich baumartig aufbauen und sich ja alles um Objekte dreht.

Der Weg, den die Java-Entwickler genommen haben, bestand darin, alle fehlerträchtigen Konstruktionen, soweit dies möglich ist, innerhalb der Sprache zu kapseln. Wenn der Programmierer selber nicht mit Pointern rechnen darf, sondern diese nur unter Namen ansprechen kann, die zur Übersetzungszeit vergeben werden, dann kann er sich nicht verrechnen. Wenn alle Zuweisungen auf Typverträglichkeit vom Übersetzer geprüft werden, kann zur Laufzeit keine Zuordnung stattfinden, die die Ablaufumgebung tangiert. Wenn Speicher automatisch freigegeben wird, sobald die JVM (Java Virtual Machine) erkennen kann, dass er nicht mehr benötigt wird, kann die Freigabe nicht vergessen werden.

Die schwachen Typprüfungen des RPG-Compilers kann man durch noch so defensiven Programmierstil nur sehr begrenzt nachbessern. Die Notwendigkeit der expliziten Freigabe von dynamisch zugeordnetem Speicher kann man ebenfalls nicht umgehen. Was bleibt, ist die Möglichkeit, all dies in einem Modul zu kapseln. Damit nimmt man anderen Programmierern ab, mit fehlerträchtigen Konstruktionen umgehen zu müssen und öffnet dennoch die Möglichkeiten, flexibler zu programmieren.

Die Hashtable, ein universeller Behälter

Java hat eine Fülle an Möglichkeiten, flexibel Speicher zuzuordnen. Da gibt es dynamische Arrays in zahlreichen Varianten, mit fein gestuften Eigenschaften. Zusätzlich gibt es eine ganze Reihe von weiteren Container-Objekten, in denen man andere Objekte speichern kann; selbstverständlich geht das dann auch wieder geschachtelt und man kann auch beliebig viele gleichartige Container gleichzeitig im selben Kontext verwenden. Vieles hiervon könnte man nun in RPG nachzubauen versuchen. Wenn man in Java-Programmen allerdings nachsieht, welche dieser Container-Objekte in der Beliebtheitsskala ganz oben stehen, also am häufigsten verwendet werden, dann stößt man immer auch auf die so genannte Hashtable.

Eine Hashtable in Java kann man sich wie eine Garderobe vorstellen: Man gibt etwas ab und bekommt eine Marke zurück. Zeigt man die Marke vor, bekommt man seine Sachen wieder. Wenn die Garderobiere freundlich ist, kann man die unterschiedlichsten Sachen abgeben, ohne sich darum zu kümmern, ob der vorgesehene Platz dafür ausreicht. Ist dieser knapp bemessen, wird eben eine Tasche an den Fuß des Garderobenständers gestellt und auf einem Zettel notiert, zu welcher Marke dies gehört.

In Java geschieht das Abgeben dann über eine Methode der Hashtable, die den Namen put hat; mit dem Aufruf von put übergibt man ein beliebiges Objekt und den Namen, unter dem man es wieder haben will. Mit dem Aufruf der Methode get, der man den Namen mitgeben muss, bekommt man dann das entsprechende Objekt wieder zurück. Mit diesem Mechanismus kann man nun beliebige Objekte zur Laufzeit in einer Hashtable speichern, ohne sich um die entsprechenden Speicheranforderungen zu kümmern.

Ein RPG-Modul als Hashtable

Das Grundprinzip einer Hashtable dient nun als Schablone, dynamische Speicheranforderung in einem RPG-Modul so zu kapseln, dass andere Programme dynamisch Speicher verwenden können, ohne mit Pointern umzugehen und sich um Freigabe zu kümmern.

Für die RPG-Variante einer Hashtable benötigen wir neben den Prozeduren put und get, die wir bereits kennen gelernt haben, noch die Prozeduren remove und clearAll, die in der Java-Variante ebenfalls Pendants haben. Auf einige Eigenschaften des Java-Originals verzichten wir, da sie entweder in RPG nicht einsetzbar oder entbehrlich sind.

Im Original der Hashtable kann man Objekte speichern – und in RPG gibt es nun mal keine Objekte. Betrachtet man solche als Datentyp, dann entspricht diesem in RPG noch am ehesten eine Datenstruktur und ebendiese werden wir nun in unserer RPG-Variante als Speicher-Objekt verwenden. Die Flexibilität ist dadurch sehr groß, da Datenstrukturen universell einsetzbar sind, um in RPG-Programmen nahezu beliebig andere Daten abzubilden.

Die Quelldateien für den Prototyp und die Implementierung werden HASHTABLE heißen. Der Prototyp wird dann in der QRPGLEH gespeichert und das Programm in der QRPGLESRC. In dem nun folgenden Prototyp werden die Schnittstellen-Beschreibungen der Prozeduren als Prototypen beschrieben.

Die einleitenden Anweisungen für den Präprozessor schützen gegen unbeabsichtigtes doppeltes Einbinden, was zu Fehlern bei der Umwandlung führen würde. Alle Prozeduren werden mit dem Schlüsselwort EXTPROC umbenannt; damit alle exportierten Namen eindeutig sind, wird der Name des Moduls als Präfix vorangestellt.

Die Prozedur put, mit der Datenstrukturen zum Speichern übergeben werden können, liefert in einem Boolean-Wert zurück, ob alles geklappt hat; viele RPG-Programmierer nennen diesen Datentyp noch Indicator. Als Übergabe-Parameter werden der Name, unter dem man die Datenstruktur später zurück haben will, die Datenstruktur selber und die Länge der Datenstruktur erwartet. Die Anweisungen für den Präprozessor (/IF, /ELSE, /ENDIF) sollen an dieser Stelle dafür sorgen, dass in der Implementierung mit Pointern und beim Aufruf ohne Pointer gearbeitet werden kann. Beim Aufruf müssen dann alle Parameter gefüllt werden und es wird zurückgemeldet, ob es geklappt hat.

Die folgende Prozedur get hat die identische Schnittstelle – wie get. Bei der Verwendung von get werden wieder alle drei Übergabe-Parameter übergeben; der zweite Parameter enthält nach dem Aufruf den abgespeicherten Inhalt aus der Hashtable.

Mit dem Aufruf von remove kann ein Objekt aus der Hashtable gelöscht werden; die entsprechenden Bereinigungsaufgaben und die Speicherfreigabe erfolgen dann automatisch.
Die Prozedur clearAll bereinigt die komplette Hashtable und gibt (fast) allen Speicher frei.
Für die Implementierung wird noch ein weiterer Prototyp für die Verwendung der C-APIs memcpy benötigt, die in einer eigenen Copy-Strecke abgelegt ist.

Die C-Funktion memcpy kopiert die Anzahl der Byte, die im dritten Parameter length übergeben werden (von der Speicher-Adresse des ersten Parameters zur Speicher-Adresse des zweiten Parameters), und wird in unserer Hashtable als Hilfsfunktion verwendet.

Struktur des Moduls Hashtable

Die Prototypen der exportierten Prozeduren werden mit der /COPY-Anweisung des Pre-Compilers eingebunden – ebenso wie der Prototyp von memcpy. Die einleitende H-Zeile mit der Angabe NOMAIN deutet bereits an, dass aus dem Modul später ein Service-Programm erstellt werden soll. Die Parameter der Erstellungsbefehle sind als Kommentar in der Quelle zu sehen; die Umwandlung kann am bequemsten mit dem Präprozessor (Oktober-Ausgabe 2002 des Midrange Magazins, online aus dem Archiv abrufbar unter www.MidrangeMagazin.de) erfolgen; die Parameter können allerdings auch von Hand eingetragen werden.

Das Service-Programm bekommt eine eigene Aktivierungsgruppe zugeordnet, um die Aktivierung und Deaktivierung der Hashtable besser steuern zu können. Die Angabe des Binding Directories QC2LE ist erforderlich, damit memcpy beim Binden gefunden wird. Beim Reinziehen der Prototypen wird über die /DEFINE-Anweisung erreicht, dass die Pointer-Variante der Deklaration des Prototyps verwendet wird. Als lokale Prozeduren werden noch alocBloc und find verwendet, deren Prototypen im Deklarationsteil zu finden sind. Die Konstanten TRUE und FALSE sollen die Verwendung von Indicator als Boolean-Variablen vereinfachen.

In den globalen Deklarationen befindet sich das Gedächtnis der Hashtable, das aus den zwei Arrays Key und ObjectP besteht. Beide können maximal 32 767 Einträge aufnehmen, die Angabe des Schlüsselwortes BASED bei der Deklaration bewirkt, dass für diese beiden Variablen kein Speicher vom Compiler zugewiesen wird. Die beiden Pointer Keyp und ObjectPP in den BASED-Anweisungen werden vom Compiler automatisch als deklariert angenommen und angelegt.
In dem Array Key werden die Namen abgelegt, unter denen Datenstrukturen zu suchen sind. In dem zweiten Array ObjectP werden Pointer auf die gespeicherten Datenstrukturen abgelegt; der eigentliche Speicherplatz für die Datenstrukturen wird dann dynamisch angefordert, ebenso wie der Speicherplatz für die Arrays selber, der nach Bedarf erweitert wird.

Die zusätzlichen Steuer-Variablen allocated, size, used und current dienen in den Prozeduren dazu, den Status des Moduls über die komplette Laufzeit konsistent zu halten. Die Variable allocated steuert die Initialisierung. Zu Beginn beinhaltet sie FALSE und wird beim ersten Zuweisen von Speicher zu den beiden Arrays auf TRUE gesetzt, währen clearAll den Speicher bereinigt und allocated wieder auf FALSE setzt.

Die Variable size enthält die aktuell verfügbare Anzahl an Elementen der Arrays, used verwaltet die höchste verwendete Nummer und current zeigt auf den aktuellen Platz der zuletzt verwendeten Operation.

Abspeichern einer Datenstruktur

Bevor wir uns die Prozedur put ansehen, die zum Einlagern von Daten aufgerufen wird, befassen wir uns zunächst mit der Hilfsfunktion alocBloc, die von put verwendet wird. Immer dann, wenn alle Plätze der Arrays belegt sind, wird durch Aufruf von alocBloc Speicherplatz für weitere 100 Einträge angefordert.

Zu Beginn der Prozedur wird size, das die Anzahl der Elemente enthält, um 100 erhöht; hier wäre auch ein anderer Wert denkbar – je nach Einsatz der Hashtable. Über die Variable allocated ist bekannt, ob dies die erste Anforderung von Speicher ist oder ob das Array vergrößert werden soll.

Wird das Array lediglich vergrößert, wird mit der Built in Function %realloc der neu berechnete Speicherplatz an die beiden Pointer Keyp und ObjectPP gebunden, die auf die Arrays Key und ObjectP zeigen.

Bei der ersten Speicher-Zuweisung muss statt %realloc %alloc verwendet werden; Verwechslungen führen hier zu Laufzeitfehlern. Aus diesem Grund darf bei der Erstzuweisung auch nicht vergessen werden, die Variable allocated auf TRUE zu setzen, damit beim Holen des nächsten Blockes alles seine Ordnung hat.

Die Prozedur put prüft zunächst, ob die Arrays erweitert werden müssen. Wenn die benutzten Zellen der Arrays die momentane Größe erreicht haben, wird mit alocBloc neuer Speicher zugeordnet. Im nächsten Schritt wird mit der Prozedur find, die im Zusammenhang mit get untersucht werden wird, festgestellt, ob der angeforderte Name bereits vorher zum Speichern verwendet wurde. In diesem Fall wird die Speicher-Zuordnung für die Datenstruktur an den angeforderten Wert mit %realloc angepasst.

Ist der Name neu, wird nach einem eventuell frei gewordenen Platz gesucht. Vor der Wiederverwendung muss mit %alloc Speicher angefordert werden, da es sich um eine Neuanforderung handelt. Ist kein freier Platz mehr vorhanden und der Name neu, dann wird die nächste Zelle verwendet. Hierzu wird used um eins erhöht und current auf die entsprechende Hausnummer gesetzt. Auch für diesen Platz muss dann Speicher für die Datenstruktur angefordert werden.

Bei neuem Schlüsselwert wird die Zelle im Key Array mit dem Schlüsselwert belegt, der als Parameter übergeben wurde. Bei allen vorkommenden Fällen wird dann abschließend der in der Datenstruktur übergebene Dateninhalt mit der C-Funktion memcpy in den vorher angeforderten Speicher kopiert und die Anfangsadresse in dem ObjectP Array als Pointer abgespeichert.

Abholen von Daten

Zum Abholen der Daten, was durch den Aufruf von get geschieht, wird mit derselben Prozedur find, die auch zum Auffinden freier Plätze verwendet wurde, der Platz gesucht, der dem Schlüsselwert entspricht. Diese Prozedur benutzt die Built in Function %lookup dazu, den Schlüsselwert in dem Key Array zu suchen; die Suche geht dabei nur über die benutzten Blöcke. Bei der Anweisung return wird dann zurückgegeben, ob ein Treffer zu verzeichnen war.

War dies nicht der Fall, gibt get seinerseits ebenfalls FALSE zurück. War die Suche erfolgreich, wird der Inhalt aus der entsprechenden ObjectP-Zelle in den übergebenen Parameter kopiert. Damit dies im aufrufenden Programm ankommt, müssen die verwendeten Prototypen genau übernommen werden. Im aufgerufenen Programm wird der zweite Parameter als Pointer mit dem Schlüsselwort VALUE verwendet. Das aufrufende Programm übergibt eine Datenstruktur per Referenz; ob man dies versteht, ist nicht so ganz wichtig, wenn man sich genau daran hält, funktioniert es.

Freigabe von Speicher

Damit nicht unnötig Speicher belegt bleibt, kann ein anforderndes Programm mit remove mitteilen, dass einzelne Datenstrukturen in der Hashtable nicht mehr benötigt werden.

Die Prozedur remove sorgt dann dafür, dass der entsprechende Schlüsselwert in dem Key Array auf „Blank“ gesetzt wird. Der dazu gehörende Speicher für die Datenstruktur wird dann mit der RPG-Anweisung dealloc freigegeben; die Erweiterung(n) bei der dealloc-Anweisung sorgt dafür, dass der zugehörige Datenpointer auf „null“ gesetzt wird.

Durch den Aufruf von clearAll wird sämtlicher dynamisch zugeordneter Speicher zurückgegeben und alle Variablen entsprechend initialisiert. Danach kann dann wieder Speicher angefordert werden – soweit man dies so möchte.

Verwendung der Hashtable

Zur Illustration, wie so eine Hashtable verwendet wird, habe ich ein kleines Testprogramm angefügt. Bei der Erstellung muss das Service-Programm Hashtable mit gebunden werden, damit die Aufrufe von get, put, remove und clearAll gefunden werden.

Im Testprogramm sind zwei Datenstrukturen deklariert; es können aber auch beliebige andere Datenstrukturen mit put in die Hashtable gestellt werden – auch extern beschriebene, versteht sich.

Die Prototypen werden mit der /COPY-Anweisung eingebunden. Es werden automatisch die Varianten ohne Pointer verwendet, sodass beim Aufruf von put und get die Datenstrukturen direkt übergeben werden.

An diesem kleinen Beispiel ist bereits zu sehen, dass dynamische Speicherverwaltung verwendet werden kann, ohne dass man in der Anwendung selber Pointer verwendet – oder gar mit diesen rechnet. Alle fehlerträchtigen Operationen sind in das Modul Hashtable verlagert und damit dort gekapselt.

Mit Verwendung einer solchen Hashtable kann man nun zum Beispiel Auftragspositionen in dem Auftragsverwaltungsprogramm in einer dynamischen Struktur speichern. Man muss sich nur den ersten und den letzten Key merken, wenn man die Positionsnummer dafür verwendet. Spielereien mit QTEMP und OVRDBF, die zu den riskanten Praktiken gehören, werden damit überflüssig.

Den Autor Dieter Bender erreichen Sie unter dieter.bender@midrangemagazin.de