Introduzione alle API Win32

Capitolo 28: l'interprete: stub+scheletro

Ecco, dunque, un tipico stub+scheletro per la nostra interfaccia astratta IndiEs. Abbiamo realizzato lo stub come applicazione di console, per massima semplicità; essa usa come 'comandi' i propri argomenti, o, in mancanza, le righe di standard input. Ogni 'comando' è un numero (se inizia con cifra o '-', ignorando comunque gli eventuali spazi iniziali), e allora chiede la valutazione dell'espressione in quel numero; oppure, se inizia con altri caratteri, una nuova espressione da impostare come 'espressione corrente':

// IndiEs_Skel.c // // scheletro di test per IndiEs (a console) // #include <stdio.h> #include <ctype.h> // simuliamo i BOOL, visto che manca // l'include di <windows.h> typedef int BOOL; const BOOL TRUE = 1; const BOOL FALSE = 0; // dichiarazioni dell'interfaccia IndiEs BOOL IndiEs_Init( const char* sOpzioni, // opzioni-stringa unsigned long lOpzioni // opzioni-intero ); BOOL IndiEs_Finis(void); BOOL IndiEs_Espr(const char* espressione); BOOL IndiEs_Valuta(double X, double* pY); // // scheletro di test a console // BOOL fai(const char* cmd) { // ignoriamo gli spazi iniziali while(*cmd && isspace(*cmd)) ++cmd; // ignoriamo 'comandi' vuoti o bianchi if(!*cmd) return TRUE; // se inizia con una cifra o un '-'...: if(isdigit(*cmd) || *cmd=='-') { // lo prendiamo come un numero, e // valutiamo l'espressione per quel // valore double x = strtod(cmd,0); double y; if(!IndiEs_Valuta(x, &y)) return FALSE; printf("%g -> %g\n", x, y); } else { // lo prendiamo come espressione, e // settiamo l'espressione corrente if(!IndiEs_Espr(cmd)) return FALSE; printf("Expr: %s\n", cmd); } return TRUE; } int main(int argc, char* argv[]) { // inizializzazione if(!IndiEs_Init(0,0)) { puts("Errore di Init"); return 1; } if(argc>1) { // se abbiamo argomenti, eseguiamo quelli int i; for(i=1; i<argc; ++i) if(!fai(argv[i])) break; } else { // se no, loop interattivo char buf[80]; for(;;) { printf("Cmd> "); if(!gets(buf)) break; if(!fai(buf)) break; } } // terminazione if(!IndiEs_Finis()) { puts("Errore di Finis"); return 1; } return 0; }

Come stub, possiamo ad esempio implementare una classica 'calcolatrice a 4 operazioni', con le tecniche più tradizionali e semplici: parsing a discesa recursiva, un semplicissimo "analizzatore lessicale" con un singolo push-back, espressioni rappresentate internamente come alberi.

Se il lettore non ha mai studiato questi argomenti, può essere che li trovi abbastanza misteriosi, ma non è il caso di preoccuparsi: si tratta in ogni caso di argomenti privi di connessione diretta al nostro tema principale delle API di Windows, che qui presentiamo interamente solo per permettere, anche a chi non abbia una versione aggiornata di Windows, di realizzare il nostro esempietto di "grafico di una funzione", senza bisogno di scaricare dalla rete software magari massiccio e per lui privo di vero interesse.

A chi invece fosse interessato ai problemi di parsing, interpretazione, e compilazione, suggeriamo di ricorrere agli ottimi testi esistenti su questi temi classici, fra i quali la nostra preferenza resta al vecchio ma intramontabile "Dragon Book" (Aho, Sethi, e Ullmann, "Compilers -- Principles, Techniques, and Tools", Addison-Wesley); come ebbero ad osservare a suo tempo gli autori del compilatore Turbo Pascal, e più tardi, indipendentemente, quelli del compilatore LCC, l'unico aspetto sul quale ci permettiamo di differire da A, S, ed U, è la loro passione per le tecniche automatiche di analisi lessicale e sintattica, due campi in cui è invece assai facile ed agevole scrivere manualmente codice piu` efficiente e compatto di quello automaticamente generabile, senza che la fatica del programmatore cresca di molto.

