Gepufferte Ein- und Ausgabe für M2-Modula Seitdem ich in Modula-2 programmiere und für die Ein- und Ausgabe das Modul InOut benutze, hat mich die langsame Geschwindigkeit geärgert und deshalb kam in mir das Bedürfnis auf, etwas dagegen zu tun. Beim Programmieren mit C ist man ja von dessen schneller Ein- und Ausgabe verwöhnt, wieso sollte das nicht auch mit Modula-2 gehen. Nach ein paar Experimenten mit Amiga-Dos konnte die Langsamkeit von InOut nur damit zu tun haben, daß jedes Zeichen einzeln ein- oder ausgegeben wird. Dagegen hilft nur eins: Gepufferte Ein- und Ausgabe. Mein neues Modul NewInOut ist ein vollständiger Ersatz für InOut. InOut braucht von nun an nicht mehr benutzt zu werden. Alle Prozeduren ( bis auf WriteOct ), die von N.Wirth vorgeschlagen wurden, werden unterstützt. Zusätzlich gibt es noch ein paar neue Prozeduren, die die Arbeit mit Amiga-Dos vereinfachen. Wenn ein Programm abstürzt oder es mit CTRL-c abgebrochen wird, werden alle geöffneten Files geschlossen. Es geht also kein Speicherplatz verloren und Fensterleichen bleiben nicht zurück. Ein- und Ausgabeumleitungen im Cli werden übernommen. Wenn das Programm von der Workbench gestartet wird, wird automatisch ein neues Fenster geöffnet ( etwas größer als das von InOut ). Alle Prozeduren in NewInOut sind darüber hinaus erheblich schneller als die in InOut. Man mag einwenden, daß M2-Modula auch noch ein Modul File- System liefert. Doch man hat sicherlich keine Lust, alle schon geschriebenen Programme oder Programme, die man in irgendwelchen Zeitschriften und Büchern entdeckt umzu- schreiben. Deshalb ist, bis auf ein paar Ausnahmen, die später beschrieben werden, NewInOut vollständig kompatibel zu InOut. Nur der Name in der Importliste eines Programms muß von InOut in NewInOut umgeschrieben werden. Prozeduren in InOut, die auch in NewInOut vorhanden sind: OpenInput, OpenOutput, CloseInput, CloseOutput, Read, ReadString, ReadInt, ReadLongInt, ReadCard, ReadLongCard, Write, WriteLn, WriteString, WriteInt, WriteCard, WriteHex; darüber hinaus noch die boolsche Valiable done und die Konstante eol. Wenn man diese Prozeduren bisher benutzt haben, braucht man an einem Programm ( fast ) nichts zu verändern. Inkompatibilitäten zu InOut: Die Prozeduren OpenInput und OpenOutput unterstützen nicht mehr die Angabe einer Extension. Wenn man also bisher ... OpenInput ( ".mod" ); ... geschrieben hat, konnte man einen zur Laufzeit einzugeben- den Filenamen mit einem Punkt beenden, und es wurde "mod" an das Ende angehängt. Jetzt muß man ... OpenInput ( "" ); .. schreiben und den Filenamen vollständig ausschreiben, z.B. "Programm.mod", wenn man nur ein '*' eingibt, wird die Ein- oder Ausgabe nicht umgeleitet ( es wird das aktuelle Window weiterbenutz ). Die Tastenkombination CTRL - '\' ( neben der Backspace - Taste ) simuliert ein eof - Zeichen, wenn man die Eingabe vom Bildschirm beenden wollen. Wenn man einen String als Parameter angibt, wird versucht das File gleichen Namens als neue Ein- oder Ausgabefile zu öffnen. Alle Prozeduren die eine Zahl anfragen, z.B. ReadCard ( zahl ), verlangen am Ende der Eingabe ein eol - Zeichen ( weil die Prozedur ReadStringFile benutzt wird ). Wenn in einer Datei also eine Menge von Zahlen stehen, müssen diese mit eol getrennt sein und nicht wie bisher mit einem ungültigen Zeichen, z.B. ein Space. Beispiel: bisher: zahl1 zahl2 zahl3 ... jetzt: zahl1 zahl2 zahl3 ... Bei Fileende muß nun nicht mehr extra das gelesene Zeichen auf eof überprüft werden, denn done wird beim Erreichen des Fileendes FALSE. Beispiel: bisher: ... WHILE ( c # eof ) AND done DO READ ( c ); ... jetzt: ... WHILE done DO READ ( c ); ... Das war alles, was man beachten muß, um alte Programme anzupassen. Neue Prozeduren in NewInOut: Open, Close, WriteLnFile, WriteStringFile, ReadStringFile, FlushOutput, ReadCardFile, ReadLongCardFile, ReadIntFile, ReadLongIntFile, WriteCardFile, WriteIntFile, WriteHexFile ; dazu der Datentyp FILE, die Konstanten ModeNew und ModeOld und die Variablen stdin und stdout. Das wichtigste an diesen neuen Sachen ist der Datentyp FILE. Um die neuen Prozeduren anzusprechen, wird immer ein Parame- ter vom Typ FILE gebraucht. Er ist ein RECORD, in dem zwei Puffer für die Ein- und Ausgabe, ein paar Zeiger auf die aktuelle Stelle in diesen Puffern und ein Zeiger auf die Amiga-Dos Filehandlestruktur. Das alles braucht den Nur- Anwender nicht zu interessieren. Wichtig ist nur, daß jedes neue File auch eine Variable vom Typ FILE braucht, z.B: ... VAR out : FILE; ... out = Open ( "Name", ModeNew ); ... Close ( out ); ... Hiermit wird ein File mit Namen "Name" neu ( ModeNew ) eröffnet. Wenn etwas schief läuft, wird done auf FALSE gesetzt. Das sollte also immer abgefragt werden! Am Schluß wird out wieder geschlossen. Wer schon mit C programmiert hat, den wird dieses alles sehr bekannt vorkommen. Alle neuen Prozeduren, an die ein "File" angehängt ist benötigen einen Parameter vom Typ FILE. Ansonsten machen sie das gleiche, wie ihre Namensvettern ohne "File", nur kann man jetzt das Ziel ihrer Ein- oder Ausgabe explizit angeben, z.B: ... WriteStringFile ( out, "Hallo, Welt!" ); WriteLnFile ( out ); In den Variablen stdin und stdout stehen die Files, die die Standartein- und Standartausgabe bezeichnen. Wenn man im Cli die Standartein- und Ausgabe umgeleitet hat, z.B. mit MeinProg Ausgabe steht in stdin das File Eingabe und in stdout das File Ausgabe. Die Prozeduren OpenInput und OpenOutput belegen die Variab- len stdin und stdout mit neuen Werten, so daß man die Proze- duren ohne "File" auch so schreiben kann: WriteString ( String ) wird zu WriteStringFile ( stdout, String ). Read ( c ) wird zu ReadFile ( stdin, c ) usw. Die Konstanten ModeNew und ModeOld werden als zweiter Para- meter für Open gebraucht. ModeNew wird gebraucht, wenn man ein neues File zum Schreiben öffnen will. Ein schon bestehendes File wird gelöscht. Fenster werden auch mit ModeNew geöffnet. ModeOld wird gebraucht, um ein bestehendes File zum Lesen zu öffnen. Der Ausgabepuffer wird genau dann vollständig in die Ausgabe geschrieben, wenn a) Der Puffer voll ist. b) WriteLn oder WriteLnFile aufgerufen wird. c) Irgend eine Read... Prozedur aufgerufen wird. d) Explizit FlushOutput aufgerufen wird. e) Close aufgerufen wird. f) Das Programm abstürzt oder normal beendet wird. Die Prozedur FlushOutput braucht man wenn man einen Text ( ohne WriteLn ) ausgibt z.B. "Bitte Warten" und danach eine lange Berechnung erfolgt. Bieser Text wird unter Umständen erst ausgegeben wenn nach der langen Berechnung wieder eine Ausgabe kommt. Wenn man nicht will, daß bei jeder neuen Zeile der Ausgabepuffer geflusht wird kann man statt WriteLn -> Write ( eol ) WriteLnFile ( out ) -> WriteFile ( out, eol ) schreiben. Realisierung der gepufferten Ein- und Ausgabe Für die gepufferte Ein- und Ausgabe braucht man, wie der Name schon sagt, Puffer. Weil ein File, z.B. der Bildschirm Ein- und Ausgabefile sein kann; am besten zwei: einen für die Eingabe, einen für die Ausgabe. Öffnet man ein File mit Open, sind die Puffer immer leer. Wenn man Zeichen ausgibt, wird es nicht sofort mit der Amiga-Dos Funktion Write in das angegebene File geschrieben, sondern in den Puffer. Erst, wenn der Puffer voll ist, wird der gesamte Puffer in das File geschrieben und der Puffer wieder als leer markiert. Es wird also nicht 500 mal ein Zeichen, sondern ein mal 500 Zeichen in das File geschrieben. Weil aber Operationen im Hauptspeich- er des Computers viel schneller sind als Zugriffe auf externe Speicher ( z.B. Diskette ), läuft die Ausgabe nun viel schneller ab. Die Datenstruktur FILE ist als RECORD mit zwei Puffern aufge- baut. Für den Ausgabepuffer wird nur ein Zeiger gebraucht, der anzeigt, wann der Puffer voll ist. Für den Eingabepuffer braucht man zwei Zeiger, weil man mit der Tastatur z.B. nur wenige Zeichen für einen Dateinamen eingeben muß. Ein Zeiger zeigt also auf die Anzahl der Zeichen im Puffer und der andere auf das Zeichen, das mit ReadFile an das Programm übergeben werden soll. Zeigen beide Zeiger auf die selbe Stelle, ist der Puffer leer und es werden mit der Amiga-Dos Funktion Read wieder neue Zeichen angefordert. Realisierung des Schließens aller geöffneten Files Alle einmal geöffneten Files werden in einer einfachen linearen Liste verwaltet. Ein mit Open neu geöffnetes File wird immer an das Ende dieser Liste angehängt. Bei Programm- ende ( normales Ende, Programmabsturz oder Programmunter- brechung ) wird die Prozedur TermNewInOut aufgerufen, in der alle Files in dieser Liste durchlaufen und nacheinander geschlossen werden. Eigentlich braucht man den Befehl Close gar nicht. Zusätzliche Programme: Damit die Mathematiker nicht alleingelassen werden, habe ich auch alle Realzahlenformate mit entsprechenden Modulen bedacht, die da sind: NewRealInOut, NewFFPInOut, NewLongRealInOut. Die Ausgabeprozeduren dieser Module haben alle folgendes Format: Write... ( ... ; m, n : CARDINAL ); m ist dabei die Feldbreite, in die die Zahl geschrieben wird. Ist m positiv, wird rechtsbündig, ist m negativ, wird linksbündig ausgerichtet. n ist die Anzahl der Nachkommastellen. Die Mathe-Module exportieren auch alle eine eigene Variable zum feststellen des Erfolges der letzten Prozedur und zwar ffpdone, readdone und longrealdone. Ich habe sie umbenammt um nicht mit InOut.done in konflikt zu geraten. Das Programm More zeigt alle Arten von Dateien auf dem Bildschirm an und ist ein einfaches Beispiel für die Mächtigkeit des neuen Moduls NewInOut. Die Programme OldSimpleType und NewSimpleType zeigen die Kompatibilität zwischen InOut und NewInOut. In OldSimpleType wird InOut benutzt und in NewSimpleType NewInOut. In den Importlisten können InOut durch New- InOut ersetzt werden und umgekehrt. Die Programme zeigen auch deutlich den Geschwindigkeitsvorteil von NewInOut. Das Programm CrashTest öffnet solange Windows bis kein Speicher mehr frei ist. Beim Programmabbruch werden alle geöffneten Windows wieder geschlossen ( dauert ein bischen ). Das Programm ZahlenTest testet die Module NewInOut, NewFFPInOut, NewRealInOut und NewLongRealInOut auf die Ein- und Ausgabe von Zahlen. Das Programm ZahlenFILETest schriebt ein paar Zahlen in eine Datei und ließt sie danach wieder aus derselben aus. Unterstützung von raw: Fenstern ( Nur für Programmierfreaks empfohlen! ) Wenn man die Prozedur ReadStringFile und die Anweisungen zum Starten des Moduls umschreibt werden raw: Fenster unterstützt. D.h., alle Eingaben in dieses Fenster werden sofort dem Programm übergeben. Z.B. muß die Eingabe eines einzelnen Zeichens nicht mehr mit ENTER abgeschickt werden. Eingegebene Zeichen werden von ReadStringFile bzw. ReadString extra auf den Bildschirm "geechot", weil sie sonst nicht sichtbar sind. Leider ist mit raw: Fenstern die Umleitung der Ein- und Ausgabe im Cli nicht möglich, weil auch beim Programmstart aus dem Cli ein neues Fenster geöffnet wird ( ReadStringFile arbeitet leider nicht mehr in con: Fenstern ). Will man nur einzelne Zeichen in raw: Fenst- ern eingeben braucht man NewInOut nicht umzuschreiben, das ist nur für String- und Zahleneingabe notwendig. Bei String- und Zahleneingabe wird das abschließende eol-Zeichen nicht auf den Bildschirm geechot. Wenn Zeichen mit Read oder ReadFile eingelesen weden, muß man die Zeichen exra "echoen". Mit con: Windows: ... WHILE done DO Read ( c ); END; ... Mit raw: Windows: ... WHILE done DO Read ( c ); Write ( c ); END; ... Damit verhält sich NewRawInOut im wesentlichen wie MS-DOS. Weiter unten ist beschrieben, was in Modul NewInOut ausgetauscht werden muß um raw: Fenster verarbeiten zu können. Sonstiges: Die Eingabe von einem con: Fenster hat einen Nachteil: Wenn man nur ein Zeichen eingeben will, z.B. nur ein 'j' oder 'J' als Bestätigung einer Anfrage, werden in Wirklichkeit zwei Zeichen an das Programm übergeben, nämlich 'j' oder 'J' und zusätzlich noch das eol - Zeichen. Wenn nun nach dieser Zeicheneingabe noch eine Zahleneingabe folgt, wird eol übergeben, was ja nicht beabsichtigt ist. Beispiel: ... WriteString ( "Weiter? (j/n) " ); Read ( Antwort); IF CAP ( Antwort ) = 'J' THEN WriteString ( "Zahl eingeben: " ); ReadCard ( Zahl ); ... Obiges Programm funktioniert nicht ( auch nicht mit InOut ), stattdessen sollte man eingeben: ... WriteString ( "Weiter? (j/n) " ); Read ( Antwort); IF CAP ( Antwort ) = 'J' THEN WriteString ( "Zahl eingeben: " ); REPEAT ReadCard ( Zahl ) UNTIL done; ... ReadCard liefert bei Eingabe eines eol - Zeichen done = FALSE. Das Programm läuft jetzt wie beabsichtigt. Mit InOut kommt man gar nicht weiter, da ReadCard ein eol - Zeichen als die Zahl 0 interpretiert wird und done nicht auf FALSE gesetzt wird. Programmfragment NewRawInOut Um das Modul NewInOut für raw: Windows umzuschreiben muß die Prozedur ReadStringFile und der Modul-Start-text ausgetauscht werden. Das ist aber nur Programmierfreaks zu empfehlen. Ich selber habe raw: Windows nur ein mal benutzt. Mit con: Windows sollte man eigentlich auskommen. Die Beispielprogramme More, NewSimpleType, ZahlenTest und ZahlenFILETest arbeiten auch mit NewRawInOut zusammen. Dabei geht man folgendermaßen vor: NewRawInOut.mod compilieren und die oben genannten Programme neu linken. In folgenden Sachen unterscheidet sich NewInOut von NewRawInOut: CONST defwin = 'raw:0/50/640/150/NewRawInOut'; (* Eingabe eines Strings aus file. Prozedure ist für raw: Windows und bel. Files geschrieben. Kann gegen Prozedur für con: Windows ausgetauscht werden. *) PROCEDURE ReadStringFile ( file : FILE; VAR Text : ARRAY OF CHAR ); PROCEDURE read () : CHAR; VAR c : CHAR; len : LONGINT; BEGIN len := Dos.Read ( file ^. handle, ADR ( c ), 1 ); RETURN c; END read; PROCEDURE write ( c : CHAR ); VAR len : LONGINT; BEGIN len := Dos.Write ( file ^. handle, ADR ( c ), 1 ); END write; VAR len : INTEGER; c : CHAR; echo : BOOLEAN; BEGIN (* Nur auf eigenes Window die Eingabe echoen. *) echo := file ^.handle = oldin ^.handle; FlushOutput ( file ); len := 0; REPEAT c := read (); UNTIL ( c = eof ) OR ( c # ' '); WHILE ( c # eof ) AND ( c # cr ) AND ( c # eol ) AND ( len <= HIGH ( Text ) ) DO IF ( c >= ' ' ) AND ( c <= '~' ) OR ( c >= 240C) AND ( c <= 377C) THEN (* Normales Zeichen. *) Text [ len ] := c; INC ( len ); IF echo THEN write ( c ); END; ELSIF echo AND ( c = bs ) AND ( len > 0 ) THEN (* Backspace-Taste gedrückt. *) write ( bs ); write ( sp ); write ( bs ); DEC ( len ); ELSIF c = csi THEN (* Funktions- oder Cursor-Taste gedrückt. *) c := read (); IF ( c < 'A' ) OR ( c > 'D' ) THEN (* Keine Cursor-Taste. *) c := read (); END; END; c := read (); END; IF len <= HIGH ( Text ) THEN Text [ len ] := nul; END; done := ( len # 0 ) AND ( c # eof ); END ReadStringFile; BEGIN TermProcedure ( TermNewInOut ); (* Nach Programmschluß ( auch bei Absturz ) wird TermNewInOut aufgerufen. *) (* Beim Benutzen eines raw: Windows muß ein neues Window geöffnet werden auch wenn das Programm vom CLI aus auf- gerufen wird. *) waitCloseGadget := TRUE; FileListe := NIL; out := NIL; diskobjectptr := NIL; stdin := NIL; stdout := NIL; (* Tooltype holen, falls vorhanden. *) wbstartupptr := startupMsg; diskobjectptr := GetDiskObject ( wbstartupptr ^. argList ^ [ 0 ] . name ); (* diskobjectptr = NIL kann vorkommen wenn mit dem Debugger gearbeitet wird. Deshalb hier kein Assert verwenden. *) IF diskobjectptr # NIL THEN toolarrayptr := diskobjectptr ^. toolTypes; stringptr := toolarrayptr ^; END; IF stringptr # NIL THEN (* WINDOW-Eintrag suchen. *) stringptr := FindToolType ( diskobjectptr ^. toolTypes, ADR ( Tooltype ) ); IF stringptr # NIL THEN out := Open ( stringptr ^, ModeNew ); Assert ( out # NIL, ADR ( winerror ) ); ELSE (* Window-Voreinstellung. *) out := Open ( defwin, ModeNew ); Assert ( out # NIL, ADR ( winerror ) ); END; ELSE (* Window-Voreinstellung. *) out := Open ( defwin, ModeNew ); Assert ( out # NIL, ADR ( winerror ) ); END; stdout := out; stdin := stdout; oldin := stdin; oldout := stdout; END NewInOut. Neue Version 1.2 Dazugekommen ist die Möglichkeit im Tooltypes-Feld des Programm-Icons die Größe des Windows anzugeben. Das Format ist: WINDOW=WindowTyp:v/w/x/y/[Beliebiger Text] WINDOW muß groß geschrieben werden. Als Window-Typ können con:, raw:, newcon: und andere verwendet werden. Bei Verwendung von raw: Windows siehe oben. v,w,x,y sind Dezimalzahlen, die die Größe des Windows angeben. Am Schluß kann ein beliebiger ( auch leerer ) Text stehen. Beispiele: WINDOW=con:0/0/640/256/MeinWindow WINDOW=con:0/100/640/150/ WINDOW=raw:0/0/640/200/Hallo WINDOW=newcon:0/0/640/200/Hallo Dabei geht man folgendermaßen vor: Das fertig compilierte Programm anklicken, dann im Workbench-Menu Info anwählen. Es erscheint ein Window mit Informationen über das angeklickte Programm. Jetzt im TOOL TYPES-Feld das Gadget ADD anklicken und links im Stringgadget den Text eingeben. Die Boolsche Variable waitCloseGadget aus dem Modul Terminal ist dazugekommen. Sie ist voreingestellt auf TRUE gesetzt. Wird sie auf FALSE gesetzt wartet das Programm am Programmende nicht mehr auf ENTER ( wenn das Programm von der Workbench gestartet wurde ). Zukünftige Verbesserungen: ( wenn ich viel Zeit und Lust habe ) -doppelte Pufferung. Während ein Puffer bearbeitet wird, wird der andere bereits eingelesen. -Simultane raw: und con: Window Benutzung.