Introduzione ai tool per lo sviluppo Vedremo in queste pagine la presentazione e l'utilizzo dei fondamen tali tool di sviluppo presenti in pressoché tutti i sistemi GNU/Linux attualmente in circolazione
Sirnone Contini <s . contini@linuxpratico.com> e
Lorenzo Mancini <l .mancini@linuxpratìco.coni=
GNU/Linux, in quasi la totalità delle sue distri buzioni, incarna un ambiente di sviluppo
completo e potente con il quale è possibile sia modificare e ricompilare il software in dotazione,
Passiamo quindi in rassegna alcune delle opzioni
che scriverne di nuovo.
più utili e comuni.
Tutto questo è basato su molti potenti e flessibili tool a riga di comando che permettono di svolgere
Percorsi
tutti i passi necessari allo sviluppo e al debug del
L'opzione -I permette di specificare una directory
software; la limitata disponibilità di IDE scoraggia e
aggiuntiva per la ricerca degli header file; non si
disorienta però buona parte degli utilizzatoci alle
deve dimenticare che i percorsi specificati in questo
prime armi.
modo hanno precedenza rispetto a quelli di sistema
Vediamo quindi alcuni dei più importanti di essi in
e che non è possibile modificare tale comportamento
modo da avere le basi che ci serviranno in futuro
indicando con tale opzione un percorso di sistema;
per sviluppare le nostre applicazioni, per modificare
tn ta! caso infatti il compilatore se ne accorge e si
le esistenti o semplicemente per capire come fun
limita ad ignorare il percorso replicato.
zionano alcuni noti tool che automatizzano i proces
Se realmente avete bisogno di aggirare la restri
si di sviluppo. Iniziarne quindi la nostra panoramica
zione appena descritta potete usare l'opzione
degli elementi fondamentali, sui quali torneremo in
■nostdinc, che obbliga il compilatore a cercare solo
futuro per approfondirne i dettagli man mano che ci
nei percorsi da voi specificati.
interessano.
Quando nei nostri programmi facciamo uso di librerie
GtiU Compìler Collectàon
aggiuntive, avremo spesso bisogno di specificare il percorso che contiene i relativi file da linkare, nel
Cominciamo dal compilatore GCC (in particola
caso in cui esso non sia presente tra i percorsi di
re faremo riferimento ai frontend C e C+ + ) fornito
sistema; in maniera del tutto analoga a quanto
dalla GNU.
visto per gli header file possiamo utilizzare il para
Anzitutto ricordiamo che con i semplici:
metro - L seguito dal percorso desiderato.
gec
--version
più volte sulla riga di comando in modo da permet
gec
--help
gec
-v
Entrambe le opzioni appena viste possono apparire tere di specificare più di una directory per tipo.
--target-help
--help
Librerie
da riga di comando, otteniamo con il primo coman
L'opzione -l consente di linkare una libreria al file
do il numero di versione e alcune informazioni su
risultante dalla compilazione; è da notare il fatto
di essa; con il secondo un breve riassunto delle
che la parte del nome della libreria da specificare
opzioni più comuni, ed infine un lungo elenco delle
non comprende il prefisso lib, presente invece nel
opzioni relative ai vari frontend per il target del
nome del relativo file.
compilatore con il terzo.
Quindi ad esempio per utilizzare nel proprio proget
Per ottenere dettagliate spiegazioni vi rimandiamo
to la libreria matematica del C dovremo specificare
al contenuto delia guida di GCC, visualizzabile con il
-Im invece di -llibm.
comando info
gec. Passiamo all'uso pratico: molti
di voi avranno fatto qualche piccola prova con il comando:
Warning Nel caso di uno o più errori sintattici presenti nel codice, il compilatore si arresta e segnala le righe
gec
sorgente.e
-o
nomeorograirona
incriminate. Oltre a questi casi esistono anche delle categorie
ovvero una delle forme più semplici per l'utilizzo di
di operazioni che, pur non implicando l'arresto
GCC, e alcuni di voi saranno incappati in errori di
della compilazione, necessitano probabilmente
compilazione relativi all'assenza di header file o in
dell'attenzione del programmatore. In questo caso il
errori di linking relativi all'assenza di funzioni o
compilatore emette dei warning.
librerie.
Esempi tipici in cui il compilatore genera warning
sono l'uso di valori floating point all'interno di test
programmazione a runtime con l'utilizzo di un
di uguaglianza, la dichiarazione di variabili che poi
debugger ci viene offerto con la famiglia di
rimangono inutilizzate, cast impliciti che possono
opzioni -g.
portare a perdite di dati, e cosi via.
Questo causa l'aggiunta delle informazioni, all'interno
Per la gestione dei messaggi di warning il compila
dei file oggetto, necessarie al nostro debugger pre
tore mette a disposizione tutta una serie di para
ferito a mostrare le corrispondenze tra il codice
metri: ognuno di questi è formato da -W seguito da
sorgente e il codice macchina in esecuzione.
una descrizione del tipo di errore che vogliamo che
Da notare che ciascun debugger richiede che le
il compilatore sia in grado di rilevare.
informazioni siano memorizzate nel file binario
Esistono anche le opzioni -Wall che abilita un'intera
seguendo un formato ben preciso, quindi dovremo
serie di test senza doverli specificare tutti a mano,
specificare anche il debugger che intendiamo
e -w che invece sopprime del tutto la generazione
usare, scegliendolo tra quelli supportati da GCC.
di warning.
Per ciascuno di essi esiste un'opzione relativa, soli
Oltre a questo, è possibile istruire il GCC a proposito
tamente però in ambiente Linux si usa gdb o
del tipo di sintassi o standard al quale si sta facendo
comunque frontend grafici ad esso; per questo si fa
riferimento nel nostro codice sorgente: questo in
uso dell'opzione -ggdb.
certi casi è molto importante, in quanto fornisce un valido aiuto per aderire alle specifiche previste per
Ottimizzazione
il nostro progetto.
Vediamo adesso come fare in modo che il compi
L'opzione -pedantic genera tutti i warning previsti
latore svolga un po' di lavoro sporco, ottimizzando
dallo standard ISO C. Per specificare una versione
il codice al posto nostro. I! numero di tecniche di
deilo standard è possibile usare l'opzione -std,
ottimizzazione che GCC può applicare è davvero
seguita dal codice di interesse come nel seguente
troppo elevato per farne una trattazione comple
esempio:
ta, pertanto per i dettagli vi rimandiamo alla rela tiva manpage.
gcc
-Werror
-Wall
-std=c99
-pedantic
sorgente.e ■o eseguibile
Una prima opzione adatta allo scopo è -0, che abili ta alcuni flag base di ottimizzazione, in questo caso l'obiettivo è quello di ridurre il tempo di esecuzione
Compilazione a passi
senza eseguire modifiche sul codice da compilare, ma agendo sul criterio con il quale viene generato il
Sia per scopi didattici, di controllo, ottimizzazione o
codice macchina corrispondente.
per particolari necessità di compilazione, è possibile
L'opzione -02 abilita altri flag in aggiunta a quelli
utilizzare alcuni parametri del GCC per interrompere
retativi a -0; ancora una volta le prestazioni del
il processo di compilazione quando è in un determi
codice potranno risultare migliori a spese però di un
nato stato precedente la generazione dell'eseguibi
maggiore tempo di compilazione.
le finale.
Vale un discorso analogo per -03, con la particolante
Con l'opzione -E il GCC si arresterà immediatamente
che al compilatore viene data la facoltà di espandere
dopo il postprocessing: il risultato dell'elaborazione
inline le funzioni più semplici.
sarà quindi il sorgente di partenza con l'eventuale
Questo, a fronte di un certo risparmio di tempo in
aggiunta di tutte le parti che derivano dall'inclusione
esecuzione, può portare ad una consistente crescita
dei file header e dall'espansione delle macro.
delle dimensioni del binario compilato; al contrario,
Se invece siamo interessati a vedere il codice
l'opzione -Os abilita una serie di ottimizzazioni volte
assembly generato dal GCC relativo al sorgente spe
a limitarle.
cificato e alie opzioni di compilazione in uso, possia
Perché il lavoro del compilatore risulti più efficace, è
mo ricorrere all'opzione ■ S. Infine con l'opzione - e è
possibile specificare per quale tipo di CPU stiamo
possibile procedere alia generazione dei file ogget
compilando il programma, in modo da permettere
to senza però effettuare il linking.
al compilatore di posizionare efficacemente le
Naturalmente è possibile riprendere e portare a ter
istruzioni in funzione dell'architettura: questo può
mine il processo completo di compilazione partendo
essere fatto sfruttando l'opzione -mcpu=tipo
da uno qualsiasi di questi stadi intermedi.
Da notare che il codice generato in questo modo
cpu.
Consideriamo ad esempio il caso in cui vogliamo
risulta avvantaggiato sul tipo di CPU scelta ma girerà
esaminare l'output assembly prodotto dal compila
comunque su una qualunque unità della famiglia
tore, eventualmente ritoccarlo e solo successiva
i386. Per sfruttare invece appieno le peculiarità di
mente produrre l'eseguibile finale.
un particolare tipo di CPU, al costo di perdere la
Per far questo possiamo utilizzare una serie di
compatibilita con le altre, è possibile usare
comandi del tipo:
l'opzione -march=tìpo cpu.
gcc
zazioni, specialmente quelle di livello più avanzato,
Bisogna comunque notare che l'utilizzo delle ottimiz
vi
program.e
-S
hanno l'effetto collaterale di rendere difficoltoso il
program.s
gcc
program.s
-o
program
debugging: potreste scoprire che alcune variabili da voi dichiarate sono state eliminate dal compilatore,
Simboli per il debug La possibilità di analizzare e individuare errori di
©te
che il flusso di esecuzione non procede come vi aspettereste e così via, motivi per i quali si tende ad usarle solo su codice testato e stabile.
~
Esempì d'uso dì GCC Per chiudere mostreremo un paio di casi semplici di
Ad esempio possiamo provare con: make
fifteen
utilizzo di GCC, prendendo in esame alcuni dei sor genti già pubblicati in questa serie.
Noteremo che make esegue, come prevedibile, la
Minichat client/server: in questo caso i file da
compilazione dì fifteen,e e tenta di effettuarne il
compilare sono due, senza dipendenze tra loro o
linking per ottenere un file eseguibile.
verso librerie esterne, quindi sarà sufficiente
A questo punto però scopriamo che make non ha informazioni sufficienti per procedere, infatti ne! ca
immettere:
so specifico abbiamo bisogno delle librerie ncurses. gcc
client.c
-o
client
gcc
server.e
-o
server
Potremmo allora scrivere un makefile per dare tutte le istruzioni necessarie a make, ma prima vediamo come è possibile intervenire sulla compilazione uti
Fifteen: abbiamo un solo file da compilare, che
lizzando delle variabili speciali esterne:
però richiede la libreria esterna ncurses: make
gcc
fifteen.e
-o fifteen
fifteen LDFLAGS=-lncurses
-Incurses
a questo punto make ha tutte le informazioni che
Automatizziamo la
compilazione eoo make
servono a procedere, infatti la variabile LDFLAG5
può essere utilizzata per specificare le opzioni da passare al linker! Allo stesso modo ad esempio è possibile indicare le opzioni da utilizzare con il com
Nonostante gli esempi di compilazione che abbiamo
pilatore, tramite la variabile CFLAGS:
appena visto siano davvero molto semplici, al cre scere dei numero dei file sorgente, e delle dipen
make
fifteen
LOFLAGS=-lncurses CFLAGS=-02
denze tra di essi, la complessità dei comandi da
immettere aumenta velocemente; a quel punto, a
Per conoscere in qualsiasi momento la lista delle
qualcuno potrebbe venire in mente di automatizzare
regole implicite e di quali variabili influiscono sulle
il processo di compilazione del proprio progetto
operazioni svolte da make, è possibile utilizzare il
attraverso la scrittura di file batch ad hoc.
comando:
Certamente questo semplificherebbe molto la com
pilazione, ma GNU/make può quasi sempre svolgere
make
-p
|
less
questo compito meglio di quanto si possa fare con uno script, per quanto complicato esso sia.
Vediamo adesso come modificare il comportamento
make infatti permette di definire (e definisce) delle
di make secondo le nostre necessità, make per
regole con le quali trattare i file in ingresso per otte
prima cosa verifica ia presenza di un file makefile;
nere determinati target, posti i vincoli di dipendenza
se presente inizia ad eseguire l'azione di default o
tra le varie entità.
quella specificata sulla riga di comando.
Una caratteristica molto vantaggiosa di make,
La struttura di questo file è la seguente:
soprattutto nell'ottica della compilazione di progetti
di grosse dimensioni, è che questo strumento si
notne target:
occupa di ricompilare solo i file che realmente ne
lista dipendenze
azioni
hanno necessità, in base alle modifiche effettuate al codice del progetto e seguendo le informazioni
azioni
relative alle dipendenze.
Ovviamente make ricorre all'uso dei vari tool GNU,
si deve ricordare che make richiede che ogni linea
GCC compreso; la sua funzione è quella di "direttore
di azione sia preceduta almeno da un carattere di
d'orchestra".
tabulazione. Solitamente un target dipende da uno
Questo ci permette di gestire progetti complicati
o più file, può anche dipendere da altri target.
e di grosse dimensioni con uno sforzo ridotto, per
Può esistere anche la necessità di definire dei
mettendoci di automatizzare non solo la compilazio
target associati ad un comando da eseguire su
ne, ma anche l'installazione o disinstallazione, la
richiesta; il tipico esempio è l'azione di "pulizia":
pacchettizzazione o qualsiasi altra operazione intendiamo fare sui file.
.PHONY:
Il sistema più semplice di utilizzo di make consiste
clean:
semplicemente nell'invocarlo con il nome dell'ese
clean
rm
-f
•.o
progetlo
guibile da produrre come parametro, make in que sto caso userà le regole implicite di compilazione,
II target clean in questo caso deve essere specificato
secondo le quali cercherà nella directory corrente
con la direttiva .PHONY, per eliminare un'ambiguità
un file con lo stesso nome usato come parametro e
altrimenti presente se nella directory di progetto
con suffisso uguale ai possibili tipi di file conosciuti
venisse creato un file chiamato con lo stesso nome
(.e, .cc, .cpp, .0); una volta trovato intraprenderà
del target.
l'azione di default per quel tipo di file (compilazione
In tal caso infatti, visto che il target clean non ha
e, c+ + , linking, compilazione fortran).
dipendenze, esso sarebbe sempre considerato
INFORMAZIONI
Hutoconf, Automake & e Questi strumenti permettono di avere un'rnfrastruttura con la quale è possibile configurare, compilare e installare un pro getto in modo che questo si adatti alle necessità dell'utilizza-
Da notare che. nel caso in cui si modifichi solo uno
dei due file sorgente, ad esempio main.c, make effettuerà, correttamente, solo la ricompilazione di quest'ultimo:
cc
-e
CC
o nain.o main.c
rnain.o
utils.o
-o main
tore e al sistema per il quale viene compilato. Attraverso alcuni tool e gli opportuni file da interpretare è possibile serTiplilkdru la compilazione di progetti complessi in modo che questa abbia successo su più piattaforme, integrando gli script necessari per i test sull'ambiente di compilazione, l'installazione e la rimozione dell'applicazione
Alcune opzioni interessanti Prima di chiudere questa introduzione a make,
ricordiamo alcune opzioni di questa utility che pos
generata, il creare archivi o pacchetti del proprio software e
sono tornare utili anche per la gestione di progetti
molto altro.
di non elevata complessità. Poco fa abbiamo detto
Questo sistema di compilazione è attualmente il più diffuso
che la prima operazione effettuata da make consi
per i progetti OpenSource e potete trovare informazioni su di
ste nella ricerca di un makefile nella directory dove
esso su:
http://vww.gnu.org/software/autoconf/ http://vww.gnu.org/software/automake/ Naturalmente in futuro non potremo non affrontare l'argo
l'utility viene invocata; come prevedibile, è possibile
forzare da riga di comando il makefile da utilizzare tramite l'opzione -f:
mento su queste pagine! make
-f
nome makefile
Questo consente di avere più makefile per lo stesso WEBOGRAFIA
Riferimenti wefa http://gcc.gnu.org/ - Homepage di GCC
ftp://ftp.gnu.org/gnu/gcc/gcc-3.4-3/- Sorgenti di GCC http://gcc.gnu.org/onlinedocs/ - Documentazione e manuali online di GCC
http://www.gnu.org/software/make/- Homepage del progetto make http://ftp.gnu.org/gnu/make/ - Sorgenti di make http://www.gnu.org/software/fnake/manuaX/make.ritml Documentazione e manuali online di make
progetto, ad esempio ciascuno per una piattaforma
software/hardware differente, senza obbligare gli sviluppatori ad averne uno solo che gestisca tutti i vari casi.
Non è infrequente il caso in cui un progetto sia composto da più sottoprogetti di grosse dimensioni, magari portati avanti da team di sviluppo diversi. In questo caso risulterebbe scomodo avere un makefile
singolo per la gestione dell'intero progetto, sia per le dimensioni del makefile stesso che per le diffi
coltà di manutenzione da parte dei vari team. La soluzione ideale consisterebbe nell'avere un makefile per ciascun sottoprogetto. Questo può essere fatto sfruttando l'opzione -C: essa permette di specificare la directory di ricerca di make. Nel makefile principale basterà quindi definire un
aggiornato, e pertanto le azioni presenti al suo
target relativo ad ogni sottoprogetto, nel modo
interno non sarebbero mai eseguite.
seguente:
Vediamo a questo punto un semplice esempio in cui
abbiamo un progetto composto da due file, main.c e utils.c; ii primo sfrutta alcune funzioni definite
stibpro]ect :
make
-C
subproject directory
nel secondo, per cui vediamo come sia possibile
esplicitare questa dipendenza in un makefile:
questo causerà l'invocazione di make all'interno delle varie sottodirectory di ogni progetto; se fosse
main:
necessario è possibile proseguire ulteriormente
main.o utils.o
nella hcorsione, finché non si raggiunge un buon main.o:
utils.o:
compromesso tra nidificazione dei sottoprogetti e
main.c
utils.c
utiAs.h
complessità dei makefile.
Se si ha a disposizione una macchina dotata di più .PHONY:
CPU, ad esempio come unità di build, risulta ovvia
clean
mente appetibile la possibilità di eseguire in paral
clean: rm
-f
*.o
lelo la compilazione di più fiie. Questo è possibile
ma in
grazie all'opzione -j, che controlla il numero di
Con le nozioni finora acquisite siamo in grado di
comandi eseguiti in parallelo da make.
prevedere la sequenza di azioni che make eseguirà
Infine, dopo aver citato tutte queste possibilità, può
per la costruzione del nostro progetto:
tornar comoda l'opzione
-n, che fa in modo che
make scriva a video, senza eseguirli, i comandi pre cc
-c
-o
cc
-e
-o utils.o utils.c
cc
Ofe
main.o
main.o utils.o
ma 1n.e
-o mairi
visti durante una normale esecuzione: in questo modo potrete verificare rapidamente la correttezza dei vostri makefile.
Manipolazione dell9l/O: la scrittura di filtri in C Un programma di esempio per analizzare la gestione dello standard input e standard output: vedremo come leggere da file un sorgente C e creare da esso una pagina web di con tanto di sintassi colorata!
Simone Contini <s.CQntinitalj.nuxpratico.com>
Lorenzo Mancini <l.mancini@linuxpratico.coii>
Coloro che hanno familiarità con la shell avranno notato che sono a disposizione molte piccole utility a riga di comando che
possono essere combinate tra loro per ottenere i
formattazione come tali durante il rendering della
risultati desiderati. Esse non fanno altro che pro
pagina {essi altrimenti sarebbero ignorati). La
cessare opportunamente lo standard input e scri
nostra applicazione eseguirà come prima ed ultima
vere il risultato dell'elaborazione sullo standard
operazione rispettivamente la stampa dello header
output.
e del footer, in modo da confezionare un file XHTML
pipe
In questo modo è possibile utilizzare il
| per trasferire l'output di un comando come
ad hoc.
input del successivo, e > per indirizzare l'output Righe 15-24: Sempre per comodità, definiamo tre
finale su un file.
array di stringhe d3 utilizzare come insiemi omoge
Obiettivo
nei di parole chiave. Questi ci serviranno per stabilire,
In questo articolo realizzeremo un'utility con lo
secondo il gruppo di appartenenza, lo stile da appli
stesso criterio di funzionamento e dall'indubbia uti
care alla parola in esame, cprep contiene quindi
lità pratica: in particolare vedremo nei dettagli
l'elenco delle possibili direttive per il preprocessore,
come sia possibile scrivere un programma che,
ctypes le parole chiave del linguaggio C che iden
preso in ingresso un sorgente ANSI C via standard
tificano dei tipi di dato e i loro modificatori, infine
input, generi a partire da esso il contenuto di un file
ckeywords definisce le rimanenti parole riservate
XHTML opportunamente formattato, e lo invii sullo
secondo lo standard ANSI C. Si intuisce la semplici
standard output. Implementeremo quindi il ricono
tà con la quale è possibile far riconoscere altre
scimento di alcune entità del linguaggio C, in modo
parole per ogni classe)
da produrre una pagina web con il codice in syntax highlighting (sintassi evidenziata) personalizzabile
Le funzioni
attraverso un foglio di stile CSS.
In modo da non spezzare la trattazione nel momento
Il procedimento da noi scelto permette di ricono
meno opportuno, vediamo subito le piccole funzioni
scere un buon numero di entità, e di poter inter
di utilità che ci siamo creati e che useremo durante
venire abbastanza facilmente sul codice del pro
il ciclo principale dell'applicazione.
gramma nel caso in cui si vogliano aggiungervi
altre funzionalità.
in set(char
'word,
char
**set)
Dichiarazioni
data una parola word e un insieme dì parole set ter
Righe 5-14: Dopo le canoniche direttive di include
minato con "". la funzione determina e restituisce
per il preprocessore definiamo le stringhe hoader e
la posizione di questa nel gruppo; nel caso essa non
footer. Queste ci serviranno come "template" per
sia presente si restituisce -1.
la parte statica della pagina e potrete personaliz zarle a piacimento, ricordando di mantenere alme
Righe 26-31: Questo viene fatto prelevando, ad
no il tag "pre", che impone al browser di interpreta
ogni iterazione del ciclo, l'i-esimo elemento, finché
re tabulazioni, ritorni a capo o aitri caratteri di
non si arriva alla stringa vuota. Righe 32-37: Ogni elemento estratto viene con
TIPS'n'TRICKS
Suggerimento Scaricate il sorgente da Internet e visualizzatelo con un
frontato con la stringa da ricercare; se il controllo
da esito positivo ritorniamo dalla funzione con il valore corrente di
i, altrimenti a conclusione del
cicio ritorniamo -1 per indicare l'insuccesso.
editor di testo, possibilmente in grado di fornire la nume razione delle righe e la sintassi colorata. In questo modo
to_html(char
e,
FILE
* fp)
potrete analizzare comodamente il codice durante la let tura dell'articolo.
Alcuni caratteri, per essere visualizzati in una pagina web, devono essere rappresentati secondo
SORGENTE
delle precise identità definite per l'html: è il caso dei caratteri &, < e >, che devono essere rappresentati
Parte del sorgente
rispettivamente con le sequenze &amp;, &T.t; e &gt;.
Un estratto del sorgente che potete scaricare completo dal sito
La nostra funzione permetterà quindi di scrivere il
riportato nel riquadro "compilazione e uso"
carattere passato per parametro sul canale di output indicato, effettuando se necessario la conversione.
Righe 39-42: Definiamo due array, in e out, da mettere in corrispondenza: il primo è costituito da
caratteri e contiene i simboli che necessitano di una
27
-
<
28
Int
29
char
i ■tohen
;
39 31
mule
32
lf
(MIO >en
setllhl
-
('SII MplkC rd,
{
tokcnll
sostituzione, nel secondo troviamo le rispettive stringhe di traduzione. Riga 43: Per verificare se il carattere e ha bisogno
ld
di essere tradotto, controlliamo che esso compaia all'interno della stringa in. Per farlo utilizziamo la
-
FILE
H
"dlt;".
eli
(P) fpuli(oul[p
nella quale si trova il carattere passato come secon
<P)
= { "S.amp;",
char *p • slrcnrlin,
locazione delf'array, indicato dal primo parametro,
■
"&<>";
crtar *out|l
funzione strchr: questa ritorna un puntatore alla
■
in),
*
fpl;
else
do. Nel caso in cui all'interno della stringa non ci siano occorrenze del carattere specificato, la funzio
to__ntnlicnar e,
char ln[l
fputclc,
49
}
53
(
fpl:
ne restituisce NULL.
Riga 45-49: Se p. contenente il valore restituito dalla funzione appena descritta, è diverso da NULL,
ailora troveremo la sequenza di caratteri da scrivere
53
55
out; questa espressione è una differenza tra punta
59
quale esso è contenuto, che vista la corrispondenza
Un - strUn(j);
(ten
•
1
•
siici
:
Mlen] • e;
57 58
è stato trovato il carattere e la base dell'array nel
II
56
sul file destinazione alla posizione p-in dell'array
tori, e misura lo scostamento tra la locazione dove
int
54
illefi «11 -
'\B'i
) )
78
wordlBl -
79
««ne
86
'XB':
(fgetslbuffer,
slieoflbufferl.
wtille
Cpl
etiar e ■
quale tradurre il carattere processato. Altrimenti
lf
non è richiesta la conversione, quindi ci limitiamo a
[
'p;
Korev.c ■■ tó
mandare in output il carattere così com'è.
enmy
*\n"
-■
*s,
int
size,
11
nreproi
enlily ■ untfefined:
lf
char(char
{
81
tra i due array, è proprio l'indice della stringa con la
add
stdinll
p ■ buffer;
char e)
enttiy
ntil lf
Aggiunge il carattere e in coda alla stringa s, solo
)
se tale operazione non eccede la dimensione massi
llsalpruU)
e
—
"_'l
l
else 1 lf
ma, indicata con size, con la quale la stringa è
II
lentity
if
■■
undefined
(ln_5etlword, SA entlty
stata allocata.
■■
|I
cntity
c_prep)
!■
ct
preprocesso'
-1
preorotessori
{
fputil"«b closs-\"preproce5sor\*>", tpiitilword.
Riga 51-59: Per prima cosa memorizziamo la lun ghezza attuale della stringa; questo ci serve sia come posizione alla quale inserire il nuovo caratte
re, che per il test di lunghezza. Il controllo viene effettuato verificando che la lunghezza attuale, sommata ad 1 (il carattere che si sta per aggiunge
re), sia inferiore a size.
tpgis("</b»".
:■ :
enllly
IBI
unflefinefl;
1B3
tpuls!"*b clas5=\"keyword\">",
1B4
fputilword.
} else if
I- -1)
{
steioutk
sidout);
(puti|-</0>".
105
ìee
stctout):
lln^ieilnord. c.typti)
107
fpgt»("<b class=\"type\">".
168
fpulilword.
'- -1)
(.
sttìout);
stdoutl;
fputl(--;/b>-.
169
stflouti;
) else
Se è così si copia e nella posizione successiva all'ul
TDutMword,
timo carattere della stringa, dove quindi c'è il '\9'
StdOUt) I
) lise
che la termina, e nella posizione seguente mettia
113
mo il nuovo terminatore.
115
Nel caso la stringa sia già lunga il massimo previ
116
sto, non facciamo nessuna operazione.
-
stdoui);
) else lf lin_setlword, c_*eywords)
162
sldc
itdouU;
fpmslword.
stoouti;
11J
lf
117
fprev.c
!- '\V>
u_esca(>e
118
■ 0;
119
l'applicazione Righe 61-67: Per comodità, in modo da gestire gli stati nei quali si viene a trovare il parser durante il suo funzionamento, definiamo per enumerazione
delle costanti che rappresentano le possibili entità che valuteremo:
14
undefined,
if le — *\\" U. len
128
comment,
string.
121
iswescape
122
fputct'W'i
-
) else il (e ■■
123
siring
(
'W'
«. llcnilly -■ coment
124
|| enlliy — char«ewr))
'ii_escaEe:
stdout): ||
enlity ■■ stringi]
(
[...] 160 161
}
~
character, preprocessor; le discuteremo più avanti
valori visti nella dichiarazione enum, ci consentirà di
nella trattazione.
trattare contestualmente ogni blocco da interpretare.
Righe 69-74: Eccoci al maini ): dato che lavoriamo
Righe 76-78: Da questo momento in poi iniziamo a
direttamente sullo standard input e sullo standard
creare la pagina XHTML, quindi prima di iniziare il
output non abbiamo bisogno né di scomodare argc
parsing e l'output formattato del codice C dobbia
e argv per la lettura da parametro dei file sui quali
mo scrivere l'intestazione. Questa è già pronta,
leggere e scrivere, né di gestire l'apertura e la chiu
definita secondo i nostri gusti, comprensiva di tag
sura di essi; definiamo invece alcune variabili che ci
<body> e <pre>; utilizziamo quindi fputsO che
serviranno più avanti, buffer è la stringa che ospi
invia la stringa passata come parametro, header nel
terà, una linea alla volta, l'input del programma, la
nostro caso, sul canale indicato dal file descriptor,
stringa word sarà costruita a partire dai caratteri
che trattandosi dello standard output è già aperto e
esaminati, per poi essere confrontata con le parole
definito dalla libreria con il nome stdout.
chiave del linguaggio. Per parola si intende un grup po di caratteri alfanumerici, compreso l'underscore
Riga 79: Per leggere dallo standard input usiamo la
_, non interrotto da spazi, punteggiatura ecc...
funzione complementare di fputs(}, fgets().
Il puntatore p ci servirà invece nel ciclo principale
Questa accetta tre parametri: il buffer di destina
dell'applicazione, per spostarci all'interno del buffer
zione, il numero massimo di caratteri che vi dobbia
di input, prev e conterrà sempre il carattere prece
mo memorizzare è il canale da usare in lettura. La
dente a quello che stiamo esaminando, in modo da
funzione restituisce il numero di caratteri effettiva
poter interpretare correttamente le sequenze di
mente letti, il massimo imposto come parametro
escape o le entità che iniziano o finiscono con due
infatti non viene raggiunto nel caso in cui venga
specifici caratteri {ad esempio i commenti),
incontrato un carattere EOF o il carattere newline
isescape indicherà se stiamo trattando un codice
\n; se non si è arrivati alla fine del file, con chiama
escape, ne! qual caso il carattere corrente dovrà
te successive a fgets () sarà possibile continuare a
essere interpretato di conseguenza, entity sarà
leggere dal canale di input.
l'indicatore di stato del parser che, assumendo i
Righe 80-83: Adesso che abbiamo acquisito una linea cominciamo a scorrere i caratteri che la com
INFORMAZIONI
gets[] vs fgets[]
pongono. Come intuibile, il carattere attuale, il suo precedente e lo stato del parser verranno usati per
trattare contestualmente la stringa contenuta in word secondo l'entità di appartentenza.
Alcuni di voi si chiederanno come mai non si utilizzi gets( ), che lavora implicitamente su stelin, al posto di fgets t ) che
Righe 85-90: Dopo una terminazione di linea alla
richiede di specificare il canale di input da usare.
fine di una direttiva del preprocessore, il tipo di
La risposta è semplice: cjets( ) non deve mai essere utilizza ta, in nessun caso! Questa infatti, non permettendo di speci
ficare la quantità massima di caratteri da leggere, è molto rischiosa in termini di sicurezza, dato che l'input potrebbe
entità torna a undefined; viceversa, quando il carattere che stiamo esaminando è un # e non siamo all'interno di una particolare entità, il tipo di
eccedere la dimensione del buffer indicato come destinazio
questa diventa relativo ad una direttiva del prepro
ne dei dati. Il buf fer infatti viene allocato prima di sapere
cessore; in questo modo sapremo quando una
quanti caratteri arriveranno dal canale di input, pertanto l'u
determinata parola chiave possa essere considerata
nico metodo sicuro per leggere da stdin è, come per gii altri file, tramite fgets( ). Questa permette di leggere al massimo il numero di caratteri
tale invece che altro.
specificato in modo da non eccedere la quantità memorizza-
Righe 92: La nostra idea è quella di formare, carat
bile nel buffer, e continuare a leggere dal canale di input in
tere dopo carattere, una parola completa in word, in
blocchi con chiamate successive fino a quanto necessario.
modo da verificare che corrisponda o meno ad una parola chiave contenuta
nei nostri
elenchi.
Eseguiamo il test per vedere se il carattere corrente
INFORMAZIONI
Esercizi per casa
potrebbe far parte di una parola...
Riga 93: In tal caso aggiungiamo alla stringa word il carattere corrente con la funzione add
char()
Lo scheletro proposto potrebbe stimolarvi a estendere il pro
precedentemente descritta, per poi passare alle
getto con qualche funzione aggiuntiva. Ad esempio potreste
istruzioni che cominciano daila riga 153
introdurre il supporto per lo standard ANSI C99. Per farlo, oltre
ad aggiungere le nuove parole chiave nei tre insiemi previsti, sarete obbligati ad implementare qualche controllo in più riguardo all'individuazione dei commenti, dato che il nuovo
Righe 94-95: Se il carattere non è tra questi, vuoi dire che è appena terminata una parola, quindi ini
standard eredita dal C++ l'uso di // per indicare la colonna di
ziamo una serie di controlli per vedere se dobbiamo
testo a partire dalla quale considerare ogni altro carattere
fare il syntax highlighting del contenuto di word.
della nga corrente come commento.
Questo dobbiamo farlo solo nel caso in cui lo stato
Un altro sviluppo interessante potrebbe consistere nel fare in
del parser sia indefinito, o stia ad indicare che si sta
modo che l'utility sia in grado di riconoscere, e di conseguen
za marcare, le costanti numeriche.
valutando una paroia del preprocessore; negli altri
casi, come ad esempio all'interno di stringhe o
~ commenti, non vogliamo evidenziare alcunché, in
delle parole chiave sono terminati, adesso ci occu
quanto tali parole non fanno parte del codice C del
piamo di determinare codici di escape, costanti di
programma.
testo e commenti. La prima cosa da fare è verifica-
Righe 96-101: Eccoci finalmente a qualcosa di
come ad esempio un \" all'interno dì una stringa,
re se il carattere attuale è una sequenza escape, concreto! Se il contenuto attuale di word è nell'in
che non deve assolutamente essere confuso con un
sieme delle parole chiave de! preprocessore, e lo
" di fine stringa, pena un'errata colorazione di
stato dei parser indica che abbiamo appena trovato
buona parte del sorgente!
un #, inviarne la stringa sullo standard output for-
mattandola opportunamente. Per farlo poniamo,
Righe 120-126: Se il carattere attuale è un backslash
prima e dopo la chiamata a fputsO per la parola
e ci troviamo all'interno della definizione di una
attuale, quelle per i tag XHTML desiderati, nel
stringa o di un carattere, invertiamo il valore di
nostro caso con un riferimento allo stile del CSS che
ìs escape. In modo da sapere come dovrà essere
verrà utilizzato dal browser per il rendering della
inteso il carattere successivo: se come appartenen
pagina. Fatto questo possiamo resettare lo stato del
te ad una sequenza speciale o no. Ad ogni modo il
parser con il valore undef ined.
backslash può intanto essere inviato sullo standard
Si deve notare che nel caso specifico del preproces
output. Se non ci troviamo all'interno di un com
sore si usa uno stato speciale, a differenza dei due
mento o di una stringa impostiamo is
casi che vedremo tra poco. Infatti alcune direttive
falso, dato che tali sequenze hanno senso solo
sono uguali ad alcune parole chiave del linguaggio,
all'interno di costanti stringhe o carattere.
escape a
quindi l'unico modo per sapere se la parola in
esame deve essere considerata come parola chiave
Righe 127-133: Implementiamo la gestione di
del preprocessore e non del linguaggio è tenere
costanti di testo controllando la presenza del carat
memoria dello stato, che indica appunto se sulla
tere ", e tenendo conto dell'entità attuale in esame (non dobbiamo trovarci all'interno di un commento
linea attuale abbiamo trovato un #.
o di una stringa). Righe 102-109: Questi due casi, analogamente a
Se eravamo all'interno di una costante carattere è
quanto appena visto, controllano l'eventuale pre
ovvio che il " ne determina la fine, dobbiamo dun
senza della parola che si sta considerando all'interno
que chiudere il tag corrispondente nella nostra
di ckeywords o e types, e di conseguenza produ
pagina XHTML; viceversa lo apriremo. Infine,
cono l'output di word preceduto e seguito dai tag
aggiorniamo opportunamente il valore di entity.
desiderati.
Righe
134-140: Per la gestione delle stringhe
Riga 110-115: Se nessuna delle condizioni previ
viene usato un approccio del tutto analogo al
ste si verifica siamo in presenza di una parola che
precedente.
ai fini della colorazione e della formattazione del l'output non ha nessun particolare significato,
Righe 141-148: Per i commenti devono essere
quindi stampiamo direttamente word. In ogni caso
riconosciute le sequenze / • e */ al di fuori di una
la stringa appena inviata in output viene azzerata,
stringa: il presentarsi della prima determina l'entra
in modo che possa essere nuovamente riempita un
ta in modalità commento, con stampa del tag di
carattere alla volta-fino a formare una nuova parola
apertura relativo; nel caso invece venga rilevata la
intera.
seconda, entity tornerà in modalità undefined e verrà stampato il tag di chiusura.
Riga 117-118: I controlli necessari per la colorazione
Il riconoscimento della sequenza di due caratteri viene fatto con strncmpO che controlla al più la
INFORMAZIONI
Compilazione e uso
quantità di caratteri specificata. In ognuno dei due casi incrementeremo p, in modo da compensare il
fatto di aver valutato nello stesso ciclo due caratteri invece di uno.
I sorgenti sono scaricabili da: http://www.linuxpratico.codi/risorse/sviluppo/
Righe 149-150: Se nessuno dei casi precedenti si
Per la compilazione non è necessario far altro che:
è verificato, ci limitiamo a stampare in uscita il
S
gec
c2web.c
-d
carattere attuale, ovviamente tramite chiamata a
c2web
to
htmlO.
A questo punto potete provare con qualche sorgente ad esempio quello del programma stesso: S
cat
c2web.c
./c2web
>
c2web.html
In accordo all'obiettivo che ci eravamo preposti, abbiamo
Righe 153-155: E' il momento di passare al carat tere successivo del buffer, quindi memorizziamo
quello attualmente processato in prev e e incre mentiamo il puntatore p.
realizzato un'utility che può lavorare combinata con altre, al fine di ottenere il risultato desiderato: S
cat
c2web.c
|
indent
./c2web > c2web.html
Righe 158-161: Ce l'abbiamo fatta, non rimane altro che chiudere la pagina XHTML analogamente per quanto fatto con l'intestazione e quindi uscire dall'applicazione ritornando 0.
Parsing della linea di comando con la GNU libc hi queste pagine vedremo come gestire le opzioni a linea di comando con argp, una funzionalità della GNU lìbc, estendendo il progetto
dweb visto nelle precedenti pagine...
Siraone Contini <s.continiglinuxpratlco,com>
Lorenzo Mancini <l.mancini@linuxpratico.com>
La flessibilità e la potenza di un'applicazione a riga di comando dipendono, tra le altre cose, dall'offrire all'utente una serie di parametri e opzioni per modificare il comportamento di essa, al
avrà il duplice scopo di essere utilizzata dalle fun
fine di coprire la più ampia quantità possibile di casi
zioni di manipolazione della command line e di fun
specifici. Per passare all'applicazione i! contenuto
gere da riferimento alle scelte fatte dall'utente. Il
della corrimano line, il linguaggio C definisce due
contenuto di tale struttura è completamente a dis
parametri per la funzione mairi: arge che indica il
crezione del programmatore. Vedremo tra poco la
numero di parti separate da spazio presenti sulla
semantica che le variabili da noi definite assumono
riga di comando, e argv che è il puntatore ad un
nel nostro caso.
array contenente ognuna di queste stringhe. È dunque necessario interpretare ed utilizzare tali
Righe 57-60: queste stringhe rappresentano alcu
informazioni.
ne delle informazioni che verranno visualizzate a
il controllo delle opzioni e dei parametri passati può
video nel caso il programma venga invocato con
essere fatto autonomamente a partire da queste
alcuni parametri standard tipo --version, --help,
informazioni, ma non si tratta certo della strada
ecc... Qui possiamo inserire il testo che desideria
più comoda da seguire; piuttosto esistono alcune
mo, ovviamente attenendoci al significato che deve
librerie le cui caratteristiche permettono alle nostre
avere ogni stringa. Definiamo quindi una variabile
applicazioni di interpretare la linea di comando ed
argp
offrono anche alcune facilitazioni, ad esempio quel
la versione dell'applicazione e una variabile
la di costruire automaticamente un utile elenco dei
argp program bug address per inserire gli indirizzi
parametri accettati (secondo il coding standard
E-Mail dei programmatori da contattare per i bug
program version per memorizzare il nome e
GNU; http://www.gnij.org/prep/standards 18.html),
report. Le altre due variabili, doc e argsdoc, sono
da mostrare a video quando la nostra applicazione
stringhe contenenti rispettivamente una breve
viene chiamata col parametro - - help.
descrizione del programma e la rappresentazione di
come l'utente deve specificare i parametri sulla riga
Obiettivo
di comando.
Per convincervi, metteremo mano al sorgente del convertitore C - HTML. presentato nelle pagine pre
Righe 62-71: l'array di strutture argp option rap
cedenti. La prima parte dell'articolo sarà dedicata
presenta in forma tabellare tutte le informazioni
alle impostazioni necessarie per interpretare la riga
relative alle possibili opzioni. Ogni riga è costituita
di comando, dopodiché passeremo a risolvere alcu
da una struttura argp option i cui campi hanno,
ni dei compiti per casa che avevamo lasciato in sospeso. Al termine della lettura sarete in grado di espandere agevolmente !e funzionalità di questo e dei programmi che vorrete. Per questo scopo useremo argp, un'interfaccia
uuge:
cJweù
[CPTIQN
1
messa a disposizione dalla libreria C GNU.
Dichiarazioni
-o,
■ -omput-*f
Perché tutto funzioni correttamente dobbiamo crea
-b.
■-body-cnly
re e riempire opportunamente alcune strutture dati definite in argp.li, l'header file che appunto defini
OLI*
IMt htlp
llll
sce i! necessario per utilizzare la funzione GNU per il parsing della riga di comando. Vediamo in detta glio cosa c'è da fare. Righe 48-55: per prima cosa definiamo una strut tura che conterrà i valori da acquisire attraverso ogni opzione impostabile a riga di comando: questa
Ecco come si presenta l'hetp per la nostra rinnovata utility
nell'ordine, i seguenti significati: nome lungo del
coda si deve aggiungere la struttura vuota {0} in
l'opzione, ovvero la versione dell'opzione alla
modo da indicare l'ultimo elemento.
quale si farà riferimento con la forma --opzione
Il parametro flag è un intero formato dalla combi
sulla riga di comando; carattere che indica l'op
nazione, tramite l'operatore OR (|), delle seguenti
zione corta; nome del parametro; eventuali
maschere: OPTION ARG^OPTIONAL specifica che la
flag aggiuntivi che esamineremo tra poco;
presenza di un parametro associato a questa opzio
descrizione estesa del parametro. Da notare
ne non è obbligatoria; OPTION HIDDEN fa sì che l'op
che il nome del parametro e la descrizione estesa
zione non venga elencata in nessun messaggio del
servono solo ai fini della visualizzazione dell'help. In
l'applicazione, nemmeno nell'help; 0PTION_ALIAS
i ^ II nuovo sorgente c2web.c char headerl]
= "<7m\ vcrsionnV'l,0\"7>\n"
96
break;
98
argurent5-»output ■
99
break;
169
7 \-http://wwrf.w3.org/T(VxhtmUl/0TD/.Mmlll.dtd\->\n-'
case 'e':
IBI
arguments->css
= arg;
break;
8 9
163
"<head>\n"
10
M<Title»»tó</title»\n"
104
11
"■mela http-equiv*\"Conlent-Type\"
105
case
"<llnk rel=\"stylesheet\" type=\"text/cssV href-\"Hs\* />\n*
u
't';
argumentS'»tltle - arg; break;
106
case
b' ;
187
argunents->body only = 1;
1B8
break;
169
14
default:
na
25
arg;
char "cc typesl] • {
-auto",
"Boni",
"char',
"class",
[...]
):
in
return ARGP ERH UtWttiN;
112
}
113
29
114
39
char "prep!) - { e prep, e prep );
48
char ••typesl] = (
41
char "keywrdsl] = { e keyworfls,
43
char "languages1] ■ { "e", "c++", "" );
return 0;
11S
}
117
static struet argp argp = { options,
ctypes, cc_types }; cc keywords
};
119
int
120
1
parse clilmt arge,
Struet S .irci vi?'.
121
argurents.language = LANG C;
49
char language;
122
argunents.rows
56
int
rows;
123
argurents.output = "";
51
char 'output;
124
arguments.css ■
52
char 'css;
125
argurrents.tltle = "c?«jb output*;
53
char -tltle;
126
argiments.body only ■ 0;
54
char body only;
127
argixients;
ìza
55
)
=
args doc, doc );
char "argv)
48
{
parse opt,
-1;
"css.css";
argp oarsel&argp,
anjc, argv, 9,
NULL,
&argurents);
129 57
const char 'argp program verslon = "c2web 6.2";
130
58
const char 'argp program bug address ■
131
>
return 0;
59
static char dcc|] = "Creates a web page
165
sdefine is cooaent(e)
173
FILE *fout = stdout;
((e = coment)
]|
(e = si corment))
from a program source flle.\n"; 66
static char args doc|] = "*j
61 62
static struet argp optici optlons[]
63
{8, fl. B, 8,
"Generai options:").
64
("language".
'V,
65
{"output",
'o'.
"<languaoe>",
"<fllename>",
D.
185
■ {
0.
"Lariguage: e,
{0, 0,
67
{■css-,
68
{"title",
69
("Cody-onty",
76
(0)
71
B,
0,
"Use filename
-Header (. footer optlons:"),
"<title>", 8, 'b1. 0, 9,
195
If ([orev e ™ '\n'
196
Si
197
'e1, '<css>", B, "link to specified CSS"), f,
(iarguments.body only)
fprintflfout, header,
"Set title page"},
"Sklp rieader and (ooter").
if
193
fputs("</l>",
199
entity = unciefinetì;
289
|
(entity — si coment)))
(entity — sl_coment)
fout);
)
...
215
);
static error t parse optflnt key, char -arg. struet
B7
|| prev_c = '\S'}
[(eniity = preprocessor)
1 else if (in setlword. keywords[arguments.language])
216
86
arguments.title, argionents.css);
c<-f">,
insteafl of stdout"), 66
if
186
(
88
lnt
89
struet
sw;
switch
92
case
"state]
fputsr«ti class^\"keyword\"i",
218
fputslword,
219
fputsl"</b>",
fout);
fout); fout);
...
s argunents
'arguneMs ■ state-iinput;
9Q 91
argp state
1- -11 <
217
(key)
{
'l'i
266
} else if
(arguments.language = LANG CC
267
SA strncrap(p,
268
t&
269
"//",
2) = 6 (& entity
lis coment (entity))
!= string
{
entity = si comment;
93
argunents->language ■ in setlarg. languagesl;
278
fputs("<i tlass=\"comment\">//",
94
if (arguments->language < 0)
271
«p:
95
retum ARGP ERfl UNKNOViN;
272
) else
fout);
{
w
serve a definire l'opzione relativa come alias della
già presentati: options, parse opt, args doc e doc.
precedente, ad esempio può essere utile per man tenere la compatibilita con una versione precedente
Righe 119-131: tutto è pronto per essere utilizza
del programma, in cui una stessa opzione aveva un
to, tuttavia riteniamo opportuno aggiungere l'inizia-
nome diverso da quello nella versione attuale;
lizzazione di default per tutte le opzioni messe a
OPTION DOC è utile per inserire una finta opzione,
disposizione dell'utente. Questo oltre a definire un
che in realtà serve solo per aggiungere delle
comportamento standard funzionante per l'applica
informazioni nell'output di - -help a discrezione del
zione, ci solleva dal compito di controllare al
programmatore.
momento del parsing della riga di comando quali
Implementazione
e quali devono essere impostate con valori di
Righe 86-89: vediamo adesso parse opt, l'ultimo
default. Questa operazione viene effettuata tramite
ingrediente che daremo in pasto ad argp. Si tratta
la funzione parse eli, che ha anche il compito di
opzioni sono effettivamente specificate dall'utente
della funzione che definirà le azioni da effetturare
avviare il parsing della riga di comando invocando
per ie possibili opzioni passate alia nostra applica
argp parse con gli opportuni parametri.
zione; il suo prototipo è stabilito dalla libreria, noi
Il primo è la struttura argp definita in precedenza,
dobbiamo fornire l'ìmplementazione. Per ogni opzio
seguono arge e argv che nel nostro caso saranno
ne incontrata sulla riga di comando, parse opt
quelli ottenuti dal mairi. Il parametro successivo è
viene chiamata con i seguenti parametri: key con
una maschera di bit, che può essere creata tramite
tiene il carattere associato all'opzione stessa
combinazione in OR. Tra i flag utilizzabili citiamo:
(opzione corta); arg contiene, sotto forma di strin
ARGP PARSE ARGVO permette di applicare il parsing
ga, il parametro relativo all'opzione specificata se
anche al primo elemento di argv, solitamente igno
questo è presente (altrimenti o). da notare che se
rato in quanto contenente il nome dell'eseguibile;
viene specificato un parametro per una opzione che
ARGP IN ORDER inibisce il normale comportamento
non lo prevede, argp riporterà un errore prima della
di argp secondo il quale le opzioni vengono riordi
chiamata a parseopt; state punta ad una struttu
nate e processate prima dei parametri del program
ra argpstate che contiene le informazioni relative
ma, al contrario specificando tale flag gli argomenti
all'attuale stato del parsing. Quest'ultima in partico
saranno processati nell'ordine col quale appaiono
lare ci serve per ottenere un puntatore alla nostra
sulla riga di comando; ARGP NOEXIT forza il prose
struttura che vogliamo utilizzare per raccogliere le
guimento del parsing anche in caso di errori. Il suc
informazioni relative ai parametri specificati dall'u
cessivo parametro della funzione permette di speci
tente via via che vengono processati i parametri dal
ficare l'indirizzo di una variabile intera alla quale
parser; definiamo quindi un puntatore al membro
assegnare l'indice della prima opzione non sottopo
input di state dello stesso tipo della struttura da
sta a parsing; per i nostri scopi questo non è neces
noi precedentemente creata.
sario dato che vogliamo delegare completamente il
Righe 91-115: all'interno della funzione scegliamo
tanto abbiamo specificato NULL. Infine passiamo
parsing della riga di comando ad argp parse, per con un costrutto switch( ) il codice da eseguire per
l'indirizzo alla struttura arguments che come abbia
l'opzione in esame, discriminando i vari casi grazie
mo visto servirà a parseopt.
al carattere contenuto in key. Per l'opzione l (language) è possibile specificare le stringhe e e c++:
Riga 175: il lavoro relativo ad argp è finito, a que
noi vorremmo memorizzare un identificativo nume
sto punto all'interno del nostro mairi() non dobbia
rico a seconda della scelta fatta, in modo da sempli
mo far altro che chiamare la funzione da noi defini
ficare più avanti il controllo delle operazioni da svol
ta
gere a seconda del linguaggio in input. Per fare
impostato farà sì che nella struttura arguments ven
questo utilizziamo la già presentata funzione
gano memorizzati automaticamente i valori relativi
in set, dove languages è un array che contiene !e
alle opzioni e agli eventuali parametri specificati
due stringhe citate, e memorizziamo il valore da
dall'utente.
essa restituito. Da notare che se l'utente ha specifi
cato una stringa non riconosciuta, o più in generale ha tentato di utilizzare un'opzione non gestita (caso
parse
eli;
il
meccanismo
che
abbiamo
Implementazione delle funzionalità aggiunte
Default del costrutto) questo viene segnalato ritor
nando il valore ARGP ERR UNKNOWN.
Ora che aryp non ha (quasi) più segreti per noi,
Per le altre opzioni è previsto un trattamento più
abbiamo a disposizione una struttura che contiene i
semplice, in quanto memorizziamo il valore del
risultati del parsing della riga di comando, e quindi
parametro così come viene passato alla funzione o,
le opzioni desiderate o di default; passiamo a vede
nei caso dell'opzione body-only che non richiede
re cosa è cambiato nel resto dell'applicazione per
alcun parametro, impostiamo un valore booleano.
implementare le funzionalità aggiuntive che abbia
La corretta terminazione della callback è segnalata
mo introdotto.
ritornando il valore o.
Righe 6-14: per quanto riguarda la possibilità di Riga 117: a questo punto abbiamo definito tutte le
modificare il CSS di riferimento e il titolo della pagi
entità che ci servono per definire la struttura argp,
na abbiamo modificato la stringa header mettendo
specificando come membri di essa ì vari elementi
dei °à5 al posto delle parti della stringa che prima
erano costanti, ma che adesso abbiamo intenzione
preferenza sul file di output da utilizzare apriamo in
di poter modificare dalla riga di comando. Questo
scrittura tale file e memorizziamo in fout il punta
come vedremo più avanti ci permetterà di persona
tore per manipolarlo. Altrimenti, come appena
lizzare l'intestazione al volo con l'utilizzo di
accennato, verrà utilizzato stdout. In caso di errore nell'apertura del file visualizziamo il messaggio di
fprintf.
errore corrispondente con perror e terminiamo
Righe 25-41: questi sono i gli array che descrivono
l'applicazione.
le parole chiave del C++, preparati con lo stesso criterio visto per la versione precedente riguardo al
Righe 185-186: analogamente alla volta prece
C. A questi si aggiungono altri tre array che ci ser
dente possiamo iniziare a scrivere l'output, comin
vono per mettere in relazione, per ogni linguaggio
ciando appunto dall'header. Se è stato imposto il
supportato, gli insiemi delle parole chiave. In que
flag per la scrittura del solo corpo della pagina
sto modo utilizzando lo stesso valore di indice per
HTML, l'fprintf relativo all'intestazione della pagi
ognuno di questi tre possiamo ottenere esattamen
na viene saltato. La volta scorsa l'header veniva
te le parole chiave di quel certo linguaggio. Gli
scritto con fputs perché la stringa predefinita era
array quindi sono costituiti da puntatori agli insiemi
esattamente quella da stampare; questa volta inve
precedentemente definiti. Il primo elemento di
ce vogliamo personalizzarla, quindi come già accen
ognuno (di indice G) fa riferimento al corrispondente
nato usiamo fprintf in modo che la stringa prede
insieme per il C, il secondo (di indice 1) per il C+ + ;
finita contenente i due "ùS possa essere completata
è da notare che per prep i due valori sono uguali
con le informazioni contenute in arguments .title e
dato che il preprocessore, per i due linguaggi, rima
in arguments.css.
ne invariato. Righe 195-200: questo blocco si occupa di uscire Riga 165: per comodità definiamo una macro che
dalle entità che terminano la loro esistenza con la
ci permette di verificare se l'entità attuale è un
fine di una linea. Aggiungiamo quindi il caso in cui
commento oppure no; nei caso del C + + infatti
ci si trovi in sl_comment ovvero in un commento
abbiamo due possibili sistemi per delimitare i com
C++ di una singola linea. In questo caso trovandosi
menti: quello in stile C e quello a linea singola pre
alla fine della linea imposteremo lo stato al valore
ceduto dalla coppia di caratteri //. Per far funziona
indefinito subito dopo aver chiuso il tag HTML che
re correttamente l'individuazione dei commenti
abbiamo scelto di utilizzare per i commentì.
abbiamo aggiunto lo stato si comment da attivare quando siamo su un commento introdotto da //; la
Righe 208-226: oltre alla sostituzione già descritta
macro permette di stabilire, senza peggiorare la
di stdout con fout, cambia leggermente il mecca
leggibilità, se attualmente ci troviamo in un com
nismo per determinare l'appartentenza o meno di
mento indipendentemente dal suo tipo.
una parola ad un certo insieme di parole chiave.
Tutto è risolto utilizzando arguments . language Riga 173: definiamo il file di output che per default
come indice per gli array prep, keywords e types. In
sarà il puntatore a stdout. fout sostituirà stdout in
questo modo, secondo il linguaggio scelto dalla
tutte le chiamate a fputs e simili; in questo modo,
riga di comando, verrà utilizzato l'insieme corri
ad esempio, per scrivere su un file piuttosto che
spondente secondo quanto visto nella descrizione
sullo standard output basterà semplicemente asse
delle righe da 25 a 41.
gnare ad esso un puntatore a fife diverso. Righe 236-261: per tutto quello che deve essere
Righe 177-183: questo è il primo esempio concre
identificato solo fuori dai commenti usiamo la
to dell'utilizzo delle informazioni all'interno della
macro iscomment al posto del semplice test se
stuttura arguments, raccolte dalla nostra funzione
l'entità è comment. Per quanto riguarda i commenti
di parsing parse eli. Se l'utente ha specificato una
in stile C dobbiamo invece verificare che la coppia /* o •/ non sia all'interno di un commento in stile
INFORMAZIONI
Compilaiione ed uso I sorgenti, come al solito, sono scaricabili da http://www.linuxpratico.eom/5viluppo/c/
Per la compilazione non è necessario far altro che: i gec c2web-l7.c
-o c2web
precedente, con la differenza che possiamo utilizzare anche
un sorgente C++ come input e che possiamo utilizzare le nuove opzioni previste: cat
sorgente.ee
|
,/c2web
da falsi inizio o fine commento.
Righe 262-265: se è stata imposta l'interpretazione di un sorgente C + + verifichiamo la presenza della coppia // al di fuori di stringhe o altri com
menti. Se il test ha esito positivo inizialìzziamo lo stato come si
comment, che sarà attivo fino alla
fine della linea corrente, e inviamo in output i
il funzionamento è identico a quanto visto per la versione
S
C++ in modo che il nostro parser non sia confuso
-l
c++
-b c2web.html
caratteri di commento preceduti dalla formattazio ne desiderata.
Righe 275-276: abbiamo concluso, non resta che
inviare in output il footer della pagina XHTML con la stessa condizione prevista per l'intestazione e ter minare l'applicazione.
~
~-
La programmazione dei socket in C Nei sistemi operativi Uìiix-like come Linux la conoscenza di un
linguaggio di programmazione è estremamente utile: ecco come utilizzare ì socket in C per creare un pori scanner...
Marco Ortisi <m.ortisi@linuxpratico.com>
La programmazione in C è un concetto stretta mente collegato alla nascita di Linux; non a caso gran parte del codice del kernel (com preso quello relativo al networking) o degli applica tivi che attorno ad esso ruotano sono interamente
Analisi del sorgente: gli header
scritti in questo linguaggio. E' importante perciò imparare questo linguaggio sin
Dalla riga 1 alla riga 6: i file header sono impor
dal primo momento in cui si sceglie il pinguino
tanti perché permettono il richiamo di procedure,
come sistema operativo amico, soprattutto perché
funzioni, strutture e variabili esterne al corpo del
in tal modo si riesce a "governarlo" e comprenderlo
proprio programma. I file di header inclusi nel codi
meglio. La programmazione dei socket è un ambito
ce d'esempio sono sufficienti a raggiungere l'obiet
affascinante che ci permette di interagire, attraver
tivo poc'anzi posto. Come potete vedere nel riqua
so l'ausilio delle cosiddette API socket, con applica
dro in queste pagine, ognuno di questi header ha
zioni di rete in modo semplice ed intuitivo.
un compito specifico, che è bene conoscere anche
Linux rispetta in pieno le specifiche standard POSIX
aiutandosi con la relativa pagina di manuale.
che assicurano la portabilità del codice da una piat taforma Unix all'altra. In queste pagine verrà analizzato un piccolo sorgen
Analisi del sorgente: la mainQ
te dimostrativo che, sfruttando la programmazione dei socket, permetterà di creare un semplice scanner
Un consiglio che sono solito dare sempre (e che ha
TCP in grado di rilevare le porte aperte di un host.
aiutato l'autore stesso sin da quando ha iniziato a
le fasi dì un client
dal corpo main( ) e solo quando ci s'imbatte in chia
programmare) è di osservare un sorgente partendo
Per fare ciò che vogliamo non abbiamo bisogno di
mate a procedure o funzioni, analizzarne il contenu
cimentarci nella creazione di un applicativo server
to. Nella lettura del sorgente qui inserito si procede
che gestisca le richieste in ingresso, bensì principal
rà , perciò, in questo modo.
mente dovremo essere noi ad interrogare un host,
Fortunatamente l'esempio proposto sarà sufficien
pertanto bisognerà semplicemente recitare la parte
temente lineare e non comporterà bruschi rimandi
del client.
all'interno del codice.
Quando un client tenta di connettersi ad un server,
di solito esegue i seguenti passi:
Dalla riga 36 alla riga 39: all'esecuzione del pro gramma viene fatto un controllo sul numero dei
a) crea un socket attraverso il quale stabilire il collegamento desiderato; b) stabilisce una connessione all'host
parametri che vengono passati da linea di comando
(il cui numero viene conservato nella variabile inte ra arge); se essi sono meno di 4, viene stampato un
desiderato attraverso il socket appena
messaggio a video che illustra il corretto utilizzo del
creato;
tool. In seguito ne viene terminata l'esecuzione
e) eventualmente invia e riceve dati:
attraverso exit{ -1).
d) chiude la connessione attraverso la distruzione del socket utilizzato.
Nel nostro esempio prenderemo per buoni i punti a), b) e d}. Il penultimo non è d'interesse per rag
giungere lo scopo prefissato, perché dal momento in cui si stabilirà se una porta è aperta o meno non si avrà più interesse nello scambiare dati con la
INFORMAZIONI
Leggere il manuale E' possibile trovare il completo manuale di tutte le funzioni usate tea le pagine non che tra quelle info. Dato che il client
destinazione; pertanto la connessione verrà chiusa.
testuale può risultare scomodo da utilizzare, può convenire
Arrivati a questo punto non resta che passare
sostitirlo con quello grafico presente sia in KDE che in
in rassegna le porzioni di codice d'interesse e
GNOME, che trovate sui rispettivi pannelli delle applicazioni.
commentarle.
Dalla riga 40 alla riga 45: in 2 variabili intere
SORGENTE
(p start e p end) vengono conservati i valori pun
tati da argv[2] ed argv[3] equivalenti rispettiva
Codice completo
mente alla porta dalla quale dovrà iniziare lo scanning ed a quella in cui dovrà terminare.
1
«include
-=stdio.h»
2
«Include <string.ri>
3
«include <unistd.ii>
A
«include <sys/types.h>
5
«include
6
«include <arpa/inet.h>
7
volt) scan connect{
e
{
Trattandosi però di valori stringa essi vengono con-
vertiti in formato numerico attraverso la funzione atoi(), in modo da poter essere correttamente immagazzinati nelle variabili p start e p end.
<sys/socket.h>
Su quest'ultime viene infine fatto un check per assi curarsi che l'utente (intenzionalmente o sbadata
char "host,
int p stari,
mente) non inserisca nella prima un valore numeri
ini p end
co superiore all'altra o che una delle 2 non
9
struct
sockaddr
19
int
s,
pori,
11
int
connection;
12
pori
13
whi\el port <■ p.end )
14
=
sa;
contenga una porta superiore alla 65535 (il massi
check;
mo valore consentito per le porte UDP e TCP).
L'operatore "| |" utilizzato equivale al più conosciuto
p_start;
S - socketl
lfl
in
s
!=
"OR", e indica che se almeno una delle due condi
AF
-1
)
INET,
50CK STREMI,
sizeofl
family =
AF
sa
)
port INET,
inet
pton|AF
check <= 9
)
);
alla quale vengono passati 3 parametri:
INET;
check =
(
sarà anch'essa vera.
);
Riga 46: viene lanciata l'unica procedura scan connectl)
6,
sa.sin port - htonsl
ìf
B
{
memset[&sa. sa.Sin
zioni da verzicare risulta vera, tutta la condizione
{
); host,
>
Srsa.sin addr) ;
argv[l) contenente l'indirizzo IP oggetto della scansione;
<
>
exitf-Di
p start contenente la prima porta su cui effettuare lo scanning;
■ connection ■
connectl
s,
(struct
sockaddr
&sa, if(
25
connection == 9
pnntfl
26
•)
sizeofl
));
Analisi del sorgente: la prneedura scan_cnnnect()
)
port
else
);
^^
perror<
28 29
closei
30
-*port;
31
effettuare lo scanning. sa
"Connessione accettata sulla porta TCP \d\n",
}
27
s
"socketU"
p end contenente l'ultima porta su cui
La procedura scanconnect ( ) è il cuore dei nostro
);
piccolo sorgente: essa si occupa dello scanning
);
vero e proprio, creando la connessione e verificando le porte aperte dell'host specificato.
)
32
}
33
int main{
3-1
{
Riga 9: Viene dichiarata sa, una struttura di tipo int
35
int
p start,
36
if(
argc
37
pnntf<
argc,
!= 4
Char "argvll
)
sockaddr in. In essa vengono immagazzinati tutti i dati principali inerenti la connessione che deve
p end;
)
essere effettuata. I membri più importanti sono:
{
«sporta inizialo <porta finale>\n" 3B
exit
AF INET ed AF INET6, i quali indicano
)
40
p slart=atoi(argv(2|);
41
p end=atoi(argv[3]);
42
if
rispettivamente quando si vuole lavorare in
(
I
p start > p end
IPv4 e quando in IPv6;
>
)
||
(
p start
> 65535
printf("porta iniziale > porta tinaie o
44
exit(-l);
porta maggiore di
}
46
scan connecti
*
)
(p,end > 6553S))
-13
45
sin port: identifica la porta alla quale ci si vuole connettere;
||
48
sin family: specifica la famiglia del protocollo utilizzato. I valori maggiormente usati sono
);
1-1);
39
47
>
"scanport «indirizzo numerico dell'host>
6S535\n"|;
sin addr: identifica l'indirizzo IP al quale ci si vuole connettere.
{
Nel caso in cui sia d'interesse utilizzare la famiglia
di protocollo IPv6 il tipo di struttura utilizzata dovrò essere sockaddr sin6 con i membri sin6 family, argv[l],
p start,
p end
);
sin6 port e sin6 addr al posto di quelli poc'anzi visti.
return
}
Righe 12, 13 e 30: alla variabile intera port viene assegnato il contenuto di p
E' possibile effettuare il download del sorgente all'indirizzo http://wwrf.Unuxpratico.eom/sviLuppo/c/
start, quindi viene
avviato un ciclo while finché port non diventa
minore o uguale a p end. Questo assicurerà che ogni porta compresa e inclusa tra p
start e pend
—
INFORMAZIONI
family: specifica la famiglia del protocollo
utilizzato ed i valori maggiormente usati sono
A cosa servono ì
gli stessi descritti per il membro safamily
vari header inclusi
delia struttura sockaddrin;
type: specifica il tipo di socket che deve essere creato;
<arpa/inet.h>
Contiene le funzioni che permettono la conversione di un
protocol: specifica il tipo di protocollo che si
indirizzo IP decimale puntato in network byte order e vicever
vuole utilizzare; questo valore viene
sa (da network byte order a host byte order). Include al suo
solitamente settato a zero tranne per i socket
interno anche netinet/ìrt.h in cui è dichiarato ti formato
di tipo SOCK RAW.
della struttura sockaddr in e le funzioni che permettono la
conversione di valori short e long da host byte order a network byte order, come accade per esempio con le porte
attraverso ntohs( ) e htons{ ).
La funzione socket () ritorna
un descrittore non
negativo se non vi sono stati errori nella fase di
creazione o viceversa "-1". Un descrittore può
<sys/socket.h> Include tutte le funzioni che operano sui socket come ad esempio le chiamate connectf ), socket ( ), bind( ), send( ),
essere banalmente spiegato come un "valore" che identifica univocamente un socket tra tutti gli altri. Nella riga 14 questo valore viene immagazzinato
recvf), listenl), shutdown(),etc.
nella variabile intera s. Successivamente si control
<strings.h>
Include tutte le funzioni che operano sulle stringhe come ad
la che tale variabile contenga un valore diverso da
esempio memsetf ), memcpyt ), memcmpf ), memmove( ):
-1 per accertarsi del fatto che non vi siano stati
strncpyl ), strncmpO. bcopyt), etc.
errori durante la fase di creazione del socket. Se non è così il controllo passa alla riga 28 che
<stdio.h> Include tutte le costanti e le funzioni che permettono le ope
stampa a video la stringa "socketO" seguita dall'er
razioni di input ed output del sistema. Qui sono contenuti i
rore che ha causato tale comportamento; dì questo
prototipi di funzione di chiamate come printf (), fprintf ( ),
si occupa perrorl ).
vsprintf ( ), etc. <unistd.h>
Dalla riga 16 alla riga 23: se il socket è stato
Include strutture e funzioni che operano su permessi, gruppi
creato con successo si procede all'azzeramento
e utenti nel sistema. Sono definite anche funzioni che opera
della struttura sa (riga 16). Ciò è dovuto al fatto che
no sui descrittori come cLosel ), read( ), write( ). etc.
durante ogni iterazione del ciclo while le informa
<sys/types.h>
zioni di questa struttura di tipo sockaddr
Include strutture e tipi di variabili che possono essere utiliz
biano (ed in particolare la porta a cui ci si vuole
zati in fase di programmazione come u char, u short, etc.
in cam
connettere), pertanto si ha la necessità di azzerarla in modo da evitare che essa possa contenere infor mazioni sbagliate. La funzione memsetf)
INFORMAZIONI
in questo
caso si occupa di inserire degli "0" all'interno della struttura sa per tutta la sua lunghezza, ottenuta con la
Tipi di socket
chiamata alla funzione sizeof (sa). Successivamente il membro sinfamìly viene set
Le costanti maggiormente usate sono:
tato ad AF
> SOCK_STREAM: per intraprendere connessioni che si
INET (protocollo IPv4) e sin
port alla
variabile intera port. Nella riga 18 si noti l'utilizzo della funzione htons(). Essa si occupa di immagaz
basano sul protocollo TCP;
- SOCKDGRAM: per intraprendere connessioni che si basano sul protocollo UDP (non coperte in questo articolo); > SOCK_RAW e SOCKPACKET: per creare dei socket che
zinare nel modo corretto il valore contenuto da port all'interno di sin port.
Ma che significa "corretto"? Esistono 2 modi per
permettano l'alterazione manuale dei singoli campi
immagazzinare dei dati in memoria ed il loro ordi
contenuti negli header dei protocolli interessati (non
namento viene definito "byte order". Essi sono il
coperti da queste pagine).
little-endian ed il big-endian.
La differenza tra i 2 consiste nel modo in cui i byte vengono immagazzinati in memoria (da destra verso sinistra nel primo caso e da sinistra verso verrà contattata presso ia destinazione. Alla fine del
destra nel secondo).
corpo whìle (ed esattamente alla riga 30) la varia
Il byte order utilizzato da un sistema viene solita
bile port viene aumentata di volta in volta di 1. Per
mente definito "host byte order" e può essere sia
ogni iterazione del ciclo vengono intraprese le ope
little-endian che big-endian.
razioni incluse tra la riga 14 e la riga 31.
Certi dati che vengono trasmessi in rete (come gli
Righe 14, 15 e 28: nella programmazione mirata
vertiti in un ordinamento differente di byte chiama
ai networking prima di eseguire quaisiasi operazio
to "network byte order" che fa uso esclusivo del
ne in rete bisogna creare un socket attraverso il
big-endian. Pertanto la funzione htons{) si occupa
quale connettersi al servizio interessato. Ciò è quel
di convenire il valore contenuto in port da host
lo che fa la funzione socket(). Il suo prototipo è
byte order a network byte order. Il nome della fun
int
zione stessa dice ciò: h sta per host, to dall'inglese
indirizzi IP e le porte) hanno bisogno di essere con-
socketfint
family, int
type, int
protocol)
da cui si evince che essa accetta 3 parametri:
tradotto significa "a", n sta per network ed s per
(§0
short (quindi: converti da "host" "a" "network" byte
desiderato. Questo avviene tramite la chiamata alla
order questo valore "short").
funzione connect( ).
Dato che le porte sono valori di 16 bit ed uno short
Nel momento in cui essa viene invocata si da il via
assume la dimensione (nei sistemi x86) di 2 byte,
di fatto al 3way handshake (il famoso scambio di
questa funzione fa proprio al caso nostro.
pacchetti precedente alla connessione vera e pro
Nella riga 19 viene invece utilizzata la funzione
pria) che fa muovere un socket dallo stato CLOSED a
inet_pton(). Essa converte un indirizzo IP in for
SYNSENT fino ad arrivare in ESTABLISHED. Il suo
mato ASCII (come quello contenuto nella variabile
prototipo di funzione è int
host che altri non sarebbe argv[l] passato dal
const
main() alla procedura scan connectO) in network
addrlen) da cui si evince che il primo parametro
struet
connect(int
sockaddr *servaddr,
sockfd,
socklent
byte order ed il risultato viene immagazzinato nel
passato deve essere il socket attraverso il quale si
membro sin
vuole avviare la comunicazione (nel nostro caso s)
addr della struttura sa. inet pton()
ritorna il valore "1" se la conversione va a buon
ed in seguito i dati contenuti all'interno della strut
fine, "0" se la stringa ASCII non è in un formato vali
tura sa fino alla sua dimensione totale (al solito
do. "-1" se vi è un errore.
ottenuta con una chiamata a sizeof (sa)).
Nella variabile intera check viene infatti immagazzi
La funzione in questione ritorna "0" se la connessio
nato il valore di ritorno della funzione in questione
ne è andata a buon fine o "-1" su errore, pertanto
per assicurarsi che esso non sia minore o uguale a
senza controllare a basso livello i singoli pacchetti
0. Ciò viene fatto per evitare che l'utente (sbadata
inviati/ricevuti attraverso il socket creato, si ha la
mente o intenzionalmente) non digiti da linea di
possibilità di comprendere se una porta è aperta o
comando un nome di host o un indirizzo IP conte
meno semplicemente se il 3way handshake va a
nenti valori alfanumerici oppure valori numerici
buon fine e quindi se la funzione connect ( ) ritorna il
errati. In tal caso il programma verrebbe interrotto
valore "0". La variabile intera connection ha pro
tramite la chiamata ad exit() con a video un mes
prio il compito di immagazzinare tale valore. Infine essa viene comparata a "0" e se il risultato
saggio d'avvertimento.
coincide viene scritto a video (tramite la printfO) Dalla riga 24 alla riga 26: dopo aver creato un
"Connessione accettata sulla porta TCP x".
socket e riempito una struttura di tipo sockaddr in
non rimane che effettuare la connessione con l'host
Riga 29: la funzione closet) si occupa della chiu sura del descrittore "s" il che equivale principal mente a:
APPROFONDIRE
Risorse online
■
liberare dalla memoria i riferimenti ad esso in
modo da poterlo rendere riutilizzabile
In Internet si possono trovare molte risorse interessanti:
attraverso altre chiamate a socket ( ) ; >
»
Unix Socket Programming FAQ (in tnglese)
http://www.developerweb.net/sock-faq/ >
>
connessione.
Spencefs Socket Site (in inglese) http://www.towtek.com/sockets/
Per il nostro scopo la funzione close{) fa proprio
Un capitolo del libro Gapil (in italiano)
ciò di cui avremo bisogno al punto 2, ma in applica
http://WBW.Ulik.it/-mirko/gapil
zioni che necessitano di una certa sicurezza nello
/gapilchl5.html#gapitse45.htmt
>
chiudere (anche se bruscamente) la
scambio dei dati e di minor superficialità, sarebbe
Articoli sui numeri 2 e 3 di BFt (in italiano)
opportuno utilizzare la funzione shutdownO per
http://WrtW.sOftpj.org/it/site.html
chiudere correttamente una connessione.
Ritorna al maino Riga 47: a questo punto la nostra funzione tf
gec
-o
ti
./scan
\
scan 3can.c
192 . 169.0.25-)
1
102-1
Connessione
accettata
sulla
poeta
TCP
23
Connessione
accettata
sul la
poeta
TCP
ao
Connessione
accettata
sulla
porta
TCP
2BQ
Connessione
accettata
sulla
porta
TCP
515
Connessione
accettata
sul la
porta
TCP
631
n
termina. Non avendo altro fa fare
ecco il perché della presenza di return 0.
Compilazione e Testing II codice appena descritto può essere compilato da shell utilizzando il compilatori gec in questo modo:
n tt
scan_connect ( )
anche l'esecuzione del nostro programma termina:
./scan
192 .163.0.253
1
10 24
sulla
porta
TCP
21
Connessione
accettata
sulla
porta
TCP
23
Connessione
accettata
sulla
porta
TCP
60
Connessione
accettata
sul la
porta
TCP
SII
Connessione
accettata
sulla
porta
TCP
515
Connessione
accettata
sulla
porta
TCP
531
$
la sintassi è semplicissima
scan.e
-o
scan
ed eseguito in questo modo:
S
Una sessione di utilizzo del port scanner:
gec
./scan
192.168.G.45
1
1924
In figura 1 è possibile osservare una sessione di lavoro: come si può verificare, l'uso del port scanner è davvero semplice.
~
Programmare una chat in linguaggio C Per approfondire la conoscenza della programmazione di rete in C
sotto Liniix, vediamo come creare un piccolo server che ci permetta di chatiare con gli amici!
5imone Contini <s.contini@linuxpratico.com>
Lorenzo Mancini <l.mancini@linuxpratico.com>
Procediamo nei nostri esperimenti sui socket,
questa volta realizzando una piccola chat con architettura ciient/server che ci consen
ta di scambiare messaggi tra i client collegati allo
Al termine della realizzazione avremo un sistema di
stesso server. La realizzazione, composta da due
comunicazione funzionante che sarà possibile
piccoli sorgenti, ci permetterò di approfondire come
estendere a piacere, ad esempio aggiungendo il
è possibile accettare connessioni, capire come
supporto per i nickname.
porre un'applicazione in ascolto su una porta, invia
Il server
re e ricevere informazioni.
Tutta la parte di codice compresa tra l'inizio del pro
l'obiettivo
gramma e il ciclo while si occupa delle inizializzazio-
II problema dello scambio di dati tra un numero indefi
ni necessarie a preparare una porta in ascolto. Il
nito di host può essere risolto in maniera elegante
significato delle variabili dichiarate sarà chiarito al
ricorrendo all'architettura ciient/server. L'idea è quella
momento in cui saranno utilizzate. L'utilità degli
di spostare buona parte delle responsabilità sul server,
header file aggiunti rispetto a quelli visti per lo
che dovrà occuparsi di smistare e propagare le infor
scanner di porte è specificata nell'apposito riquadro
mazioni ai client ad esso connessi. Questi ultimi
di informazioni. Passiamo quindi all'analisi del codice.
dovranno invece gestire la sola connessione al server,
curandosi di inviare e ricevere i dati da esso. Nel nostro
Righe 19-22: Per prima cosa prepariamo il socket
caso specifico scriveremo innanzitutto una piccola
di tipo SOCK STREAH che metteremo in ascolto per le
applicazione i cui compiti fondamentali sono;
connessioni TCP da parte dei ciient. Ovviamente in caso
di fallimento non ha senso continuare l'esecuzione. > porsi in ascolto su una porta per accettare eventuali richieste di connessione;
Righe 24-27: A questo punto, sulla traccia di quan
> verificare la presenza di pacchetti in arrivo dai
to già visto, prepariamo la struttura myaddr di tipo
client ed acquisirne il contenuto;
sockaddr in; l'unica nota da sottolineare è che il
> inviare ad ogni client, escluso quelli di
campo sinaddr.s addr questa volta sarà posto a
provenienza, i dati ricevuti.
INADDR ANY in modo da indicare che si vogliono accettare connessioni usando qualsiasi 1P associato
L'applicazione così scritta permetterà di scambiare
all'host sul quale sarà messo in esecuzione il server.
messaggi tra host utilizzando telnet sulla porta del
Per capire meglio supponiamo di utilizzare la
nostro server. Per completare però la panoramica
costante INADDR LOOPBACK, in questo caso il server
sull'argomento, affiancheremo alla prima applica
accetterebbe solo connessioni attraverso l'inter
zione proposta un client in grado di:
faccia di loopback, da ciient in esecuzione sul medesimo host.
> stabilire una connessione con il server sulla porta
specificata;
Righe 29-32: La struttura appena creata e l'identì-
> verificare l'eventuale presenza di pacchetti in
ficativo del socket forniscono tutte le informazioni
arrivo dal server e stamparli a video;
necessarie alla funzione bind ( ). Con questa sì asse
> verificare l'eventuale presenza di dati sullo
gnano al socket gli indirizzi e la porta che il server uti
standard input e inviarli al server.
lizzerà per determinare la presenza di richieste di connessione. Righe 34-37: II completamento della fase di prepa
Attenti al firewall
razione si ottiene con la chiamata alla funzione
Ricordatevi che per consentire il traffico tra client e server è necessario che la porta 5091 non sui bloccata dal firewall. In tal caso potete impartire sul PC con il server in esecuzione: *
iptables
-I
INPUT 1
-p
tep
--dport
5891
-j
ACCEPT
J
listen{ ) che ìndica l'uso che si vuole fare del socket specificato, ovvero l'intercettazione di richieste dì connessione, ponendolo nello stato di LISTEN. Il
secondo parametro, backlog, da noi imposto a 5, è la dimensione massima della coda di connessioni
~ rilevate, ma in attesa di essere accettate; nel caso
SORGENTE
si giunga alla saturazione della coda ogni altro ten
Codice completo server
tativo di connessione avrà esito negativo. Questo limite non deve essere confuso con il numero mas
'include
simo di connessioni totali che il server può gestire!
«sseic.h*
«include <stdlib.h»
Le connessioni pendenti infatti vengono rimosse
«include tunisid.H» •include -citrini. h>
dalla coda al momento in cui sono confermate
attraverso la chiamata alla funzione acceptl), «include <area/inet.h>
restituendo così spazio per nuove connessioni in attesa di conferma. Siamo quasi nel cuore dell'ap
«rteflne PORT 5691
plicazione, dove inizia il ciclo che si occupa di stabi
Meflne BUfrW.SIZE 1B24
lire se giungono al server richieste di connessione o
ini
dati dai client, per poi agire di conseguenza.
l
Dovremo quindi monitorare i descrittori relativi ad ogni connessione che verrà stabilita, più quello rela
Minii
fd.set
fds.
Int
(8,
»,
(truci
rfds; fdc.
fd™;
tuH.idilr
in myaddr.
romoteaddr;
cnar Bui[BUFFER 5IZE];
tivo al socket in ascolto- Come vedremo tra poco ci affideremo alla funzione selectO, che per il suo
lf
[li • SOCket|PF_INET.
SOCK STREAH.
BM
= -11
{
perrorCsocketl )") i
funzionamento ha bisogno di almeno una struttura
entr-lli
di tipo fd set correttamente inizializzata; quest'ul
I:
tima ha il compito di rappresentare un insieme di
■yaadr.sln^Iarally
file descrìptor (descrittori). Per manipolarla abbia
~
* AF_INET;
Bya<tdr.sin_addr.s_addr = htonlllNADDR^ANY] ; «iyaddr.sin_port • NtonMPORT);
mo a disposizione 4 macro: FD ZERO(), FD SET(),
Miitet l&lmyMdr.sliwero).
0,
si!CQff«y«dar, sincera) );
FDCLRf), FD ISSETO. i(
[Dindi*.
Istruct
sockaddr •) inyaddr,
ilieot(nysdor)|
••
oerror("bintì(!");
Righe 39-41: Utilizziamo \'fd_set fds per tenere
e.Hl ]J;
traccia dei descrittori che vogliamo monitorare. quindi per prima cosa ne azzeriamo il contenuto con
FDZEROU
e aggiungiamo ad essa, con
FD_SET(), il primo file descriptor da tenere sotto controllo: s ovvero quello relativo al socket posto in ascolto. In seguito ci occuperemo di mantenere aggiornato fds con i descrittori relativi alle connes sioni attive. Vedremo tra poco che abbiamo bisogno di sapere in ogni momento qual è il descrittore
numericamente più alto, adesso che abbiamo un
M
lf
ItlltenlJ.
porr«r(-Usten<)"):
36
•xltM);
37
)
19
fd_:eroi(,kj-.i:
te
fd.seus, Udì):
li
fdn
•
i;
(2 13 14
ntille 11) rfds
lf
-
1 fds;
ISflfttlfdil » 1, irfdS. NULL.
NULL.
NULL)
perrorCieleell)*); ««Itili;
descriptor max) al valore di s.
di fds, ovvero l'insieme di descrittori per i quali
(
ìa
solo file descriptor possiamo impostare fdm (file
Righe 44-49: Inizializziamo rfds con il contenuto
5) -. -1)
35
for
Ifd ■ 6; lf
fd <=
ffl»;
IFD.ISSETIfd. lf
Ifd
»«fdl
Srfdsl)
— j)
|
(
{ adtìri:
vogliamo verificare la presenza di dati in input, e
chiamiamo selectO. Questa, di norma, mantiene
ìf
IIfdc acceotls,
sospesa l'esecuzione del processo fino a quando si verificano eventi per almeno uno dei file descrìptor
if
fd set, il primo parametro indica quanti sono quelli passato a select saranno i file descriptor set da monitorare: il primo di essi sarà controllato per veri ficare la presenza di socket pronti ad essere letti, i!
)
soli primi due parametri: il primo, posto a fdm
+
1,
indica a select () il descrittore con valore più alto
fcremoteaddr,
(Oc; FD Sd\n".
inet_ntoalre«oteidd''.sin_»der), Me}:
ette
perror("acctpt(1*1j 1 «Ise ( lnt
if
e ■
reculftì,
(le > 8) for
U.
uuf,
('bui
Ifdc - 9; lf
quelli dai quali giungono condizioni eccezionali. Nel complicare inutilmente l'esempio, facciamo uso dei
"I
ifdsl;
oriniirccnnessione da Vs.
secondo per quelli pronti alla scrittura e il terzo per nostro caso, sia per le limitate necessità che per non
sochaddr
-11 <
(fdc > fdm)
fdn *
stato tramite l'ultimo parametro. Dato che il con
da controllare. Il secondo, terzo e quarto parametro
I-
FD_SETMdt.
set che sta monitorando, o scade il timeout impo trollo viene fatto sui primi n descrittori di ogni
(struct
SadOrlenH
slzecflBut),
!■ EQFI)
fdc « fdm:
«ftìcl
(FD_ISSET|fdc, 6ffls) U U
Ifdc
lf
liprdlfdc,
81;
{ Ifdc
'■
fn|
'■ ■)) bui,
e,
81
< BJ
Sd\n",
fdJ:
perrof(-iendl)");
) elM { if
[< < 81
perrar(*rMv()")i prmtfCscoLlegaiTwnTo 01 closelfd); FD_CLR(fO,
ifdsl;
contenuto nell'insieme di file descriptor da tenere sotto controllo, maggiorato di 1 tenendo conto che si parte a contare da 0; il secondo deve contenere l'indi rizzo di memoria dell'insieme di descrittori dai quali ci
aspettiamo dati in ingresso, quindi &rfds. selectO infatti modifica l'insieme di descrittori passato per
~
s_
INFORMAZIONI
* Eli header inclusi, i file descriptnr, select
parametro, per segnalare chi ha generato un evento; per questo motivo non abbiamo passato a tale fun zione fds, ma una sua copia, in modo da non per
dere l'informazione relativa ai descrittori da control lare durante le successive chiamata a select(). 1
<stdlib.h> Incluso per utilizzare la funzione exit ( ). definisce anche (unzioni
parametri relativi alle condizioni che non si deside
di conversione tra variabili numeriche e stringhe, per la generazio
rano controllare devono essere a NULL, cosa che noi
ne di numeri pseudo-casuali, di allocazione della memoria, ecc.
facciamo per gli ultimi tre.
<sys/select,h > Definisce select ( ), pselecto e la definizione delle macro FD CLR(), FD.ISSETO, FD 5ET(). FD ZÉRO().
<netdb.h> Definisce le funzioni gethostbyname(). gethostbyaddrf) e in generale le funzioni che manipolano il nome degli host.
File
Righe 51-52: Se ia select termina, siamo certi che si sta venficando una connessione, una disconnessione o una ricezione di dati, Individuiamo su quali file descriptor si è effettivamente verificato l'evento, con trollando con FD ISSETi ) quali di questi sono stati atti
descriptor
E' un numero intero positivo che viene fornito e usato balle funzio
vati da select ( ). Nel nostro caso particolare possiamo
ni di I/O per fare riferimento ad un canale di comunicazione.
subito distinguere due diverse azioni da intraprendere.
fd_set (File Descriptor Set) Questa struttura rappresenta un insieme di file descriptor ed è soli
tamente implementata come un array di bit, ciascuno dei quali è
Righe 53-58: Se il file cfescriptor è relativo al socket
destinato a rappresentarne uno. Attivando o disattivando i bit con
che abbiamo posto in LISTEN significa che c'è in
te macro presentate nell'articolo, si indica quali file descriptor
coda almeno una connessione da accettare. Per
appartengono all'insieme.
farlo si usa la funzione acceptO che restituisce un
select') Permette il cosiddetto I/O multiplexing controllando la possibilità di
operare su un gruppo di file descriptor aperti. Il primo parametro è
descrittore per il socket relativo alla prima richiesta
di connessione in coda; con questo sarà possibile
il numero di descrittori da manipolare (partendo da 0). I tre succes
comunicare con il nuovo client. Il parametro addrlen,
sivi sono gli insiemi di descrittori per i quali verifìcare rispettiva
che inizialmente deve contenere la dimensione
mente: la presenza di dati da leggere, la possibilità di scrivere dati, la presenza di eventi particolari. L'ultimo parametro è una struttu
ra di tipo timeval che indica ti tempo massimo di attesa della select. che sarà indeterminato nel caso il parametro sia NULL.
della struttura remoteaddr di tipo sockaddr
in,
verrà modificato dalla funzione con la dimensione in byte dell'indirizzo restituito; per questo motivo ia variabile deve essere passata tramite il suo indiriz zo usando &.
Righe 59-63: Questo non è sufficiente per i nostri
/■
file
Modifica
uralica
(ilionetioroana code]I ttmneiJlone da
Terminale
Tabi
scopi; il file descriptor ottenuto deve infatti essere
Aiuto
aggiunto alla lista di quelli da monitorare a partire
/server
117.0 0
1.
FD *
io«ne:ilone da 127 0 0 1.
FB 5
dalla prossima chiamata di select O, in modo che sia possibile elaborare l'input proveniente dal nuovo client. Per farlo impostiamo opportunamente fds
file
Modifica
I-!».-i.».>• •
VI 'M alca
i j ■■..!
Temi ma ie
'-|1
TaM
con la macro FD_SET() fornendo il valore del nuovo
Almo
/client localhoit
5091
ciao1
file descriptor. Ovviamente se necessario modifi cheremo frinì in modo tale che contenga sempre il
valore corrispondente al file descriptor con identifi-
f File
Modica
Vtuuleza
Témunale
[-,i»!>n-§»iirijàn» coOfll
Tabi
cativo maggiore.
Aiuto
/client localhoit tesi
ciao1
Righe 66-67: Altrimenti ci troviamo nel caso in cui
uno dei client attualmente collegati sta inviando un pacchetto. In questo caso l'operazione che dobbiamo tn alto il terminale con il server in
effettuare è leggermente più complicata. Per prima
esecuzione (A), in basso i due client
cosa chiamiamo recvt) per trasferire su un buffer
su altrettanti terminali (B, C)
quello che t'host remoto sta inviando. Ovviamente
l'area di memoria in cui saranno copiati i dati è già INFORMAZIONI
U
Compilazione e oso _
I sorgenti sono scaricabili da: http://w-w.linuxpratico.eom/sviluppo/c/
Per la compilazione non è necessario far altro cho: S
gec
chat-server.c
S gec chat-client.c
-o
server
stata allocata, ma niente paura: nel caso essa non sia di dimensioni sufficienti ad accogliere tutti i dati, quelli in eccesso verranno tenuti in attesa in modo che sia possibile prelevarli con la lettura successi va, finché non saranno ottenute tutte le informazio ni inviate dall'host remoto. Analogamente è possibi le che non tutto il buffer venga riempito. La
quantità di dati ottenuta mediante recv( ) è riporta ta nel valore di ritorno di tale funzione.
-o client
A Questo punto lanciate il server con . /server, quindi avvia te alcuni client da diversi terminali con ./client <host>
Righe 69-72: E1 il momento di inviare i dati ricevu ti a tutti gli altri client. in modo che la comunicazio
<porta> dove host è il nome della macchina sulla quale avete
ne abbia effettivamente luogo. Infatti è compito del
avviato il server e porta la porta in ascolto ovvero 5091.
server fare in modo che ogni messaggio inviato da
~ II client
un host venga ricevuto da tutti gli altri. Per ogni file
descriptor memorizzato in fds. diverso da quello
Per completezza, in modo da esaminare alcune fun
mediante il quale riceviamo connessioni e ovviamen
zioni interessanti non utilizzate nella scrittura dei
te da quello da cui abbiamo appena ricevuto i dati,
server e dello scanner di porte, analizziamo le parti
inviamo quanto appena memorizzato con recv ( ).
più significative del client da noi realizzato.
Righe 73-74: La funzione complementare di
Riga 26: La prima differenza che salta all'occhio
recvf) è send(). L'uso è il medesimo con l'ovvia
rispetto at sorgente dello scanner di porte, che
differenza che quest'ultima permette l'invio di un
anch'esso aveva il compito di stabilire una connes
buffer di dati attraverso il socket specificato. Il valo
sione, è la presenza di gethostbyname(} per la riso
re di ritorno conterrà, in caso di successo, il numero
luzione del nome dell'host specificato nel suo corri
di caratteri inviati, altrimenti -1.
spondente IR II risultato viene memorizzato in una
struttura di tipo hostcnt così definita: Righe 75-81: Nel caso recv( ) fallisca, restituisca 0
struet
hostent
{
byte o nel caso in cui il primo valore contenuto nel
char
• n naso;
/•
oftidal nane ol
buffer sia EOF, interrompiamo la comunicazione: lo
char
• •h aliases;
/•
alias list
int
h
addrtype;
/*
host adflress
ìnt
h
length;
/*
length
char
• •h
facciamo semplicemente chiudendo con close() il file descriptor che stiamo processando. A questo punto, con la macro FD CLR(), eliminiamo il riferi
«ilpfine h addr
mento al descrittore dall'insieme fds in modo che
addr
h .lOdr
of
host V
*/ type
V
address
■/
1* list of addresses •/
list;
llst[G] (or
backward
compalibility
select() e ia parte di inoltro dei pacchetti verso i
V
client, non tengano più conto dell'esistenza dell'host
h aliases e h addr list sono due array terminati
che abbiamo appena disconnesso. Come client è
con 9 che contengono rispettivamente i nomi e gli
possibile utilizzare telnet specificando la porta sulla
IP alternativi per l'host specificato. Nel nostro caso
quale è in ascolto il server. Come abbiamo visto
prenderemo semplicemente il primo IP deila lista:
~
infatti la chat da noi scritta permette lo scambio di rayaddr.sin afldr.s
testo tra client senza basarsi su un protocollo defi
addr
■
((Struet
in
i,
nito per incapsulare testo e comandi.
•(
atJdr
■)
addr Usti
0
))->s addr;
Righe 42-47: Stabiliamo la connessione in modo
del tutto analogo a quanto fatto per lo scanner TCP.
SORGENTE
In caso di insuccesso terminiamo l'applicazione.
II sorgente del client
Righe 50-57: Per gestire l'input da tastiera e per
«include «netitb.to 11
far fronte all'arrivo di pacchetti da parte del server
tàetint BUFFER,S1ZE
ricorriamo anche questa volta a select( ) per tene
1824
1 26
If
ìf
(!(h
- gethoslbyn»
re sotto controllo l'eventuale presenza di dati in arrivo
HI))) {
dal server tramite il socket, o dallo standard input.
(Uonncction ■ connecUs.
fstruct
sockadd
ùtyafldr,
!»rror<-connectl)"): clsstd);
FD_SET|STuIN,
che venga visualizzato, quindi trattiamo il contenuto
iridi);
(mlcctli
•
del buffer appena ricevuto come una stringa da stam
1, irlds,
NULL.
NULL. NULL)
mente terminarlo con \0. Aggiriamo il problema
ISSET(j,
e • reevli, bui [e]
63
H
64
■
It <• 8) il
65 67
e>
68
i ci»
-
1.
'\8\
il
Righe 63-69: Verificando il valore di e gestiamo i casi di disconnessione o di errore di lettura, in caso di esito positivo invece stampiamo i dati ricevuti
rciiiruniN.
con printf().
Bui):
Srids» { bui.
SWPOflbui));
Righe 72-82: Se invece sono in arrivo dallo standard
bui.
e,
e funzionamento sono del tutto analoghi a recv().
74 75
76
il
(e > 0) 11
78
input, procediamo alla lettura con read{) il cui uso
(
Isendls.
77
79
B)
■: 0)
perror("sendtl'l;
) •Ih ( il
(e
caratteri e ponendo in coda ad essi i caratteri \0
aiutati dal valore di ritorno e di recv( ).
mi);
(FD_1SSET(STOIN, e
01:
<
printl("> \i *.
69
73
riempiendo al massimo solo i primi sizeof (buf )-1
{
sizeol(but)
ptrror fretv D'I; ci gufili
72
iridili
bui.
U < 81
66
pare. Per farlo dobbiamo tenere presente che recv( )
restituisce un buffer e la sua lunghezza, senza ovvia
oill-ll;
|FD
Righe 59-61: Nel primo caso effettuiamo la lettura
client e l'altro è esattamente quello che vogliamo
irlds);
perrnr|-seleei(ri;
11
(
abbiamo deciso che quello che viene inviato tra un
FD.ZEROIir(ds);
Lf
!> S)
di questi con recv{). Come vincolo di progetto
e«U-11;
FD.SETd,
sizeaffnyaòdr)))
se
perforfroadD'l;
31
cloieli);
82
e«il(ll:
Se la lettura ha esito positivo inviamo il buffer appena letto al server tramite sendt ): la quantità di
< 91
byte da inviare è pari la valore restituito da read{). Il server ricevendo tali dati si occuperà di propagarli agli altri client, come abbiamo già visto.
~
Gestire l'output testuale con ncurses Vediamo come sia possibile sfruttare le funzioni avanzate degli emula toti di terminale tramile tuia comoda API.
Simone Contini <s.contini@linuxpratico.com> e Lorenzo Mancini <l.manciniiaiiniixpratica.coin=
uando sentono parlare di output testuale, probabilmente molti di voi pensano alle schermate di testo mostrate da una utility a
riga dT comando, magari richiamata con qualche
tessere del gioco.
flag --verbose di troppo. Tuttavia tale concetto è
ben più vasto: non tutti sanno che gli emulatori di
Righe 7-8: la tavola di gioco è rappresentata tra
terminale che usiamo tutti i giorni, i discendenti dei
mite l'array bidìmensionale board: ogni elemento di
vecchi display teietype, dispongono di una serie di
questo conterrà il numero della tessero alla posizio
comandi particolari per applicare attributi di vario
ne corrispondente. Definiamo anche px e py che
tipo ai caratteri, produrre del testo colorato, posizio
terranno traccia della posizione della casella vuota,
nare l'output a piacimento del programmatore, ad
in modo da semplificare le operazioni che vedremo
esempio per creare interfacce utente pseudografi
più avanti.
che ed interattive, ed altro ancora. Le applicazioni possono sfruttare tale funzionalità
Funzioni di gioca
inviando in successione al terminale dei particolari
Righe 9-18: la funzione initboard inizializza i
caratteri, detti sequenze di escape. L'inconveniente
valori deli'array board, posizionando le varie tesse
di questo sistema risiede nella differente quantità e
re sulla tavola nella configurazione ordinata, che in
codifica dei comandi disponibili per ogni singolo
seguito rappresenterà l'obiettivo da raggiungere.
tipo di terminale. Per questo motivo si ricorre solita
L'operazione effettuata consiste semplicemente
mente a ncurses, una libreria che permette di
nello scorrere con due cicli for nidificati i vari ele
superare questi problemi fino ad arrivare a creare
menti, impostando ordinatamente ciascuno di essi
interfacce complesse composte da menu, finestre
ad un valore tra 1 e 15. Al termine dei cicli resta da
ed altri elementi tipici delle GUl.
Obiettivo
impostare l'elemento in basso a destra alla casella vuota: questo viene fatto inizializzando px e py rispettivamente a maxx-1 e maxy-1, e assegnando il
Per prendere la mano con ncurses realizzeremo un
valore 0 (zero) all'elemento corrispondente di
simulatore del rompicapo noto come gioco de! quìndi
board.
ci. Su una tavola di dimensioni 4x4 troviamo quindi ci tessere, ognuna contrassegnata da un numero da
Righe 19-29: la funzione is
1 a 15. ed uno spazio vuoto; in seguito ad un effica
it giocatore è riuscito a riordinare correttamente le
completed verifica se
ce rimescolamento di esse, il compito dell'aspirante
tessere sulla tavola. Analogamente alla funzione
risolutore sarà quello di rimettere le tessere in ordi
appena vista utilizziamo due cicli for nidificati per
ne, potendo effettuare solo gli spostamenti nei
scorrere l'array board. Per ogni elemento memoriz
sensi orizzontale e verticale consentiti dallo spazio
ziamo nella variabile p il valore corrispondente e
vuoto.
verifichiamo che esso sia lo stesso che abbiamo
Le versioni reali del giocattolo in questione presen
impostato durante l'esecuzione di initboard, in
tano spesso due colorazioni diverse su tessere pari
caso contrario usciamo immediatamente dalla fun
e dispari, per facilitare la risoluzione: naturalmente
zione ritornando il valore 0. Se tutti i test sui valori
ne terremo conto nella stesura dell'applicazione.
eseguiti all'interno del ciclo sono eseguiti con suc
Dichiarazioni Righe 1-6: vediamo per prima cosa gli include
cesso, la funzione ritornerà 1. Da notare che il con trollo non viene eseguito per la celia in basso a sini
stra, che nella configurazione risolutiva è vuota.
necessari: oltre al canonico stdlib.h ed a ncurses.h,
.
troviamo anche time.h, che consente l'accesso ad
Righe 30-34: do move si occupa di eseguire sulla
alcune funzioni utili per temporizzare le animazioni
tavola di gioco la mossa identificata dal valore m,
della tavola, come vedremo in seguito. Più avanti
passato per parametro: questo può assumere i
definiamo maxx e maxy, ovvero rispettivamente lar
valori da 0 a 3. A ciascuno di essi facciamo corri
ghezza e altezza della tavola, al valore 4.
spondere un elemento deli'array d, che descrive lo
SHUFFLE HOVES è il numero di mosse casuali che
spostamento relativo per ciascuna mossa. Possiamo
verranno effettuate iniziaimente per mescolare le
quindi utilizzare queste informazioni per trovare la
f
\ m[ II codice di esempio 1
2
«include <time.h>
3
«Include <ncurses.h>
)
58
}
61 63
}
63
void
64
1
shuttle tilesdnl
«ovest
4
«define maxx A
65
int
5
«define maxy 4
66
for
6
«detire SHUFRE HOVES
68
t.tv sec
7
char boardimaxxlliiaxyl;
69
t.tv nsec >
8
int
7G
nanosleepl&t,
9
void
71
wnile(!do_moYe||lnt!
pc,
18
{
11
ini
166
lnlt
boardl)
li
= 0;
struct
67
py;
i; 1
< movcs;
tlnespec -
..1)
(
t;
8; 18668986; 6);
|4.o
■
rand|) x,
for
12
ly » B; for
13
y < maxy;
(x • 9;
px
■
iraxx
-
-t-ty)
x < *a*f.
board[x]|y|
14
15
"x)
board[px]|py)
18
} ini
1
21
int
x,
y;
22
for
(y
• B;
for
y
< maxy;
{x ■ 8;
24
Char p ■
25
lf
((p
26
*-y)
I « «axx;
»+x)
{
board[x)[y];
B;
)
28
return
) int
31
{
refresh()i
75
>
76
int nain()
-
78
ini
soves
•
8;
79
initscrd;
sa
start colori);
81
noechof):
82
curs
B3
keypadfstdscr,
84
ebreak ( ) ;
85
imi boardl ) ;
5et(8); TIUJE);
B6
29
30
draw boardl);
73
!■ y • aaxn * x ti) SS p 1
return
27
72
77
■ 8;
is conpletedO
23
1.8))>1;
1;
17
19
IMND MAX -
_
- y • nBxx ♦ x + 1;
16
26
/
y:
1 ;
do move!ini
m)
32
lnt
33
lnt dx - px • di«i]|e];
d|4]U]
.
((8.-1).
3-1
int
35
tf
36
retjrn 8;
37
boardlpullpyj
38
board(dx]|dyl - 6:
dy « py -
(dx < B
| |
(6,-1),
{-1,6},
M,9»;
d["i|l];
d> >• *a»
■
11
fly < 0
U
dy
>- uiy
1
88
ref restili;
89
getch[);
96
srand{time(NULL));
91
sruifUe tileslSHUFFLE MOVES):
92
NhiU |lis coapletedO)
93
int
C
94
int
res
<
■ getchl);
■
-lj
switch
(e)
board[ax] [dy] ;
95
{
96
case KEY UP:
res
■ do «oveiO);
break;
39
p*
dx;
97
caie KEY DWN:
res
•
do movell);
break;
43
Py ■ dy;
98
case KEY LEFT:
res
•
do nov«U);
break;
99
case KEY RIGHT:
■
41
return
42
)
43
«oia
44
{
1;
10B
>
181
draw boardO;
182
if
res
-
do
move!3);
break:
draw boarfll)
45
int
46
int oy ■
(L1NES
47
int
(COLS
x,
y;
ox
-
183
■ ■
maxy maxi
• *
3) S}
/ 2;
ìnit
patrii,
COLOft WHITE.
COLOR REDI;
49
in 11 pair(2.
COLGA BLACK,
COLOR WHITE):
se
clearUi
51
for
ly ■ 9; (x
y < naxy;
. B;
53
cdar
54
iftpl
p
-
»yl
x < «axx;
+*xl
board[x)(y];
55
atlronlCOLOR PHRIp \ 2 +
56
nvprlntwloy
57
■iiprint»(oy •
5B
nvprlntuloy
59
attrofflCOLOR PAIRIp V 2 * 11):
-
•
y
■
186
«ivpnntH(13,6, ref restiti;
108
}
169
nvarintw(13.
111
refreshl):
112
getchl1;
113
endwini):
114
return 8;
lì).
3,
0«
*
x
•
5.
"
y *
3 • 1.
ox •
x
•
5,
*Wd
y •
3 « 2,
Ox • X • 5,
"
");
',
p);
");
[res — 8)
167
(
l
1)
nvprmtwIlS,
105
ja
for
else If
1S4
/ 2:
{res — ♦•Boves;
115
9,
8,
"Mossa
non
uallda'");
"Mosse effettuate: Vd",
"Gioco
completato
ooves);
In U mosse!",
aoves);
posizione finale che assumerà la casella vuota dopo
Disegnare la tavola:
la mossa, memorizzandola nelle variabili dx e dy.
finalmente ncurses!
Righe 35-42; a questo punto possiamo verificare
Righe 43-47:
che la mossa indicata sia corretta, semplicemente
occupa di disegnare a video la tavola di gioco:
draw board è la funzione che si
controllando che la posizione finale della casella
cominciamo da qui a vedere le chiamate alle funzio
vuota sia ancora all'interno della tavola d! gioco; se
ni della libreria ncurses. È importante ricordare che,
non è così la funzione ritornerà immediatamente
visto il contesto in cui ci muoviamo, lo schermo
con valore 0. Altrimenti si procede all'esecuzione
deve essere considerato diviso uniformemente in
della mossa, scambiando i valori delle due caselle
unità di dimensione pari ad un carattere di testo. La
interessate, aggiornando la posizione della casella
dimensione a video dì ciascuna tessera è stata impostata a 5x3 caratteri, per cui possiamo subito
vuota e infine ritornando 1.
calcolare la posizione dalla quale iniziare a disegna Righe 63-75: abbiamo già accennato alla necessi
re la tavola per ottenerla centrata a schermo. Per
tà di scombinare la tavola di gioco prima di iniziare
farlo usiamo LINES e COLS, due variabili messe a
la risoluzione del puzzle. Di fare questo si occupa la
disposizione da ncurses che contengono le dimen
funzione shuffletil.es, che esegue un numero di
sioni in linee e colonne del terminale, le dimensioni
mosse casuali pari a SHUFFLE MOVES. assicurandosi
della tavola in tessere maxx e maxy e quelle di cia
che ciascuna di esse sia valida. Per mostrare l'evoluzio
scuna tessera in caratteri.
ne della tavola durante questo processo, viene definito un ritardo pari a un centesimo di secondo, applicato
Righe 48-49: nell'ambito dei terminali che suppor
tramite la funzione nanosleep prima dell'esecuzione
tano l'output colorato, ogni carattere ha due attri
di ogni mossa, in modo che la visualizzazione della
buti principali: il colore del testo e quello di sfondo.
scacchiera aggiornata sia visivamente percettibile.
Questi vanno sempre utilizzati in coppia quando si
L'output a video viene aggiornato tramite le funzio
vuole stampare qualcosa a video, per questo ncurses
ni drawboard e ref resh che saranno descritte tra
mette a disposizione la funzione initpair che
poco.
consente dì definire una coppia di colori. Il primo parametro da passare è l'identificativo col quale
INFORMAZIONI
Compiti per casa Oltre alle innumerevoli migliorie che possono venirvi a mente suggeriamo di introdurre un supporto minimo per la riga di
comando, argomento del quale abbiamo già trattato, in modo
faremo riferimento in seguito alla coppia, poi speci
fichiamo rispettivamente il colore del testo e quello di sfondo, scegliendoli tra le alternative elencate nel riquadro nella prossima pagina. Righe 50-54: a questo punto, dopo aver ripulito lo
da rendere possibile specificare la dimensione della tavola di
schermo con clear, effettuiamo due cicli for nidifi
gioco e quindi la quantità di tessere in esso contenute. Si
cati per il disegno delle varie tessere. Per ciascuna
otterrà cosi una prima variabile che determina la difficoltà del
di esse preleviamo il valore relativo: se non si tratta
gioco che il giocatore deve risolvere.
della casella vuota {che ovviamente non necessita
Oltre a questo potreste rendere variabile il numero di mosse necessarie al mescolamento tramite un altro parametro, in modo da rendere più difficile fa risoluzione del rompicapo.
di essere disegnata) possiamo selezionare la coppia di colori con la quale avverrà il rendering; la prima
I più pignoli potrebbero ricordare che non tutti i terminali
che abbiamo definito sarà utilizzata per le tessere
hanno il supporto per i colori... bene, questo è un altro compi
pari, la seconda per le dispari.
to per voi; stabilire se il terminale sul quale sta girando l'ap plicazione è solo monocromatico ed in tal caso fornire una visualizzazione alternativa della tavola di gioco. Buon lavoro!
Riga 55: questa scelta viene attuata mediante la funzione attron, che serve per abilitare attributi da utilizzare per il disegno a video. Nel nostro caso uti
lizzeremo come attributo il risultato della macro
COLOR PAIR, richiamata su una semplice operazione di somma e modulo per capire quale coppia di colo ri associare alla tessera in esame; infatti per ogni riga le tessere dei due colori si alternano. Righe 56-62: possiamo passare al disegno della tessera, questo viene effettuato tramite la funzione
mvprintw. Il suo comportamento è analogo a quello della printf che già tutti conoscete, con la differen za che i primi due parametri rappresentano la
posizione nella quale spostare il cursore prima di scrivere la stringa formattata desiderata. Nel nostro caso effettuiamo tre chiamate, una per ogni riga che compone una tessera a schermo, aggior nando di volta in volta le coordinate alle quali effet Ecco ia tavola di gioco durante una partita...
Riusciremo a riordinare tutte le tessere?
tuare l'output. Nella riga centrale riportiamo il numero della tessera. Infine disattiviamo l'attributo
successiva chiamata a mvprintw stamperemo un
impostato in precedenza, con attroff.
messaggio di introduzione al gioco. Righe 76-80: eccoci arrivati al main(). Per prima
cosa definiamo la variabile moves, che servirà per
Riga 88:
tenere conto del numero di mosse fatto dal giocato
cruciale all'interno di ncurses. Essa ha il compito di
refresh è una funzione di importanza
re. La funzione di ncurses che deve essere chiama
aggiornare lo schermo in maniera da eseguire in un
ta per prima in ordine cronologico è initscr. Essa
colpo solo le molteplici chiamate di disegno. Queste
si occupa di inizializzare le strutture interne della
infatti non scrivono direttamente sul terminale, ma
libreria e di allocare la memoria per il terminale cor
su un buffer in memoria che Io rappresenta. Questo
rente, che da questo momento in poi potrà essere
approccio ha molteplici vantaggi, tra questi la pos
riferito tramite il puntatore globale stdscr; oltre a
sibilità di ottimizzare l'aggiornamento dello scher
questo, su alcune implementazioni ripulisce lo
mo limitandosi alla porzione di caratteri modificati
schermo.
dall'ultima chiamata a refresh.
Immediatamente dopo
chiamiamo
startcolor. che prepara il terminale all'uso dei colore e inizializza le tinte di base riportate nel
Righe 89-94: dopo aver atteso la pressione di un
Riquadro. É possibile anche verificare l'effettivo
tasto e chiamato shuffle_tiles, ad ogni passo del
supporto del colore da parte del terminale in uso
loop principale controlliamo se il giocatore è riuscito
mediante la funzione has
a risolvere il puzzle, condizione che provoca l'uscita
color. gli interessati pos
sono documentarsi sulla relativa manpage.
dal ciclo. La variabile e serve per memorizzare il codice del prossimo tasto premuto per la successi
Righe 81-82: normalmente quando ad un termina
va analisi, e res verrà utilizzata per memorizzare
le vengono mandati eventi relativi alla pressione di
l'esito di un'eventuale chiamata a do move.
tasti, esso riproduce a schermo i caratteri relativi; per la nostra applicazione non vogliamo, però, che
Righe 95-101: arrivati a questo punto del codice,
questo succeda, per cui chiamiamo noecho.
in e è memorizzato un codice di carattere: in base a
Analogamente non vogliamo che sia visualizzato il
questo possiamo chiamare la funzione do move spe
cursore, e con cursset possiamo modificare il
cificando il codice di mossa corrispondente. Fatto
comportamento del terminale riguardo ad esso: il
questo invochiamo draw board per aggiornare di
valore specificato 0 lo rende invisibile, altri valori
conseguenza lo stato del terminale.
possibili sono 1 e 2 che ripristinano la presenza del cursore, rispettivamente impostando la modalità
Righe 102-105: è arrivato il momento di controlla
normale e quella ad alta visibilità.
re l'esito di do move, tramite la variabile res: se la mossa specificata era valida incrementiamo di uno
Righe 83-84: trattandosi di un simulatore di gioco
il valore di moves, altrimenti scriviamo a schermo un
del quindici appare logico collegare i movimenti
avviso per il giocatore distratto.
delle tessere ai tasti con le quattro frecce. Per ren dere disponibili gli eventi generati da questi ultimi
Righe 106-108: non ci resta che scrivere a video il
sul terminale stdscr si usa la funzione keypad, spe
numero delle mosse effettuate fino a questo
cificando il valore TRUE. La seguente chiamata a
momento, e infine invocare refresh in maniera da
ebreak disabilita invece il buffering dell'input; dopo
provocare l'aggiornamento effettivo della videata
la chiamata della funzione, i codici dei tasti saranno
del terminale.
inviati immediatamente al terminale invece che dopo la ricezione di un carattere dì fine linea.
Righe 109-115: una volta usciti dal loop principale sappiamo che il giocatore è riuscito a completare il
Righe 85-87: a questo punto la fase di preparazio
puzzle, quindi scriviamo a video un messaggio di
ne relativa ad ncurses è terminata, possiamo pas
congratulazioni e attendiamo la pressione di un
sare agli aspetti più specifici per la nostra applica
tasto. Segue la chiamata ad endwin: questa funzio
zione.
Richiamiamo
quindi
le
funzioni
già
presentate initboard e drawboard; con la
ne libera la memoria allocata dalla libreria e ripristi na lo stato originale del terminale, motivi per i quali
è importante non dimenticarsela mai! INFORMAZIONI
Compilazione e Usa I sorgenti, come al solito, sono scaricabili da
Colori di default
http://www.linuxpratico.eom/sviluppo/c/
Questi sono gli identificativi dei colori di default messi a disposi
Per la compilazione non è necessario far altro che:
zione da ncurses: COLOR_BLACK, COLOR_RED, COLOR_GREEN. COLOR YELLOW, COLOR BLUE, COLOR_MAGENTA, COLOR CYAN,
$ gec
fifteen.c
^o fifteen
-Incurses
COLORWHITE. Essi possono essere ridefiniti a vostro piacere, per farlo potete usare la funzione
int
mit
colori
short
color short
il funzionamento è stato descritto durante tutto l'ar r
short
g
short
D
);
alla quale dovrete passare: l'identificativo del colore che vole
ticolo, non vi resta che avviare il programma con
$
./fifteen
te modificare, e le tre componenti di rosso, verde e blu, cia scuna espressa in un range 0-1000.
e cercare di risolvere il puzzle. Buon divertimento!