// IndiEs_Stub.c // // lo stub di IndiEs -- espressioni a 4 operazioni // nell'unica variabile X // #include <stdio.h> #include <ctype.h> #include <stdlib.h> #include <string.h> BOOL IndiEs_Init( const char* sOpzioni, // opzioni-stringa unsigned long lOpzioni // opzioni-intero ) { return TRUE; } BOOL IndiEs_Finis(void) { return TRUE; } // // rappresentiamo le espressioni come alberi di nodi // typedef enum tiponodo { costante, variabile, binario, unario } tiponodo; // rappresentazione attiva per gli operatori binari e unari typedef double (*bifun)(double, double); typedef double (*unfun)(double); // per semplicita`, non facciamo la normale "union", ma // lasciamo che si sprechi un poco di memoria (tutti i // campi non usati saranno sempre zero) typedef struct nodo { tiponodo t; double valore; // se costante bifun biop; // se binario unfun unop; // se unario struct nodo* psin; // se unario o binario struct nodo* pdes; // se binario } nodo; // allocatore di nodi nodo* fanodo() { nodo* risult = (nodo*)malloc(sizeof(nodo)); memset(risult, 0, sizeof(nodo)); return risult; } // butta un nodo e tutti i nodi-figli void butta(nodo* pn) { if(pn) { butta(pn->psin); butta(pn->pdes); free(pn); } } // per verificare il comportamento; definire a // nullo per evitare output #define TRAC(c) putchar(c) // interprete di un albero in un punto // ("lavar" e` il "punto", cioe` il valore // rilevante della variabile X) double valuta(nodo* pn, double lavar) { switch(pn->t) { case costante: TRAC('K'); return pn->valore; case variabile: TRAC('V'); return lavar; case binario: // recursione doppia TRAC('B'); return pn->biop(valuta(pn->psin,lavar),valuta(pn->pdes,lavar)); case unario: // recursione semplice TRAC('U'); return pn->unop(valuta(pn->psin,lavar)); } // non possiamo mai arrivare qui...! printf("Errore interno, t=%d\n", pn->t); return 0; } // gli operatori: + e - unario, i 4 binari (non "div" perche` // e` il nome di una funzione standard del C!) double pos(double x) { return x; } double neg(double x) { return -x; } double add(double x, double y) { return x+y; } double sub(double x, double y) { return x-y; } double mul(double x, double y) { return x*y; } double dvd(double x, double y) { return x/y; } // espressione corrente nodo* lespres = 0; // flag "errore nel parsing" int iserror = 0; // "analizzatore lessicale": ignora gli spazi, torna il // prossimo carattere, gestisce un singolo push-back, // e quando trova qualsiasi numero lo considera come // un singolo 'carattere' '0' (il valore del numero, // se richiesto, e` tornato tramite un puntatore; se // quel puntatore e` nullo, viene ignorato [sara` poi // sempre richiesto ancora tramite push-back]) char lexer(const char* testo, double* curval, int* pInd) { if(iserror) return 0; static int pushed_back = -1; if(!testo) { // richiesta di push-back if(pushed_back<0) { // non dovremmo mai essere qui! printf("errore interno pb\n"); iserror = 1; return 0; } *pInd = pushed_back; pushed_back = -1; return 1; } // salta e ignora gli spazi while(testo[*pInd] && isspace(testo[*pInd])) ++*pInd; // salva l'indice corrente per ev. push-back pushed_back = *pInd; char result = toupper(testo[*pInd]); if(!result) return 0; if(isdigit(result)) { char *poi; double v = strtod(testo+*pInd, &poi); if(curval) *curval = v; *pInd = poi-testo; return '0'; } ++*pInd; return result; } // dichiarazione forward, causa mutua recursione nodo* pars_expr(const char* testo, int* pInd); // unario ::= costante | variabile | '(' espressione ')' nodo* pars_unar(const char* testo, int* pInd) { if(iserror) return 0; double val; char nch = lexer(testo, &val, pInd); if(nch == 'X') { nodo* result = fanodo(); result->t = variabile; return result; } if(nch == '0') { nodo* result = fanodo(); result->t = costante; result->valore = val; return result; } if(nch == '(') { nodo* result = pars_expr(testo, pInd); nch = lexer(testo, 0, pInd); if(nch != ')') iserror=1; return result; } iserror = 1; return 0; } // fattore ::= '+' unario | '-' unario | unario nodo* pars_fact(const char* testo, int* pInd) { if(iserror) return 0; char nch = lexer(testo, 0, pInd); if(nch == '+' || nch == '-') { nodo* nuovo = fanodo(); nuovo->t = unario; nuovo->unop = (nch=='+')?pos:neg; nuovo->psin = pars_unar(testo, pInd); return nuovo; } lexer(0, 0, pInd); return pars_unar(testo, pInd); } // termine ::= fattore { '*' fattore | '/' fattore } nodo* pars_term(const char* testo, int* pInd) { if(iserror) return 0; nodo* fact = pars_fact(testo, pInd); char nch = lexer(testo, 0, pInd); while(nch=='*' || nch=='/') { nodo* nuovo = fanodo(); nuovo->t = binario; nuovo->biop = (nch=='*')?mul:dvd; nuovo->psin = fact; nuovo->pdes = pars_fact(testo, pInd); fact = nuovo; nch = lexer(testo, 0, pInd); } lexer(0,0,pInd); return fact; } // espressione ::= termine { '+' termine | '-' termine } nodo* pars_expr(const char* testo, int* pInd) { if(iserror) return 0; nodo* term = pars_term(testo, pInd); char nch = lexer(testo, 0, pInd); while(nch=='+' || nch=='-') { nodo* nuovo = fanodo(); nuovo->t = binario; nuovo->biop = (nch=='+')?add:sub; nuovo->psin = term; nuovo->pdes = pars_term(testo, pInd); term = nuovo; nch = lexer(testo, 0, pInd); } lexer(0,0,pInd); return term; } BOOL IndiEs_Espr(const char* espressione) { if(lespres) butta(lespres); iserror = 0; int cur = 0; lespres = pars_expr(espressione, &cur); if(iserror || !lespres || lexer(espressione, 0, &cur)) return FALSE; return TRUE; } BOOL IndiEs_Valuta(double X, double* pY) { if(!lespres) return FALSE; *pY = valuta(lespres, X); TRAC('\n'); return TRUE; }

 

Armati di questi skeleton e stub, potremo, già dal prossimo capitolo, riprendere il duplice progresso nell'uso delle funzionalità di Windows: da una parte, realizzare un cliente migliore, con una GUI e delle effettive capacità grafiche; dall'altra, ottenere da Windows la capacità di riutilizzare interpreti assai migliori, più ricchi, più veloci, eccetera.


Capitolo 27: l'interprete: l'interfaccia
Capitolo 29: GUI per grafico di funzione
Elenco dei capitoli