C++Guns – RoboBlog

20.12.2014

Fortran COMMON is deadly

Filed under: Allgemein — Tags: — Thomas @ 22:12

In Fortran 77 gibt es den COMMON Block. Das sind sowas wie globale Variablen die man umbenennen kann. Gleich im nächsten Standard (1990) hat man das wieder rausgenommen, weil es eine unheimliche Scheisse ist. (Die Erkenntnis hat ja nur 13 Jahre gedauert).

Folgenden Beispiel zeigt warum.


      SUBROUTINE PREVENTSTART
        COMMON /ROCKETCONTROL/ ISTARTCOUNTDOWN, ITIMELEFT

        ! stop the countdown and reset time
        ISTARTCOUNTDOWN = 0
        ITIMELEFT = 1000
      END SUBROUTINE

      PROGRAM ROCKET

        COMMON /ROCKETCONTROL/ ITIMELEFT, ISTARTCOUNTDOWN

        ! initital values
        ITIMELEFT = 1000
        ISTARTCOUNTDOWN = 0

        ! now we start the countdown
        ISTARTCOUNTDOWN = 1

        ! some countdown loop here
        ! 999
        ! 998
        ! 997
        ! ...
        
        ! Oh no! Stop the countdown
        CALL PREVENTSTART

        IF(ITIMELEFT == 0) THEN
          WRITE(*,*) "ROCKET STARTED, YOU ARE DEAD!"
        ELSE
          WRITE(*,*) "COUNTDOWN STOPPED!"
        ENDIF
      END PROGRAM
$ gfortran -Wall rocket.for -o rocket
$ ./rocket 
 ROCKET STARTED, YOU ARE DEAD!

Shit ;) Ok die Erkenntnis hat gefruchtet. Man hat den Fortran Standard nicht umsonst schon 4 mal geändert. Damit ihr auch versteht wie es richtig geht, hier ein Beispiel im Fortran 2003 Standard.


module RocketControlClass
  implicit none
  private

  type, public :: RocketControl
    integer, private :: timeLeft
    integer, private :: countdownIsRunning

    contains

    procedure :: startCountdown
    procedure :: preventStart
    procedure :: getTimeLeft
  end type

  interface RocketControl
    module procedure createRocketControl
  end interface

  contains

  function createRocketControl() result(this)
    implicit none
    type(RocketControl) :: this

    this%timeLeft = 1000
    this%countdownIsRunning = 0
  end function

  subroutine startCountdown(this)
    implicit none
    class(RocketControl), intent(inout) :: this

    this%countdownIsRunning = 1
  end subroutine

  subroutine preventStart(this)
    implicit none
    class(RocketControl), intent(inout) :: this

    this%countdownIsRunning = 0
  end subroutine

  pure function getTimeLeft(this) result(timeLeft)
    implicit none
    class(RocketControl), intent(in) :: this
    integer :: timeLeft

    timeLeft = this%timeleft
  end function
end module

program rocket
  use RocketControlClass
  implicit none

  type(RocketControl) :: control

  ! inititalise values
  control = RocketControl()

  ! now we start the countdown
  call control%startCountdown

  ! some countdown loop here
  ! 999
  ! 998
  ! 997
  ! ...

  ! Oh no! Stop the countdown
  call control%preventStart

  if(control%getTimeLeft() == 0) then
    write(*,*) "rocket started, you are dead!"
  else
    write(*,*) "countdown stopped!"
  endif
end
$ gfortran -std=f2003 -Wall rocket.f03 -o rocket2003
$ ./rocket2003 
 countdown stopped!

Gott sei Dank. Diesmal hat es funktioniert, wir leben noch.
Für alle die immernoch nicht kapiert haben warum der 77er Code nicht funktioniert: Ich habe die Variablennamen im COMMON Block vertauscht. Einmal ISTARTCOUNTDOWN, ITIMELEFT und einmal ITIMELEFT, ISTARTCOUNTDOWN. Das ist kein Fehler, das ist im Standard so vorgesehen. Kein Compiler und kein forcheck wird diesen Fehler jemals erkennen können. Scheisse, ich sag es ja.

Kommen wir nun zum 2003er Code. Der ist erstmal deutlich länger. Aber das ist Absicht um einige Konzepte zu zeigen die man für ein ordentlichen Programm so braucht.

Kommen wir gleich zum wichtigsten: Statt COMMON nutze ich TYPE. Ein type kann wie COMMON Variablen zusammen fassen. Aber er existiert nur einer einzigen Stelle im Code und die Variablennamen kann man nicht mehr umbenennen. Das alleine würde schon langen um den 77er Fehler zu erkennen.

