Samstag, 31. Januar 2009

Exceptions in c - wie bauen?

Weil mir in c das Fehlerhandling ein bischen auf die Nerven ging, hab ich beschlossen Exceptions einzubauen. Die Basis für die Exceptions bilden longjmp und setjmp. Was die Exceptions können
  • Sehr schnelles Behandeln einer Exception
  • Verschiedene Typen von Exceptions
Was die Exceptions nicht können
  • (Noch) Keine Unterstützung für Threads :(
  • Funktionen die keine Exceptions werfen können auch nicht aufgefangen werden
  • Der Compiler prüft nicht, ob Exceptions abgefangen werden
Vorkenntnisse
  • Grundkenntnisse in c und den Präprozessor sind verdammt wichtig
Springen in Programmen?? Das Springen in Programmen ist allgemein nicht gern gesehen. Die meißten Sprünge können mit Funktionsaufrufen, Schleifen und Abfragen ersetzt werden, welche vom menschlichen Gehirn gut interprotiert werden. Dagegen setzt es bei Spaghetti Code schnell aus, was Sicherheitslücken und Unstabilität zur Folge hat. Zusätzlich wird der Stack bei einem normalen Stack nicht angepasst. Warum verwende ich trotzdem Sprünge? Hier sind Jumps meiner Meinung nach absolut in Ordnung, weil der Benutzer selber mit den Sprüngen überhaupt nicht in Kontakt kommt. Es wird nur bei TRY mit setjmp eine Marke gestzt und bei THROW mit longjmp gesprungen. Das ist auch das Geheimnis. Was ist aber mit dem Stack? Die Makros setjmp und longjmp kümmern sich um das Problem. setjmp setzt nicht nur eine Marke, sondern speichert auch den aktuellen Stack und speichert einen buffer. Wird longjmp mit dem buffer Wert aufgerufen, springt der Programm Counter auf die Markierung und stellt den Stack wieder her. Erste einfache Implementierung Also im Endeffekt werden nur ein paar Macros erstellt:
  • TRY CATCH wird versteckt zu einer switch case Abfrage.
  • THROW springt
Folgender Code soll funktionieren:
#include <stdio.h>
#include "exception.h"

void function(int number)
{
    if (number < 0)
            THROW;
}

int main(int argc, char **argv)
{
    int number;

    TRY;
    scanf("%i", &number);
    function(number);

    CATCH;
    printf("Exception\n");
    return 1;
    TRYEND;

    return 0;
}
Dieses Programm soll bei der Eingabe einer negativen Zahl Exception ausgeben und 1 zurückgeben. Bei einer positiven Zahl erfolgreich beenden. Dazu muss die exception.h implementiert werden:
#ifndef EXCEPTION_H
#define EXCEPTION_H

#include <setjmp.h>
jmp_buf marker;

#define TRY switch(setjmp(marker)) {                    \
case 0:

#define CATCH break;                                    \
default:

#define TRYEND }

#define THROW longjmp(marker, 1);

#endif /* EXCEPTION_H */
Was passiert hier genau? Um das zu erklären muss erst mal gezeigt werden, wie die c Datei mit aufgelösten Exception Macros auschaut:
#include <stdio.h>

void function(int number)
{
    if (number < 0)
            longjmp(marker, 1);
}

int main(int argc, char **argv)
{
    int number;

    switch(setjmp(marker)) {
    case 0:
            scanf("%i", &number));
            function(number);
            break;
    default:
            printf("Exception\n");
            return 1;
    }

    return 0;
}
Das Makro setjmp setzt nicht nur ne Marke sondern gibt auch einen Wert zurück:
  • 0 wenn nicht gesprungen wurde sondern die Stelle durch den normalen Programmfluss aufgerufen wurde
  • ansonsten einen von setjmp mitgelieferten Wert. Im Beispiel oben ist es die 1.
Deshalb geht das Programm zunächst in den case 0, was den Code zwischen TRY und CATCH repräsentiert. Wenn keine Excepton geworfen wird, springt das Programm wie gewohnt aus dem Switch Block, wenn doch springt das Programm zurück zur Auswertung im switch, wo der Wert 1 zurückgegeben wird. Es wird dann der default case ausgeführt, was den Teil zwischen CATCH und TRYEND repräsentiert. Nun sind schon einfache Exceptions möglich, jedoch noch ohne Fallunterscheidung. Schlimmer ist jedoch, dass sie nicht verschachtelt werden können. Das ist jedoch alles ganz billig. Fallunterscheidungen Fallunterscheidungen werden einfach mit anderen cases realisiert. Dazu bekommt erst einmal THROW einen Parameter, der für den Exceptiontyp steht:
#define THROW(exc) longjump(buffer, exc);
Um die Exception abzufangen muss noch das Makro CATCHIF implementiert werden:
#define CATCH_IF(exc) break;                                          \
case exc:
Jetzt sollte auch folgender Code funktionieren:
#include <stdio.h>
#include "exception.h"

#define EXC_TOOSMALL 1
#define EXC_OMGOMGOMG 2

void function(int number)
{
    if (number < -100)
            THROW(EXC_OMGOMGOMG);
    else if (number < 0)
            THROW(EXC_TOOSMALL);
}

int main(int argc, char **argv)
{
    int number;

    TRY;
    scanf("%i", &number);
    function(number);

    CATCH_IF(EXC_TOOSMALL);
    printf("Too small\n");
    return 1;

    CATCH;
    printf("Exception not handled\n");
    return 2;
    TRYEND;
}

EXC_TOOSMALL wird behandelt, EXC_OMGOMGOMG jedoch nicht.
Verschachtelung kommt ein anderes Mal. Hierzu wird dann ein Stack von marker verwendet. Fröhliches Coden!