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
#include
// simuliamo i BOOL, visto che manca
// l'include di
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 ");
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
#include
#include
#include
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