BOB+ ist eine von Ralf-Erik Ebert weiterentwickelte Version der Programmiersprache BOB die sich für den Portfolio hervorragend eignet.
Es ist eine Hybridsprache, d.h. eine ihrem Konzept nach prozedurale Sprache mit objektorientierten Erweiterungen. Syntaktisch lehnt sich BOB+ an C/C++ sowie Script-Sprachen wie JavaScript an.
Es ist weder ein Compiler, noch ein reiner Interpreter. Die Quelldateien werden in einen Byte-Code
übersetzt, der anschließend interpretiert wird.
Eines der wichtigsten Kriterien bei der Entwicklung von BOB+ war sparsamster Umgang mit Ressourcen. BOB+ kommt mit knapp 50 KB Platz auf einer Speicherkarte und weniger als 128 KB Arbeitsspeicher aus. Somit ist es möglich, auf einem Atari-Portfolio in Normalausstattung BOB+-Programme nicht nur auszuführen, sondern auch zu entwickeln.
BOB+ hält noch zwei Finessen parat; es ist möglich BOB+ Quellcode unter DOS in Batch-Dateien auszuführen (siehe GRAPH.BAT) und BOB+ unterstützt die Einbindung von DOS-DLLs (siehe AESTEST.BP).
Ab der Version 1.1d gibt es außerdem die Möglichkeit, ein in Bytecode übersetztes BOB+-Programm mit der Laufzeitumgebung zu einer eigenständig ausführbaren (EXE-) Datei zu binden, was besonders dann interessant ist, wenn man das Programm an Nutzer weitergeben will, die keine BOB+-Umgebung eingerichtet haben.
Version 1.1e ist ein „kleines“ Update, das die Implementierung tieferer Klassenhierarchien in BOB+ vereinfacht.
Achtung: Für die Version 1.1e gibt es nochmals eine Bugfix-Release, die einen Fehler bei der Konvertierung von float-Werten in Zeichenketten behebt. Sie ersetzt ab sofort (25.12.2013) die bisherige Version. Weitere Änderungen gibt es nicht, daher auch keine Änderungen an der Dokumentation.
Bezugsquelle: http://www.kiezsoft.de/
Im Downloadbereich von KiezSoft sind immer die neusten Versionen vom BOB+, samt Dokumentation und Quell-Codes erhältlich. Hier werden die Portfolio-relevanten Dateien der Version 1.1e aufgelistet.
Diese Version ist hier zum direkten Download verfügbar.
Name | Beschreibung | Größe | Datum |
---|---|---|---|
BP.EXE | Source-Code Interpreter/Compiler | 49371 Bytes | 24.12.13 |
BPR.EXE | Byte-Code Interpreter | 40240 Bytes | 24.12.13 |
BPI.EXE | kleinere Nur-Integer-Version von BP.EXE | 35497 Bytes | 24.12.13 |
BPRI.EXE | kleinere Nur-Integer-Version von BPR.EXE | 26582 Bytes | 24.12.13 |
BPC.EXE | seperater Byte-Code Compiler | 33077 Bytes | 24.12.13 |
BP.EXE und BPR.EXE sind die „eigentlichen“ Programme, BP zum Ausführen und Kompilieren von Quelldateien (*.BP) und BPR zum Ausführen von kompiliertem Byte-Code. BPI und BPRI sind kleinere Varianten für Systeme mit wenig Ressourcen. Sie Verzichten auf die Unterstützung von Fließkomma-Zahlen und benötigen entsprechend weniger Speicherplatz.
BPC.EXE ist ein reiner Bytecode-Compiler, so kann man an Stelle von BP.EXE, BPC.EXE und BPR.EXE verwenden, wenn man wenig Arbeitsspeicher (und dafür etwas mehr Platz auf der CCM-Karte) hat. Nachteil ist allerdings, dass der Trace-Modus zur Fehlersuche nicht verwendet werden kann und keine BOB+-Quellen in Batch-Dateien eingebunden werden können. Dafür gibt es mehr freien Speicher für das Programm. Außerdem arbeitet der Bytecode-Interpreter in den reinen Laufzeitversionen etwas schneller.
Name | Beschreibung | Größe | Datum |
---|---|---|---|
STRSPLIT.BP | Zerlegt eine Zeichenkette in ihre Bastandteile | 438 Bytes | 08.08.05 |
GRAPH.BP | Zeichnet Linienmuster im Grafik-Modus | 410 Bytes | 21.08.05 |
MIN.BP | Zeigt verfügbaren Speicher | 34 Bytes | 27.08.05 |
SQRT.BP | Zeichnet Wurzelfunktion im Grafik-Modus | 395 Bytes | 02.09.05 |
HELLO1.BP | „Hallo-Welt“-Programm | 313 Bytes | 28.09.05 |
RANDOM.BP | Gibt eine Liste von 10 Zufallszahlen aus | 306 Bytes | 29.09.05 |
OP.BP | Testet Operatoren (Vorsicht) | 559 Bytes | 29.09.05 |
MINILIB.BP | Beispiel einer Bibliothek | 238 Bytes | 29.09.05 |
MINI.BP | Verwendung der Bibliothek MINILIB.BPM | 482 Bytes | 29.09.05 |
TYPE.BP | Gibt den eigenen Quellcode aus | 271 Bytes | 29.09.05 |
GRAPH.BAT | BOB+ -Script in Batch-Datei | 542 Bytes | 18.10.05 |
VARARGS.BP | Beispiel für den Gebrauch von Variablen und Argumenten | 1173 Bytes | 18.10.05 |
SORTLIST.BP | Sortiert Eingetippte Liste alfabetisch | 4130 Bytes | 18.10.05 |
Anders als bei BOB ist die Standard-Dateierweiterung für Quellcodes *.BP (nicht *.BOB), für Byte-Code-Module *.BPM. Der Grund dafür ist die Tatsache, dass BOB+-Programme Code enthalten können, den BOB nicht versteht (BOB Programme hingegen werden von BOB+ verstanden).
Speziell für den Portfolio hat Erik Ebert eine DOS-DLL geschrieben, die die Einbindung von Portfolios AES-Funktionen in BOB+ Quell-Codes erleichtert. Das Paket AES.ZIP kann im hier heruntergeladen werden und besteht aus folgenden Dateien:
Name | Beschreibung | Größe | Datum |
---|---|---|---|
AES.EXE | DOS-DLL mit Portfolios AES-Funktionen | 2128 Bytes | 25.01.08 |
AES.8 | Assembler Quell-Code von AES.EXE | 19293 Bytes | 24.01.08 |
AES.BP | Wrapper-Klasse für die DLL in BOB+ | 17094 Bytes | 27.01.08 |
AES.CHM | Referenzdokumentation (MS-HTML-Help) | 83939 Bytes | 28.01.08 |
AES.BPM | AES.BP als kompilierter Byte-Code | 4259 Bytes | 27.01.08 |
AESTEST.BP | Test-Routine für die AES-DLL | 1586 Bytes | 25.01.08 |
MENU.TXT | Menüdefinition für aestest.bp | 175 Bytes | 18.11.07 |
AESDEMO.BP | AES-DLL-Demo (Quelltext) | 9499 Bytes | 27.01.08 |
AESDEMP.BPM | AES-DLL-Demo (vorkompiliert) | 10736 Bytes | 27.01.08 |
cpp/AES.H | Wrapper-Klasse (Header) für die DLL in C++ | 5213 Bytes | 16.03.08 |
cpp/AES.CPP | Wrapper-Klasse (Implementierung) für die DLL in C++ | 14470 Bytes | 24.03.08 |
cpp/AES.H | Assembler Quell-Code von AES.EXE | 19293 Bytes | 24.01.08 |
cpp/ARSDEMO.CPP | AES-DLL-Demo in C++ | 9889 Bytes | 24.03.08 |
cpp/AESDEMO.PRJ | Turbo-C++ 1.01-Projektdatei | 4057 Bytes | 16.03.08 |
cpp/USEDLL.H | Binding-Bibliothek für DOS-DLLs (Header) | 7235 Bytes | 20.10.07 |
cpp/USEDLL.CPP | Binding-Bibliothek für DOS-DLLs (Implementierung) | 8826 Bytes | 20.10.07 |
cpp/DOSDLL.H | DOS-DLL-Delarationen | 7235 Bytes | 27.10.07 |
cpp/DLLGLOB.H | Compiler-spezifische Definitionen für DOS-DLL | 14637 Bytes | 16.03.08 |
Geben sie BP AESDEMO
oder BP AESTEST
in der DOS-Befehlszeile ein, um zu sehen, wie BOB+ dank der DOS-DLL Portfolios AES-Funktionen ausführt.
Das Unterverzeichnis cpp
enthält zusätzlich die Quellen für eine entsprechende Anbindung der AES-DLL in C++.
Das Konzept von BOB+ vereint die Vorteile eines Semi-Compilers mit denen einer Script-Sprache. Der modulare Charakter erlaubt es, den Einsatz perfekt an Portfolios Möglichkeiten und Grenzen anzupassen. Die Verwendung von Quellcode- und Byte-Code Dateien erleichtert das Programmieren und bietet große Entscheidungsfreiheit in der Handhabung der Ressourcen.
Byte-Code Module sind platzsparender als ihre unkompilierten Quellen. Die Ersparnis ist unterschiedlich und hängt vom Inhalt des Quellcodes ab. Das Beispiel-Programm SORTLIST.BP hat unkompiliert eine Größe von 4130 Bytes, das kompilierte Byte-Code-Modul nur 1319 Bytes, eine Ersparnis von mehr als 68%. Allerdings lassen sich Byte-Code-Module nicht mehr dekompilieren oder editieren. Sie können aber bei der Kompilierung anderer Programme eingebunden, oder zur Laufzeit aufgerufen werden. Letzteres erlaubt den Aufbau einer sehr modularen und platzsparenden Umgebung; häufig verwendete Routinen sind dann nicht mehrfach in verschiedenen Programmen enthalten, sondern nur einmal auf der Speicherkarte, und jedes der Programme ruft diese zur Laufzeit bei Bedarf auf.
Als wäre dies nicht genug, können - ähnlich wie unter Windows - auch Dynamic-Link-Libraries (DOS-DLLs) eingebunden werden.
Die Möglichkeit BOB+ Scripts in Batch-Dateien zu speichern (siehe GRAPH.BAT) ist wie eine offene Tür zu DOS.
All dies macht BOB+ wie für den Portfolio geschaffen.
BP.EXE selbst benötigt insgesamt 78304 Bytes Arbeitsspeicher, auf einem Standard Portfolio (128KB) mit einem Laufwerk C: mit 16KB, ohne installierte Treiber, bleiben so 16896 Bytes für Quellcode oder Byte-Code und Datenspeicher (arbeitet man nur mit BPR.EXE und Byte-Code-Dateien, dann 26768 Bytes).
Damit sind die Bedingungen für eine minimale Standard-Ausrüstung erfüllt.
Bei einem größeren Laufwerk C: oder mit etwaigen installierten Treibern (der FileManager FM.COM will auch seine 10480 Bytes) könnte es aber eng werden, es sei denn, man hat eine Speicherweiterung, oder einen aufgerüsteten Portfolio.
Eine minimale Programmierumgebung (nur mit BP.EXE) benötigt etwa 50 KBytes, es ist also mindestens eine 64KB-Speicherkarte notwendig, 128KB zweckmäßig. Mit ein paar DOS-Utilities und zusätzlichen Byte-Code-Dateien ist diese dann auch schon bald voll. Hat man kein größeres Speichermedium, muss Laufwerk C: oder u.U. ein zusätzliches Laufwerk B: zur Hilfe genommen werden.
Für fertige Programmpakete (anwenderspeziefische Software), bestehend aus nur BPR.EXE (ca. 40 KB) und Byte-Code-Dateien, reicht schon eine 64KB-Karte, ein mit BPRI zur EXE gebundenes Programm passt mit etwas Glück auch auf eine mit 32KB.
Das Laden von BP.EXE oder BPR.EXE dauert auf einem Standard-Portfolio mit 4,9MHz weniger als 3 Sekunden.
KiezSoft arbeitet an der Erweiterung der Sprache, verbunden mit einer neuen Architektur und einer eine vollständigen Reimplementierung. Die Ausrichtung auf extrem geringen Ressourcenbedarf steht hierbei weniger in Vordergrund, so dass die Lauffähigkeit auf einem Standard-Portfolio nicht unbedingt gewährleistet ist.
Die neue Version wird voraussichtlich BOB+ 2 heißen.
Für die Versionen 1.x wird es Updates und ggf. Erweiterungen geben, solange dafür ein Anwenderinteresse besteht. Dabei bleibt der Fokus auf Minimalsysteme erhalten.
© 2011 Ralf-Erik Ebert, Berlin
Die in diesem Text verwendeten Bezeichnungen von Hard- und Software-Produkten sowie Firmennamen sind in der Regel – auch ohne besondere Kennzeichnung – eingetragene Warenzeichen und sollten als solche behandelt werden.
Der hier veröffentlichte Text wurde mit großer Sorgfalt erarbeitet. Dennoch können Fehler und Irrtümer nicht
ausgeschlossen werden. Für entsprechende Hinweise und Verbesserungsvorschläge ist der Autor jederzeit dankbar.
Die im Text verwendeten Beispiele dienen ausschließlich der Illustration und dem leichteren Verständnis des Textes.
Der Autor übernimmt keinerlei Haftung für durch die Verwendung der Software BOB+, dieser
Dokumentation und der darin enthaltenen Code-Beispiele entstehende Schäden.
Änderungen, die der Korrektur, der Weiterentwicklung der Software BOB+ sowie der Ergänzung ihrer
Dokumentation dienen , behält sich der Autor vor.
BOB+ ist FREEWARE und wird als Open Source zur Verfügung gestellt.
Die Software darf frei kopiert und weitergegeben sowie – direkt oder in modifizierter Form – in beliebigen kommerziellen oder nicht-kommerziellen Umgebungen eingesetzt werden, sofern die Urheberschaft an geeigneter Stelle erwähnt wird.
Die Version 1.1 von BOB+ war eigentlich nicht geplant. Sie ist sozusagen als Zwischenschritt auf dem Weg zur Version 2, die im ersten Halbjahr 2008 erscheinen und neben einer komplett überarbeiteten Architektur und Implementierung zahlreiche Erweiterungen der Sprache selbst mitbringen wird, entstanden.
BOB+ 1.1 basiert wesentlich auf dem Code der Version 1.0 und behält den Fokus auf Systeme mit extrem knappen Ressourcen bei. Sie ist außerdem Quell- und Bytecode-kompatibel zur Vorgängerversion. Intern wurden einige Änderungen bzw. Erweiterungen vorgenommen, die einerseits die Möglichkeiten systemnaher Programmierung erweitern und andererseits die Anpassung an sparsam ausgestattete Systeme noch verbessern sollen. Anlass für den Entschluss, die Version 1 weiter zu entwickeln waren einige kritische Anmerkungen von Volker Hamann, einem
der Betreiber des PofoWiki zum erhöhten Speicherbedarf einer Vorab-Variante der Version 2 sowie die inzwischen vorhandene Möglichkeit, auch unter MS-DOS dynamische Bibliotheken zu verwenden (siehe [4]).
Aus Sicht des Benutzers bringt die Version 1.1 folgende Neuerungen mit:
Das hier vorliegende Handbuch wurde aktualisiert und um ein zusätzliches Kapitel ergänzt, das sich mit der Implementierung und Verwendung von dynamischen Bibliotheken für BOB+ beschäftigt.
Auf Unterschiede zwischen den beiden BOB+-Versionen und zur BOB-Ur-Version von David M.
Betz [2] wird im Text ausdrücklich hingewiesen.
Version 1.1a ist eine kleine Erweiterung der Version 1.1. Hier sind drei Anweisungen hinzugekommen, die das Suchen von Fehlern während der Entwicklung vereinfachen sollen. Ihre Bedeutung ist in Abschnitt 8 erklärt. Ansonsten gelten alle Aussagen zur Version 1.1 auch für die Version 1.1a
Die Version 1.1b übernimmt den Umgang mit Escape-Codes von BOB+2, d.h., sie gestattet die Verwendung von Zeichencodes in Escape-Sequenzen – siehe hierzu Abschnitt 5.3. Ansonsten ist sie mit Version 1.1a identisch.
Version 1.1c fügt Funktionen zum direkten Lesen bzw. Schreiben des Arbeitsspeichers bzw. zum Kopieren von Speicherbereichen hinzu (vgl. Abschnitt 12.7), wodurch die Möglichkeiten systemnaher Programmierung verbessert werden sollen. Durch einige Code-Optimierungen wurden außerdem die Größen der ausführbaren Dateien und der benötigte Speicherplatz weiter reduziert.
Mit Version 1.1d besteht die Möglichkeit, übersetzte Bytecode-Module an die Laufzeitumgebung zu binden und so eigenständig lauffähige Programme zu erzeugen – siehe Abschnitte 3.3 und 3.4.
Version 1.1e behebt ein Problem beim Aufruf geerbter virtueller Methoden in tiefen Klassenhierarchien (siehe Abschnitt 11.4).
Als ich im Jahr 2004 (nicht ohne einen leisen Anflug von Nostalgie) einen ATARI Portfolio
erstanden hatte, begab ich mich auf die Suche nach einer Möglichkeit, das gute Stück auch zu
programmieren. Genauer: Das Programmieren für den Portfolio sollte auf dem Portfolio selbst
möglich sein – schließlich ging das mit meinem Eigenbau-Homecomputer aus den achtziger Jahren,
der noch viel ärmlicher ausgestattet war, ja auch!
Nun sind aber mit den Jahren Ansprüche und Erwartungen gewachsen, so dass das Ergebnis der
Suche nach geeigneten Programmiersprachen/-systemen – zumindest für meinen Geschmack –
etwas ernüchternd ausfiel. Prinzipiell möglich war die Programmiererei mit GoFolio, mit dessen
Syntax ich mich aber nicht recht anfreunden konnte sowie dem doch recht steinzeitlichen POBASIC.
Und dann war da noch BOB. BOB fand sich mitsamt C-Quellcode und ein paar Beispielen in einem
ZIP-Archiv [1], versprach Objektorientierung und sah auf den ersten Blick so ähnlich wie C++ aus.
Leider war keinerlei Dokumentation dazu aufzutreiben. Eine längere Internet-Suche förderte
immerhin BOBs Ursprung, einen 1991 entstandenen Artikel von David Michael Betz im Dr.Dobbs
Journal [2] zutage. Allerdings ist dieser Artikel nicht hinreichend, um die Sprache tatsächlich
verwenden zu können. Neben einer groben Erläuterung der hinter BOB stehenden Ideen, beschränkt
sich der Autor auf die Aussage, dass es sich um keinen Ersatz für C oder C++ handele und es
einfach sei, BOB um eigene Funktionen zu erweitern. Dass letzteres für eine sinnvolle Benutzung
auch notwendig ist, zeigte dann ein erster Blick auf die Originalquellen und die darin
bereitgestellten vordefinierten Funktionen.
Eine ausführlichere Recherche im Internet ergab, dass BOB von D. M. Betz selbst weiterentwickelt
worden ist [3]. Die aktuelle Version lehnt sich ihrem Konzept nach (insbesondere was die
Deklaration und Verwendung von Klassen betrifft) jedoch stärker an JavaScript als an C++ an und
ist in ihrem Codeumfang bedeutend angewachsen, so dass ein Betrieb auf minimal ausgestatteten
Rechnern – wie dem erwähnten Portfolio – nicht mehr möglich ist. Sie schied damit als einfache
Alternative aus 1).
Also machte ich mich ans Werk, mit der Absicht, BOB etwas tiefer zu verstehen, die Sprache so zu
dokumentieren, dass es möglich wird, sie sinnvoll einzusetzen und dabei um einige wichtige
Funktionen zu erweitern. Dabei zeigte sich, dass es an einigen Stellen notwendig bzw. zweckmäßig
war, die Sprache selbst zu erweitern und ihre Implementierung zu überarbeiten. Zur besseren
Unterscheidung vom Original habe ich die neue Version BOB+ getauft.
Wichtig war in diesem Zusammenhang, das ursprüngliche Ziel – die Benutzbarkeit auf
Minimalsystemen, wie dem ATARI Portfolio – nicht aus den Augen zu verlieren, d.h. die
Gesamtgröße von BOB+ weiterhin so klein zu halten, dass er auf solchen Systemen lauffähig bleibt
und noch genügend Arbeitsspeicher für die eigentlichen Programme übrig lässt.
Der vorliegende Text ist keine Einführung in die Programmierung. Vielmehr wird versucht, die
Sprache BOB+ in knapper Form darzustellen. Dabei müssen grundlegende Programmierkenntnisse
in einer anderen – vorzugsweise C-ähnlichen – Sprache sowie Grundkenntnisse der
objektorientierten Programmierung vorausgesetzt werden. Gleichzeitig werden die
Gemeinsamkeiten und Unterschiede zu BOB dargestellt, so dass die Lektüre hoffentlich auch als
Wegweiser für die Benutzung des Vorfahren von Nutzen ist.
BOB ist eine Hybridsprache, d.h. eine ihrem Konzept nach prozedurale Sprache mit
objektorientierten Erweiterungen. Syntaktisch lehnt sich BOB an C/C++ sowie Script-Sprachen wie
JavaScript (D. Betz sieht eher eine Nähe zu Lisp) an. BOB ist eine sehr kleine Sprache mit nur 11 „echten“
Schlüsselwörtern (vgl. Abschnitt 8), die schnell und einfach erlernbar ist. BOB ist zudem eine sehr gutmütige Sprache, die keine explizite Typisierung und keinen Deklarationszwang kennt – mit allen Vor- und Nachteilen, die das mit sich bringt.
BOB ist weder ein Compiler noch ein „echter“ Interpreter. Der Quellcode eines Programms wird zunächst in einen Bytecode übersetzt, der anschließend interpretiert wird.
BOB+ übernimmt das Konzept BOB, erweitert jedoch sowohl die Sprache als auch deren Implementierung um einige grundlegende Elemente, die für Anwendungen, die über Programmierexperimente hinausgehen, erforderlich sind. Neben einer Vielzahl zusätzlicher vordefinerter Funktionen (vgl. Abschnitt 12) gehören hierzu insbesondere:
Dabei bleibt BOB+ abwärtskompatibel zu BOB. D.h. für BOB geschriebene Programme können
ohne Änderung mit BOB+ übersetzt und ausgeführt werden.
BOB+ besteht aus einer einzigen ausführbaren Datei (BP.EXE) und wird von der Kommandozeile gestartet. Um Aufrufe von beliebiger Stelle zu vereinfachen, sollte der Pfad zu dieser Datei in die PATH-Umgebungsvariable aufgenommen werden. Für den eigentlichen Aufruf gilt dann folgende Syntax:
bp [-i][-d][-t][-c][-e][-I] [sourcefile [..]] [-r objfile [..]] [-o outfile] [# userarg [..]]
Dabei haben die einzelnen Optionen folgende Bedeutung:
Quelldateien und einzulesende Module können ohne Dateiendungen angegeben werden. In solchen Fällen werden implizit die Erweiterungen „.bp“ bzw. „.bpm“ angenommen 4).
Bei Angabe mehrerer Quelldateien werden diese in der angegebenen Reihenfolge in Bytecode übersetzt, bevor die eigentliche Programmausführung beginnt. Die zu ladenden vorkompilierten Module werden vor dem Kompilieren der Quellen (und ebenfalls in der angegebenen Reihenfolge) gelesen.
BOB+ erlaubt zusätzlich die Angabe einer Liste von Benutzer-Argumenten, die an das auszuführende Programm weitergegeben werden. Diese Liste wird durch ein Doppelkreuz (#) eingeleitet und bildet das Ende der Kommandozeile. Innerhalb eines BOB+-Programms kann mit den vordefinierten Funktionen getargs()
bzw. getusrargs()
auf die gesamte bzw. die Benutzer-Argumentliste zugegriffen werden (vgl. Abschnitt 12.9).
Für den Aufruf von Funktionen und die Zwischenspeicherung von Argumenten bzw. temporären Werten verwendet BOB/BOB+ einen Stack mit einer Kapazität von 500 Einträgen. BOB+ erlaubt die Modifikation dieses Wertes mit Hilfe der Umgebungsvariablen BPSTACK. So kann z.B. mit dem Eintrag SET BPSTACK=1000
die Stack-Größe verdoppelt werden.
Für die Ausführung von vorkompilierten Bytecode-Modulen steht die BOB+-Laufzeitungebung BPR.EXE zur Verfügung. Gegenüber der Vollversion von BOB+ besteht die Einschränkung, dass nur vorkompilierte Module ausgeführt – also keine Quelltexte direkt übersetzt – werden können 5).
Dem entsprechend werden die Optionsschalter –c, -r, -o, -d und –t nicht unterstützt.
Der Aufruf der Laufzeitumgebung erfolgt nach der Syntax
bpr [-i] [ objfile [..]] [# userarg [..]]
Dabei gibt die Option –i einen Copyright-Hinweis und die Aufrufsyntax aus. Für die zu ladenden vorkompilierten Module und die Benutzerargumente gelten sinngemäß die Aussagen aus Abschnitt 3.2. Mindestens ein Modul muss eine main-Funktion als Einsprungpunkt besitzen. Wie in der Vollversion besteht die Möglichkeit, die verfügbare Stack-Größe mit der Umgebungsvariablen BPSTACK einzustellen.
In Version 1.1 von BOB+ existiert außerdem ein separater Bytecode-Compiler. Er kommt ohne den den Bytecode-Interpreter und die interne Funktionsbibliothek aus und benötigt dadurch gegenüber der Vollversion wesentlich weniger Platz im Arbeitsspeicher und auf dem Datenträger.Der Bytecode-Compiler wird nach der Syntax
bpc [-i] [-d][-e][-I] [sourcefile [..]] [-r objfile [..]][-o outfile]
aufgerufen. Dabei entspricht die Bedeutung der einzelnen Optionen mit folgenden Ausnahmen denen der Vollversion:
Hinweis:
In Version 1.1 von BOB+ gibt es sowohl von der Vollversion als auch von der Laufzeitumgebung jeweils eine Variante ohne Unterstützung von Gleitpunkt-Arithmetik. Diese Varianten heißen bpi.exe bzw. bpri.exe und kommen mit etwa 20KB weniger Speicher aus. Sie sind für den Einsatz auf Systemen mit extrem knappen Ressourcen gedacht.
Mit Version 1.1d wird das Erzeugen direkt ausführbarer (EXE-)Dateien unterstützt. Dabei wird jedoch nicht unmittelbar Maschinencode erzeugt sondern der von Compiler erzeugte Byte-Code mit einer der Laufzeitumgebungen (BPR bzw. BPRI) zu einem eigenständigen Programm gebunden. Benötigter Speicherplatz und Laufzeitverhalten entsprechen dabei der Ausführung der jeweiligen Laufzeitumgebung mit einem vorkompilierten Modul. Das Erzeugen eigenständiger Programme erleichtert dagegen die Weitergabe, da nicht zusätzlich eine die Laufzeitumgebung mitgeliefert und installiert werden muss.
Beim Zusammenfügen eines solchen Programms wird eine Kopie der Laufzeitumgebung6) mit dem per -o
- Option angegebenen Namen (bzw. out.exe, wenn die Angabe fehlt) angelegt und an deren Ende einfach der Bytecode-Abschnitt sowie eine Kennung angehängt. Die Kennung ist 8 Bytes lang und besteht aus dem Offset des angehängten Bytecodes in der Zieldatei (4 Bytes, entspricht der Länge der Original-Laufzeitumgebung), gefolgt von Zeichenkette „BPR“ und einem abschließenden NULL-Byte. Die Zeichentte wird bei der späteren Ausführung benötigt, um ein zusammengesetztes Programm zu erkennen. Der Offset dient dem Auffinden des Bytecode-Abschnitts.
Das einfache Anhängen des Bytecodes an die Laufzeitumgebung hat den Vorteil, dass zur Laufzeit kein zusätzlicher Speicherplatz zur Pufferung des Bytecodes benötigt wird. Außerdem erlaubt es dieses Prinzip auch, für die Laufzeitumgebung selbst eine – z.B mit PKLITE – komprimierte Variante zu verwenden.
Hinweis:
Bei der Verwendung so erzeugter eigenständiger Programme verändert sich die Auswertung übergebener Kommandozeilen-Parameter. Weil hier keine Steuerung von BOB+ mehr möglich (bzw. notwendig) ist, werden alle Parameter als Benutzer-Argumente angesehen, so als wären sie hinter dem ‚#’-Symbol bei „normaler“ Verwendung notiert worden.
Jedes BOB/BOB+-Programm besteht – wie in C – aus einer Menge von Funktionen (siehe Abschnitt 10). Einsprungpunkt ist die parameterlose Funktion „main()“. Neben reinen Funktionen können auf der obersten Strukturebene zusätzlich Klassendeklarationen stehen (vgl. Abschnitt 11). Anders als in C erfolgt die Definition von Variablen stets innerhalb einer Funktion oder einer Klasse. Dennoch können in Funktionen definierte Variablen einen globalen Gültigkeitsbereich besitzen. Wie in C und C++ ist es nicht möglich, Funktionen lokal, d.h. innerhalb anderer Funktionen zu definieren.
Das nun folgende unvermeidliche „Hello World“-Beispiel zeigt den typischen Programmaufbau:
/* BOB+ Hello World example */ // write a string to console printString(str; i, n) { n = strlen(str); for (i=0; i<n; i++) putc(str[i], stdout); } main() { printString("Hello World – this is BOB+\n"); return 0; }
Wie man sieht, besteht eine große syntaktische Ähnlichkeit zu C/C++.
Zunächst ist zu sagen, dass BOB+ (wie auch BOB) formatfrei ist. Das bedeutet, dass Einrückungen, Leerzeilen, Zeilenumbrüche usw. keine syntaktische Bedeutung haben. Sie dienen lediglich der besseren Lesbarkeit des Quelltextes. Die Länge einer einzelnen Quelltextzeile darf 300 Zeichen (in BOB sind es 80) nicht überschreiten.
Zeilen 1 und 3 in obigem Beispiel enthalten Kommentare. Danach wird eine Funktion printString definiert (siehe Abschnitt 10), welche eine Zeichenkette im Parameter str übernimmt und deren Inhalt zeichenweise auf die Konsole ausgibt. Hierbei verwendet sie die vordefinierten Funktionen 7) strlen (Länge der Zeichenkette bestimmen) und putc (Dateiausgabe eines Zeichens) sowie die ebenfalls vordefinierte Variable stdout, die einen Verweis auf die Standardausgabe – üblicherweise die Konsole – darstellt.
Die im Funktionskopf hinter dem Semikolon aufgeführte Liste ( i, n) definiert lokal innerhalb der Funktion gültige Variablen.
Die Einsprungfunktion main ruft nun printString mit der auszugebenden Zeichenkette als Argument auf und gibt anschließend mit Hilfe der return-Anweisung den Wert 0 zurück.
In diesem Zusammenhang ist auf die besondere Bedeutung des Rückgabewertes der main-Funktion hinzuweisen. Da es innerhalb des Programmes keinen Aufrufer dieser Funktion gibt, wird der Wert an das Betriebssystem selbst (unter DOS als Errorlevel) zurückgegeben8). Daraus ergibt sich die Einschränkung, dass der Rückgabewert nur eine Ganzzahl sein kann. Aus diesem Grunde prüft BOB+ nach Beendigung der main-Funktion zunächst den Typ des Rückgabewertes9). Handelt es sich um einen Integer-Wert, so wird dieser, andernfalls 0 zurückgegeben.
In diesem Abschnitt werden eine Reihe syntaktischer Grundelemente erklärt, die für das Schreiben von BOB+-Programmen von Bedeutung sind. Bitte beachten Sie, dass es sich hierbei um keine vollständige Syntaxbeschreibung handelt. Vielmehr sollen die wichtigsten Sprachelemente im Vergleich zu C/C++ dargestellt werden.
BOB+ kennt drei Arten von Kommentaren:
Solche Kommentare haben die Form /* Kommentartext */.Sie können sich über mehrere Zeilen erstrecken, dürfen jedoch nicht verschachtelt werden.
Diese Kommentare werden in der Form / / Kommentartext notiert. Sie reichen jeweils bis zum Ende der aktuellen Quelltextzeile.
Diese Form des Kommentars dient der Einbettung von BOB+-Quellen in Komando-Scripts
(Batch-Dateien) unter DOS. Für BOB+ selbst ist sie identisch mit den C++-Kommentaren. Da
das Zeichen @ in Batch-Scripts aber eine gültige Kommandozeile einleitet, deren Ausgabe
lediglich unterdrückt wird, lässt sich diese Art des Kommentars verwenden, um BOB+-Code in
Stapeldateien einzubinden. Ein Beispiel hierzu findet sich in Abschnitt 5.5.
Für Bezeichner gelten die gleichen Regeln wie in C bzw. C++:
Bezeichner bestehen aus einer Folge von Buchstaben (ohne Umlaute und ß) und Ziffern bzw. dem
Unterstrich (_). Sie dürfen nicht mit einer Ziffer beginnen und eine Gesamtlänge von 200 Zeichen 10) nicht überschreiten. Die Groß- bzw. Kleinschreibung ist signifikant.
Beipiele für Bezeichner:
MyClass myValue _anInternalVariable p1
Generell sollten Bezeichner so gewählt werden, dass sie das spätere Lesen eines Quelltextes
erleichtern. Das bedeutet insbesondere, dass Bezeichner möglichst einen direkten Schluss auf ihren
Verwendungszweck zulassen sollten.
Literale sind direkt im Quellcode notierte Werte. BOB+ kennt hiervon vier Typen:
Ein Zeichenliteral ist ein einzelnes Zeichen mit einem Zeichencode zwischen 0 und 255, das in
Hochkommata geklammert ist, z.B. ‘A‘,‘x‘,‘ä‘,‘7‘,‘\n‘.
Das letzte Zeichen weist dabei eine Besonderheit auf. Es repräsentiert kein druckbares Zeichen
sondern ein Steuerzeichen. Die Darstellung spezieller Zeichen wird stets mit einem Backslash (\ ,
oft auch Escape-Zeichen genannt) eingeleitet. Tabelle 1 listet die von BOB+ interpretierten
Escape-Codes mit ihrer Bedeutung auf.
Escape-Code | Bedeutung | Zeichencode (Hex) |
---|---|---|
\a | Alarmton | 07 |
\b | Backspace | 08 |
\f | Seitenvorschub | 0C |
\n | Zeilenvorschub* | 10 |
\r | Wagenrücklauf | 0D |
\t | Tabulator horizontal* | 09 |
\v | Tabulator vertikal | 0B |
\ \ | Backslash* | 5C |
\‘ | Apostroph* | 27 |
\“ | Anführungszeichen* | 22 |
Tabelle 1 Von BOB+ unterstützte Escape-Codes 11)
Achtung: Anders als in C/C++ interpretiert BOB+ bis zur Version 1.1a keine hexadezimal oder oktal angegebenen
Zeichencodes in Escape-Sequenzen. Ein Konstrukt wie ‘\0xFF‘ ist also nicht das Zeichen mit dem
Code 255 sondern ein ungültiger Ausdruck.
Ab Version 1.1b ist es zulässig, hinter dem Backslash ein gannzzahliges Literal (siehe unten) in dezimaler, oktaler oder hexadezimaler Schreibweise zu notieren, dessen niederwertigstes Byte dann als Zeichencode interpretiert wird.
Intern werden Zeichenliterale wie Ganze Zahlen behandelt, wobei der Zeichencode dem Zahlenwert
entspricht. Dem entsprechend können sie im Quelltext überall dort verwendet werden, wo auch eine
Ganze Zahl stehen kann.
Zeichenkettenliterale sind Folgen beliebiger druckbarer Zeichen oder Escape-Codes (vgl. Tabelle
1), die in Anführungszeichen eingeschlossen sind. Kommt das Anführungszeichen selbst in einer
Zeichenkette vor, so muss es als Escape-Code angegeben werden. Implementierungsbedingt dürfen
Zeichenkettenliterale eine Länge von 200 Zeichen (bei BOB 50 Zeichen) nicht überschreiten.
Beispiele für Zeichenkettenliterale:
“Hello World!“ “Er sagte \“Ich bin müde\“.“ “Adresse:\n\tName:\n\tStrasse:\n\tOrt:\n“ “c:\\prog\\bobplus\\bp –c hello –o hello.bpm“
Ganzzahlige Literale sind Darstellungen Ganzer Zahlen in dezimaler, oktaler oder hexadezimaler Schreibweise, denen optional ein Vorzeichen (+ oder -) vorangestellt sein kann. Dabei werden Oktalzahlen – wie in C – dadurch gekennzeichnet, dass die erste Ziffer eine Null ist und die zweite Ziffer im Bereich zwischen 1 und 7 liegt. Hexadezimalzahlen beginnen mit 0x. Alle anderen Zahlen sind Dezimalzahlen. Wegen der internen Darstellung als 32-Bit-Integer 12) haben ganzzahlige Literale einen Wertebereich von –2^31 bis +2^31-1.
Beispiele für ganzzahlige Literale:
17 -234567 0x7FFFFFF 021 0x2dff0
Fließkommaliterale sind Darstellungen rationaler Zahlen nach der Syntax
[+|-] digit[.digit {digit}][e|E [+|-] digit{digit}].
Dabei ist digit eine Dezimalziffer. Die interne Darstellung erfolgt mit einfacher Genauigkeit (4
Byte, wie Typ float in C), wodurch die Genauigkeit auf 7 Stellen der Mantisse begrenzt ist und der
Wertebereich zwischen -3.4E38 und 3.4E+38 liegt.
Beispiele für Fließkommaliterale:
3.141593 -123.4567E23 123e-4 -1.234567e-2 2.5E+6 3E14
Wie bereits im Abschnitt 4 erwähnt, besteht ein BOB+-Programm auf der obersten Quelltextebene im wesentlichen aus Klassen- und Funktionsdefinitionen (hinzu kommen noch Verarbeitungsanweisungen – siehe 5.5.
Jede dieser Definitionen besteht aus einem Kopf, der u.a. den Namen des jeweiligen Codeobjekts beinhaltet sowie einem Block, welcher den eigentlichen Inhalt einschließt.
Ein Block ist eine Zusammenfassung von aufeinander folgenden Anweisungen und (optional) Variablendeklarationen. In BOB+ wird ein Block – wie u.a. in C/C++ - durch geschwungene Klammern ( { und } ) begrenzt.
Jede in einem Block enthaltene Anweisung oder Definition muss – ebenfalls wie in C/C++ - mit einem Semikolon (;) abgeschlossen 13) werden. Überall dort, wo syntaktisch eine Anweisung vorgesehen ist, kann wiederum ein Block stehen. Dies ist besonders im Zusammenhang mit den Steuerstrukturen (siehe Abschnitt 9) von Bedeutung.
Nachfolgend werden die beschriebenen Elemente anhand eines etwas ausführlicheren Beispiels veranschaulicht. Es handelt sich dabei um ein Programm, welches die zeilenweise Eingabe von Zeichenketten (z.B. Namen) erwartet, sie in einer sortierten Liste speichert und am Ende den Listeninhalt wieder ausgibt. Das Beispiel geht über das bisher Besprochene hinaus, indem von Elementen (Klassen, Funktionen, Steuerstrukturen) Gebrauch gemacht wird, deren Erläuterung Gegenstand späterer Abschnitte ist. Es ist jedoch ein „typisches“ BOB+-Programm und sollte – einige Programmierkenntnisse in anderen Sprachen vorausgesetzt – leicht nachvollziehbar sein.
/* sorted stringlist example */ // ================================================================== // ListEntry class declaration class ListEntry { ListEntry(str); // constructor ~ListEntry(); // destructor getValue(); // method for reading the value getNext(); // get pointer to next entry setNext(entry); // set pointer to next entry _strValue; // value of the entry _next; // pointer to next entry } // ================================================================= // Implementation of ListEntry class // constructor ListEntry::ListEntry(str) { _strValue = str; // initialize value _next = null; // initially _next points to nothing } // get value method ListEntry::getValue() { return _strValue; // simply return current value } // destructor ListEntry::~ListEntry() { if (_next) delete _next; // destroy next entry } // get next method ListEntry::getNext() { return _next; } // set next method ListEntry::setNext(entry) { _next = entry; } // =============================================================== // Declaration of List class class List { List(); // constructor ~List(); // destructor addEntry(str); // add entry to list printList(); // print entries to console _first; // pointer to first ListEntry object } // ============================================================== // Implementation of List class // constructor List::List() { _first = null; // new list is empty } // destructor List::~List() { if (_first) delete _first; // delete contents } // add new entry to sorted list List::addEntry(str;newentry,p) // addEntry uses local variables newentry and p { newentry = new ListEntry(str); // create new entry if (!_first) // nothing to compare in empty list _first = newentry; // single statement else { // statement block if (strcmp(_first->getValue(),str) > 0) { newentry->setNext(_first); // -> operator references a member function _first = newentry; } else { // Walk through the list until correct position found. // Variable p is used like a pointer. for(p=_first;p->getNext();p=p->getNext()) { if (strcmp(p->getNext()->getValue(),str) > 0) { newentry->setNext(p->getNext()); break; // insertion is done so return } } p->setNext(newentry); // close chain of entries } } } // print sorted values to console List::printList(;p) //p is a local variable { p=_first; while ( p != null) { print(p->getValue(),"\n"); p = p->getNext(); } } // ============================================================ // programs entry point main(;s,nameList) { nameList = new List(); // read text lines from console until empty line was read while (1) // loops forever { print("Name: "); s = gets(); if (strlen(s) == 0) break; // break un empty string nameList->addEntry(s); } print("---- sorted -----\n"); nameList->printList(); delete nameList // free list return 0; // return code 0 to OS }
Bitte beachten Sie, dass das Beispiel Spracherweiterungen und vordefinierte Funktionen verwendet, die in BOB nicht zur Verfügung stehen.
Verarbeitungsanweisungen sind eine mit BOB+ eingeführte Spracherweiterung. Obgleich BOB+ keinen Präprozessor besitzt, haben sie eine gewisse Ähnlichkeit zu Präprozessorbefehlen in C/C++ und werden auch so notiert.
Verarbeitungsanweisungen werden – im Gegensatz zu allen anderen Anweisungen – vom Compiler während der Übersetzung des Quellcodes und nicht vom Bytecode-Interpreter ausgeführt. Sie sind syntaktisch außerdem nur auf der äußersten Quelltextebene – also außerhalb aller Klassen- und Funktionsdefinitionen – zugelassen und werden nicht mit einem Semikolon abgeschlossen.
Die aktuelle Version von BOB+ kennt folgende Verarbeitungsanweisungen:
Die #use-Anweisung bindet eine vorkompilierte Bytecode-Bibliothek in das aktuelle Programm ein. Die in der Bibliothek enthaltenen Symbole werden damit in das Programm übernommen. Das bedeutet, dass beim Übersetzen eines Programmes, welches mit #use eine Bibliothek einbindet, Bytecode entsteht, der den kompletten Inhalt der Bibliothek mit enthält.
Verwendung: #use “filename.ext“
Dem Dateinamen kann ein relativer oder absoluter Pfad vorangestellt sein. BOB+ sucht die angegebene Datei zunächst im aktuellen Verzeichnis. Hat die Suche dort keinen Erfolg, so werden zusätzlich die in der Umgebungsvariablen PATH angegebenen Verzeichnisse durchsucht.
Anwendungsbeispiel:
Eine Bibliothek soll den Namen tstfunc.bp haben und eine Funktion mit dem Namen testfunc
exportieren.
/* using #use example – module tstfunc.bp */ testfunc(n) { print(“testfunc(“,n,“) from tstfunc.bp\n“); }
Die Bibliothek wird nun in Bytecode übersetzt:
bp –c tstfunc –o tstfunc.bpm
… und von nachfolgendem Programm benutzt:
/* using #use example – main module */ #use “tstfunc.bpm“ main() { testfunc(“Hello World“); }
Mit #defvar können initialisierte globale Variablen außerhalb von Funktionen definiert werden.
Verwendung: #defvar varname value
Dabei ist varname ein gültiger Bezeichner und value der zugehörige Wert. Bitte beachten Sie, dass die Initialisierung des Variablenwerts zur Übersetzungs- und nicht zur Laufzeit erfolgt. Deshalb können für value nur Literale angegeben werden.
Beispiele:
#defvar PI 3.141593 #defvar Version “2.1.0“ #defvar maxSize 2048
Die #endsrc Anweisung kennzeichnet das (vorzeitige) Ende eines BOB+-Quelltextes, oder anders ausgedrückt, jeglicher Text hinter #endsrc wird vom Compiler ignoriert. Der Anwendungszweck besteht vor allem in der Einbindung von BOB+-Quellen in Batch-Scripts (vgl. 5.1). Auch hierzu ein Beispiel. Das folgende Programm ist ein Batch-Script mit dem Namen graph.bat, das BOB+- Quelltext enthält:
@bp graph.bat @goto end // global settings #defvar sizeX 240 #defvar sizeY 64 #defvar graphMode 4 #defvar textMode 7 main(;px,py,ms,x,n,x2,y2,y,r) { px=sizeX; py=sizeY; setscrmode(graphMode); // switch to graph mode ms = timer(); n=0; for (x=0;x<px;x+=2) { n++; x2= px -x; line(x,0,x2,py-1,1); } for (y=0;y<py;y+=2) { n++; y2= py-1 -y; line(0,y,px-1,y2,2); } ms = timer() - ms; r = n*1000 / ms; gets(); setscrmode(textMode); // switch back to text mode print(r, " lines per second\n"); } #endsrc :end
Das Programm füllt den Bildschirm mit Linien und misst die dafür benötigte Zeit. Am Ende wird die Zeichengeschwindigkeit (Linien je Sekunde) ausgegeben.
Die ersten beiden Zeilen enthalten Anweisungen für den DOS-Kommandoprozessor. Das vorangestellte @-Zeichen unterdrückt unter DOS das Bildschirmecho und kennzeichnet die Zeilen unter BOB+ als Kommentare. Der BOB+-Quelltext endet mit der #endsrc Anweisung.
Die letzte Zeile gehört wieder dem Kommandoprozessor – es ist die das Programmende kennzeichnende Sprungmarke.
BOB+ (und auch BOB) ist eine Sprache, die gewöhnlich als nicht oder schwach typisiert bezeichnet wird. Etwas genauer gilt folgendes:
In BOB+ haben nicht die Variablen sondern allein die Werte einen Datentyp.
Variablen sind demnach einfach Ablageorte für einen beliebigen Wert. Da sie selbst über keine Typeigenschaften verfügen, ist es i.a. auch nicht erforderlich, sie explizit14) zu deklarieren. Eine Variable entsteht im einfachsten Fall automatisch durch Zuweisung eines Wertes an einen frei wählbaren Bezeichner.
Der Gültigkeitsbereich einer Variablen ist – sofern keine besonderen Vorkehrungen getroffen werden – global, d.h. auf eine Variable kann, gleichgültig wo sie definiert wurde, von jeder Stelle des Programmes zugegriffen werden. Dies kann bei größeren Programmen zu unerwünschten
Seiteneffekten führen, wenn man versehentlich an verschiedenen Stellen ein- und dieselbe Variable zu unterschiedlichen Zwecken benutzt. Hierzu ein Beispiel:
// calculate n! fac(n) { if (n==0) return 1; for(f=n, i=n-1; i>1; i--) f*=i; return f; } main() { for (j=1;j<=10;j++) print("fac(",j,")=",fac(j),"\n"); } /* Variable i in this version of main will be modified by fac. main() { for (i=1;i<=10;i++) print("fac(",i,")=",fac(i),"\n"); } */
Wenn Sie die hier auskommentierte Variante der main-Funktion aktivieren, berechnet das Programm bis in alle Ewigkeit die Fakultät von 1. Der Grund dafür ist, dass sowohl fac als auch main die globale Variable i verwenden (ohne etwas von einander zu „wissen“).
Um dieses Problem zu beheben, besteht die Möglichkeit, Variablen mit lokaler Gültigkeit bezüglich einer Funktion zu verwenden. Solche Variablen müssen am Ende des Funktionskopfes, durch ein Semikolon von der Parameterliste getrennt, deklariert werden. Nachfolgend wird obiges Beispiel entsprechend modifiziert:
/* using function-level variables */ // calculate n! fac(n;i,f,n) { if (n==0) return 1; for(f=n,i=n-1; i>1; i--) f*=i; return f; } main(;i) { for (i=1;i<=10;i++) print("fac(",i,")=",fac(i),"\n"); }
Wie man sieht, werden hier ausschließlich lokale Variablen verwendet. Wenn Sie wiederverwendbare und gut wartbare Programme schreiben wollen, sollten Sie globale Variablen so weit wie möglich vermeiden – und wenn sie schon unbedingt notwendig sind, gut dokumentieren.
In BOB+ werden alle in einem Programm vorkommenden Werte in sogenannten Value-Objekten abgelegt. Ein solches Objekt besitzt im wesentlichen eine Typinformation sowie den jeweils zugeordneten Wert selbst oder eine Referenz darauf. Dem entsprechend kann man zwischen Werttypen und Referenztypen unterscheiden15). Diese Unterscheidung ist für die Verwendung in zweierlei Hinsicht von Bedeutung:
Ein Beispiel:
main(;s1,s2) { s1 = “Hello“; // initialize s1 as string s2 = s1; // assign variable s1 to s2 s2[1] = ‘a‘; // modify second character in s2 print(s1,“\n“); // print value of s1 }
Hier wird „Hallo“ und nicht – wie man vielleicht vermuten könnte – „Hello“ ausgegeben.
Werttypen sind in BOB+ die numerischen Typen sowie der Typ null.
Der Datentyp int repräsentiert eine Ganze Zahl mit 32 Bit Breite 16). Daraus ergibt sich ein Wertebereich von -2^31 bis +2^31-1. Werte dieses Typs werden auch zur Darstellung einzelner Zeichen und boolescher Werte verwendet.
Zulässige Operationen für den Typ int sind:
Werte vom Typ int können in numerischen Ausdrücken und Vergleichen ohne explizite Typkonvertierung mit Werten des Typs float kombiniert werden. In diesem Fall erfolgt eine implizite Umwandlung des gesamten Ausdrucks nach float.
Bei Verwendung von int-Werten in string-Ausdrücken (Verkettung) wird der int-Wert als Zeichencode interpretiert.
Der Datentyp float ist nur in BOB+ (nicht in BOB) definiert. Er repräsentiert eine Rationale Zahl mit einfacher Genauigkeit (32 Bit). Der Wertebereich liegt zwischen -3.4E38 und 3.4E+38 bei einer numerischen Genauigkeit von 7 Stellen.
Zulässige Operationen für den Typ float sind:
Werte vom Typ float können in numerischen Ausdrücken und Vergleichen ohne explizite Typkonvertierung mit Werten des Typs int kombiniert werden. In diesem Fall erfolgt eine implizite Umwandlung des gesamten Ausdrucks nach float.
Der Datentyp null kennzeichnet einen nicht initialisierten Wert.. Im Quelltext eines BOB+-Programmes werden Daten dieses Typs durch das Schlüsselwort null 17) beschrieben, das man auch als Konstante und einzigen möglichen Wert des Typs null auffassen kann. Außerdem können eine Reihe vordefinierter Funktionen null zurückgeben.
Zulässige Operationen für den Typ null sind:
Werte vom Typ null können in Vergleichen ohne explizite Typkonvertierung mit allen anderen Typen kombiniert werden. Dabei ist der Wert null kleiner als jeder andere Wert.
Alle Datentypen in BOB+, die keine Werttypen sind, sind Referenztypen.
Der Datentyp string repräsentiert eine Zeichenkette. Werte dieses Typs können mit Hilfe von Zeichenkettenliteralen (vgl. 5.3) oder durch die vordefinierte Funktion newstring (vgl. 12.1) explizit erzeugt werden. Darüber hinaus entstehen sie bei der Anwendung des Verkettungsoperators (+) sowie als Rückgabewert einiger weiterer vordefinierter Funktionen. Bitte beachten Sie, dass alle Werte des Typs string, die nicht durch Literale erzeugt wurden, mit Hilfe der Funktion free wieder freigegeben werden sollten, sobald sie nicht mehr benötigt werden.
Zulässige Operationen für den Typ string sind:
Der Datentyp FILE ist ein Verweis auf eine geöffnete Datei. Instanzen dieses Typs werden mit der vordefinierten Funktion fopen erzeugt und mit fclose wieder freigegeben. In BOB+ (und auch in BOB) existieren – wie in C – drei vordefinierte Konstanten des FILE-Typs, die auf die Standard Ein und –Ausgabedateien des Systems verweisen:
Diese Standarddateien können nicht mit fopen bzw. fclose geöffnet oder geschlossen werden.
Zulässige Operationen für den Datentyp FILE sind:
Der Datentyp vector repräsentiert ein Datenfeld fester (d.h. bei der Erzeugung festgelegter) Größe, dessen Elemente beliebige (auch typverschiedene) Werte sein können. Instanzen dieses Typs werden mit den vordefinierten Funktionen newvector oder Vec bzw. T erzeugt und mit free wieder freigegeben.
Zulässige Operationen für den Datentyp vector sind:
Der Datentyp function repräsentiert eine innerhalb des Programmes aufrufbare Funktion. Intern werden vordefinierte und benutzerdefinierte (d.h. selbst in BOB+ geschriebene) Funktionen nochmals unterschieden 18). Aus Benutzersicht ist diese Unterscheidung nicht notwendig. Eine explizite Instanzenbildung von Funktionen ist nicht möglich. Von einer Funktion existiert stets genau eine Instanz, die für vordefinierte Funktionen bei der Initialisierung von BOB+ und für benutzerdefinierte Funktionen während der Übersetzung der Funktionsdeklaration (siehe Abschnitt 10) erzeugt wird.
Zulässige Operationen für den Datentyp function sind:
Der Datentyp class repräsentiert eine innerhalb des Programmes verfügbare Klasse (vgl. 11.1). Klassen sind Vorlagen für konkrete Objekte, Deshalb ist eine explizite Instanzenbildung von Klassen ist nicht möglich. Von einer Klasse existiert stets genau eine Instanz, während der Übersetzung der Klassendeklaration erzeugt wird.
Zulässige Operationen für den Datentyp class sind:
Der Datentyp object repräsentiert konkrete Instanzen einer Klasse (vgl. 11.2). Objekte werden mittels des Operators new aus Vorlagen (Typ class) erzeugt und müssen mit dem Operator delete oder der vordefinierten Funktion free explizit wieder freigegeben werden 19).
Zulässige Operationen für den Datentyp object sind:
Das Konzept der Operatoren in BOB+ (und auch BOB) entspricht weitgehend dem von C. Dieser Abschnitt beschränkt sich daher auf eine kurze Referenz der in BOB+ verfügbaren Operatoren und vorhandener Unterschiede zu C. Für Klassen (bzw. Objekte) können einige der Operatoren neu bzw. umdefiniert werden – siehe hierzu Abschnitt 11.7.
Die arithmetischen Operatoren realisieren in Verbindung mit numerischen Operanden (Datentypen int und float) die vier Grundrechenarten. Der Additionsoperator ist darüber hinaus auch auf Zeichenketten (Datentyp string) anwendbar. In diesem Fall dient er als Verkettungsoperator.
BOB+ kennt folgende arithmetischen Operatoren:
Operator | Bedeutung | Beispiel |
---|---|---|
+ (unär) | positives Vorzeichen | +a |
- (unär) | negatives Vorzeichen | -a |
* | Multiplikation | a * b |
/ | Division | a / b |
% | Divisionsrest | (Modulo) a % b |
+ | Addition | a + b |
- | Subtraktion | a – b |
Tabelle 2 Arithmetische Operatoren in BOB+
Hinweise:
Die Vergleichsoperatoren – auch als relationale Operatoren bezeichnet – dienen dem Größenvergleich ihrer (stets zwei) Operanden.
BOB+ kennt folgende Vergleichsoperatoren:
Operator | Bedeutung | Beispiel |
---|---|---|
== | gleich | a == b |
!= | ungleich | a != b |
> | größer als | a > b |
>= | größer oder gleich | a >=b |
< | kleiner als | a < b |
< = | kleiner oder gleich | a < = b |
Tabelle 3 Vergleichsoperatoren in BOB+
Hinweise:
Die logischen Operatoren verknüpfen Wahrheitswerte miteinander. BOB+ besitzt keinen eigenen Datentyp für Wahrheitswerte, statt dessen gelten folgende Regeln:
Folgende logische Operatoren sind in BOB+ definiert:
Operator | Bedeutung | Beispiel |
---|---|---|
! | NOT | !a |
&& | AND | a && b |
║ | OR | a ║ b |
Tabelle 4 Logische Operatoren in BOB+
Hinweise:
&&
bzw. ||
werden von links nach rechts ausgewertet. Die Auswertung bricht ab, sobald das Ergebnis des Ausdrucks feststeht. Der Rückgabewert ist dann derjenige Operand, der zum Abbruch der Auswertung führte. Deshalb dürfen logische Werte nicht implizit als Integer-Werte angesehen werden.
Die Bitoperatoren sind nur auf Operanden des Typs int anwendbar. Sie realisieren Operationen auf den einzelnen Bits der Operanden.
Folgende Bitoperatoren sind in BOB+ definiert:
Operator | Bedeutung | Beispiel |
---|---|---|
~ | bitweises NOT | ~a |
& | bitweises AND | a & b |
| | bitweises OR | a | b |
^ | bitweises XOR | a ^ b |
<< | bitweise Linksverschiebung | a << 3 |
>> | bitweise Rechtsverschiebung | a >> 3 |
Tabelle 5 Bitoperatoren in BOB+
Die Zuweisungsoperatoren dienen der Zuweisung eines Wertes an eine Variable. Wie in C/C++ wird die Zuweisung mit dem Operator = realisiert, der mit einigen arithmetischen Operatoren kombiniert werden kann. Nachfolgende Tabelle zeigt die Möglichkeiten:
Operator | Bedeutung | Beispiel |
---|---|---|
= | einfache Zuweisung | a = b |
+= | Zuweisung mit Addition | a += b |
-= | Zuweisung mit Subtraktion | a -= b |
*= | Zuweisung mit Multiplikation | a *= b |
/= | Zuweisung mit Division | a /= b |
%= | Zuweisung mit Divisionsrest 21) | a %= b |
Tabelle 6 Zuweisungsoperatoren in BOB+
Hinweise:
a += b
identisch mit a = a + b
.Die Inkrement- und Dekrement-Operatoren sind unär. Sie erhöhen (Operator ++) bzw. erniedrigen (Operator --) ihren Operanden jeweils um den Wert 1. Der Operand muss dabei eine mit dem Datentyp int initialisierte Variable sein.
Beide Operatoren können in Präfix- oder Postfix-Notation verwendet werden. Ein Unterschied zwischen diesen Notationen besteht bei der Verwendung innerhalb von zusammengesetzten Ausdrücken. Bei der Präfix-Notation ist der Wert des Teilausdrucks gleich dem Wert des Operanden nach dem Inkrement/Dekrement während bei der Postfix-Notation der Wert des Operanden vor dem Inkrement/Dekrement zurückgegeben wird.
Nachfolgende Tabelle soll die Varianten veranschaulichen:
Operator | Bedeutung | Beispiel | Entsprechung |
---|---|---|---|
++x | Inkrement (Präfix) | b = ++a | a = a+1, b = a |
x++ | Inkrement (Postfix) | b = a++ | b = a, a = a+1 |
--x | Dekrement (Präfix) | b = --a * 7 | a = a-1, b = a * 7 |
x-- | Dekrement (Postfix) | b = a-- * 7 | b = a * 7, a = a-1 |
Tabelle 7 Inkrement und Dekrement
Hinweise:
c = a || (–b == 1)
die Variable b niemals dekrementiert, solange a true ergibt.Der Zugriffsoperator wird durch eckige Klammern [] ausgedrückt. Er dient dem indexbasierten Zugriff auf einzelne Elemente eines Vektors oder einer Zeichenkette.
Beispiel:
/* operator [] example */ main() { vec = T(1,2,3,4); // create a vector vec[1] = 7; // assign 7 to second element of vec print(vec[1],“\n“); // prints 7 to console free(vec); }
Der Operator ist in seiner vordefinierten Form nur auf Variablen der Typen vector und string anwendbar. Der Operand in der Klammer muss vom Typ int sein.
Runde Klammern zählen ebenfalls zu den Operatoren. Sie werden in zwei Zusammenhängen verwendet:
myFunc(a, b, c)
x = (a + b) * c
Der Bedingungsoperator hat die allgemeine Form condexpr ? expr1 : expr2
.
Dabei wird zunächst der Ausdruck condexpr (eine Bedingung) ausgewertet. Sein Resultat wird als Wahrheitswert interpretiert. Ist das Ergebnis true, so wird anschließend expr1, sonst expr2 ausgewertet. Das jeweilige Resultat ist das Ergebnis des gesamten Ausdrucks.
Beispiel:
/* get minimum */ min(a, b) { return a < b ? a : b; }
Hinweis:
Der Typ des Rückgabewerts der Funktion in obigem Beispiel ist nicht unbedingt bekannt. Ist beispielsweise a vom Typ int und b vom Typ float, so wird der Bedingungsausdruck (a < b
) temporär nach float konvertiert. Abhängig vom Ergebnis ist der Rückgabewert der Funktion vom Typ int oder float.
Der Komma-Operator gestattet die Verkettung von mehreren Ausdrücken zu einer Ausdrucksliste. Anders ausgedrückt: Mit Hilfe dieses Operators lassen sich mehrere Ausdrücke dort notieren, wo syntaktisch nur einer vorgesehen ist. Dabei ist das Ergebnis des Gesamtausdrucks (also der Liste) das des letzten Teilausdrucks.
Hinweis:
Der Komma-Operator ist nicht zu verwechseln mit dem Trennzeichen Komma, das in Parameteroder Variablenlisten verwendet wird.
Diese Operatoren haben nur im Zusammenhang mit Klassen und Objekten Bedeutung. Sie werden in den Abschnitten 11.2 bis 11.4 ausführlicher erläutert und sind in nachfolgender Tabelle nur der Vollständigkeit halber aufgeführt.
Operator | Bedeutung | Beispiel |
---|---|---|
new | Objektinstanz aus Klasse erzeugen | a = new A() |
delete 22) | Löschen einer Objektinstanz | delete a |
:: | Definition von Methoden,Aufruf von Klassenmethoden | A::A() { … } i = A::getMaxId() |
-> | Aufruf von Instanzmethoden | name = a->getName() |
Tabelle 8 Objektbezogene Operatoren in BOB+
Werden mehrere Operatoren innerhalb eines Ausdrucks verwendet, so ergibt sich die Frage nach der Reihenfolge ihrer Auswertung. Um eine solche Reihenfolge festlegen zu können, wird eine Rangfolge (Hierarchie) der Operatoren definiert. BOB/BOB+ hält sich dabei weitgehend an die Regeln von C/C++. In Tabelle 9 ist die Hierarchie der Operatoren in BOB+ aufgeführt. Dabei werden Operatoren höherer Priorität vor jenen niederer Priorität ausgewertet. Bei Operatoren der gleichen Prioritätsstufe erfolgt die Auswertung von links nach recht entsprechend der Notation.
Priorität | Operatoren |
---|---|
0 | , |
1 | = += -= *= /= %= |
2 | ?: |
3 | || |
4 | && |
5 | | |
6 | ^ |
7 | & |
8 | = != |
9 | < <= >= > |
10 | << >> |
11 | + - |
12 | * / % |
13 | ! ~ ++x –-x new delete |
14 | () [] x++ x-- -> |
Tabelle 9 Operatorhierarchie in BOB+
Schlüsselwörter sind im syntaktischen Sinne Terminalsymbole. Das bedeutet, dass sie bereits bei der syntaktischen Analyse identifiziert und speziellen internen Symbolen zugeordnet werden.
Innerhalb eines Programms ist den Schlüsselwörtern eine feste (zentrale) Bedeutung zugeordnet. Deshalb können keine benutzerdefinierten Bezeichner definiert werden die den Schlüsselwörtern entsprechen.
Die Schlüsselwörter in BOB/BOB+ sind im wesentlichen eine Teilmenge der aus C/C++ bekannten. Nachfolgende Tabelle gibt eine Übersicht. Die darin mit (*) gekennzeichneten Schlüsselwörter sind nur in BOB+ definiert, die mit (a) gekennzeichneten wurden mit BOB+ 1.1a eingeführt.
Schlüsselwort | Kategorie | Bedeutung |
---|---|---|
null (*) | Typkonstante | Bezeichner für den Datentyp null und dessen einziger Wert |
nil (*) | Typkonstante | identisch mit null, zur Kompatibilität mit BOB |
do | Steuerung | Konstruktion von Wiederholungsanweisungen |
while | Steuerung | Konstruktion von Wiederholungsanweisungen |
for | Steuerung | Konstruktion von Wiederholungsanweisungen |
break | Steuerung | Abbruch einer Wiederholungsanweisung |
continue | Steuerung | vorzeitiger Sprung zum Anfang einer Wiederholungsanweisung |
if | Steuerung | Konstruktion bedingter Anweisungen |
else | Steuerung | Konstruktion bedingter Anweisungen |
return | Steuerung | (vorzeitige) Rückkehr aus einer Funktion |
class | OOP | Beginn einer Klassendeklaration |
static | OOP | Deklaration eines Klassen-Members |
new | OOP | Erzeugen einer Objektinstanz |
delete (*) | OOP | Zerstören einer Objektinstanz |
TRON (a) | Debugging | Trace-Modus einschalten |
TROFF (a) | Debugging | Trace-Modus ausschalten |
TRSTEP (a) | Debugging | Trace-Einzelschritt-Modus einschalten |
Tabelle 10 Schlüsselwörter in BOB+
Die mit Version Bob+ 1.1a eingeführten Schlüsselwörter zum Debugging gehören streng genommen nicht zur Sprache sondern dienen lediglich zur Unterstützung bei der Fehlersuche. Aus Sicht des Compilers werden sie jedoch wie alle anderen Schlüsselwörter behandelt und innerhalb des BOB+-Quelltextes wie eigenständige Anweisungen gebraucht (d.h. insbesondere, dass sie stets mit einem Semikolon abzuschließen sind). Sie haben keinerlei Einfluss auf die Abarbeitung des eigentlichen Programms und werden in den reinen Laufzeitungebungen (bpr bzw. bpri) ignoriert23).
In den vollständigen Varianten von BOB+ 1.1a (bp bzw. bpi) dienen sie der Erleichterung des Aufspürens von Fehlern während der Programmentwicklung.
Die Anweisungen TRON;
bzw. TROFF;
schalten den Trace-Modus ein bzw. aus. Sie überschreiben also die globale Einstellung (Kommandozeilenoption –t, vgl. Abschnitt 3.2).
Die Anweisung TRSTEP;
schaltet den Bytecode-Interpreter in den Einzelschrittmodus. In diesem Modus wird für jeden Bytecode-Befehl eine Ausgabe wie im Trace-Modus erzeugt und anschließend auf die Betätigung einer Taste gewartet. Mit der Taste ESC wird der Einzelschrittmudus abgebrochen, mit jeder anderen Taste der nächste Schritt ausgeführt.
Im Zusammenhang mit der objektorientierten Programmierung kennt BOB+ noch folgende reservierte Bezeichner (mitunter auch “magic names” genannt):
Die reservierten Bezeichner sind keine Schlüsselwörter im oben beschriebenen Sinne. Sie besitzen jedoch implizit eine besondere Bedeutung im Programm und sollten deshalb nicht als allgemeine Bezeichner verwendet werden.
Für jedes Programm, das aus mehr als einer bloßen Aneinanderreihung von Befehlen bestehen soll, werden Möglichkeiten zur Steuerung des Programmablaufs benötigt. Grundsätzlich unterscheidet man hier zwischen Verzweigungen des Programmablaufes auf Grund von Bedingungen und Wiederholungen von Programmabschnitten.
Viele Programmiersprachen erlauben zudem noch unbedingte Sprunganweisungen. Solche Anweisungen sind in BOB/BOB+ nicht vorgesehen 25) und werden deshalb nicht weiter behandelt.
Zur Steuerung der Programmabarbeitung in Abhängigkeit von einer Bedingung dient die if-else-Anweisung, die in Syntax und Semantik dem gleichnamigen Konstrukt in C/C++ entspricht:
if ( bedingung ) anweisung1 [ else anweisung2 ] .
Ergibt der Ausdruck bedingung den Wahrheitswert true, so wird anweisung1, andernfalls anweisung2 ausgeführt. Die auszuführenden Anweisungen können einfache Anweisungen oder Anweisungsblöcke sein. Insbesondere kann hierfür die if-else-Anweisung selbst verwendet werden, wodurch die Formulierung geschachtelter Bedingungen bzw. Fallunterscheidungen 26) möglich ist.
Beispiel:
main(;fp) { fp = fopen(“testfile.txt“,“rt“); if (fp) // same as fp != null { // do anything fclose(fp); } else print(“opening testfile.txt failed\n“); }
Für die Formulierung von Wiederholungen von Codeabschnitten kennt BOB/BOB+ die Anweisungen while, do-while und for. Syntax und Semantik dieser Anweisungen entsprechen dabei den gleichnamigen Konstrukten in C/C++.
Die while-Anweisung hat die allgemeine Form
while ( bedingung ) anweisung
Dabei wird anweisung so lange (wiederholt) ausgeführt, wie bedingung den Wert true ergibt.
Beispiel:
/* type contents of file to console */ main(;fp,s) { fp = fopen(“testfile.txt“,“rt“); if (fp) // same as fp != null { while(!feof(fp)) { s = fgets(fp); print(s); free(s); } fclose(fp); } else print(“opening testfile.txt failed\n“); }
Die do-while-Anweisung hat die allgemeine Form
do anweisung while ( bedingung )
Sie ist der while-Anweisung sehr ähnlich. Auch hier wird anweisung so lange ausgeführt, wie bedingung den Wert true ergibt. Der wesentliche Unterschied besteht darin, dass die Bedingung erst am Ende geprüft und damit anweisung stets mindestens einmal ausgeführt wird.
Beispiel:
/* prompt user for input until empty line was entered */ main(;s) { s = null; do { if (s != null) free(s); print(“enter line: “); s = gets(); // do anything } while (strlen(s) > 0); free(s); }
Die for-Anweisung ist die mächtigste Form der Formulierung einer Wiederholung. Wie in C/C++ (und im Gegensatz zu Sprachen wie BASIC oder PASCAL) dient sie nicht zwingend der Abarbeitung einer festgelegten Anzahl von Schleifendurchläufen sondern ist eher eine Verallgemeinerung bzw. Erweiterung der while-Anweisung. Die for-Anweisung hat die allgemeine Form
for ( init ; bedingung ; reinit) anweisung
Vor dem Beginn der Abarbeitung einer for-Schleife wird zunächst einmalig die Initialisierungsanweisung init27) ausgeführt. Vor jedem Schleifendurchlauf wird der Ausdruck bedingung ausgewertet. Ergibt er true, so wird anweisung ausgeführt, andernfalls ist die Schleife beendet. Die Anweisung reinit wird am Ende jedes Schleifendurchlaufs (d.h. nach Ausführung von anweisung) ausgeführt.
Beispiel:
/* list all arguments of commandline to console */ main(;argvec,n,i) { argvec = getargs(); // get argument vector n = vecsize(argvec); // get number of elements for(i=0; i<n; i++) // print each element to console print(argvec[i],"\n"); free(argvec); // free allocated memory of argvec }
Schleifen können ineinander geschachtelt werden. Ein typischer Fall für die Anwendung geschachtelter Schleifen ist das Füllen eines mehrdimensionalen Feldes, wie in nachfolgendem Beispiel gezeigt:
/* create and fill a 2-dim array */ main() { rows = 8; cols = 8; /* create a table */ array = newvector(rows); for (i=0; i<rows; i++) array[i] = newvector(cols); /* intialize table */ for (i=0; i<rows; i++) for (j=0; j<cols; j++) array[i][j] = rows * i + j; // do anything // ... /* free array */ for (i=0; i<rows; i++) free(array[i]); free(array); }
Die maximale Schachtelungstiefe ist dabei in BOB+ auf 20, in BOB auf 10 Ebenen begrenzt.
Die Anweisungen break und continue können innerhalb von Schleifen (und nur dort) als zusätzliche Hilfsmittel zur Ablaufsteuerung eingesetzt werden. Mit break kann eine Schleife vorzeitig (d.h. unabhängig von der eigentlichen Abbruchbedingung) abgebrochen werden. Mit continue wird der Rest des aktuellen Anweisungsblocks einer Schleife übersprungen und (sofern die Abbruchbedingung nicht erfüllt ist) direkt zum nächsten Schleifendurchlauf übergegangen.
Beispiel:
/* write all printable characters of testfile.txt to console */ main() { fp = fopen(“testfile.txt“,“rt“); if (!fp) { print(“error opening file\n“); return 1; } while(1) // loops forever { if (feof(fp)) break; // abort loop if end of file reached c = getc(fp); if ((c<‘ ‘) && (c != ‘\n‘)) continue; // ignore control characters other than EOL putc(c,stdout); // write characrter to console } fclose(fp); return 0; }
Wie bereits in Abschnitt 4 angedeutet, sind Funktionen die zentralen Strukturierungselemente eines jeden BOB+-Programmes. Grundsätzlich ist dabei zwischen zwei Typen von Funktionen, den vordefinierten und den benutzerdefinierten zu unterscheiden. Vordefinierte Funktionen (siehe Abschnitt 11) sind fester Bestandteil des BOB/BOB+-Laufzeitsystems. Sie sind in jedem Programm verfügbar und können vom Benutzer (d.h. Programmierer) normalerweise28) nicht geändert werden. Das eigentliche Verhalten eines Programms wird mit Hilfe der benutzerdefinierten Funktionen festgelegt (der Einsprungpunkt main – siehe Abschnitt 4 – ist selbst eine solche Funktion).
Eine benutzerdefinierte Funktion hat die allgemeine Form
funcname ( parameterlist ; localvarlist ) body
Dabei ist funcname der Funktionsname, der ein gültiger Bezeichner (siehe 5.2) sein muss. parameterlist ist eine kommaseparierte Liste benannter Funktionsparameter und localvarlist definiert die Namen der innerhalb der Funktion gültigen weiteren lokalen Variablen (ebenfalls als kommaseparierte Liste). body schließlich ist ein Anweisungsblock, der den Funktionsrumpf enthält. Sowohl parameterlist als auch localvarlist sind optional. Fehlt localvarlist, so kann auch das als Trennzeichen zwischen den Listen verwendete Semikolon entfallen.
Benannte Parameter werden innerhalb des Funktionsrumpfes wie lokale Variablen behandelt. Alle Variablen, die innerhalb des Funktionsrumpfes verwendet werden und nicht in parameterlist oder localvarlist enthalten sind, haben – auch wenn sie nicht außerhalb der Funktion explizit deklariert wurden – globale Gültigkeit.
Anders als in C/C++ geben Funktionen in BOB/BOB+ immer einen Funktionswert zurück. Wird eine Funktion mit return
, gefolgt von einem Argument, verlassen, so ist der Wert dieses Arguments der Funktionswert. Ansonsten ist der Wert undefiniert – genau genommen der Wert, der gerade zuoberst auf dem Stack liegt.
Beispiele für Funktionsdeklarationen:
/* parameterless function without defined return value */ sayHello() { print(“Text from function body\n“); } /* named parameter, local variables and return value */ factorial(n; i, result) { for(result=1, i=n; i>1; i++) result *= i; return result; } /* no named parameters but local variable and return value */ queryName(;name) { name =““; while (strlen(name) == 0) { print(“Tell me your name: “); name = fgets(stdin); } return name; }
Der Aufruf einer Funktion erfolgt durch Nennung des Funktionsnamens, gefolgt von einer geklammerten kommaseparierten Liste der Parameter. Syntaktisch kann ein Funktionsaufruf als einzelne Anweisung oder an Stelle eines Wertes stehen.
Beispiel:
main() { sayHello(); // simple statement – return value is ignored f = factorial(17); // assign return value to variable g = sqrt(2.0) * 0.5; // use function in expression print(queryName()); // use function as parameter of another one }
Hinweis:
Beim Aufruf einer Funktion müssen mindestens so viele Parameter angegeben werden, wie in der Funktionsdeklaration angegeben. Werden mehr Parameter angegeben, so werden den benannten Parameternamen die letzten (d.h. am weitesten rechts stehenden) Parameterwerte zugewiesen. Wie C/C++ unterstützt BOB/BOB++ den rekursiven Aufruf von Funktionen.
Beispiel:
factorial(n) // recursive version of factorial function { if (n > 1) return n * factorial(n-1); return 1; }
Die mögliche Rekursionstiefe wird dabei durch die Größe des Stacks begrenzt. Für einen Funktionsaufruf werden mindestens vier Stack-Einträge benötigt. Hinzu kommt jeweils ein Eintrag für jeden übergebenen Parameter und jede lokale Variable. Demnach berechnet sich die maximale Rekursionstiefe (Rmax) wie folgt:
Rmax = Stackgröße / (Parameterzahl + Anzahl lokaler Variablen + 4)
In BOB+ (nicht in BOB) besteht die Möglichkeit, die Größe des Stacks (und damit die mögliche Rekursionstiefe) mit Hilfe der Umgebungsvariablen BPSTACK zu beeinflussen (vgl. 3.2)
Wie im vorigen Abschnitt erwähnt, ist es möglich, einer Funktion beim Aufruf mehr Parameter zu übergeben als in der Deklaration angegeben. BOB+ erlaubt unter Ausnutzung dieser Eigenschaft die Konstruktion von Funktionen mit einer beliebigen Anzahl von Parametern. Für den Zugriff auf die übergebenen Parameter wird dann an Stelle von Parameternamen die vordefinierte Funktion arg verwendet. Die Bestimmung der Parameteranzahl erfolgt mit argcnt (siehe 12.9).
Beispiel:
sum(;i,result) { result = 0; for (i=0; i<argcnt(); i++) result += arg(i); return result; } main() { print(sum(0,1,2,3,4,5,6,7,8,9),“\n“); }
In Abschnitt 6.2.2 wurde der Datentyp function erklärt, der einem Wert vom Funktionstyp zugeordnet ist. Tatsächlich lässt sich eine Funktion in gewissen Grenzen wie ein „normaler“ Wert behandeln, d.h. insbesondere an eine Variable zuweisen oder anderen Funktionen als Parameter übergeben. Da Werte vom Funktionstyp nichts anderes als einen Zeiger auf den ausführbaren Code der jeweiligen Funktion speichern, sind sie mit den Funktionszeigern in C/C++ vergleichbar und können in analoger Weise verwendet werden.
Beispiel:
myFunc() { print(“myFunc called\n“); } main(;f) { f = myFunc; // assign myFunc to variable f f(); // call function }
Hinweis:
In BOB+ haben Funktionszeiger neben der Verwendbarkeit als Argumente anderer Funktionen oder Funktionsvariablen ein weiteres Einsatzgebiet: Sie können helfen, die Ausführungsgeschwindigkeit eines Programmes zu steigern und/oder die Größe des vom Compiler generierten Bytecodes zu verringern. Der Grund dafür ist die interne Organisation der BOB+-Laufzeitumgebung. Hier werden Funktionen (ebenso wie Klassen und globale Variablen) in Dictionaries verwaltet, die als lineare Listen aus Name-Wert-Paaren implementiert sind. Dementsprechend erfolgt der Zugriff auf Funktionen normalerweise namensbasiert. Das bedeutet, dass bei jedem Funktionsaufruf eine Suche im Funktionsverzeichnis durchgeführt wird. Außerdem steht der jeweilige Funktionsname für jeden Aufruf als Zeichenkette im Bytecode. Daraus resultiert sowohl ein gewisser Zeitaufwand als auch – zumindest bei vielen Funktionsaufrufen – einer Vergrößerung des Bytecodes und damit des Speicherbedarfs.
Lokale Variablen einer Funktion werden dagegen in einem per Index adressieren Datenfeld verwaltet29). Deshalb ist es zweckmäßig, häufig verwendete Funktionen zunächst einer lokalen Variablen zuzuweisen und sie dann über diese Variable aufzurufen.
BOB/BOB+ identifiziert eine Funktion ausschließlich anhand ihres Namens und nicht (wie z.B. C++) anhand der vollständigen Signatur. Damit ist ein Überladen (d.h. die Definition mehrerer Funktionen gleichen Namens) im eigentlichen Sinne nicht möglich. Die Neudefinition einer bereits vorhandenen Funktion führt zwar nicht zu einem Fehler, ersetzt die vorher definierte gleichnamige Funktion aber vollständig.
Unter Ausnutzung der in den vorangegangenen Abschnitten beschriebenen variablen Parameterlisten und Funktionszeiger ist in BOB+ jedoch ein Pseudo-Überladen möglich.
Betrachten wir dazu nachfolgendes Beispiel:
myFunc(i) { print("myFunc(i) called\n"); } myFunc2() { if (argcnt() != 2) myFunc1(arg(0)); else print("myFunc(i,j) called\n"); } main() { myFunc1 = myFunc; myFunc = myFunc2; myFunc(1,2); myFunc(1); }
Ziel ist es hier, zwei Funktionen zu definieren, die beide über den Namen myFunc
aufrufbar sind, wobei die eine einen Parameter und die andere zwei Parameter verarbeitet.
Zuerst wird unter dem Namen myFunc die Variante mit einem Parameter definiert. Die Variante für zwei Parameter erhält zunächst einen anderen Namen (myFunc2) und wird so implementiert, dass sie eine variable Anzahl von Parametern verarbeiten kann. Ist die Anzahl der übergebenen Parameter nicht zwei, so ruft sie eine Funktion myFunc1 auf, andernfalls wird der eigene Code ausgeführt.
Die Funktion main weist nun der (globalen) Variablen myFunc1 den Code von myFunc und myFunc den Code von myFunc2 zu.
Das Konzept der Objektorientierung in BOB/BOB++ ist trotz syntaktischer Ähnlichkeit wesentlich einfacher als das von C++. Dennoch werden die grundlegenden Paradigmen objektorientierter Programmierung – Datenkapselung (zumindest ansatzweise), Vererbung und Polymorphie – berücksichtigt. Darüber hinaus bestehen zusätzliche Möglichkeiten, wie die Konstruktion partieller Klassen, nachträgliche Redefinition von Member-Funktionen sowie (in gewissen Grenzen und nur bei BOB+) die Definition von Operatoren für Objekte. In den nachfolgenden Abschnitten werden die Prinzipien und Möglichkeiten beschrieben.
Zur Erläuterung des Aufbaus einer Klasse in BOB/BOB+ soll nachfolgendes Beispiel dienen. Es stellt den Anfang einer fiktiven Klassenbibliothek mit der Wurzel MyBaseClass und einer davon abgeleiteten Klasse MyDerivedClass dar. Alle Objektinstanzen dieser Bibliothek sollen einen eindeutigen Identifikator (_ID) haben, dessen Vergabe die einzige Aufgabe der Basisklasse ist.
// declaration of MyBaseClass class MyBaseClass { MyBaseClass(); getID(); _ID; static createID(); static _maxID = 0; // initialization works only for BOB+ } // implementation of MyBaseClass MyBaseClass::MyBaseClass() { _ID = createID; } MyBaseClass::getID() { return _ID; } MyBaseClass::createID() { return ++_maxID; } // declaration of MyDerivedClass class MyDerivedClass : MyBaseClass { MyDerivedClass(name, value); getName(); setName(name); getValue(); setValue(value); _name, _value; } // implementation of MyDerivedClass MyDerivedClass::MyDerivedClass(name, value) { MyBaseClass(); _name = name; _value = value; // following line is needed for BOB (but not BOB+) // return this; } MyDerivedClass::getName() { return _name; } MyDerivedClass::setName(name) { _name = name; } MyDerivedClass::getValue() { return _value; } MyDerivedClass::setValue() { _value = value; }
Die Klassendeklarationen wie auch die Implementierungen ähneln auf den ersten Blick dem von C++ gewohnten. Allerdings gibt es eine Reihe von wichtigen Unterschieden:
In BOB/BOB+ sind Member- und Klassenfunktionen sowie Klassenvariablen (static) implizit öffentlich (public im Sinne von C++).
Member-Variablen sind implizit geschützt (protected im Sinne von C++), also nur in der deklarierenden Klasse und ihren Ableitungen sichtbar. Für den Zugriff von außen müssen entsprechende Zugriffsmethoden implementiert werden.
Anders als in C++ hat eine BOB/BOB+-Klasse höchstens eine Basisklasse. Auch ein Interface-Konzept (wie etwa von Java oder C# bekannt) gibt es nicht. Die Vererbung selbst ist immer öffentlich, d.h. alle öffentlichen Elemente der Basisklasse werden auch öffentliche Elemente der abgeleiteten Klassen.
In BOB/BOB+-Klassen ist die Nennung von Member-Funktionen in der Klassendeklaration optional. Etwa hätte man MyDerivedClass in obigem Beispiel auch so deklarieren können 30):
MyDerivedClass : MyBaseClass { _name, _value; }
Diese Eigenschaft erlaubt es, Klassen bei Bedarf nachträglich um Methoden zu ergänzen (siehe hierzu auch 11.6).
Member-Variablen und alle statischen Elemente müssen deklariert werden.
Steht der static-Modifizierer vor einer kommaseparierten Variablenliste, so gilt er für alle ihre Elemente.
In BOB können Klassenvariablen nicht explizit initialisiert werden31). In BOB+ ist dies möglich, jedoch erfolgt die Initialisierung bereits zur Kompilier- und nicht erst zur Laufzeit. Deshalb sind zur Initialisierung nur Literale (und keine Funktionsaufrufe) verwendbar. Die Initialisierung muss stets innerhalb der Klassendeklaration notiert werden (siehe _maxID in MyBaseClass).
Alle Instanzen von Klassen (Objekte) haben den internen Typ object und sind damit Referenztypen (siehe 6.2.2). Der einzige Weg, sie zu erzeugen, führt über den Operator new:
myObj = new MyDerivedClass(“anumber“,1000);
Deshalb ist es nicht möglich, wie in C++ direkt im Anschluss an eine Klassendeklaration Variablen dieses Typs anzulegen, was auch erklärt, weshalb hinter der schließenden Klammer der Klassendeklaration kein Semikolon steht.
Ein Konstruktor ist eine spezielle Member-Funktion eines Objekts, deren Aufgabe hauptsächlich darin besteht, bei der Objekterzeugung die Member-Variablen mit geeigneten Anfangswerten zu initialisieren.
In BOB/BOB+ hat ein Konstruktor – wie in C++ – den selben Namen wie die Klasse selbst. Da ein Überladen von Funktionen im eigentlichen Sinne nicht möglich ist (siehe 10.5), kann eine Klasse nur einen Konstruktor besitzen. Dieser Konstruktor muss einen Verweis auf das erzeugte Objekt als Funktionswert zurückgeben. In BOB bedeutet dies, dass ein Konstruktor stets mit der Anweisung
return this;
verlassen werden muss. Der Compiler von BOB+ fügt den entsprechenden Bytecode automatisch an das Ende jeder Konstruktorfunktion an, so dass die Anweisung hier nur dann notiert werden muss, wenn die Funktion vorzeitig (auf Grund einer Abruchbedingung) verlassen wird. Anders als z.B. in C++ ruft ein Konstruktor in BOB/BOB++ nicht automatisch auch den Konstruktor einer ggf. vorhandenen Basisklasse auf. Ein solcher Aufruf muss explizit kodiert werden. Ein Beispiel zur Verdeutlichung:
class Base { Base(); } Base::Base() { print(“ctor of Base called\n“); // return this; // uncomment when using BOB } class Derived : Base { Derived(); } Derived::Derived { Base(); print(“ctor of Derived called\n“); // return this; // uncomment when using BOB } main() { obj = new Derived(); // do anything obj = delete obj; // comment out when using BOB }
Anmerkungen:
Ein Destruktor ist gewissermaßen das Gegenstück zum Konstruktor. Er dient dazu, beim Zerstören eines Objekts ggf. von ihm belegte externe Ressourcen (zusätzlicher Speicher, offene Dateien etc.) wieder freizugeben.
BOB hat keine Möglichkeit, ein erzeugtes Objekt vor dem Programende explizit freizugeben, womit sich hier die Frage nach einem Destruktor erübrigt. BOB+ implementiert eine Objektfreigabe mit Hilfe des Operators delete32) und übernimmt im wesentlichen das Konzept der Destruktoren aus C++. Konkret heißt dies:
Zur Verdeutlichung erweitern wir obiges Beispiel:
class Base { Base(); ~Base(); getClassName(); } Base::Base() { print(“ctor of Base called\n“); } Base::~Base() { print(“dtor of Base called\n“); } Base::getClassName() { return “Base“; } class Derived : Base { Derived(); ~Derived(); getClassName(); } Derived::Derived { Base(); print(“ctor of Derived called\n“); } Derived::~Derived { print(“dtor of Derived called\n“); } Derived::getClassName() { return “Derived“; } main() { obj = new Derived(); // do anything obj = delete obj; }
Anmerkungen:
Enthält die Implementierung einer Memberfunktion Funktionsaufrufe oder Variablenzugriffe, so wird zunächst in der aktuellen Klasse und ggf. deren Basisklassen nach einem passenden Member gesucht. Verläuft die Suche ergebnislos, so wird der jeweilige Funktions- oder Variablenbezeichner als globaler Bezeichner angenommen. Wird vor dem Bezeichner das Schlüsselwort this mit nachfolgendem Dereferenzierungsoperator (->) angegeben, so wird der Bezeichner nur im jeweiligen Objekt gesucht.
Für statische Member ist das Verhalten ähnlich, jedoch wird hier zur Spezifikation einer konkreten Klasse vor dem eigentlichen Bezeichner der Klassenname und der Bereichsauflösungsoperator (::) angegeben.
In diesem Zusammenhang entsteht das Problem, dass unter Umständen in der Implementierung einer Member-Funktion auf ein globales Symbol (Variable oder Funktion) zugegriffen werden muss, dass namensgleich mit einem Element der
aktuellen Klasse ist. BOB bietet dafür keine Lösung, d.h., der Programmierer muss dafür sorgen, dass solche
Situationen nicht entstehen. In BOB+ kann – wie in C++ – der Zugriff auf die globalen Symbole mit Hilfe des
Bbereichsauflösungsoperators erzwungen werden.
Beispiel:
// globally defined function message() { print(“global function message called\n“) }; class MyClass { MyClass(); message(); // member-function message doAction(); } MyClass() {} MyClass::message() { print(“function MyClass::message\n“);} MyClass::doAction(;obj) { message(); // callinternal message method this->message(); // callinternal message method, // search only in current class ::message() // call global function message }
Da in BOB/BOB+ nicht die Variablen sondern lediglich die Werte einen Typ besitzen, gibt es keine „frühe Bindung“. Das bedeutet, dass alle Methoden einer Klasse – auch die statischen – virtuell sind. Betrachten wir dazu nochmals das Destruktor-Beispiel aus Abschnitt 11.2 und ändern dessen main-Funktion wie folgt:
main() { obj = new Base(); print(obj->getClassName(),“\n“); obj = delete obj; obj = new Derived(); print(obj->getClassName(),“\n“); obj = delete obj; }
Hier werden ein- und derselben Variablen (obj) nacheinander Instanzen der Klassen Base bzw. Derived zugewiesen und jeweils deren getClassName()
-Methoden aufgerufen. Dabei wird beim ersten Aufruf die ursprüngliche Version aus Base und beim zweiten die überschriebene aus Derived aktiviert.
Häufig kommt es vor, dass eine abgeleitete Klasse eine Methode ihrer Basisklasse mit dem Ziel überschreibt, deren Funktionalität zu erweitern, sie aber nicht vollständig zu ersetzen. Dann ist es wünschenswert, den Inhalt der geerbten Methode einfach aufzurufen, anstatt ihn vollständig neu zu implementieren. In BOB ist eine solche Möglichkeit nicht vorgesehen. BOB+ erlaubt dagegen den Aufruf der überschriebenen Methode, benutzt hierfür aber eine besondere syntaktische Konstruktion:
Um innerhalb der Implementierung einer überschreibenden Methode die geerbte Vorfahrmethode aufzurufen, wird dem Funktionsnamen das Präfix BC_ (für BaseClass) vorangestellt.
Beispiel:
class MyBaseClass { MyBaseClass(); writeInfo(); } MyBaseClass::MyBaseClass() {} // ctor does nothing MyBaseClass::writeInfo() { print(“MyBaseClass::writeInfo() called\n“); } MyDerivedClass : MyBaseClass { MyDerivedClass(); writeInfo(); } MyDerivedClass::MyDerivedClass() {} // ctor does nothing MyDerivedClass::writeInfo() { print(“MyDerivedClass::writeInfo() called\n“); this->BC_writeInfo(); // call inherited writeInfo method } main(;obj) { obj = new MyDerivedClass(); obj->writeInfo(); delete obj; return 0; }
Hinweise:
Der Aufruf der geerbten Vorfahrmethode muss über den this-Zeiger erfolgen (siehe obiges Beispiel). Andernfalls würde der Compiler einen „gewöhnlichen“ Funktionsaufruf generieren.
Der Aufruf mit dem BC_-Präfix bezieht sich auf die Basisklasse derjenigen Klasse, von deren Typ das aktuelle Objekt (repräsentiert durch den this-Zeiger) ist. Dies kann bis zur Version 1.1d von BOB+ zu unerwünschten Rekursionen führen, falls das Konstrukt in einer mehrstufigen Klassenhierarchie benutzt wird.
Ab Version 1.1e erkennt der Bytecode-Interpreter solche Situationen, so dass dieses Problem nicht mehr auftritt. Darüber hinaus wurde zusätzlich das Präfix DC_ (für defining class) eingeführt, welches erzwingt, dass die entsprechende Methode genau auf der Klasse aufgerufen wird, in der sie definiert wurde. Es wird analog zum BC_-Präfix gebraucht.
BOB/BOB+ unterstützt partielle Klassen in ähnlicher Form wie C# 2.0. Dabei meint Partielle Klasse eine Klasse, deren Definition auf mehrere Blöcke (die ggf. auch in unterschiedlichen Quelldateien stehen können) verteilt ist. Dies eröffnet mehrere Möglichkeiten:
Für das folgende Beispiel nehmen wir an, dass ein Programm eine Klasse Class1
verwendet, von der es eine Instanz bildet und darauf eine Methode doAction
aktiviert. Die Klasse Class1 selbst wird in einem anderen Modul abschließend definiert und implementiert, welches beim Aufruf mit dem Hauptprogramm kombiniert wird. Zunächst das Hauptprogramm:
/* doAction example – main module action.bp */ class Class1 // class prototype { doAction(); } main(;obj) { obj = new Class1(); obj->doAction(); delete obj; }
Class1 wird hier nur als leerer Prototyp definiert34) – den fordert der Compiler. Eine erste Version der vollständigen Definition und Implementierung erfolgt in einem Modul actcl1_1:
/* first version of Class1 module – actcl1_1.bp */ class Class1 { Class1(); // ctor ~Class1(); // dtor doAction(); _callCnt; // counter for calls of doAction } // implementation Class1::Class1() { _callCnt = 0; } Class1::~Class1() {} // dtor does nothing Class1::doAction() { print(++_callCnt,“. call of doAction in module actcl1_1\n“); }
Nun können Hauptprogramm und Modul jeweils in Bytecode übersetzt und anschließend das Hauptprogramm unter Nutzung des Bibliotheksmoduls Bibliothek gestartet werden:
bp –c action –o action.bpm bp –c actcl1_1 –o actcl1_1.bpm bp –r action actcl1_1
Ohne Änderung am Hauptprogramm können wir jetzt die Klasse Class1 in einem anderen Modul erneut implementieren und alternativ zur obigen Variante verwenden:
/* second version of Class1 module – actcl1_2.bp */ class Class1 { Class1(); // ctor ~Class1(); // dtor doAction(); } // implementation Class1::Class1() {} // ctor does nothing Class1::~Class1() {} // dtor does nothing Class1::doAction() { print(“doAction in module actcl1_2 activated\n“); }
Übersetzung und Aufruf erfolgen analog:
bp –c actcl1_2 –o actcl1_2.bpm bp action –r actcl1_2.bpm
Hinweis:
Um das Beipiel mit BOB benutzen zu können, müssen die Konstruktor-Implementierungen um die
Anweisung return this
ergänzt und die delete
-Anweisung aus dem Hauptprogramm
entfernt werden. Ein Vorkompilieren ist hier nicht möglich.
So, wie man bestehende „normale“ Funktionen durch einfache Neudefinition ersetzen kann (siehe
10.5), ist dies auch für Funktionen möglich, die Bestandteil einer Klasse sind. Die neu definierte
Variante ersetzt die alte vollständig. Diese Eigenschaft lässt sich zur nachträglichen Änderung des
Verhaltens einer Klasse nutzen. Betrachten wir dazu wieder das Beispiel aus dem vorangegangenen
Abschnitt. Jetzt wird jedoch die Klasse Class1 sofort implementiert.
/* doAction example – main module action.bp */ class Class1 { Class1(); doAction(); } Class1::Class1() {} Class1::doAction() { print(“Class1::doAction called\n“); } main(;obj) { obj = new Class1(); obj->doAction(); delete obj; }
Das Beispiel sollte in BOB+ direkt lauffähig sein. Nun wollen wir ein Patch-Modul schreiben, das das Verhalten der doAction-Methode aus Class1 ändert:
/* module patch.bp replaces doAction method of Class1 */ Class1::doAction() { print(“patched version of CtClass::doAction called\n“); }
Bitte beachten Sie, dass unser Patch-Modul nicht separat übersetzbar ist, da es keine Deklaration
von Class1 enthält, folgender Aufruf ist aber möglich:
bp action patch
Hier wird der Patch nach dem ursprünglichen Programm übersetzt, so dass Class1 bereits
vorhanden und der Compiler zufrieden ist.
In BOB+ (nicht in BOB) ist es möglich, einige Operatoren für Objekte (oder genauer: deren Klassen) umzudefinieren bzw. überhaupt zu definieren. Die Möglichkeiten sind in dieser Hinsicht nicht so umfangreich wie bei C++, sollten in vielen Fällen aber dennoch ausreichend sein.
Generell wird ein Operator indirekt definiert, indem eine Klasse eine Memberfunktion definiert, deren Namen einem der reservierten Bezeichner der Form OP_XXX (siehe Abschnitt 8) entspricht.
Wenn eine Klasse den Funktionsoperator () umdefiniert, werden ihre Instanzen zu Funktionsobjekten (mitunter auch als Funktoren bezeichnet). Solche Objekte können innerhalb des Programms wie Funktionen gebraucht werden.
Der Operator wird mit Hilfe einer Member-Funktion OP_CALL definiert.
Beispiel:
/* operator () example */ class StringWriter { StringWriter(file); ~StringWriter(); OP_CALL(); _fp; } StringWriter::StringWriter(file) { _fp = fopen(file,“wt“); // open textfile for write } StringWriter::~StringWriter() { fclose(_fp); } StringWriter::OP_CALL(;n,i) { n = argcnt(); for (i=0;i<n; i++) fputs(arg(i),_fp); return n; } main() { writer = new StringWriter(“txtfile.txt“); writer(“Hello“,“World“); delete writer; }
Der Zugriffsoperator [] dient normalerweise dem indizierten Zugriff auf einzelne Elemente einer Zeichenkette oder eines Vektors. Wird der Operator für Objekte definiert, so sind hierfür zwei Member-Funktionen – eine für den Lesezugriff (OP_VREF) und eine für den Schreibzugriff (OP_VSET) – zu definieren35). Im Gegensatz zur Standardvariante für Strings und Vektoren muss das Index-Argument nicht zwingend eine Ganzzahl sein.
Als Beispiel soll ein Fragment einer Klasse Point dienen, die einen n-dimensionalen Punkt repräsentiert. Die Koordinaten sollen mit Hilfe des Zugriffsoperators gelesen und geschrieben werden können.
class Point() { Point(dim); ~Point(); OP_VSET(index,value); OP_VREF(index); _coords; // vector of coordinates } Point::Point(dim;i) { _coords = newvector(dim); for (i=0;i<dim;i++) _coords[i] = 0; } Point::~Point() { free(_coords); } Point::OP_VSET(index value) { _coords[index] = value; } Point::OP_VREF(index) { return _coords[index]; } main(;point) { point = new Point(2); point[0] = 12; // set x-coordinate point[1] = 4.7; // set y-coordinate; print(“x=“,point[0],“ y=“,point[1],“\n“); delete point; }
Neben dem Funktions- und dem Zugriffsoperator können folgende weitere Operatoren umdefiniert werden:
Operator | Funktionsname |
---|---|
Arithmetische Operatoren | |
+ | OP_ADD |
- | OP_SUB |
* | OP_MUL |
/ | OP_DIV |
% | OP_REM |
Bitweise Operatoren | |
| | OP_BOR |
& | OP_BAND |
^ | OP_XOR |
Verschiebeoperatoren | |
<< | OP_SHL |
>> | OP_SHR |
Tabelle 11 Weitere umdefinierbare Operatoren in BOB+
Das Prinzip ist für alle diese Operatoren gleich und soll hier am Beispiel des Additionsoperators gezeigt werden. Dazu erweitern wir die Klasse Point aus 11.7.2 (siehe auch 11.5) und definieren den Operator + so, dass mit seiner Hilfe ein neuer Punkt erzeugt wird, dessen Koordinaten als Summe der Koordinaten der Operanden gebildet werden.
class Point { Point(dim); // replaced ctor OP_ADD(otherPoint); // operator function getDim(); // get dimension _dim; // saved dim value } Point::Point() { _dim = dim; _coords = newvector(dim); for (i=0;i<dim;i++) _coords[i] = 0; } Point::getDim() { return _dim; } Point::OP_ADD(otherPoint;n,i) { n = otherPoint->getDim(); if (n > _dim) n = _dim; result = new Point(n); for (i=0;i<n;i++) result[i] = _coords[i] + otherPoint[i]; return result; } main(;p1,p2,pt) { p1 = new Point(2); p1[0] = 12; // set x-coordinate p1[1] = 4.7; // set y-coordinate; p2 = new Point(2); p2[0] = 24; // set x-coordinate p2[1] = 14.7; // set y-coordinate; pt = p1 + p2; print(“x=“,pt[0],“ y=“,pt[1],“\n“); delete p1; delete p2; delete pt; }
Hinweis:
Bitte beachten Sie, dass der Additionsoperator in diesem Beispiel ein neues Objekt erzeugt, das
explizit wieder freigegeben werden sollte.
Nachfolgend werden die verfügbaren vordefinierten Funktionen im Sinne einer Referenz
beschrieben. Die Typen der Parameter und Rückgabewerte werden in den jeweiligen Signaturen mit
angegeben.
Die mit (*) gekennzeichneten Funktionen dieses Abschnitts stehen nur in BOB+ zur Verfügung.
null free(variable)
Die Funktion gibt den Speicherplatz einer Variablen des Typs String, Vektor oder Objekt frei.
Parameter:
variable: freizugebende Variable
Rückgabewert:
Die Funktion gibt stets den Wert null zurück.
Hinweise:
Die Funktion dient der Freigabe des Speicherplatzes von nicht mehr benötigten Variablen
der Typen String, Vektor und Objekt. Wird sie auf andere („einfache“) Datentypen
angewendet, bleibt sie wirkungslos.
Achtung: Die versehentliche mehrmalige Anwendung auf ein- und dieselbe Variable kann
zum Programmabsturz führen. Deshalb sollte einer freigegebenen Variablen stets explizit
der Wert null zugewiesen werden (als Funktionsergebnis von free oder null).
Beispiel:
/* example for using free-function */ main(;text) { text = “This Text “; text += “should be disposed after using.\n“; print(text); /* free memory and set variable to null */ text = free(text); print(text); /* writes “null“ to stdout */ }
int memsize([int farmem = 0])
Die Funktion prüft die Größe des noch verfügbaren freien Speichers.
Parameter36):
farmem: legt fest, ob die Größe des freien lokalen (0) oder globalen (!= 0) Speichers bestimmt werden soll
Rückgabewert:
Größe des verfügbaren freien Speichers in Bytes
Hinweis:
In Version 1.1 von BOB+ kann mit dem optionalen Parameter farmem festgelegt werden, ob die Größe des für das aktuelle Programm verfügbaren Speichers oder die außerhalb des lokalen Heaps verfügbare Speichergröße bestimmt werden soll. Da BOB+ im Small-Modell übersetzt wurde, sind für den lokalen Heap stets weniger als 64KB verfügbar. Der global verfügbare Speicherplatz ist z.B. vor dem Laden dynamischer Bibliotheken interessant.
string newstring(int size)
Die Funktion erzeugt einen neuen String aus size Zeichen. Alle Elemente werden mit 0x00
initialisiert
Parameter:
size: Anzahl der Elemente
Rückgabewert:
neu erzeugter String
Hinweise:
Genau genommen erzeugt die Funktion einen Zeichenpuffer (Zeichen-Array) der angegebenen
Größe. Die Größe des Puffers ist fest, d.h. es gibt keine Möglichkeit einer dynamischen
Größenänderung. Der Zugriff auf die einzelnen Elemente erfolgt über den Zugriffsoperator
[].
Ein mit dieser Funktion angelegter Puffer sollte, sobald er nicht mehr benötigt wird, mit free wieder freigegeben werden.
vector newvector(int size)
Die Funktion erzeugt einen neuen Vektor aus size Elementen. Alle Elemente werden mit null
initialisiert
Parameter:
size: Anzahl der Elemente
Rückgabewert:
neu erzeugter Vektor
Hinweise:
Ein mit dieser Funktion angelegter Vektor sollte, sobald er nicht mehr benötigt wird,
mit free wieder freigegeben werden.
Die Größe des so angelegten Vektors ist fest, d.h. es gibt keine Möglichkeit einer dynamischen Größenänderung.
Die Elemente eines Vektors können beliebigen Typs sein. Insbesondere können sie auch
selbst Vektoren sein, was den Aufbau mehrdimensionaler Strukturen erlaubt. Der Zugriff
auf die einzelnen Elemente erfolgt über den Zugriffsoperator [].
vector T(…)
oder
vector Vec(…)
Beide Funktionen sind synonym verwendbar. Sie konstruieren einen neuen Vektor, dessen
Elemente die übergebenen Argumente in der angegebenen Reihenfolge sind.
Parameter:
kommaseparierte Liste beliebiger Werte
Rückgabewert:
neu erzeugter Vektor
Hinweis:
Ein mit dieser Funktion angelegter Vektor sollte, sobald er nicht mehr benötigt wird,
mit free wieder freigegeben werden.
Achtung: Argumente, die als Literale oder Variablen übergeben werden, werden von der
Funktion nicht kopiert. Deshalb ist darauf zu achten, dass sie nicht mehrfach freigegeben
werden dürfen.
Beispiel:
/* T() example */ main(;vec,i) { /* make a vector using strsplit-function */ vec = strsplit("This is a text."," "); for(i=0;i<vecsize(vec);i++) print(vec[i],"\n"); /* make new vector using T-Function Note that element at index 3 is the vector constructed above. */ vec = T(1,2,vecsize(vec),vec,"TEST"); for(i=0;i<vecsize(vec);i++) print(vec[i],"\n"); /* free inner vector */ free(vec[3]); /* free outer vector */ free(vec); }
int vecsize(vector v)
Die Funktion liefert die Anzahl der Elemente von v
Parameter:
v: Vektor
Rückgabewert:
Anzahl der Elemente von v
BOB/BOB+ kennt keine impliziten Typumwandlungen und keinen cast-Operator. Deshalb sind Werte verschiedener Typen innerhalb eines Ausdrucks im allgemeinen inkompatibel37). Zur Lösung dieses Problems wurden Funktionen für die explizite Umwandlung einfacher Datentypen (int, float, string) eingeführt.
Alle Funktionen dieses Abschnitts stehen nur in BOB+ zur Verfügung.
object dynamic_cast(class dest, expr)
Die Funktion verhält sich ähnlich dem dynamic_cast-Operator in C++. Zunächst wird der Ausdruck
expr ausgewertet. Ist das Ergebnis ein Objekt der Klasse dest oder einer ihrer Ableitungen, so wird dieses Ergebnis, in allen anderen Fällen null zurückgegeben.
Parameter:
dest: Klasse, zu der die Zugehörigkeit von expr geprüft werden soll
expr: Ausdruck beliebigen Typs
Rückgabewert:
Ergebnis von expr bei erfolgreichem Test, sonst null
Hinweis:
Im Gegensatz zu C++ wird kein Laufzeitfehler erzeugt, wenn expr kein Objekt ist.
Beispiel:
/* dynamic_cast example */ class Base { Base(); } Base::Base() { } class Derived : Base { Derived(); } Derived::Derived() { } main() { obj1 = new Base(); obj2 = new Derived(); o = dynamic_cast(Base, obj1); // returns obj1 o = dynamic_cast(Base, obj2); // returns obj2 o = dynamic_cast(Derived, obj1); // returns null o = dynamic_cast(Derived, obj2); // returns obj2 i = dynamic_cast(Base, 17); // returns null }
float float(expr)
Die Funktion konvertiert einen Ausdruck der Typen int, float oder string in einen float-Wert.
Parameter:
expr: Ausdruck des Typs int, float oder string
Rückgabewert:
float-Wert von expr
Hinweis:
Bei der Umwandlung aus einer Ganzzahl können bis zu vier Stellen Genauigkeit verloren
gehen. Bei der Umwandlung aus einem String wird das Ergebnis des entsprechenden
Aufrufs der C-Funktion sscanf zurückgegeben.
string getclassname(expr)
Die Funktion wertet den Ausdruck expr aus und gibt den Klassennamen des Resultats zurück, falls
expr ein Objekt oder eine Klasse ergibt.
Parameter:
expr: Ausdruck des Typs class oder object
Rückgabewert:
Klassenname von expr bei Erfolg, sonst null
Hinweis:
Das Ergebnis wird dynamisch erzeugt und sollte mittels free explizit wieder freigegeben
werden.
int gettype (expr)
Die Funktion wertet den Ausdruck expr aus und gibt dessen Typ zurück.
Parameter:
expr: beliebiger Ausdruck
Rückgabewert:
Datentyp-ID von expr
Hinweise:
Das Ergebnis kann mit gettypename in eine lesbare Darstellung umgewandelt werden.
Die ermittelte ID ist zum Typvergleich mit anderen Ausdrücken verwendbar.
string gettypename(int typeid)
Die Funktion ermittelt den Namen des durch typeid spezifizierten Typs.
Parameter:
typeid: ID des Datentyps, dessen Name ermittelt werden soll.
Rückgabewert:
Typname von typeid
Hinweise:
Das Ergebnis wird dynamisch erzeugt und sollte mittels free explizit wieder freigegeben
werden.
Die ID eines Datentyps kann mit gettype ermittelt werden.
int int(expr)
Die Funktion konvertiert einen Ausdruck der Typen int, float oder string in einen int-Wert.
Parameter:
expr: Ausdruck des Typs int, float oder string
Rückgabewert:
int-Wert von expr
Hinweis:
Bei der Umwandlung aus float kann ein Überleuf des Zahlbereichs eintreten. Bei der
Umwandlung aus einem String wird das Ergebnis des entsprechenden Aufrufs der C-Funktion
sscanf zurückgegeben.
string string(expr[,string format])
Die Funktion konvertiert einen Ausdruck der Typen int, float oder string in einen string-Wert. Optional kann in BOB+ 1.1 als zweiter Parameter eine Format-Spezifikation nach den Regeln der C-Funktion sprintf angegeben werden, um die konkrete Form der Darstellung festzulegen.
Parameter:
expr: Ausdruck des Typs int, float oder string
format: Format-String nach den Regeln der C-Funktion sprintf
Rückgabewert:
string-Wert von expr
Hinweise:
Die Implementierung von BOB+ 1.0 begrenzt die Länge des zurückgegebenen Strings auf maximal 199 Zeichen, in Version 1.1 besteht diese Beschränkung nicht mehr.
Das Ergebnis wird dynamisch erzeugt und sollte mittels free explizit wieder freigegeben werden.
Die optionale Formatangabe muss – wenn verwendet – genau eine Variable enthalten, die zum Typ des Wertes von expr passt. Die Funktion verwendet intern die C-Funktion sprintf
und gibt den Format-String ungeprüft an diese weiter! Das bedeutet, dass eine fehlerhafte Formatangabe schlimmstenfalls zum Programmabsturz führen kann!
Die mit (*) gekennzeichneten Funktionen dieses Abschnitts stehen nur in BOB+ zur Verfügung.
int fclose(FILE fp)
Die Funktion schließt eine vorher mit fopen geöffnete Datei.
Parameter:
fp: Handle der zu schließenden Datei
Rückgabewert:
Bei Erfolg 0, sonst Wert ungleich 0
Hinweis:
Die Funktion darf nicht mit den Standard-Streams stdin, stdout und stderr verwendet
werden.
int feof(FILE fp)
Die Funktion testet, ob das Ende der angegebenen Datei erreicht ist..
Parameter:
fp: Handle der zu überprüfenden Datei
Rückgabewert:
Wert ungleich 0, wenn Dateiende erreicht, sonst 0
string fgets(FILE fp)
Die Funktion liest eine Zeichenkette bzw. Zeile aus der in fp angegebenen geöffneten Textdatei.
Das Lesen endet, wenn ein Zeilenumbruch oder das Dateiende erreicht bzw. die interne Puffergröße
von 200 Zeichen ausgeschöpft ist.
Parameter:
fp: Handle der Datei
Rückgabewert:
Bei Erfolg wird die gelesene Zeile als Zeichenkette zurückgegeben. Bei Fehler oder
erreichtem Dateiende ist das Ergebnis eine leere Zeichenkette.
Hinweise:
Das Erreichen des Dateiendes sollte mit der Funktion feof geprüft werden.
Ein ggf. aus der Datei gelesener Zeilenumbruch (‘\n‘) bleibt im Ergebnis erhalten.
Da für das Funktionsergebnis dynamisch Speicher belegt wird, sollte dieser mit Hilfe von
free wieder freigegeben werden.
FILE fopen(string filename, string mode)
Die Funktion versucht, eine Datei mit dem in filename angegebenen Namen im durch mode
spezifizierten Modus zu öffnen. Sie entspricht in ihrem Verhalten der gleichnamigen C-Funktion.
Parameter:
filename: Name der zu öffnenden Datei. Hier ist die Angabe eines einfachen Dateinamens oder
eines Dateipfades (absolut oder relativ) im DOS-Stil möglich.
mode: Der Parameter beschreibt den Öffnungsmodus der Datei. Dabei sind folgenden Modi
möglich:
Rückgabewert:
FILE-Handle bei Erfolg, sonst NULL
Hinweis:
Mit dieser Funktion geöffnete Dateien sollten stets mit fclose geschlossen werden.
int fputs(string text, FILE fp)
Die Funktion schreibt die Zeichenkette in text in die durch fp angegebenee geöffnete Textdatei.
Parameter:
text: zu schreibende Zeichenkette
fp: Handle der Datei
Rückgabewert:
Bei Erfolg der Zeichencode des zuletzt geschriebenen Zeichens, andernfalls der Code von
EOF zurückgegeben.
var freadval(FILE fp)
Die Funktion liest einen (beliebigen) Wert aus einer Binärdatei und gibt ihn als Ergebnis zurück.
Das Format der Datei muss dem von der Funktion fwriteval erzeugten entsprechen.
Parameter:
fp: Handle einer geöffneten Binär-Datei
Rückgabewert:
Bei Erfolg wird der gelesene Wert, andernfalls null zurückgegeben.
Hinweis:
Der Rückgabewert sollte i.a. einer Variablen zugewiesen und mit free wieder
freigegeben werden, sobald er nicht mehr benötigt wird.
int fwriteval(value, FILE fp)
Die Funktion schreibt einen (beliebigen) Wert in eine geöffnete Binärdatei.
Parameter:
value: beliebiger Wert
fp: Handle einer geöffneten Binär-Datei
Rückgabewert:
Bei Erfolg 1, sonst 0
Hinweis:
Mit Hilfe der Funktionen fwriteval und freadval lassen sich komplexe Datenstrukturen –
insbesondere auch Objekthierarchien – serialisieren und deserialisieren. Dabei werden bei
der Deserialisierung Querverweise zwischen Objekten automatisch wieder hergestellt.
Beispiel:
/* Serialization example using fwriteval and freadval */ /* first we define a class */ class ListEntry { static _maxID; _a; _b; _ID; _next; } ListEntry::ListEntry(a,b) { if (!_maxID) _maxID = 0; _ID = ++_maxID; _a=a; _b=b; _next = null; } ListEntry::getA() { return _a; } ListEntry::pr() { print("ID: ",_ID," ",getA(), " ",_b,"\n"); } ListEntry::getNext() { return _next; } ListEntry::setNext(next) { _next = next; } ListEntry::append(var) { if (_next) _next->append(var); else { _next = var; } } ListEntry::~ListEntry() { if (_next) _next = delete _next; print("delete ",_ID,"\n"); } main() { lst = new ListEntry(-1,0); for(i=0;i<9;i++) // append more objects to lst { lst->append(new ListEntry(i,i+1)); } for(p = l;p;p=p->getNext()) // print contents p->pr(); fp=fopen("obj.dat","wb"); // open file for write fwriteval(lst,fp); // write entire list to file fclose(fp); lst = delete lst; // free list print(lst,“\n“); // should print „null“ fp=fopen("obj.dat","rb"); // open file for read lst=freadval(fp); // read contents and assign to lst fclose(fp); for(p = lst;p;p=p->getNext()) // print contents p->pr(); lst = delete lst; // free list }
int getc(FILE* fp)
Die Funktion liest ein einzelnes Zeichen aus der durch fp spezifizierten Datei.
Parameter:
fp: Handle der Datei
Rückgabewert:
Zeichencode des gelesenen Zeichens. Bei Fehler oder Dateiende wird EOF zurückgegeben.
string gets()
Die Funktion liest eine Zeichenkette von maximal 199 Zeichen von der Standardeingabe. Das
Lesen endet bei Eingabe eines Zeilenwechsels.
Rückgabewert:
Bei Erfolg wird die gelesene Zeile als Zeichenkette andernfalls eine leere Zeichenkette.
Hinweise:
Der abschließende Zeilenumbruch (‘\n‘) ist nicht im Ergebnis enthalten.
Da für das Funktionsergebnis dynamisch Speicher belegt wird, sollte dieser mit Hilfe von free
wieder freigegeben werden.
void print(…)
Die Funktion schreibt die Werte der übergebenen Argumente auf die Standardausgabe.
Parameter:
Kommaseparierte Liste von Argumenten beliebigen Typs. Die Argumente werden in der
angegebenen Reihenfolge in die Standardausgabe geschrieben. Dabei werden für primitive
Typen die jeweiligen Werte implizit in Zeichenketten konvertiert. Für alle anderen Typen
(File, Vektor, Klasse, Objekt, Funktion) wird der jeweilige Typ mit der zugehörigen Adresse
ausgegeben.
Hinweis:
Im Gegensatz zu printf aus der C-Standardbibliothek kennt print keine Format-
Spezifikation.
int putc(int c, FILE* fp)
Die Funktion schreibt ein einzelnes Zeichen in die durch fp spezifizierte Datei.
Parameter:
c: Zeichencode des zu schreibenden Zeichens
fp: Handle der Datei
Rückgabewert:
Zeichencode des geschriebenen Zeichens. Bei Fehler wird EOF zurückgegeben.
Alle Funktionen dieses Abschnitts stehen nur in BOB+ zur Verfügung.
int strcmp(string s1, string s2)
Die Funktion vergleicht die Zeichenketten s1 und s2 zeichenweise, wobei zwischen Groß- und
Kleinschreibung unterschieden wird. Sie verhält sich wie die gleichnamige Standard-C-Funktion.
Parameter:
s1: erster Operand
s2: zweiter Operand
Rückgabewert:
< 0, falls s1 < s2\\ 0, falls s1 == s2\\ > 0, falls s1 > s2\\
int stricmp(string s1, string s2)
Die Funktion vergleicht die Zeichenketten s1 und s2 zeichenweise, wobei zwischen Groß- und
Kleinschreibung nicht unterschieden wird. Sie verhält sich wie die gleichnamige Standard-CFunktion.
Parameter:
s1: erster Operand
s2: zweiter Operand
Rückgabewert:
< 0, falls s1 < s2\\ 0, falls s1 == s2\\ > 0, falls s1 > s2\\
Hinweis:
Die Funktion arbeitet nur fehlerfrei, wenn in s1 und s2 keine Zeichen mit einem ASCII-Code > 128 (z.B. Umlaute) vorkommen.
int strlen(string s)
Die Funktion liefert die Länge der Zeichenkette s. Sie verhält sich wie die gleichnamige Standard-C-Funktion.
Parameter:
s: Zeichenkette
Rückgabewert:
Anzahl der Zeichen in s
int strsize(string s)
Die Funktion existiert nur in BOB+ 1.1. Sie liefert die maximal mögliche Länge der Zeichenkette s.
Parameter:
s: Zeichenkette
Rückgabewert:
Maximale Anzahl der Zeichen in s
vector strsplit(string source, string delimiter)
Die Funktion zerlegt die in source übergebene Zeichenkette in Teilketten entsprechend der in
delimiter angegebenen Trennzeichen. Sie verwendet intern die Standard-C-Funktion strtok.
Parameter:
source: zu zerlegende Zeichenkette
delimiter: Zeichenkette mit einem oder mehreren Trennzeichen
Rückgabewert:
Die Funktion gibt einen Vektor zurück, dessen Elemente vom Typ string sind und die
einzelnen Teilketten enthalten.
Hinweis:
Sowohl der zurückgegebene Vektor als auch dessen Elemente werden dynamisch
erzeugt. Sie sollten deshalb mit free wieder freigegeben werden.
Beispiel:
/* strsplit example */ main(;text,result,cnt,i) { text = “This text will be splitted.“; result = strsplit(text,“ .“); cnt = vecsize(result); /* print result */ for (i=0;i<cnt;i++) print(result[i],“\n“); /* free allocated memory */ for (i=0; i<cnt; i++) free(result[i]); result = free(result); text = free(text); }
Alle Funktionen dieses Abschnitts stehen nur in BOB+ zur Verfügung
string ctime(int t)
Die Funktion erzeugt aus der numerischen Datums- und Zeitangabe in t eine druckbare
Zeichenkette. Sie verhält sich wie die gleichnamige C-Funktion.
Parameter:
t: numerischer Datums- und Zeitwert (wie time_t in C)
Rückgabewert:
Zeichenkettendarstellung von t in der Form
Wed Aug 10 21:52:54 2005\n
Hinweis:
Die zurückgelieferte Zeichenkette sollte mit free explizit freigegeben werden.
vector localtime(int t)
Die Funktion erzeugt aus der numerischen Datums- und Zeitangabe in t einen Vektor mit den
Bestandteilen der lokalisierten Zeitangabe.
Parameter:
t: numerischer Datums- und Zeitwert (wie time_t in C)
Rückgabewert:
Vektor aus neun ganzzahligen Elementen entsprechend der tm-Struktur aus C mit folgender
Bedeutung:
Index | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
Wert | sec | min | hour | mday | mon | year | wday | yday | isdst |
Dabei erfolgt die Jahresangabe (year) relativ zum Jahr 1900. Das Feld isdst hat den Wert 1,
wenn sich die Zeitangabe auf Sommerzeit bezieht, sonst 0.
Hinweis:
Der zurückgelieferte Vektor sollte mit free explizit freigegeben werden.
int time()
Die Funktion liefert die aktuelle Zeit in Sekunden seit dem 1. Januar 1970 0:00 Uhr GMT. Sie
verhält sich wie die gleichnamige C-Funktion.
Rückgabewert:
Vergangene Sekunden seit dem 1. Januar 1970 0:00 Uhr GMT
int timer()
Die Funktion liefert die verstrichene Zeit seit dem Programmstart in Millisekunden.
Rückgabewert:
Vergangene Zeit seit dem Programmstart in ms.
Hinweis:
Die Funktion verwendet clock()-Funktion aus C. Deshalb ist die tatsächliche
Auflösung geringer als 1 ms. Unter MS-DOS liegt sie bei etwa 55 ms.
Alle Funktionen dieses Abschnitts stehen nur in BOB+ zur Verfügung. Wenn nicht gesondert
ausgewiesen, akzeptieren die Funktionen anstelle eines float-Arguments auch ein Argument von
Typ int.
int abs(int x) oder float abs(float x)
Die Funktion berechnet den Absolutbetrag des als Argument übergebenen Wertes.
Parameter:
x: numerischer Wert vom Typ int oder float
Rückgabewert:
Betrag von x. Der Typ des Rückgabewertes entspricht dem des Arguments.
float atan(float x)
Die Funktion berechnet den Arcus-Tangens des als Argument übergebenen Wertes.
Parameter:
x: Argument
Rückgabewert:
Winkel im Bogenmaß im Bereich (-pi/2, +pi/2)
float cos(float rad)
Die Funktion berechnet den Kosinus des als Argument übergebenen Winkels.
Parameter:
rad: Winkelangabe im Bogenmaß
Rückgabewert:
Kosinus von rad
float exp(float x)
Die Funktion berechnet Wert der Expotentialfunktion ex.
Parameter:
x: Exponent
Rückgabewert:
Wert von ex
float log(float x)
Die Funktion berechnet natürlichen Logarithmus ln(x).
Parameter:
x: Argument
Rückgabewert:
Wert von ln(x)
float pow(float x, float y)
Die Funktion berechnet Wert der Expotentialfunktion xy.
Parameter:
x: Basis
y: Exponent
Rückgabewert:
Wert von xy
float sin(float rad)
Die Funktion berechnet den Sinus des als Argument übergebenen Winkels.
Parameter:
rad: Winkelangabe im Bogenmaß
Rückgabewert:
Sinus von rad
float sqrt(float x)
Die Funktion berechnet die Quadratwurzel von x.
Parameter:
x: Argument
Rückgabewert:
Quadratwurzel von x
float tan(float rad)
Die Funktion berechnet den Tangens des als Argument übergebenen Winkels.
Parameter:
rad: Winkelangabe im Bogenmaß
Rückgabewert:
Tangens von rad
Die Funktionen dieses Abschnitts ermöglichen es, Systemaufrufe durchzuführen. Sie sind nur in BOB+ verfügbar. Die mit (*) gekennzeichneten Funktionen existieren nur in der Version 1.1 von BOB+. Mit (*c) gekennzeichnete Funktionen sind erst ab Version 1.1c verfügbar.
int addr(var)
Die Funktion liefert die Adresse der Variablen var. Sie wird u.a. für Interrupt-Aufrufe benötigt, die
Adressen als Eingabeparameter erwarten.
Parameter:
var: Variablenbezeichner
Rückgabewert:
Offset-Adresse der angegebenen Variablen.
Hinweise:
Das Verhalten der Funktion unterscheidet sich in BOB+ 1.1 von dem der Version 1.0!
Für Version 1.0 gilt:
Für Version 1.1 gilt:
int CallLibFunc(int proc, …)
Die Funktion ruft die durch proc spezifizierte Funktion in einer dynamischen Bibliothek auf.
Parameter:
Rückgabewert:
Der Rückgabewert und die Art seiner Interpretation hängt von der Implementierung der aufgerufenen Funktion ab.
Hinweise:
Beispiel:
Zur Veranschaulichung wollen wir annehmen, dass eine dynamische Bibliothek für den Zugriff auf eine Datenbank existiert, die „database.dll“ heißt und eine Anmeldefunktion mit der C-Signatur
int login(const char* userName, const char* passwd)
unter dem Namen „login“ exportiert. Diese Funktion erwartet also als Argumente einen Benutzernamen und ein Passwort (beides Zeichenketten). Sie soll bei erfolgreicher Anmeldung eine 1, sonst eine 0 zurückgeben.
Nachfolgender Code-Ausschnitt zeigt alle zum Aufruf der Funktion nötigen Schritte:
// Example for DLL function call main(;hdll,proc,name,pwd) { // first load dynamic library hdll = LoadLibrary(“database.dll”); // get address of exportet function proc = GetProcAddress(hdll,”login”); // now ask user for her/his name and password print(“Enter your name: ”); name = gets(); print(“Enter your password: “); pwd = gets(); // try to login // don’t forget to pass addresses of strings if (CallLibFunc(proc,addr(name),addr(pwd))) { // do anything } else print(“Database access denied!\n”); // release strings free(name); free(pwd); // release library FreeLibrary(hdll); }
int FreeLibrary(int hdll)
Die Funktion gibt eine mit LoadLibrary geladene dynamische Bibliothek wieder frei.
Parameter:
hdll: Handle der freizugebenden Bibliothek
Rückgabewert:
1 bei Erfolg, sonst (d.h. wenn hdll ungültig war) 0
int getmemb(int addr)
int getmemw(int addr)
int getmemdw(int addr)
Die Funktionen lesen einen Byte- (8 Bit), Wort- (16 Bit) oder Doppelwort-Wert von der durch addr angegebenen Speicherposition.
Parameter:
addr: Speicheradresse (FAR). Das höherwertige 16-Bit-Wort enthält die Segmentadresse, das niederwertige den Offset.
Rückgabewert:
Gelesener Wert (8 Bit bei getmemb, 16 Bit bei getmemw, 32 Bit bei getmemdw).
Beispiel:
Nachfolgende Beispielfunktion verwendet getmemdw
zum Auslesen der Interrupt-Vektortabelle:
listIntVecs(;i,p,pv,s) { for(i=p=0;i<256;++i,p+=4) { pvec = getmemdw(p); s = string(pvec,“ Address: %Fp\n“); // Format pointer result print(“Int: “,i,s); free(s); // release string } }
int GetProcAddress(int hdll, string funcname)
Die Funktion liefert die Adresse der unter dem Namen funcname aus der durch hdll spezifizierten DLL exportierten Funktion.
Parameter:
hdll: Handle der DLL, die die Funktion exportiert
funcname: Bezeichnung der exportierten Funktion in der Exporttabelle der DLL
Rückgabewert:
Adresse (FAR Pointer) der exportierten DLL-Funktion. Der Rückgabewert wird als Argument für den Aufruf der Funktion CallLibFunc
benötigt.
Hinweise:
Existiert die angegebene Funktion nicht, so wird das aktuelle Programm unter Ausgabe einer entsprechenden Fehlermeldung abgebrochen.
Exportiert die Bibliothek unter dem angegebenen Namen keine Funktion sondern einen Datenbereich, so ist der Rückgabewert ein Zeiger auf diesen Datenbereich.
int getreg(string regname)
Die Funktion liest den aktuellen Wert der angegebenen Register-Variablen.
Parameter:
regname: Name eines Prozessorregisters, möglich sind:
ax, bx, cx, dx, di, si, al, ah, bl, bh, cl, ch, dl, dh, cflag, flags, ds, es, ss, cs.
Die Groß- und Kleinschreibung wird bei der Angabe der Registernamen ignoriert.
Rückgabewert:
aktueller Wert der angegebenen Register-Variablen.
Hinweis:
Die Funktion liest nicht die tatsächlichen aktuellen Werte der Prozessorregister aus sondern
greift auf eine interne Datenstruktur zu, die vorher innerhalb des Programms explizit (mit
setreg bzw. segread) oder implizit (in Folge eines int86- oder int86x-Aufrufs) initialisiert
wurde.
int inport(int portid)
Die Funktion liest ein Wort (16 Bit) ab Portadresse portid. Dabei wird das Low-Byte direkt von
portid und das High-Byte von portid+1 gelesen.
Parameter:
portid: Port-Nummer
Rückgabewert
gelesenes Wort
Hinweis:
Die Funktion benutzt intern die gleichnamige Turbo-C-Funktion und verhält sich
entsprechend.
int inportb(int portid)
Die Funktion liest ein einzelnes Byte von Portadresse portid.
Parameter:
portid: Port-Nummer
Rückgabewert:
gelesenes Byte
Hinweis:
Die Funktion benutzt intern die gleichnamige Turbo-C-Funktion und verhält sich
entsprechend.
int int86(int intnr)
Die Funktion führt löst einen Aufruf des durch intnr spezifizierten x86-Interrupts aus. Vor dem
Aufruf werden die Prozessorregister AX, BX, CX, DX, DI, SI, FLAGS mit den Inhalten der
entsprechenden internen Registervariablen geladen. Nach dem Aufruf werden die Registerwerte in
die internen Variablen übernommen und die ursprünglichen Registerinhalte wieder hergestellt.
Parameter:
intnr: Nummer des aufzurufenden Interrupts
Rückgabewert:
Wert des Registers AX nach dem Interrupt-Aufruf
int int86x(int intnr)
Die Funktion löst einen Aufruf des durch intnr spezifizierten x86-Interrupts aus. Vor dem
Aufruf werden die Prozessorregister AX, BX, CX, DX, DI, SI, FLAGS, DS und ES mit den
Inhalten der entsprechenden internen Registervariablen geladen. Nach dem Aufruf werden die
Registerwerte in die internen Variablen übernommen und die ursprünglichen Registerinhalte wieder
hergestellt.
Parameter:
intnr: Nummer des aufzurufenden Interrupts
Rückgabewert:
Wert des Registers AX nach dem Interrupt-Aufruf
Hinweis:
Vor Benutzung der Funktion sollten die internen Variablen der Segmentregister mit
Hilfe der Funktion segread auf die tatsächlichen Werte gesetzt werden.
Beispiel:
/* interrupt call example – prints current directory path */ main() { buf = newstring(200); /* buffer for result */ segread(); /* init variables for seg-registers */ setreg("ah",0x47); /* dosfunc nr. */ setreg("dl",0); /* get path for current drive */ setreg("si",addr(buf)); /* load buffer adress to si */ int86x(0x21); /* call DOS-Interrupt */ print(buf,"\n"); free(buf); }
int LoadLibrary(string name)
Die Funktion lädt die dynamische Bibliothek name in den aktuellen Prozessraum und liefert ein Handle auf die Bibliothek.
Parameter:
Rückgabewert:
Handle auf die Bibliothek
Hinweise:
Das zurückgegebene Handle wird als Argument für Aufrufe der Funktionen GetProcAddress
und FreeLibrary
benötigt.
Kann die angegebene Bibliothek nicht geladen werden, wird das aktuelle Programm unter Ausgabe einer entsprechenden Fehlermeldung abgebrochen.
Eine mit LoadLibrary
geladene Bibliothek sollte, sobald sie nicht mehr benötigt wird, mit Hilfe der Funktion FreeLibrary
wieder freigegeben werden.
void movedata(int src, int dest, int cnt)
Die Funktion kopiert einen Speicherblock von cnt Byte Länge von Adresse src nach Adresse dest.
Parameter:
Hinweise:
Die Funktion benutzt intern die gleichnamige Turbo-C-Funktion und verhält sich entsprechend. Die maximal kopierbare Blockgröße beträgt wegen der Speichersegmentierung 64 KB.
Vorsicht: Mit der Funktion können beliebige Speicherbereiche überschrieben werden. Sie sollte deshalb mit entsprechender Vorsicht eingesetzt werden.
void outport(int portid, int value)
Die Funktion schreibt ein Wort (16 Bit) nach Portadresse portid. Dabei wird das Low-Byte direkt
nach portid und das High-Byte nach portid+1 ausgegeben.
Parameter:
portid: Port-Nummer
value: auszugebender Wert
Hinweis:
Die Funktion benutzt intern die gleichnamige Turbo-C-Funktion und verhält sich
entsprechend. Es wird das niederwertige Wort von value berücksichtigt.
void outportb(int portid, int value)
Die Funktion schreibt ein einzelnes Byte nach Portadresse portid.
Parameter:
portid: Port-Nummer
value: auszugebender Wert
Rückgabewert:
gelesenes Byte
Hinweis:
Die Funktion benutzt intern die gleichnamige Turbo-C-Funktion und verhält sich
entsprechend. Es wird das niederwertigste Byte von value berücksichtigt.
void segread()
Die Funktion lädt die aktuellen Werte der Segmentregister des Prozessors (DS, ES, SS, CS) in die
entsprechenden internen Variablen.
void setmemb(int addr, int val)
void setmemw(int addr, int val)
void setmemdw(int addr, int val)
Die Funktionen schreiben einen Byte- (8 Bit), Wort- (16 Bit) oder Doppelwort-Wert an die durch addr angegebene Speicherposition.
Parameter:
Hinweis:
Diese Funktionen erlauben die direkte Manipulation beliebiger Speicherstellen. Sie sollten in Anwendungsprogrammen deshalb mit entsprechender Vorsicht verwendet werden.
void setreg(string regname, int value)
Die Funktion setzt den Wert der angegebenen internen Register-Variablen.
Parameter:
regname: Name eines Prozessorregisters, möglich sind:
ax, bx, cx, dx, di, si, al, ah, bl, bh, cl, ch, dl, dh, cflag, flags, ds, es, ss, cs.
Die Groß- und Kleinschreibung wird bei der Angabe der Registernamen ignoriert.
value: neuer Wert
Hinweis:
Die Werte der internen Register-Variablen werden beim Aufruf der Funktionen int86 bzw.
int86x in die entsprechenden Prozessorregister übernommen.
Die Grafikfunktionen ermöglichen elementare Ausgaben im Grafikmodus. Intern werden BIOSAufrufe
verwendet.
Alle Funktionen dieses Abschnitts stehen nur in BOB+ zur Verfügung.
void setscrmode(int mode)
Die Funktion löscht den Bildschirm und schaltet die Anzeige in den angegebenen Modus um.
Parameter:
mode: neuer Bildschirmmodus
Hinweis:
Die konkrete Bedeutung von mode hängt vom verwendeten Grafik-Adapter bzw. dessen
BIOS ab. Allgemein steht der Wert 7 für den MDA-Modus (monochromer Textmodus) 38).
void setpixel(int x, int y, int color)
Die Funktion setzt einen einzelnen Bildschirmpunkt auf den mit color spezifizierten Farbwert.
Parameter:
x: x-Koordinate
y: y-Koordinate
color: Farbwert
Hinweise:
Die Funktion ist nur bei eingeschaltetem Grafikmodus sinnvoll verwendbar.
Die Werte für die Koordinaten sollten stets nichtnegativ sein und werden nach oben
durch die jeweilige Bildschirmauflösung begrenzt. Der Punkt (0,0) bezeichnet die linke
obere Bildschirmecke.
Beim in color angegebenen Wert sind nur die niederwertigsten 8 Bit signifikant. Die
konkrete Bedeutung hängt vom Bildschirmmodus ab 39).
void line(int x1, int y1, x2, y2, int color)
Die Funktion zeichnet eine Linie in der mit color spezifizierten Farbe vom Punkt (x1,y1) nach
Punkt (x2,y2).
Parameter:
x1: x-Koordinate des Anfangspunktes
y1: y-Koordinate des Anfangspunktes
x2: x-Koordinate des Endpunktes
y2: y-Koordinate des Endpunktes
color: Farbwert
Hinweise:
Die Funktion ist nur bei eingeschaltetem Grafikmodus sinnvoll verwendbar.
Die Werte für die Koordinaten sollten stets nichtnegativ sein und werden nach oben
durch die jeweilige Bildschirmauflösung begrenzt. Der Punkt (0,0) bezeichnet die linke
obere Bildschirmecke.
Beim in color angegebenen Wert sind nur die niederwertigsten 8 Bit signifikant. Die
konkrete Bedeutung hängt vom Bildschirmmodus ab.
Beispiel:
/* Portfolio-example for using graphic functions */ main() { /* resolution */ px=240; py=64; /* switch to graphic mode */ setscrmode(4); /* get current time */ ms = timer(); n=0; /* draw some lines */ for (x=0;x<px;x+=2) { n++; x2= px -x; line(x,0,x2,py-1,1); } for (y=0;y<py;y+=2) { n++; y2= py-1 -y; line(0,y,px-1,y2,2); } /* get elapsed time */ ms = timer() - ms; r = n*1000 / ms; gets(); /* switch to text mode */ setscrmode(7); print(r, " lines per second\n"); }
In diesem Abschnitt werden einige Funktionen beschrieben, die sich keiner der obigen Kategorien
zuordnen lassen. Alle diese Funktionen sind nur in BOB+ verfügbar.
int argcnt()
Die Funktion liefert einen die Anzahl der Argumente, die der aufrufenden Funktion übergeben
wurden.
Rückgabewert:
Anzahl der Argumente der aufrufenden Funktion
var arg(int idx)
Die Funktion liefert das Argument mit dem Index idx der aufrufenden Funktion. Sie kann in
Verbindung mit der Funktion argcnt zur Konstruktion von Funktionen mit variablen Argumentlisten
verwendet werden.
Parameter:
idx: Index eines Arguments der aufrufenden Funktion
Rückgabewert:
Argument mit dem Index idx
Hinweis:
Die Funktion prüft die Gültigkeit des in idx übergebenen Wertes.
Beispiel:
/* using functions argcnt() and arg(idx) */ f(x;i,n) // only first argument has a name { n = argcnt(); print("\n",n," arguments:\n"); for (i=0;i<n;i++) print("\t",arg(i)); print("\n---\n"); return x; } main() { print(f(1,3,"test”)); /* should print 3 arguments : 1 3 test --- 1 */ }
vector getargs()
Die Funktion liefert einen Vektor mit allen beim Aufruf von BOB+ übergebenen
Kommandozeilenargumenten zurück. Dabei ist das Element mit dem Index 0 der Name des
ausgeführten Programms (i.a. also BP.EXE oder BPR.EXE).
Rückgabewert:
Vektor mit allen Kommandozeilenargumenten
Hinweis:
Der zurückgelieferte Vektor sollte mit free explizit freigegeben werden.
vector getusrargs()
Die Funktion liefert einen Vektor mit allen beim Aufruf übergebenen Benutzer-Argumenten zurück.
Dabei ist das Element mit dem Index 0 das erste Kommandozeilenargument, das hinter dem
Kennzeichen # angegeben wurde.
Rückgabewert:
Vektor mit allen Benutzer-Argumenten
Hinweis:
Der zurückgelieferte Vektor sollte mit free explizit freigegeben werden. Er kann leer
sein, wenn keine benutzerdefinierten Argumente angegeben wurden.
Beispiel:
/* using functions getargs() and getusrargs() */ main(;vec1,vec2,n,i) { vec1 = getargs(); vec2 = getusrargs(); print("args:\n"); n = vecsize(vec1); for (i=0;i<n;i++) print(vec1[i],"\n"); print("usrargs:\n"); n = vecsize(vec2); for (i=0;i<n;i++) print(vec2[i],"\n"); free(vec1); free(vec2); }
int rand()
Die Funktion berechnet eine Pseudo-Zufallszahl. Sie verhält sich wie die gleichnamige C-Funktion.
Rückgabewert:
Zufallszahl zwischen 0 und 0x7FFF
Hinweis:
Bei mehreren Aufrufen der Funktion ergibt sich eine annähernd gleichverteilte
Zufallsfolge, die vom Initialwert des Zufallsgenerators abhängt. Ohne explizite
Initialisierung des Zufallsgenerators (vgl. Funktion srand) wird nach dem Programmstart
immer die gleiche Folge generiert.
void srand(int seed)
Die initialisiert den internen Zufallsgenerator mit dem Wert seed. Sie verhält sich wie die
gleichnamige C-Funktion.
Parameter:
seed: Initialwert für den Zufallsgenerator
Hiweis:
Beim Programmstart wird der Zufallsgenerator implizit mit dem Wert 1 initialisiert.
Um tatsächlich „zufällige Zufallsfolgen“ zu erhalten, ist eine Initialisierung mit einem
zeitabhängigen Wert üblich.
Beispiel:
/* random number generator example */ main() { for(l=0;l<5;l++) { seed = timer(); srand(seed); print(“initial value: “,seed,“\n“); for (i=0;i<10;i++) { for (j=0;j<10;j++) print(rand(),“ “); print(“\n“); } } }
int compile(string filename)
Die Funktion bindet zur Laufzeit Quellcode in das aktuelle Programm ein. Die durch filename
spezifizertee Datei wird gelesen und in Bytecode übersetzt.
Rückgabewert:
1 bei Erfolg, sonst 0
Dabei bedeutet „Erfolg“ das erfolgreiche Übersetzen der Quelle in Bytecode.
Hinweise:
Die Funktion unterscheidet sich grundsätzlich von include-Anweisungen anderer
Programmiersprachen. Der in filename enthaltene Code wird nicht zur Übersetzungssondern
zur Laufzeit eingebunden.
Der Aufruf von compile
kann an jeder beliebigen Stelle erfolgen, an der ein Funktionsaufruf
möglich ist. Die Einbindung der übersetzten Quellcodes erfolgt immer in den globalen
Kontext des Programms. Bereits vorhandene gleichnamige Symbole (Bezeichner) werden
dabei durch die neuen ersetzt. Diese Eigenschaft lässt sich verwenden, um z.B. abhängig
von Bedingungen bestimmte Funktionen zur Laufzeit auszutauschen, ohne den sie
verwendenden Clientcode ändern zu müssen.
Achtung: Diese Funktion ist in den Laufzeitumgebungen der Version 1.1 nicht verfügbar!
Beispiel:
/* using compile() example – main module */ testfunc(n) { print(“testfunc(“,n,“) from main module\n“); } main() { testfunc(“Hello World“); if (compile(„tstfunc.bp“)) testfunc(“Hello World“); } /* using compile() example – module tstfunc.bp */ testfunc(n) { print(“testfunc(“,n,“) from tstfunc.bp\n“); }
int loadmodule(string filename)
Die Funktion bindet zur Laufzeit ein bereits vorkompiliertes Bytecode-Modul in das aktuelle
Programm ein.
Rückgabewert:
1 bei Erfolg, sonst 0
Dabei bedeutet „Erfolg“ das erfolgreiche Lesen des Bytecodes.
Hinweise:
Die Funktion unterscheidet sich von der Verarbeitungsanweisung #use insofern, als der in
filename enthaltene Bytecode nicht zur Übersetzungs- sondern zur Laufzeit eingebunden
wird.
Der Aufruf von loadmodule kann an jeder beliebigen Stelle erfolgen, an der ein
Funktionsaufruf möglich ist. Die Einbindung des geladenen Bytecodes erfolgt immer in den
globalen Kontext des Programms. Bereits vorhandene gleichnamige Symbole (Bezeichner)
werden dabei durch die neuen ersetzt. Diese Eigenschaft lässt sich verwenden, um z.B.
abhängig von Bedingungen bestimmte Funktionen zur Laufzeit auszutauschen, ohne den sie
verwendenden Clientcode ändern zu müssen.
Der Satz an vordefinierten Funktionen, den BOB+ bereitstellt, sollte es prinzipiell ermöglichen, nahezu beliebige Anwendungen zu entwickeln. Allerdings ist sein Umfang auf das Nötigste begrenzt, da ein wichtiges Enwicklungsziel bei BOB+ darin bestand, mit wenig Ressourcen auszukommen. Eine größere Menge vordefinirter Funktionen hätte dem zwangsläufig entgegen gestanden. Hinzu kommt, dass es einerseits unmöglich ist, alle denkbare Funktionalität für beliebige Anwendungsfälle bereitzustellen und andererseits in einer konkreten Anwendung meist nur eine verhältnismäßig geringe Anzahl von Funktionen tatsächlich benötigt wird.
Wie schon erwähnt ist es mit den von BOB+ bereitgestellten Mitteln möglich, nahezu jede benötigte Funktion selbst zu implementieren. Wenn es allerdings auf
ankommt, ist es wünschenswert, bei Bedarf „fest programmierte“ Bibliotheken laden und verwenden zu können.
Mit Version 1.1 unterstützt BOB+ deshalb ein Konzept dynamischer Bibliotheken (DLLs), das dem von Microsoft Windows® ähnelt und in der Dokumentation der DOS-DLL-Bibliothek [4]40) näher beschrieben wird. Solche Bibliotheken können beliebige zusätzliche Funktionen bereitstellen und prinzipiell in beliebigen Programmiersprachen geschrieben sein41), sofern diese Programme im EXE-Format erzeugen können und Zeiger auf Funktionen ermöglichen.
Dynamische Bibliotheken lassen sich zur Laufzeit laden und auch wieder entladen. Sie benötigen demnach nur so lange Speicherlatz, wie sie unmittelbar verwendet werden. Außerdem kann eine dynamische Biblithek von mehreren Programmen verwendet werden, ohne mehrfach physisch auf einem Datenträger vorhanden sein zu müssen.
Eine DLL wird zunächst einmal wie ein ganz normales Programm geschrieben, welches mindestens die Funktionen, die exportiert werden sollen, als globale FAR-Funktionen definiert. Darüber hinaus werden folgende Erweiterungen benötigt:
Für das Erstellen von DLLs in (Turbo-/Borland-) C/C++ definiert der DOS-DLL-Header dosdll.h
Makros für die Implementierung dieser Erweiterungen. In anderen Sprachen müssen diese Strukturen „per Hand“ aufgebaut werden (vgl. [4]).
Hinweis: BOB+ ist selbst im Speichermodell SMALL implementiert und benutzt deshalb eine modifizierte Variante von DOS-DLL. Diese Variante ist nicht mit allen Turbo-/Borland-C/C++ Compilern verwendbar. Deshalb sollte für die die Entwicklung von DLLs in C/C++ die Originalversion der DOS-DLL-Bibliothek eingesetzt werden.
Anders als in [4] beschrieben, kann in DLLs für BOB+ neben dem Speichermodell LARGE auch das COMPACT-Modell verwendet werden.
Im Folgenden wird die Implementierung einer DLL in C++ und Assembler42) demonstriert. Die Beispielbibliothek soll den Namen SYSTEM.EXE43) tragen und folgende Funktionen bereitstellen44):
long execProgram(const char* cmd, const char* cmdArgs);
long getEnv(const char* name, long size, char* buf);
long searchPath(const char* name, long size, char* buf);
long getLastError(long size, char* errBuf);
Die Wahl der Signaturen der exportierten Funktionen ist dabei nicht beliebig. BOB+ stellt mit der Standardfunktion CallLibFunc
(Abschnitt 12.7) einen allgemeinen Mechanismus zum Aufruf von DLL-Funktionen bereit, der naturgemäß keine Kenntnis über die konkrete Funktion hat. Deshalb wird davon ausgegangen, dass DLL-Funktionen ausschließlich Argumente von 32 Bit Breite übernehmen und als Ergebnis stets einen 32-Bit-Integer zurückliefern. Demnach gilt:
In der Praxis bedeutet dies keine Einschränkung, da die Werttypen von BOB+ intern ohnehin mit 32 Bit Breite dargestellt werden und mit Hilfe der Standardfunktion addr
die Adresse des Datenbereichs von Referenztypen bestimmt werden kann.
Die Implementierung in C/C++ nutzt die durch den Header dosdll.h
aus der DOS-DLL Bibliothek bereitgestellten Makros sowie die C-Standardbibliothek45). Dadurch fällt der Quelltext vergleichsweise kurz aus. Er ist nachfolgend vollständig angegeben und wird im Anschluss diskutiert.
#define _NOFLOAT_ #include <string.h> #include <stdlib.h> #include <process.h> #include <stdio.h> #include <dir.h> #include <dos.h> #include "dosdll.h" long huge getLastError(long size,char* errBuf) { strncpy(errBuf,strerror(errno),size); return 0; } long huge execProgram(const char* cmd, const char* cmdArgs) { return (long) Spawn((char*)cmd,(char*)cmdArgs); } long huge getEnv(const char* name, long size, char* buf) { long result = 0; const char* pvar = getenv(name); if (pvar != 0) { result = strlen(pvar); if (buf != 0) strncpy(buf,pvar,size); } return result; } long huge searchPath(const char* name, long size, char* buf) { long result = 0; const char* pvar = searchpath(name); if (pvar != 0) { result = strlen(pvar); if (buf != 0) strncpy(buf,pvar,size); } return result; } BEGIN_EXPTABLE EXPENTRY(execProgram), EXPENTRY(getLastError), EXPENTRY(getEnv), EXPENTRY(searchPath), END_EXPTABLE IMPLEMENT_DEFAULT_ENTRYFUNC_ENV REGISTERENTRYFUNC #if __TURBOC__ >= 0x0300 static char dummybuf[255]; #endif int main(int,char**) { puts("Can't run standalone.\n"); return 1; }
Die _NOFLOAT_
-Definition am Amfang des Quelltextes verspricht, dass keinerlei Unterstützung für Gleitpunkt-Artihmetik benötigt wird (und macht den Umfang des Binärcodes um knapp 20 KB kleiner). Fehlt diese Definition, so muss die Übersetzung – wie bei BOB+ selbst – mit eingeschalteter 8087-Emulation erfolgen.
Mit den #include
-Anweisungen werden die Deklarationen der verwendeten Bibliotheksfunktionen sowie die Implementierungsmakros für die DLL-Schnittstelle (aus dosdll.h
) importiert.
Die exportierten Funktionen selbst sind wenig spektakulär und machen im wesentlichen von vorhandenen Bibliotheksfunktionen Gebrauch. Ein paar Anmerkungen scheinen dennoch angebracht:
huge
-Modifier in den Funktionsköpfen erzwingen FAR-Aurufe der Funktionen auch dann, wenn die Bibliothek im COMPACT-Modell übersetzt wird46).getLastError
, getEnv
und searchPath
legen ihr eigentliches Ergebnis (eine Zeichenkette) in einem Puffer ab, der vom aufrufenden Programm zur Verfügung gestellt werden muss. Der Funktionswert selbst ist von untergeordneter Bedeutung und wird lediglich als Erfolgsmeldung verwendet.execProgram
ruft eine Funktion Spawn
auf, die von BOB+ (bzw. DOS-DLL) bereitgestellt und deren Adresse der DLL-Eintrittsfunktion übergeben wird. Der Grund dafür ist, dass Bibliotheksfunktionen, die dynamische Speicheranforderungen durchführen innerhalb einer DLL nicht direkt aufgerufen werden dürfen – siehe hierzu [4].Auf die Implementierung der eigentlichen Funktionen folgt die der DLL-Schnittstelle mit Hilfe von Makros, die genau in der hier angegebenen Reihenfolge verwendet werden müssen:
BEGIN_EXPTABLE
(Kopf) und END_EXPTABLE
(Fuß) aufgebaut. Die einzelnen Tabelleneinträge entsprechen den exportierten Funktionen und werden mit Hilfe des Makros EXPENTRY(<funcname>)
definiert. Alternativ kann auch das – ebenfalls in dosdll.h definierte – Makro EXPENTRY2(<ext_name>,<funcname>)
verwendet werden, das den Export einer Funktion unter einem anderen Namen erlaubt.IMPLEMENT_DEFAULT_ENTRYFUNC_ENV
implementiert die Eintrittsfunktion der DLL. Alternativ dazu existiert das Makro IMPLEMENT_DEFAULT_ENTRYFUNC
. Der Unterschied zwischen den beiden Varianten besteht darin, dass die mit dem Suffix _ENV
zusätzlich die Umgebungsvariablen des Hauptprogramms kopiert, was (wie in unserem Fall notwendig) die Verwendung von Bibliotheksfunktionen, die auf Umgebungsvariablen zugreifen, ermöglicht47). REGISTERENTRYFUNC
schließlich wird die Registrierungsstruktur der DLL erzeugt und die Adressen der Einsprungfunktion und der Exporttabelle darin eingetragen.Was nun folgt, ist eine Verbeugung vor den 3.x-Versionen von Borland C++. Es wird ein statischer Datenbereich angelegt, der notwendig ist, um die Funktion der Standard-I/O-Funktionen der C-Bibliothek in der DLL zu gewährleisten48). Für die (frei verfügbare) Turbo-C++-Version 1.01 ist dies nicht notwendig und wird deshalb auch nicht mit übersetzt.
Die main
-Funktion gibt es vor allem deshalb, weil der Compiler sie für eine ausführbare Datei verlangt. Hier wird sie einfach dazu verwendet, einen Anwender, der die DLL versehentlich direkt startet, auf seinen Irrtum hinzuweisen. Ein anderer sinnvoller Einsatzzweck könnte es sein, sie zum Test der exportierten Funktionen zu benutzen.
Die Assembler-Variante der DLL kann weder die C-Standardbibliothek noch vordefinierte Makros zur Implementierung der DLL-Schnittstelle benutzen – hier ist Handarbeit angesagt. Dem entsprechend ist der Quellcode der Assembler-Version auch wesentlich länger49) – dafür wird man aber durch eine wesentlich kleinere DLL entschädigt.
Auch die Assembler-Version muss eine Datei im EXE-Format sein und mindestens ein Code- und ein Datensegment besitzen. Ein eigenes Stack-Segment ist dagegen nicht unbedingt erforderlich, da normalerweise der Stack des aufrufenden Programms mit benutzt wird.
Im Datensegment sind – wie nachfolgend gezeigt – mindestens
zu definieren. Unser Beispiel verwendet zusätzlich noch ein paar Zeichenketten zur Meldungsausgabe, einen Puffer für die Zwischenspeicherung von Dateipfaden sowie drei Zeiger auf Funktionen, die BOB+ bereitstellt und die in der Eintrittsfunktion (siehe weiter unten) initialisiert werden. Außerdem exportiert es gegenüber der C++-Version eine zusätzliche Funktion searchEnv
, die sich ähnlich wie searchPath
verhält, nur eben den Wert einer beliebigen Umgebungsvariablen als Suchpfad verwendet50).
; data segment DSEG SEGMENT 'DATA' ; some messages msg DB "Can't run standalone.",0DH,0AH,'$' msg1 DB "Error messages not supported.",0DH,0AH,0 pathbuffer DB 256 DUP (0) pathvar DB "PATH",0 ;Pointers to C Functions exported by main program pGetMem DW 0, 0 pFreeMem DW 0, 0 pSpawn DW 0, 0 ;names of exported functions fname0 DB 'execProgram',0 fname1 DB 'getLastError',0 fname2 DB 'getEnv',0 fname3 DB 'searchPath',0 fname4 DB "searchEnv", 0 ;export table ; each entry consists of far pointer to name followed by far pointer to function ;execProgram funcEntry0 DW OFFSET fname0 DW SEG DSEG DW execProgram DW SEG CSEG ;getLastError funcEntry1 DW OFFSET fname1 DW SEG DSEG DW getLastError DW SEG CSEG ;getEnv funcEntry2 DW OFFSET fname2 DW SEG DSEG DW getEnv DW SEG CSEG ;searchPath funcEntry3 DW OFFSET fname3 DW SEG DSEG DW searchPath DW SEG CSEG ;searchPath funcEntry4 DW OFFSET fname4 DW SEG DSEG DW searchEnv DW SEG CSEG ;DllRegStruct - the registration structure regTag DB 'D_O_S_L_I_B0' ; tag searched from main program oEntryFunc DW OFFSET entryFunc ; offset of entry point function sEntryFunc DW SEG CSEG ; segment of entry point function oEntryArray DW funcEntry0 ; address of export table sEntryArray DW SEG DSEG entryCnt DW 5 ; number of entries in export table DSEG ENDS
Die einzelnen Einträge der Exporttabelle bestehen jeweils aus einem FAR-Zeiger auf den exportierten Funktionsnamen, gefolgt von einem FAR-Zeiger auf den Anfang der jeweiligen Funktion.
Die Registrierungsstruktur beginnt immer mit einem festen Tag in Form der Zeichenfolge „D_O_S_L_I_B0“. Nach dieser Zeichenfolge sucht die DLL-Laderoutine, um die Adresse der Registrierungsstruktur zu bestimmen51). Auf dieses Tag folgen unmittelbar die FAR-Zeiger auf die Einsprungfunktion und die Exporttabelle. Den Abschluss der Struktur bildet ein 16-Bit-Wort, das die Anzahl der Einträge in der Exporttabelle angibt.
Schreiben von Funktionen
Alle aus DLLs exportierten Funktionen sind FAR-Funktionen, die nach C-Konvention aufgerufen werden. Dies bedeutet:
Zusätzlich gilt – wie schon erwähnt – die Festlegung, dass alle Argumente mit 32 Bit Breite übergeben werden. Demnach sieht der Stack unmittelbar nach dem Eintritt in eine Funktion wie folgt aus:
| | | Offset Argument n | +-----------------------------+-- SP + 4n | | | | | . | | . | | . | | | | Offset Argument 2 | +-----------------------------+-- SP + 8 | Segment Argument 1 | +-----------------------------+ | Offset Argument 1 | +-----------------------------+-- SP + 4 | Segment Rückssprungadresse | +-----------------------------+ | Offset Rücksprungadresse | +-----------------------------+-- SP | |
Abbildung: Aufbau des Stacks beim Eintritt in eine DLL-Funktion
Eine von einer DLL bereitgestellte Funktion sollte alle Register, die sie verwendet (mit Ausnahme der allgemeinen Register AX, BX, CX und DX) zwischenspeichern und am Ende wiederherstellen.
Die Rückgabe des Funktionswertes erfolgt immer im Registerpaar DX:AX, wobei DX das höherwertige und AX das niederwerige Wort enthält.
Nachfolgend sollen die Eintrittsfunktion der DLL (entryFunc
) sowie die exportierte Funktion execProgram
näher betrachtet werden. Diese Funktionen sind deshalb besonders interessant, weil entryFunc
die Funktionszeiger auf die vom Hauptprogramm (also BOB+) bereitgestellten Funktionen initialisiert und execProgram
eine dieser Funktionen benutzt.
Die Eintrittsfunktion:
Diese Funktion wird automatisch aufgerufen, wenn die DLL in den Prozess geladen wird52).
Zunächst der Quelltext:
; Entry point function ; void __DllEntry(int mode, DLLGLOBSTRUCT* pGlobals) entryFunc PROC FAR PUSH BP ; save BP MOV BP, SP PUSH ES ; save segment registers PUSH DS MOV AX, SS:[BP+6] ;mode CMP AX, DllModeLoad ;do nothing if mode not DllModeLoad JNE endEntryFunc MOV AX, DSEG ; make DS and ES pointing to DSEG MOV ES, AX MOV DS, AX MOV DI, OFFSET pGetMem ; ES:DI points to start of function pointer bock LDS SI, SS:[BP+8] ; DS:SI points to pGlobals MOV CX, 6 ; copy three function pointers (i.e. 6 words) CLD REP MOVSW endEntryFunc: ;restore segment registers POP DS POP ES POP BP ; restore BP RET entryFunc ENDP
Der Aufruf der Funktion erfolgt mit zwei Parametern. Der erste gibt den Aufrufmodus an, wobei hier nur der Modus DllModeLoad
(d.h. mode = 0) behandelt wird. Der zweite ist ein Zeiger auf eine Struktur, die (in C++-Notation) wie folgt definiert ist:
typedef void far* huge (*GetMemFunc)(unsigned long size); typedef void huge (*FreeMemFunc)(void far* block); typedef int huge (*SpawnFunc)(char far* cmd, char far* args); struct DLLGLOBSTRUCT { GetMemFunc huge GetMem; FreeMemFunc huge FreeMem; SpawnFunc huge Spawn; // more compiler specific variables may follow here // … };
An dieser Stelle ist wichtig, dass die Struktur mit Zeigern auf drei Funktionen beginnt, die BOB+ für DLLs bereitstellt, um ihnen die dynamische Anforderung und Freigabe von Speicher sowie das Ausführen externer Programme zu ermöglichen. Die Funktion entryFunc
des Beispiels kopiert diese Zeiger in die Variablen pGetMem
, pFreeMem
und pSpawn
im Datensegment. Damit können sie von den anderen Funktionen der DLL benutzt werden. Weil die drei Funktionszeiger im Datensegment in der gleichen Reihenfolge angeordnet sind, wie in der übergebenen Struktur, können sie einfach „im Block“ kopiert werden.
Die Funktion execProgram:
Die Implementierung der Funktion ruft die von BOB+ bereitgestellte Funktion Spawn
(über den Funktionszeiger pSpawn
aus dem Datensegment) zurück.
; implementation of long execProgram(const char* cmd, const char* cmdArgs) ; we simply call Spawn function in main program execProgram PROC FAR PUSH DS PUSH BP MOV BP, SP MOV AX, DSEG MOV DS, AX PUSH SS:[BP+14] ;push arguments to stack PUSH SS:[BP+12] PUSH SS:[BP+10] PUSH SS:[BP+8] CALL DWORD PTR DS:[pSpawn] ; call spawn, AX contains return value ADD SP, 8 ; remove arguments from stack POP BP POP DS CWD ; make dword result in DX:AX RET execProgram ENDP
Dabei muss sie selbst für die Einhaltung der C-Konventionen für den Funktionsaufruf sorgen.
Deshalb werden zunächst die Register DS und BP auf den Stack gerettet, dann die eigenen Funktionsargumente in umgekehrter Reihenfolge auf den Stack gelegt und anschließend die Rückruffunktion über den Zeiger pSpawn
aufgerufen. Diese Funktion liefert im Register AX den Returncode des aufgerufenen Programms.
Nach dem Aufruf werden mit dem Befehl ADD SP,8
die Funktionsparameter wieder vom Stack geräumt, die Inhalte der Register BP und DS restauriert und schließlich mit CWD ein 32-Bit-Ergebnis im Registerpaar DX:AX erzeugt.
Der direkte Weg zur Verwendung von dynamischen Bibliotheken besteht inder Benutzung der von BOB+ bereitgestellten Funktionen LoadLibrary
, GetProcAddress
, CallLibFunc
und FreeLibrary
(vgl. Abschnitt 12.7). Nachfolgendes Beispiel zeigt diesen Weg, wobei eine Bibliothek „TESTDLL.EXE“ verwendet wird, die eine einfache Funktion „sayHello“ exportiert.
main(;hdll,proc) { print("main loads testdll.exe\n"); // testdll will be handled directly // first load the library hdll=LoadLibrary("testdll.exe"); // assign address of sayHello function proc = GetProcAddress(hdll,"sayHello"); // call function and print returned result print(string(CallLibFunc(proc),"Result of sayHello: %ld\n")); // release library print("Release testdll.exe: ",FreeLibrary(hdll),"\n"); }
Die einzelnen Schritte sind also:
Dies ist im Vergleich zum Aufruf eingebauter Funktionen ein recht aufwendiges Verfahren. Zudem ist insbesondere der Aufruf der CallLibFunc
-Funktion nicht unktitisch, da hier die Übergabe korrekter Parameter allein in der Verantwortung des Programmierers liegt und dabei gemachte Fehler zumeist das gesamte Programm zum Absturz bringen.
Deshalb ist es ratsam, die Benutzung dynamischer Bibliotheken auf der Seite von BOB+ in Klassen zu kapseln und so die Details für den Anwendungsprogrammierer zu verbergen. Als Beispiel hierfür soll eine BOB+-Klasse „System“ dienen, die die Kapselung der unter 13.1 vorgestellten Bibliothek übernimmt53):
/** This class completely encapsulates using of SYSTEM.EXE library. This includes loading and unloading of library as soon as memory management, assignment of function pointers and calls to exported functions. */ class System { // method declarations System(); // ctor ~System(); // dtor exec(name); // execute child program getEnv(name); // get value of an environment variable searchPath(name); // search along PATH for a file name getLastError(); // get description of error occurred at last // member variables strbuf; // buffer for returning string results hdll; // handle of DLL execproc; // address of execProgram function in DLL getenvproc; // address of getEnv function in DLL searchproc; // address of searchPath function in DLL geterrproc; // address of getLastError function in DLL } /** Constructor loads library, assigns function pointers and creates the result buffer. */ System::System() { hdll=LoadLibrary("system.exe"); // We need not check hdll value since GetProcAddress returns null on // invalid handle. execproc = GetProcAddress(hdll,"execProgram"); getenvproc = GetProcAddress(hdll,"getEnv"); searchproc = GetProcAddress(hdll,"searchPath"); geterrproc = GetProcAddress(hdll,"getLastError"); strbuf=newstring(400); } /** Destructor frees result buffer and releases the library. */ System::~System() { free(strbuf); FreeLibrary(hdll); } /** Method encapsulates call to execProgram library function. @param cmd name of program to execute (string) @param args command line arguments for program (string) @return return code of execution (or -1 on error) */ System::exec(cmd,args) { return CallLibFunc(execproc,addr(cmd),addr(args)); } /** Method encapsulates call to getEnv library function. @param name name of environment variable to get value from (string) @return value of environment variable (empty string if not found) @note If length of result exceeds 400 characters it is truncated. */ System::getEnv(name) { strbuf[0] = 0; CallLibFunc(getenvproc,addr(name),400,addr(strbuf)); return strbuf; } /** Method encapsulates call to searchPath library function. @param name name of file to searchfor in PATH @return full file path if found, otherwise empty string */ System::searchPath(name) { strbuf[0] = 0; CallLibFunc(searchproc,addr(name),400,addr(strbuf)); return strbuf; } /** Method encapsulates call to getLastError library function. @return description of last error occurred in DLL @note Currently error description is supported only by C++ version of system library. */ System::getLastError() { strbuf[0] = 0; CallLibFunc(geterrproc,400,addr(strbuf)); return strbuf; }
Den wichtigsten Teil ihrer Arbeit erledigt diese Klasse im Konstruktor bzw. Destruktor.
Der Konstruktor übernimmt das Laden der Bibliothek, ermittelt die Adressen der benötigten DLL-Funktionen und weist sie an entsprechende Member-Variablen zu. Außerdem legt er einen internen Zeichenpuffer an, der für die Rückgabe von Ergebnissen der Funktionsaufrufe benötigt wird.
Der Destruktor sorgt bei der Zerstörung einer Objektinstanz der Klasse automatisch für die Freigabe des Puffers und der Bibliothek.
Die bereitgestellten Methoden schließlich sorgen für die korrekte Ausführung der CallLibFunc
-Aufrufe.
Entsprechend einfacher und übersichtlicher wird nun die Benutzung der Bibliothek im Anwendungsprogramm:
// import precompiled System class #use "system.bpm" main(;sys,result) { // create instance of System class sys = new System(); // now execute system library directly // a message should be printed result = sys->exec("system.exe",""); if (result < 0) { // on error print error message print("Error:",sys->getLastError(),"\n"); } // ... and print result print("Execution result: ",result,"\n"); // get value of PATH environment variable s = sys->getEnv("PATH"); // ... and print it print("PATH (",strlen(s),"): ",s,"\n"); // search for command.com and print its path print("command.com: ",sys->searchPath("command.com"),"\n"); // release system library delete sys; }
In diesem Beispiel werden alle Funktionen der System-DLL aufgerufen. Der zusätzliche Aufwand reduziert sich hier auf das Anlegen und abschließende Freigeben der Variablen sys.
Eine Ergänzung ist noch im Zusammenhang mit der Verwendung dynamischer Bibliotheken bei knappen Speicherressourcen vonnöten:
Normalerweise benutzt BOB+ den lokalen Heap (der wegen des SMALL-Speichermodells immer kleiner als 64 KB ist) für Speicheranforderungen aus dem ausgeführten Programm. Für das Laden dynamischer Bibliotheken und Anforderungen dynamischen Speichers aus diesen Bibliotheken über die GetMem
-Funktion wird dagegen der globale Heap – also der noch im System verfügbare Speicher verwendet. Ist der Speicherplatz im jeweiligen System so knapp bemessen, dass kein oder nicht genügend Platz im globalen Heap vorhanden ist54), so wird versucht, auch hier den lokalen Heap zu benutzen. Dadurch können kleine – typischerweise in Assembler geschriebene – DLLs dennoch geladen werden. Allerdings gilt dies nicht für das Ausführen externer Programme. Hierfür muss in jedem Falle Speicherplatz von System bereitgestellt werden können.
[1] Bob-Source http://www.atari-portfolio.co.uk/library/language/bob.zip
[2] Betz, D. M.: Bob: A Tiny Object-Oriented Language.
In: Dr.Dobbs Journal, Sep. 1991, S.26ff. http://www.ddj.com/documents/s=1009/ddj9415e/9415e.htm
[3] Neue Bob-Sourcen von D. M. Betz: XLISP HomePage, http://www.mv.com/ipusers/xlisper/bob.zip
[4] Ebert, R.-E.: DOS-DLL – Dynamische Bibliotheken für MS-DOS. Berlin 2007
http://www.kiezsoft.de/download/dosdll_manual_de.pdf