Montag, 28. November 2011

Long Jumps eingeführt

Wie man in C++ oder Java für verschiedene Ausnahmesituationen eigene Ausnahmeklassen definiert, kann man in C für verschiedene fatale Ausnahmesituationen eigene Ausnahmefunktionen definieren. Dabei bezeichne ich eine Ausnahmesituation als fatal, wenn die Funktion in keiner sinnvollen Weise mehr fortgesetzt werden kann, sondern sofort abgebrochen werden muss, mit Ausnahme einer allfälligen Bereinigung von Ressourcen (free, fclose) und Rückgabeparametern.

Mit der C-Technik der "langen Sprünge" - Long Jumps - ist es möglich, zu einer weiter oben im aktuellen Aufrufstack definierten Stelle zurückzuspringen und dabei alle darunterliegenden Stackebenen aufzulösen. Es handelt sich um eine altbewährte Technik, die sicher bereits in den ersten Versionen von C vorhanden war und auch im Buch von Kernighan und Ritchie dokumentiert ist (Abschnitt B8 - nichtlokale Sprünge).

Das erlaubt es, grundlegende Ausnahmen "oben" zu behandeln, ohne sich in den detaillierteren Stackebenen darum kümmern zu müssen. Es ist also nicht nötig, bei einem Stack der Tiefe N auf jeder Ebene einenen Rückgabewert abfragen zu müssen:
int retc = deep_function(t,i,&x,&y,&z);
if ( (retc == ERR_FILE_NOT_FOUND) ||
(retc == ERR_FILE_DAMAGED) ||
(retc == ERR_OUT_OF_RANGE) ) return retc;

Stattdessen schreibt man nur, was man tun möchte:
deep_function(t,i,&x,&y,&z);

Die Behandlung solcher schwerwiegenden Fehlersymptome findet an einer zentralen Stelle weit oben im Stack statt, zu der man mittels eines Long Jump direkt von der fehlerverursachenden Stelle springt.

Eine solche zentrale Stelle wäre hier die neue Funktion se_calc(). Die Stelle in se_calc(), die dem try-Block entspricht - also die Folge von Anweisungen, bei deren Ausführung potentiell ein Long Jump ausgelöst wird - muss in C in einem if-Block stehen:

int se_calc( 
double tjd,
int ipl,
int iflag,
double *xx,
char *serr,
struct swe_data *swed) {

int iflag_act = iflag,
ephemeris_requested = iflag & SEFLG_EPHMASK,
ipl_act = ipl,
retc;

if ((retc = setjmp((swed->state).env))==0) {

se_calc_prepare( tjd, &ipl_act, &iflag_act, xx, serr, swed);
if (iflag_act==ERR) return ERR;

double xint[24]; // Default result area for ephemeris computation
double *xp = xint;

int iflag_ret = swecalc(tjd, ipl_act, iflag_act, & xp, serr, swed);
return iflag_ret >= 0 ?
se_calc_map_results(
ipl_act,
iflag_ret,
ephemeris_requested,
xp,
xx )
: ERR;

}
else {
return retc;
}
}

Der Kontext, der mit setjmp erzeugt wird, muss im Aufrufstack weitergereicht werden (hier im neuen Member swed->state). Wenn die Ausnahme mit longjmp ausgelöst wird, muss dieser Kontext der Funktion longjmp als Argument mitgegeben werden. Es werden dann alle Stackebenen bis zu der Stelle abgebaut, in der setjmp aufgerufen wurde, und die Funktion kehrt mit dem als zweites Argument angegebenen Rückgabewert (der natürlich ungleich Null sein muss) zurück.

Hier ein Beispiel einer Ausnahme, die über drei Funktionen in steigender Allgemeinheit schliesslich den longjmp in die oberste Stackstufe ausführt:

static void throw_file_damaged(
struct file_data *fdp,
char* serr,
struct swe_data *swed) {

char *serr_file_damage = "Ephemeris file %s is damaged.";
char msg[AS_MAXCH];
int errmsglen = strlen(serr_file_damage) + strlen(fdp->fnam);
sprintf(msg, serr_file_damage,
errmsglen < AS_MAXCH ? fdp->fnam : "" );
throw_file_error(fdp->fptr,msg,serr,swed);
}

static void throw_file_error(
FILE *fp,
char* msg,
char* serr,
struct swe_data *swed) {

fclose(fp);
throw(ERR,msg,serr,swed);

}

static inline void throw(
int retc,
char* msg,
char *serr,
struct swe_data* swed) {
if (serr != msg) msg_append( serr, msg );
longjmp( swed->env, retc);
}


Natürlich betrifft diese ganze Springerei nur zentrale Ausnahmen, die die weitere Bearbeitung direkt verhindern. Auch spricht natürlich nichts grundsätzlich gegen Rückgabewerte von Funktionen - solange diese Rückgabewerte nur direkt nach dem Aufruf verarbeitet werden und nicht durch N Stackebenen abgefragt und weitergereicht werden müssen.

Um den Zauber der Long Jumps zu ermöglichen, benötigt man ein implementierungsabhängiges Datenhäppchen vom Typ jmp_buf - einen Array von einer nicht festgelegten Anzahl von Bytes. Es empfiehlt sich, diesen Array in eine Struktur jmp_state zu packen, um ihn bei Bedarf kopieren zu können (denn Arrays als Members werden ja durch eine Zuweisung komplett kopiert, ohne dass deren Grösse explizit verwendet werden muss):
typedef struct {
jmp_buf env;
} jmp_state;

Wann sollte es nötig sein, diese Rücksprunginformation zu kopieren? In den seltenen Fällen einer "ausnahmsweise abweichenden Ausnahmebehandlung". Die folgende Funktion zum Beispiel benutze ich, solange im aktuellen Mischcode (Altes und Neues) auch noch fatale Ausnahmen per Returncode behandelt werden sollen:
static int sweph_nothrow(
double tjd,
int ipli,
int ifno,
int iflag,
double *xsunb,
AS_BOOL do_save,
double *xpret,
char *serr,
struct swe_data *swed) {
jmp_state state_save = swed->state;
int retc;
if ((retc = setjmp((swed->state).env)) == 0) {
sweph(tjd,ipli,ifno,iflag,xsunb,do_save,xpret,serr,swed);
}
swed->state = state_save;
return retc;
}
Diese Funktion soll noch auf die alte Art arbeiten, also einen Returncode zurückgeben. Innere Funktionen sind aber bereits auf die Long Jump-Technik umgestellt. Was tun?

Ich merke mir die Information über den globalen Rücksprung in der lokalen Variablen state_save. Durch einen neuen setjmp-Aufruf wird dann ein neues Zustands-Datenhäppchen erzeugt, das im if-Zweig der setjmp-Anweisung gültig ist. Danach kehre ich wieder zum alten Zustand zurück. Gibt es keine Ausnahme, ist retc = 0. Andernfalls haben wir den bei Auslösung des Sprungs gesetzten Returncode. In allen Fällen wird dieser retc mit dem erwarteten Wert zurückgegeben.

Keine Kommentare:

Kommentar veröffentlichen