Aber die Welt dreht sich weiter und seit 1995 gehts erst so richtig rund.

Ich habe den RocketControl type in ein RocketControlClass - Module ausgelagert. Da Fortran case-insensitive ist, also nicht zwischen GROSS und kleinschreibung! unterschieden wird, muss man selbst seine Variablennamen anpassen. Module bekommen den Suffix "Module" um deutliche zu machen, dass es ein Module ist. Habe ich ein Module welches Objektorientierten Code enthält, bekommt es stattdessen den Suffix "Class" in Anlehung an das class Keyword in C++. *uff* Fortran ist anstrengend.

Okay zurück zu unserem Programm. Ich habe den type in ein module ausgelagert, und normal lagert man jedes Module in eine eingene Datei aus. Dann bleibt die Übersicht erhalten und auch das Versionskontrollprogramm freut sich.

Durch das Modul ist es möglich den type RocketControl überall im Programm zu nutzen, ohne die Definition mit allen Variabeln erneut hinschreiben zu müssen. Damit verhindert man nicht nur etliche copy&paste Fehler. Wenn man den type RocketControl um einige Variablen ergänzt, muss nur eine Stelle im Code geändert werden. Mit COMMON müssten alle Stellen im Programm geändert werden, wo RocketControl genutzt wird. Und jedesmal läuft man Gefahr einen Fehler zu machen. Variablen vergessen oder zu vertauschen. Und wie gesagt, mit COMMON kann der Compiler uns nicht vor Fehler beschützen. Werden types benutzt, kommt man erst garnicht in eine Situaton um die erwähnten Fehler zu machen. Der Compiler erledigt den Rest für uns. Und er kann noch mehr!

Für alle die es nicht wissen: Mit dem use-keyword können Module benutzt werden. Z.B. "use RocketControlModule"

Das Benutzen von types in Modulen bietet noch weitere Vorteile. Angenommen wird würden unseren type RocketControl um die Subroutine setCountDown(value) erweitern, um anzugeben wie lange der Countdown läuft bis die Rakete startet. Die Subroutine nimmt genau ein Argument "value" entgegen der vom Type Integer sein soll.

Wenn wir nun einen Fehler machen, und wir sind Menschen, wir machen etliche Fehler... Wenn wir also der Subroutine setCountDown zwei Variablen übergeben, statt nur einer, was dann? Ohne Module sagt der Compiler garnichts. Im günstigsten Falle gibt es ein Warning.
Das Programm würde laufen, aber könnte auch Abstürzen.

Das ist doch schön. Wir haben ein offensichtlich falsches Programm, welches keine Compilier und auch keine Laufzeitfehler zeigt. Das ist eine tickende Zeitbombe! Irgendwann wechseln wir den Compiler, oder wecheln den Rechner, oder schalten eine Compileroptimierung dazu und BUMM Segfault beim Funktionsaufruf. Na Geil. Das kann doch kein normaler Mensch debuggen. Vorallem nicht nach 10 20 Jahren.

Also? Genau, seine Funktionen in Module packen. Ich darf das mal demonstieren:

rocket.f03:71.32:

  call control%setCountDown(1,2)
                                1
Error: More actual than formal arguments in procedure call at (1)

Ja ist es denn Wahr, wir haben schon wieder die Welt gerettet.
Zur Erklärung warum Module die Welt retten: Sieht der Compieler ein Modul, legt er eine .mod Datei an. In unserem Fall würde sie rocketcontrolclass.mod heissen. Benutzt man den gfortran kann man diese Datei sogar mit einem einfachen Texteditor öffnen und sich ansehen. Man sieht in kodierter Form zu jede Funktion ihre Eigenschaften. Wieviel Parameter sie entgegen nimmt und vom welchen Type sie sind.
Mit einem Module kann der Compiler also sehr einfach unseren Fehler entdecken. Und genau das ist doch eine der Hauptaufgaben von Compiler. Neben der Umwandlung von Text in Binärcode muss er schauen, ob das alles Sinn macht was wir programmiert haben. Meistens stimmt es nicht...

Ich möchste dieses Beispiel benutzen um einen weiteren beliebten Fehler zu zeigen.
Angenommen der Benutzer unseres tollen Programms setzt nun CountDown von 5.5 Sekunden. Er übergibt also eine Gleitkommazahl vom Type Real einer Funktion, die einen Integer erwartet. Ohne Module sagt der Compiler nichts, bestensfalls ein Warning.
Beim Ausführen wird das Bitmuster der Real Zahl als Integer Zahl interpretiert. Dabei kann alles mögliche herauskommen. Sehr größe Zahlen die die Rakete erst nächstes Jahr starten oder negative Zahlen oder gar Null. "ROCKET STARTED, YOU ARE DEAD!" Schonwieder... Nicht so gut.

