Amiga Bibliotheken verwenden
In diesem Tutorial zeige ich, wie man Amiga Bibliotheken verwenden kann. Das Betriebssystem stellt viele Funktionen bereit um mit der Peripherie zu interagieren. Wir werden in diesem Beitrag Dateien lesen, in den Hauptspeicher kopieren und Funktionen externer Bibliotheken darauf anwenden.
Amiga Bibliotheken verwenden
Die vorangegangenen Artikel waren eine gute Einführung in die Amiga Assembler Sprache. Wir sind nun auf einem Level, an dem man schon professionelle Programme schreiben kann. Es gibt noch Befehle die wir nicht kennen, diese schauen wir uns aber in der Folge an, wenn sie eingesetzt werden. In den nächsten Artikeln beschäftige ich mich mehr mit Amiga spezifischen Funktionen, welche vom Betriebssystem bereitgestellt werden.
Amiga Libraries
Workbench bzw. das Amiga OS stellt eine Menge an Bibliotheken bereit um mit der Amiga Hardware zu interagieren. Man findet viele davon unter System/Libs, so auch bei unserem virtuellen Amiga 1200:
Die Bibliotheken sind als *.library Datei abgelegt. Im Amiga ROM Kernel Reference Manual (das Buch kann ich übrigens sehr empfehlen, da es im Internetarchiv nun frei zugänglich ist, sollte man auf diesen Schatz an Informationen auch zugreifen!) findet man folgende Liste:
Warum zeige ich diese Liste ebenfalls? Im ersten Beispiel werden wir die dos.library verwenden um Dateioperationen auszuführen (Datei öffnen, Datel lesen, Datei schließen).
Zugriff auf Bibliotheksfunktionen
Als erstes muss man wissen: man hat per Default nur Zugriff auf die EXEC Bibliothek. Der Pointer darauf liegt immer an Speicheradresse $4. Über diese Bibliothek können wir:
- Speicher allokieren und wieder freigeben
- andere Bibliotheken öffnen und schließen
Will man also Funktionen aus doc.library verwenden, dann muss man diese Bibliothek erst einmal laden. Auf den ersten Seiten des Amiga ROM Kernel Reference Manuals wird das in C und Assembler gezeigt. Der Code für Assembler sieht angepasst für uns so aus:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | LIBRARY_BASE equ $4 bra START include 'exec/types.i' include 'exec/exec.i' include 'exec/exec_lib.i' include 'libraries/dos.i' include 'libraries/dos_lib.i' START: ; load dos.library and store handle in BASELIB_DOS move.l LIBRARY_BASE,a6 lea LIBRARY_DOS,a1 move.l #0,d0 jsr _LVOOpenLibrary(a6) tst.l d0 beq .error move.l d0,BASELIB_DOS nop .error: ; todo some useful error handling BASELIB_DOS: dc.l 0 LIBRARY_DOS: dc.b "dos.library",0 even |
Ein neuer Befehl:
- JSR
Jump to Subroutine springt zur angegebenen Adresse und führt dort die Abarbeitung von Befehlen fort
Das Beispiel ist recht unspektakulär, wenn man es durchsteppt. Der erste move Befehl lädt die Adresse der exec.library von Speicheradresse $4 in das Adressregister a6. Danach laden wir die Adresse zum Namen der zu ladenden Bibliothek in a1. Geladen wird die Bibliothek dann über den JSR Befehl, dem übergeben wir die Adresse der exec Bibliothek. Wir verwenden dazu die LVOOpenLibrary Funktion. Wie auch in höheren Sprachen haben Funktionen Parameter und Rückgabewerte.
- LVOOpenLibrary
übergeben wird die Adresse auf die exec.library. Außerdem muss in a1 eine Adresse auf den Namen der zu ladenden Bibliothek liegen. In d0 wird das Handle auf die geladene Bibliothek zurückgegeben.
Nach der LVOOpenLibrary Funktion haben wir in d0 die Adresse auf die geladene Bibliothek. Im Fehlerfall ist in d0 der Wert 0 und damit das Zero Flag gesetzt, weshalb wir nach einen TST Befehl eine Fehlerbehandlung machen sollten. Hat alles geklappt merken wir uns das Handle auf die Bibliothek im Speicher (BASELIB_DOS).
In der gleichen weise kann man nun mit dem Handle auf die dos.library dessen Funktionen verwenden.
Includes
Ohne Anpassung an das Build Script in Notepad++ wird das oben angegebene Beispiel nicht compilieren. Folgender zusätzliche Parameter muss VASM mitgegeben werden:
-I C:\Users\Werner\Amiga\Source\AmigaGameDev\Include
Grund dafür ist, dass wir nun nicht nur eine einzige Datei mit dem Source Code haben werden. Es gibt zahlreiche fertige Scripts die bestimmte Funktionen anbieten, die man nicht unbedingt neu implementieren möchte. Zum Beispiel die Verwendung von bestimmten Dateitypen wie Bildern.
Datei laden
Um eine Datei in den Speicher zu laden sind einige Funktionsaufrufe nötig. Graeme Cowie hat das in seiner Tutorialreihe auf Youtube in einer Folie sehr gut dargestellt:
Im oben abgebildeten Beispiel haben wir die zweite Zeile implementiert. Eine Bibliothek geladen mit den notwendigen Parametern im Adressregister und als Ergebnis einen Wert im Datenregister abgelegt bekommen. Mit diesen Werten und zusätzlichen Parametern wie der Dateiname lässt sich nun die Datei laden (wir bekommen einen File Handle) und damit die Datei lesen und in den Hauptspeicher kopieren.
Wenn man nun vor dieser Grafik sitzt und den Code anschaut, der nötig ist um nur eine Zeile zu implementieren, kann man schon den Mut verlieren. In Assembler sind diese Abfragen viele Zeilen mit Befehlen. In einer höheren Sprache wie C wären das nur rund 10 Zeilen Code. Als Entwickler muss man nun klug vorgehen. Jede umzusetzende Erweiterung sollte geplant werden und als eigene Datei mit einer einfachen Schnittstelle und Dokumentation über dessen Verwendung erstellt werden. Einmal entwickelt kann diese Funktion wieder verwendet werden. Die Implementierung braucht man sich dann kein zweites mal ansehen oder durchdenken. Der eigentlich Code in der Hauptfunktion bleibt überschaubar kurz.
Für diesen Test erstelle ich eine neue Textdatei, die auf der DH1 Festplatte vom virtuellen Amiga abgelegt wird:
Ziel ist es nun die Datei zu öffnen und den Inhalt in den Hauptspeicher zu kopieren. Ich verwende dazu die vom Youtube Tutorial angegebenen Basiscode mit einigen Modifizierungen. Ich lese eine normale Textdatei und noch kein gepacktes Bild. Der Code der main.asm:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 | LIBRARY_BASE equ $4 bra START include 'exec/types.i' include 'exec/exec.i' include 'exec/exec_lib.i' ; include these because we use the exec lib include 'libraries/dos.i' include 'libraries/dos_lib.i' ; include these because we use the dos lib include 'fileloader.i' ; loader constants include 'fileloader.asm' ; add in loader functions START: ; load dos.library and store handle in BASELIB_DOS move.l LIBRARY_BASE,a6 lea LIBRARY_DOS,a1 move.l #0,d0 jsr _LVOOpenLibrary(a6) tst.l d0 beq .error move.l d0,BASELIB_DOS ; read file lea FILENAME,a0 move.l #0,d0 move.l #MEMF_CHIP,d1 bsr tecLoadFile tst.l d0 bmi .error nop .error: ; todo some useful error handling move.l #-1,d0 bra .rts .exit: move.l #0,d0 .rts: rts FILENAME: dc.b "dh1:test.txt",0 even BASELIB_DOS: dc.l 0 LIBRARY_DOS: dc.b "dos.library",0 even |
Das ist der erweiterte Code vom vorherigen Beispiel. Es sollte eigentlich nichts neues dabei sein. Interessant ist wohl der Aufruf von tecLoadFile. Diese Funktion ist in fileloader.asm implementiert und bildet lädt die übergebene Datei. Im Assembler File steht folgende Information:
1 2 | ; rawHandle = tecLoadFile(*filename, memtype) ; d0 a0 d1 |
Das bedeutet:
- d0
ist der Rückgabewert, dort steht das Handle auf die Datei - a0
ist der erste Parameter und muss einen Pointer auf eine Zeichenkette beinhaltet die dem Pfad inklusive Dateiname entspricht. In meinem Fall dh1:test.txt - d1
ist der Speicherbereich in welchen die Datei geladen werden soll
memtype
Der Amiga bietet 2 unterschiedliche Typen Hauptspeichers an:
- #MEMF_CHIP
das ist der Chip Ram eines Stock Amigas - #MEMF_FAST
das ist der erweiterte schnelle RAM - #MEMF_ANY
Option 3? Gibt man any an, dann wird der verfügbare Speicher verwendet. Das ist die ideale Lösung, wenn man keine Abfragen über Verfügbarkeit machen möchte.
Der gesamte Source Code der fileloader.asm Datei (in der fileloader.i sind die dort verwendeten Konstanten definiert):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 | tecLoadFile: movem.l d1-d7/a0-a1,-(a7) ; save registers move.l d1,d5 ; save requested memory type ; open the file for reading move.l a0,d1 move.l #MODE_OLDFILE,d2 move.l BASELIB_DOS,a6 jsr _LVOOpen(a6) ; handle[d0] = LVOOpenFile(filename[d1],mode[d2]) tst.l d0 beq .open_error move.l d0,d4 ; save the file handle for later use. ; allocate ram based move.l #4096,d0 ; get file length move.l d5,d1 move.l LIBRARY_BASE,a6 jsr _LVOAllocMem(a6) tst.l d0 bmi.s .alloc_error move.l d0,d5 ; save the allocated buffer origin into d5 move.l d4,d1 ; get file handle for seek move.l #0,d2 ; to 0 byte offset move.l #OFFSET_BEGINNING,d3 ; return to start of file move.l BASELIB_DOS,a6 jsr _LVOSeek(a6) ; read the entire file move.l d4,d1 ; get file handle for seek move.l d5,d2 ; buffer to read into move.l #$ffffff,d3 ; read entire file jsr _LVORead(a6) ; bytes[d0] = LVORead(handle[d1],buffer[d2],size[d3]) tst.l d0 bmi.s .read_error move.l d0,d3 ; close the file move.l d4,d1 ; result = LVOClose(handle[d1]) jsr _LVOClose(a6) movem.l (a7)+,a0-a1 move.l d6,d0 ; return number of bytes read. bra .exit ; all done. .open_error: moveq #ERROR_HANDLE_FILE_OPEN,d0 bra.s .exit .header_error: moveq #ERROR_HANDLE_HEADER_NOT_FOUND,d0 bra.s .exit .alloc_error: moveq #ERROR_HANDLE_ALLOCATE_FAIL,d0 bra.s .exit .read_error: moveq #ERROR_HANDLE_FILE_READ,d0 bra.s .exit .exit: movem.l (a7)+,d1-d7/a0-a1 rts |
Zur Laufzeit zeigt uns das Programm den Erfolg an. Der Inhalt der Datei steht im Hauptspeicher, an der zuvor allokierten Stelle. Der Ablauf von der Routine ist wie folgt:
- Datei öffnen
- 4KB Speicher reservieren
- Zeiger an den Anfang der Datei setzen
- gesamte Datei lesen und in den reservierten Puffer im Hauptspeicher ablegen
- Datei schließen
Wie am Bild ersichtlich wurde der Inhalt der Datei in den Hauptspeicher vom Amiga geladen. Der Speicherbereich wurde zuvor mit AllocMem reserviert.
Probleme?
Programmieren lernen geht nur in der Praxis und man lernt nur, wenn man Fehler macht. Je mehr Fehler man gemacht und selbst gelöst hat, desto seltener wird man zukünftig Probleme haben. Sprich: der Weg durch das Tal der Tränen bleibt dir nicht erspart. Assembler hat mich auch vor eine Herausforderung gestellt und ich hatte auch so meine Probleme den oben abgebildeten Source Code zu schreiben:
In solchen Fällen ist der Debugger sehr hilfreich. Im ersten Schritt sollte man herausfinden an welcher Stelle der Fehler passiert. Danach muss man alle dafür nötigen Voraussetzungen prüfen. Steht in jedem Datenregister etwas sinnvolles? Durch diese Information kann man den Weg des Programms zurückgehen und weiter prüfen. Assembler Debuggen ist zwar 100% transparent, mit den Werte in den Registern sind aber erst mit etwas Erfahrung gut zu interpretieren.
Fazit
Hat man einmal das System durchschaut kann man Amiga Bibliotheken verwenden. Das Betriebssystem stellt alle nötigen Funktionen bereit um die Hardware zu nutzen. Jegliche Ein- und Ausgaben sind dann auch über Assembler möglich. Das Praxisbeispiel zeigt wie man den Source Code auf mehrere Dateien verteilt und man so recht übersichtlich das eigene Projekt organisieren kann. Möglich gemacht hat das ein zusätzlicher VASM Parameter im Build Script.
Mit diesem Artikel ist die Einführung in Amiga Assembler im Blog abgeschlossen. Es werden unregelmäßige Artikel folgen die bestimmte Details und Besonderheiten vom Amiga behandeln und auf dieser Serie aufbauen. Von nun an solltest du als Leser in der Lage sein selber Assembler Code zu schreiben und diesen zu debuggen.
Alle Artikel dieser Artikelserie:
Programmieren auf dem Amiga
Programmieren auf dem Amiga – Teil 2
Amiga Assembler
Amiga Assembler – Teil 2
Amiga Assembler – Teil 3
Amiga Assembler – Teil 4
Amiga Assembler – Teil 5
Amiga Bibliotheken verwenden