BlitzMax Weiterentwicklung & Blog

Kommentare anzeigen Worklog abonnieren

Worklogs BlitzMax Weiterentwicklung & Blog

Entwicklung: Neuer bmk & Docker im Build-Prozess

Donnerstag, 31. Mai 2018 von Thunder
Ich schulde euch Mal einen Beitrag zur Entwicklung. Embarassed

bmk

Leider ist seit April nicht viel geschehen. Aber der neue bmk war damals schon relativ fertig.
Man kann das komplette Set an Standard-Modulen (brl, pub, maxgui) damit bauen.

Ein paar Features unterstützt er noch nicht, was unter anderem verhindert, dass bruceys bah-Module funktionieren Sad
Es ist mir aber großteils bekannt, woran das liegt. Zum Beispiel kann man so etwas schreiben:
BlitzMax: [AUSKLAPPEN]
ModuleInfo "CC_OPTS: -DHAVE_CONFIG_H"

und damit bezwecken, dass beim Build des Moduls, in dem diese Zeile vorkommt, der String -DHAVE_CONFIG_H zu den C-Compiler Optionen hinzugefügt wird. Das ist eine Aufgabe, die der bmk auch erledigt.

Irgendwie fehlt nach dem Linken von MaxIDE auf Windows das Icon. Das Kompilieren eines Moduls bricht nicht ab, wenn es einen Fehler gab. bmk makemods hat noch Probleme ohne das -a Flag. etc.

Zum bmk werde ich noch einen Post nachreichen!
Da gibt es interessante algorithmische Probleme und neue Features zu entdecken Smile

Automatisches builden

Jetzt gibt es auch ein Makefile im src/ Ordner, das dazu da ist, den kompletten build-Prozess durchzuführen, also bmk und bcc compilieren, dann damit die BlitzMax-Module kompilieren und schließlich makedocs und MaxIDE.

Nachdem ich das Projekt bei bitbucket hoste, wo man 50 Minuten Build-Zeit pro Monat gratis bekommt, hab ich mir deren Build-System Mal angesehen und es ist relativ einfach da Fuß zu fassen.

1. Als erstes muss man ein Docker-Image aussuchen, innerhalb dessen alle Tools installiert sind, die zum Kompilieren notwendig sind. Da es so eines speziell noch nicht gab, hab ich eines gebaut (Dockerfile findet ihr unter src/docker).

2. Dann muss man ins Repo nur noch ein YAML-File hineinschmeißen, das den Build-Prozess beschreibt (es ist im Prinzip nichts anderes als ein Shell skript).
Für BlitzMax wird dieses verwendet:

Code: [AUSKLAPPEN]
image: chtis/blitzmax-buildenv-linux:latest

pipelines:
  custom:
    build-linux:
      - step:
          script:
            - git submodule init
            - git submodule update
            - cd src
            - make clean
            - make install
            - make clean
            - cd ..
            - tar czf blitzmax-linux.tgz *
            - curl -X POST "https://${BLITZMAX_AUTH}@api.bitbucket.org/2.0/repositories/${BITBUCKET_REPO_OWNER}/${BITBUCKET_REPO_SLUG}/downloads" --form files=@"blitzmax-linux.tgz"


Ganz oben steht der Name des Docker-Image. Die Build-Server holen sich dann dieses Image, erstellen einen Container, laden das Code-Repo hinein und führen das skript aus. In den beiden letzten Schritten wird das Archiv gepackt und in die Downloads-Sektion hochgeladen.

Nachdem das dort echt flott geht (ca 5 Minuten - ihr müsst bedenken, dass alle Module viermal gebaut werden wegen debug/release und single/multi-threaded) bin ich damit voll zufrieden. Es gehen sich also ca. 10 builds im Monat gratis aus. Und ich verwende fancy Technologie (Docker) Razz

Ausblick