Also Module retten mal wieder die Welt. Hier die passende Fehlermeldung:

rocket.f03:71.28:

  call control%setCountDown(1.0)
                            1
Error: Type mismatch in argument 'value' at (1); passed REAL(4) to INTEGER(4)

Sehr schön. Der Compiler hat uns wieder gerettet.
Wir schreiben also dem Entwickler dass sein Programm scheisse ist, und es gefälligst auch Kommazahlen unterstützen soll. Der fleissige Entwickler schickt uns sofort die neue Version zu und...

Error: Type mismatch in argument 'value' at (1); passed REAL(8) to REAL(4)

Verdammt! Was ist denn nun schon wieder? In Fortran können 64Bit Zahlen nicht ohne weiteres in 32Bit Zahlen umgewandelt werden. Das ist auch richtig so. Man würde wertvolle Informationen bei der Umwandlung verlieren und gerade Zahlengenauigkeit ist in der Wissenschaft sehr wichtig. An dieser Stelle muss ich sagen: "Gut gemacht, Fortran Entwickler!" Genießt es, es kommt nicht noch einmal vor.
Wie löst man das Problem? Auf die richtige Art und Weise? Wir speichern in RocketControl 64Bit Zahlen und geben dem Benutzer die möglichkeit 64Bit oder 32Bit Zahlen einzugeben. Mit Function overloading geht das sehr elegant und die Umwandlung von 32Bit nach 64Bit geht verlustfrei.

Module können gut die Welt retten. War es das jetzt? Ha! Noch lange nicht. Was ist mit diesem Stück Code?


control%timeLeft = 1.0

Anders als bei Funktionsaufrufe wo man den Stack kaputt machen kann, ist hier die Umwandlung ohne Error Möglich. Man bekommt höchstens ein Warning

  control%timeLeft = 1.0
                     1
Warning: Possible change of value in conversion from REAL(4) to INTEGER(4) at (1)

Ja, was nutzen uns all die Funktionen im Module, wenn der Programmierer sie nicht benutzt und stattdessen direkt auf die Variable zugreift. Es sollte jedem einleuchten, dass die Welt wieder verloren ist.

Glücklicherweise wurde der Fortran Standard nachgerüstet und man kann type Variablen als private markieren.


  type, public :: RocketControl
    integer, private :: timeLeft
  end type

Per Default ist "public" vorgegeben, so dass die Variable gelesen und geschrieben werden kann. Gibt man "private" vor, kann die Variable nur noch von Procedure genutzt werden, die RocketControl enthält. Versucht man von ausserhalb auf die Variable lesend oder schreiben zuzugreifen, erhält man einen Compiler Fehler:

rocket.f03:71.18:

  control%timeLeft = 1
                  1
Error: Component 'timeleft' at (1) is a PRIVATE component of 'rocketcontrol'

Und die Welt ist wieder sicher. Geschützt vor un/absichtlichen Zugriffen von Programmierer die keine Ahnung haben was sie tun. Mein Superheld der Module-Compiler.

Wir zwingen den Benutzer also die setCountDown Funktion zu benutzen. Und es ist zu 100% sichergestellt, dass der Compiler mit Modulen die gezeigten Fehler erkennt. Jetzt können wir auch folgende Situation gut lösen:


call control%setCountDown(-1)

Negative Werte machen für einen Countdown natürlich keinen Sinn. Wir können in der Funktion den übergebenen Wert auf Gültigkeit überprüfen, eine Fehlermeldung ausgeben und den neue Wert nicht akzeptieren. Da der Benutzer die Funktion setCountDown() aufrufen MUSS, ist auch sichergestellt dass die Überprüfung stattfinden.

Und wir können uns entspannt zurück lehnen und sicher gehen, dass auch in den nächsten 10 Jahren niemand unser Programm falsch benutzen kann. Selbst wenn er es will. Compiler+Module+neuen Standard sei Dank.

Anmerkung:
Wie bereits erwähnt ist "public" default in Fortran. Bei C++ ist es "private" für Klassen. Fortran lebt also in dem Glauben, dass die Programmierer immer alles richtig machen und niemand un/absichtlich eine Variable falsch benutzt.
In C++ ist die Welt schwarz, jeder hat nur böses im Sinn, niemand kann man trauen, alles muss kontrolliert werden. Wenn wir mathematische Modulle implementieren, welche ein Teil der Umwelt abbilden, welcher Ansatz wäre da wohl realistischer? ;)

No Comments

No comments yet.

RSS feed for comments on this post.

Sorry, the comment form is closed at this time.

Powered by WordPress