LinuxPratico - Sviuluppo C

Page 1

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 &, &T.t; e >.

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,

*

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!


Turn static files into dynamic content formats.

Create a flipbook
Issuu converts static files into: digital portfolios, online yearbooks, online catalogs, digital photo albums and more. Sign up and create your flipbook.