Ich finde das Fundament ist schon ganz gut. Es ist wichtig, dass der neue bmk perfekt funktioniert, damit auch
wirklich die alten Module gebaut werden können. Ich möchte eigentlich relativ wenige breaking changes machen.

Der nächste Schritt nach der Stabilisierung von bmk wird vermutlich die Ausarbeitung einer neuen Standardbibliothek sein. Außer ich schaffe es vorher noch im Compiler Interfaces zu implementieren (die wären nämlich in BlitzMax wirklich praktisch).

Neu bedeutet nicht komplett anders, aber ich möchte core-Module, die sehr mächtig, aber klein sind, und ich möchte basic-Module (als Ablöse für die brl-Module), die auf den core-Modulen basieren und gleiche/ähnliche Interfaces bereitstellen wie die brl-Module.

Blog: Ein Henne-Ei-Problem

Montag, 2. April 2018 von Thunder
Heute geht es passend zu Ostern um ein "Problem" beim Kompilieren von BlitzMax Very Happy

In BlitzMax gibt es das Programm bcc, das ist der BlitzMax Compiler, der aus dem BlitzMax-Quelltext Assembler erstellt, und das Programm bmk, das dafür zuständig ist, bcc, fasm, gcc, g++ usw. in der richtigen Reihenfolge aufzurufen, Module zu bauen [1] und möglichst nichts neu zu kompilieren, was schon kompiliert vorliegt.

In BlitzMax dabei waren außerdem eine IDE (maxide) und ein Programm (makedocs) um die HTML Dokumentation aus dem bbdoc-Format zu erstellen. Außerdem sind ein Haufen Standard-Module dabei.
Schauen wir uns mal an, in welcher Programmiersprache die alle geschrieben sind:

bcc: C++
bmk: BlitzMax
MaxIDE: BlitzMax
makedocs: BlitzMax
Module (brl, pub, maxgui): BlitzMax, C, C++, assembler

Das bedeutet, wenn ich BlitzMax von Quelltext kompilieren möchte, baue ich als erstes bcc mithilfe eines C++ Compilers. Dann komme ich aber nicht mehr weiter, weil ich nicht mit der Hand mit bcc, g++, ld etc. so hantieren kann, dass am Ende gebaute Module und ein funktionierender bmk herauskommen.
Wer BlitzMax mit den Scripten/Batch-Files von BRL kompilieren möchte, braucht also erst Mal einen funktionierenden bmk, den man sich als exe file herunterladen muss [2].

Das ist relativ unnötig - vor allem weil der BlitzMax Compiler selbst in C++ geschrieben ist. Wenn wir eine bmk Version in C++ hätten, könnten wir als erstes bmk und bcc kompilieren und dann den Rest.
Daran arbeite ich gerade. An einem gut-konfigurierbaren und verlässlichen Ersatz für bmk, der in C++ programmiert wird - und der eventuell etwas übersichtlicher programmiert ist als der alte bmk Sad .

PS:
Es gibt übrigens noch etwas, das wir nicht von Quelltext kompilieren können: fasm [3] ist self-hosting, d.h. er ist in seiner eigenen (Assembler-)Sprache geschrieben und man braucht immer eine alte version des assemblers um eine neue zu assemblieren.

Endnoten
[1] Mit "Bauen" meine ich den nötigen Vorgang um aus Quelltext ein ausführbares Programm oder eine Library zu erstellen. Das kann oft mehrere Prozesse involvieren (nicht nur das Kompilieren, z.B. auch assemblieren oder linken).
[2] hier sind die exe files im git repo von blitzmax: https://github.com/blitz-resea...32_x86/bin
[3] fasm = flat assembler, der assembler, der in Blitzmax verwendet wird, link: http://flatassembler.net/

Blog: #13 Try/Catch Fehler in BlitzMax

Dienstag, 27. März 2018 von Thunder
Hallo,

diesen Worklog möchte ich meiner Arbeit mit/an BlitzMax widmen. Das ist nicht unbedingt nur Programmieren sondern, wie ihr in meinen Tutorials vielleicht schon gelesen habt [1][2][3], auch Fehler Aufspüren und Erforschen, was möglich ist. Andererseits werde ich hier auch zu meinem BlitzMax Fork posten.

Dieses Mal möchte ich in Kürze eure Aufmerksamkeit auf einen Fehler im BlitzMax Compiler lenken, den jemand letztes Jahr aufgespürt und ihn auf Github gemeldet hat [4] (das hat die Nummer #13).

BlitzMax Issue #13

Ich zeige euch ein leicht modifiziertes Beispiel und erkläre worum es dabei geht:

BlitzMax: [AUSKLAPPEN]
SuperStrict
Framework BRL.StandardIO

Function TryFail()
Local x:Int = 10
Try
x :* 2
Print "Try: x == "+x
Throw "BAIL"
Catch e:Object
EndTry
Print "After Try: x == " + x + " should be 20"
EndFunction

Function TryOk()
Local x:Int = 10
Local p:Byte Ptr = Varptr x ' taking the address solidifies the variable
Try
x :* 2
Print "Try: x == "+x
Throw "BAIL"
Catch e:Object
EndTry
Print "After Try: x == " + x + " should be 20"
EndFunction

TryFail()
TryOk()


Der Code zeigt zwei Funktionen mit Try/Catch-Blöcken. In beiden wird zuerst eine Variable x, die vorher mit dem Wert 10 initialisiert wurde, verdoppelt. Danach wird der Wert von x ausgegeben und eine Exception geworfen.
Es sollte unmittelbar klar sein, dass der Wert von x nicht dadurch beeinflusst werden kann, ob eine Exception geworfen wird oder nicht, weil die Exception erst nach der Verdopplung geworfen wird. Man erwartet also viermal die Zahl 20 als Ausgabe.

Ausgabe:
Code: [AUSKLAPPEN]
Try: x == 20
After Try: x == 10  should be 20
Try: x == 20
After Try: x == 20  should be 20


Shocked Shocked Shocked

In TryFail bekommen wir lustigerweise erstmal 20 im Try Block und dann 10 wenn wir wieder draußen sind (das übrigens nur, wenn im Try Block eine Exception geworfen wird).

Ich möchte kurz auf die Ursachen eingehen und eine Zusammenfassung geben, wie man sich vor dem Fehler schützt, da Mark ihn noch nicht gefixt hat und ich es noch nicht sinnvoll hinbekommen habe.

Ursachen

Ich glaube dieser Bereich ist recht technisch geworden... Confused

Wie funktionieren Exceptions eigentlich?
Ich möchte nicht zu sehr ins Detail gehen, aber es ist wichtig zu verstehen, was passiert wenn eine Exception geworfen wird:

Eine Exception beeinflusst den Control-Flow in einem Programm erheblich. Üblicherweise wird Code von oben nach unten ausgeführt, mit Abfragen und Schleifen können wir ihn lenken. Bei einer Exception wird der nächste einhüllende Try Block gesucht und zu dessen entsprechenden Catch Block gesprungen, aber nur falls die Exception von einem Typ ist, der von dem Catch-Block gefangen werden kann. Falls nicht, muss weiter außen gesucht werden. Dazu kann auch die aktuelle Funktion verlassen und der Call Stack abgearbeitet werden um schließlich den Stack der Funktion wiederherzustellen, in die man zurück springt (das nennt man dann Stack Unwinding)... Und dann heißt es immer Gotos wären so unübersichtlich Laughing

Lokale Variablen
Lokale Variablen wie dieses x aus dem Beispiel werden üblicherweise auch auf dem Stack aufbewahrt. Der Compiler kann aber meistens lokale Variablen nur in CPU-Registern halten. Das spart Speicher und auch Zeit, weil Arbeitsspeicher viel langsamer ist als die CPU.
Bei CPU-Registern gibt es aber Konventionen wofür sie verwendet werden können, wichtig hier ist: wer sichert ein Register, wenn eine Funktion aufgerufen wird? Der Aufrufende oder der Aufgerufene?

Von den sogenannten General Purpose Registern der x86 Architektur (die keinen besonderen zweck erfüllen) sind die Register ebx, esi und edi callee-saved und die restlichen sind caller-saved.
callee-saved heißt, eine Funktion muss die Register abspeichern und vor dem return wiederherstellen.
caller-saved heißt, eine Funktion darf die Register einfach verwenden wie sie will, d.h. jede Funktion muss bevor sie eine andere Funktion aufruft diese Register speichern, falls nötig.

Mist, ich gehe schon mehr ins Detail als ich wollte...

Jedenfalls ist es in der TryFail() Funktion so, dass BlitzMax den Wert von x nur in einem Register hält (bei mir ist es ebx). Das heißt der Compiler geht davon aus, dass der Wert in ebx steht und dass er nicht durch Funktionsaufrufe manipuliert wird.
Nur leider hält sich die Funktion bbExThrow (das Throw Kommando in BlitzMax) nicht an die Konvention, weil es ja den Stack von der Funktion wiederherstellen muss, in die es zurückspringt, und dazu gehören auch die callee-saved Register (da die ja von Funktionsaufrufen unberührt bleiben müssten). Das ist ein bisschen ein Henne-Ei-Problem Surprised
Gespeichert werden diese callee-saved Register (sowie der Stack Frame) am Beginn eines Try-Blocks. Damit ist also das Phänomen erklärt.

Dieses Problem wird in TryOk() dadurch vermieden, dass mit VarPtr die Speicheradresse von der Variable x genommen wird. Damit bleibt dem Compiler nichts anderes übrig, als die Variable auf dem Stack abzuspeichern. Der Stackframe wird auch beim Throw korrekt wiederhergestellt (da hier nicht die einzelnen Werte der Variablen wiederhergestellt werden, sondern die Position des Stacks).

Ich habe mich im Issue [4] auch schon zu dem Thema geäußert.

Hier noch ein Ausschnitt des Assemblercodes der beiden Funktionen nebeneinander:
user posted image

Fehlervermeidung

Wenn ihr Code mit Try-Blöcken schreibt, die längere Passagen enthalten, ist es wichtig, sich mit dem Problem auseinander zu setzen, vor allem da BlitzMax nicht aktiv entwickelt wird.

Zusammenfassung:
- wichtig: der Fehler ist nicht-deterministisch, d.h. er kann auftauchen und verschwinden je nach dem wie der Compiler euren Code optimiert, weil nur Variablen betroffen sind, die in Registern gehalten werden.
- der Fehler ist plattformunabhängig
- der Fehler betrifft nur lokale Variablen
- der Fehler betrifft nur Änderungen am Wert einer Variablen (bei Objekten geht es also um die Objektaddresse und nicht um den Inhalt des Objekts).
- der Fehler kann nicht auftreten, wenn euer Try-Block nur aus einer einzigen Zuweisung besteht. (die Try-Blöcke in den brl modulen sehen oft so aus).

Die Standard Module brl, pub und maxgui sind von dem Fehler wahrscheinlich nicht betroffen. Ich habe die Try-Catch-Blöcke durchgesehen, die sowieso schon ziemlich rar sind in den Modulen.


Nächstes Mal kommt dann wahrscheinlich Was zu meinem BlitzMax Fork.
Ich werde auch sehen, ob ich was wegen dem Issue machen kann.

Liebe Grüße!

Quellen & Verweise:

[1] BlitzMax mit Assembler/C https://www.blitzforum.de/foru...hp?t=35959
[2] BlitzMax vom Quelltext bauen https://www.blitzforum.de/foru...hp?t=40740
[3] BlitzMax Interna https://www.blitzforum.de/foru...hp?t=39579
[4] GitHub Issue #13 https://github.com/blitz-resea.../issues/13


Edit: habe mich mit den Registern in einem Satz vertan (da stand mal esi statt ebx).