risorse | idep

idep

Introduzione

Nel suo celebre “Large-Scale C++ Software Design”[2], John Lakos presenta idep, un pacchetto per l'estrazione delle dipendenze di moduli C++. Queste note descrivono come le ho applicate per studiare le dipendenze di un progetto C++ di medie dimensioni.

Compilazione

Dopo aver scaricato i sorgenti da google code[1] (una copia locale è disponibile qui) ed aver predisposto una soluzione in Visual Studio 2008, ho proceduto al build dei tre applicativi adep, cdep e ldep. Per permettere al compilatore di portare a termine con successo il suo compito è stato necessario specificare il tipo di ritorno – int – delle tre funzioni main, assente nella versione originale.

Aggiornamento [5/12/2013]

La compilazione può avvenire utilizzando CMake in versione 2.6 o successiva: dopo aver scompattato i sorgenti e modificato le dichiarazioni main dei tre applicativi, si deve aprire un prompt di comandi di Visual Studio 2008 all'interno della cartella dei sorgenti e immettere i seguenti comandi:

mkdir build
cd build
cmake.exe -G "Visual Studio 9 2008" ..
msbuild.exe idep.sln /t:Build /p:Configuration=Release

Gli eseguibili dei tre applicativi saranno disponibili nella cartella build/Release

Affinché le utility siano compilabili con Visual Studio 2012 è necessario apportare ulteriori modifiche:

Uso

Il progetto da analizzare è composto da una serie di librerie, i cui file di include si trovano nella cartella %PRJ_HOME%\include\lib-name, i sorgenti in %PRJ_HOME%\src\lib\lib-name. Ogni classe C++ è dichiarata in un omonimo file .h, e definita nel relativo .cpp. In alcuni rari casi, classi di supporto possono essere state definite e dichiarate all'interno di un file .cpp.

Le utility del pacchetto idep lavorano proprio su questo presupposto, con l'ulteriore assunto che la prima direttiva di inclusione del file sorgente si riferisca al relativo .h. Il codice del progetto da analizzare non rispetta questo secondo vincolo – in genere il primo #include è quello della libreria di base –, ma a ciò si può rimediare definendo un opportuno file di alias (cfr. applicativo adep).

cdep

cdep è l'applicativo che si occupa di estrarre le dipendenze compile-time. Per far questo analizza i file della libreria e determina, per ognuno di essi, l'elenco dei file necessari alla sua compilazione. Affinché possa risolvere tutte le dipendenze, è necessario fornirgli l'elenco di tutti i file coinvolti, .h e .cpp. Indicando con %LIB_NAME% il nome della libreria da analizzare, il primo passo da compiere è la preparazione della lista dei file della libreria:

dir /s /b %PRJ_HOME%\include\%LIB_NAME%\*.h > %LIB_NAME%-files.txt
dir /s /b %PRJ_HOME%\src\lib\%LIB_NAME%\*.cpp >> %LIB_NAME%-files.txt

Poiché cdep tratta esclusivamente percorsi unix-like, è indispensabile intervenire sul file %LIB_NAME%-files.txt eliminando il riferimento all'unità disco e sostituendo le barre rovesce con quelle normali; un'immediata conseguenza di questa operazione è che cdep deve trovarsi nella stessa unità disco che ospita i sorgenti.

Prima di invocare cdep è conveniente raccogliere in un file i percorsi di inclusione standard; in particolare, se non si ha la necessità di analizzare le dipendenze rispetto alla libreria standard, è sufficiente specificare la sola cartella di inclusione della libreria, più quella di progetto, avendo cura di farlo sempre in forma unix-like:

[file: include.txt]
 %PRJ_HOME%/include
 %PRJ_HOME%/include/%LIB_NAME%

La prima risolve tutte le inclusioni dei file .cpp, ove uso la direttiva di inclusione nella forma #include <lib/class.h>, mentre la seconda risolve le inclusioni dei file .h, ove, per includere file appartenenti alla stessa libreria, uso la forma #include "class.h".

A questo punto si può invocare cdep:

cdep -iinclude.txt -f%LIB_NAME%-files.txt > %LIB_NAME%.cdep

L'esecuzione del comando potrebbe produrre un discreto numero di errori di tipo include directory for file "XXX" not specified., che risultano accettabili se si riferiscono a file esclusi dall'analisi a ragion veduta, come ad esempio i file della libreria standard, STL, liberie di terze parti, ….

Il file %LIB_NAME%.cdep raccoglie, per ogni file elencato in %LIB_NAME%-files.txt, tutti i file .h inclusi, direttamente o indirettamente. Poiché ho usato la convenzione, nelle direttive di inclusione, di usare percorsi unix-like, il file è pronto per essere utilizzato nelle fasi successive. In caso contrario, sarebbe necessario ripetere l'operazione di bonifica dei percorsi già effettuata per il file %LIB_NAME%-files.txt.

adep

La seconda fase del processo di analisi consiste nell'individuazione dei componenti di libreria, e della loro associazione con i file che li implementano. Se avessi rispettato le due indicazioni di Lakos (nome dei file .h e .cpp uguale al nome del componente, prima direttiva di inclusione del file .cpp che include l'omonimo file .h), questo passo sarebbe superfluo. Non rispettando il mio codice la seconda delle due richieste, sono costretto a definire un file di alias che specifichi, per ogni componente, quali sono i file che concorrono alla sua definizione.

La definizione di un alias di un componente (una classe, al livello di granularità attuale), può avvenire in due forme:

[file: %LIB_NAME%.adep]
COMPONENTE path path ...

oppure:

[file: %LIB_NAME%.adep]
COMPONENTE
path
path
...
<<<riga vuota>>>

Se i file .h e .cpp risiedono nella stessa cartella, e si sono rispettate entrambe le indicazioni di Lakos, l'utility adep può essere utilizzata per costruire automaticamente il file di alias; per le condizioni in cui mi trovo, sono costretto a prepararlo manualmente. Trascurando le classi ausiliarie definite nei file .cpp, ho predisposto il file di alias a partire dall'elenco dei file in %PRJ_HOME%\include\%LIB_NAME%, usando il nome del file come nome del componente e specificando di seguito il percorso assoluto del file .h e quello .cpp, se disponibile, comunque privati dell'estensione.

ldep

Il passo finale consiste nell'esaminare le dipendenze a link-time, con l'aiuto dell'utility ldep; poiché essa tende ad aggregare i file per cartelle, è necessario esplicitare la volontà di trattare i singoli file come componenti atomici con l'opzione -U:

ldep -d%LIB_NAME%.cdep -a%LIB_NAME%.adep
 -U%PRJ_HOME%/include/%LIB_NAME% -U%PRJ_HOME%/src/lib/%LIB_NAME%

Il programma fornisce un rapporto dettagliato delle dipendenze esistenti tra i componenti analizzati, e riassume lo stato globale della libreria in una manciata di parametri:

cycles
numero di cicli individuati;
members
numero di componenti coinvolti in almeno una dipendenza circolare;
components
numero di componenti con almeno una dipendenza;
packages
numero di componenti senza dipendenze;
levels
numero di livelli del grafo delle dipendenze;
CCD
somma del numero di dipendenze di tutti i componenti;
ACD
numero medio di dipendenze (CCD/components);
NCCD
numero medio normalizzato di dipendenze (CCD rapportato al CCD di un albero binario bilanciato avente un numero di nodi pari a components).

Più NCCD è prossimo all'unità, più vicina all'ottimo risulta l'organizzazione del software.

Automatismi

Analisi di una libreria

Il seguente script effettua l'analisi della libreria specificata (ricordarsi di impostare la variabile d'ambiente %prjdir% ad un valore adeguato):

[file: idep-lib.cmd]

@echo off
if "%1"=="" goto :usage
set lib=%1

rem define the project directories
set prjdir=
set incprjdir=%prjdir%\include
set inclibdir=%incprjdir%\%lib%
set srclibdir=%prjdir%\src\lib\%lib%

rem names for auxiliary files
set incfile=~%lib%.i
set srcfile=~%lib%.s
set depfile=~%lib%.d
set akafile=~%lib%.a

rem build up the include file
call :emptyfile %incfile%
call :flushasunixpath %incprjdir% %incfile%
call :flushasunixpath %inclibdir% %incfile%

rem build up the source files list
call :emptyfile %srcfile%
for %%i in (%inclibdir%\*.h) do call :flushasunixpath %%i %srcfile%
for %%i in (%srclibdir%\*.cpp) do call :flushasunixpath %%i %srcfile%

rem build up the aliases file
call :emptyfile %akafile%
for %%i in (%inclibdir%\*.h) do call :flushalias %%i %akafile%
for %%i in (%srclibdir%\*.cpp) do call :flushalias %%i %akafile%
echo. >>%akafile%
rem add an alias for the project include folder
call :flushasunixpath "[base] %incprjdir%" %akafile%
rem add an alias for every library
for /d %%i in (%incprjdir%\*) do call :flushlibalias %%~pni %akafile%
rem aliases for other special folders
rem call :flushasunixpath "[<alias>] <dir>" %akafile%

rem launch the analysis tools
cdep -i%incfile% -f%srcfile% >%depfile% 2>nul
ldep -d%depfile% -a%akafile% -U%inclibdir:\=/% -U%srclibdir:\=/% -x %2

:cleanup
del /q %incfile% 1>nul 2>&1
del /q %srcfile% 1>nul 2>&1
del /q %depfile% 1>nul 2>&1
del /q %akafile% 1>nul 2>&1
goto :eof

:emptyfile
echo. >nul 2>%1
goto :eof

:flushasunixpath
set filepath=%~1
echo %filepath:\=/%>>%2
goto :eof

:flushalias
set dirpath=%~p1
set filename=%~n1
echo %filename% %dirpath:\=/%%filename%>>%2
goto :eof

:flushlibalias
set libdir=%1
set libname=%~n1
echo [%libname%] %libdir:\=/%>>%2
goto :eof

:usage
echo Usage: %~n0 library-name [-l^|-L]
echo.
echo     -l    Long listing: provide non-redundant list of dependencies.
echo     -L    Long listing: provide complete list of dependencies.
goto :eof

Analisi dell'intero progetto

idep può essere utilizzato anche per valutare le dipendenze tra le librerie che costituiscono il progetto; a tal scopo può tornare utile il seguente script:

[file: idep-prj.cmd]

@echo off
if "%1"=="/?" goto :usage
if "%1"=="-h" goto :usage
if "%1"=="--help" goto :usage

rem define the project directories
set homedir=\Users\dex\Temp\idep-x3\prj
set incdir=%homedir%\include
set srcdir=%homedir%\src\lib

rem names for auxiliary files
set incfile=~prj.i
set srcfile=~prj.s
set depfile=~prj.d
set akafile=~prj.a

rem build up the include file
call :emptyfile %incfile%
echo .> %incfile%
call :flushasunixpath %incdir% %incfile%
for /d %%i in (%incdir%\*) do call :flushasunixpath %%~pni %incfile%

rem build up the source files list
call :emptyfile %srcfile%
for /d %%i in (%incdir%\*) do ^
for %%j in (%%i\*.h) do ^
call :flushasunixpath %%j %srcfile%

for /d %%i in (%srcdir%\*) do ^
for %%j in (%%i\*.cpp) do ^
call :flushasunixpath %%j %srcfile%

rem build up the aliases file
call :emptyfile %akafile%
rem add an alias for the project include folder
call :flushasunixpath "[base] %incprjdir%" %akafile%
rem add an alias for the base library global include file
call :flushasunixpath "[base] %incprjdir%\Base" %akafile%
rem add an alias for every library
for /d %%i in (%incdir%\*) do call :flushlibalias %%~pni %akafile%
for /d %%i in (%srcdir%\*) do call :flushlibalias %%~pni %akafile%
rem aliases for other special folders
rem call :flushasunixpath "[<alias>] <dir>" %akafile%

rem launch the analysis tools
cdep -i%incfile% -f%srcfile% >%depfile% 2>nul
ldep -d%depfile% -a%akafile% -x %1

:cleanup
del /q %incfile% 1>nul 2>&1
del /q %srcfile% 1>nul 2>&1
del /q %depfile% 1>nul 2>&1
del /q %akafile% 1>nul 2>&1
goto :eof

:emptyfile
echo. >nul 2>%1
goto :eof

:flushasunixpath
set filepath=%~1
echo %filepath:\=/%>>%2
goto :eof

:flushlibalias
set libdir=%1
set libname=%~n1
echo [%libname%] %libdir:\=/%>>%2
goto :eof

:usage
echo Usage: %~n0 [-l^|-L]
echo.
echo     -l    Long listing: provide non-redundant list of dependencies.
echo     -L    Long listing: provide complete list of dependencies.
goto :eof

L'uso di includere nei file .h file appartenenti alla stessa libreria con la direttiva #include "class.h" crea qualche problema a cdep quando differenti librerie definiscono oggetti omonimi. Supponendo che lib1 e lib2 contengano entrambe una classe Widget (in due namespace diversi, ad esempio), e che la classe Container di lib2 includa la “compagna” con la direttiva #include "Widget.h", se la cartella di inclusione di lib1 precede quella di lib2 nel file include.txt, cdep registrerà una dipendenza di lib2::Container nei confronti di lib1::Widget anziché in quelli di lib2::Widget:

[file: /temp/test/lib1/widget.h]
namespace lib1 {

struct Widget { };

} // namespace lib1



[file: /temp/test/lib2/widget.h]
namespace lib2 {

struct Widget { };

} // namespace lib2



[file: /temp/test/lib2/container.h]
#include "Widget.h"

namespace lib2 {

struct Container { };

} // namespace lib2



[file include.txt]
/temp/test/lib1
/temp/test/lib2



[file sources.txt]
/temp/test/lib1/widget.h
/temp/test/lib2/container.h
/temp/test/lib2/widget.h

A fronte dei file sopra illustrati, il responso di cdep è chiaramente errato:

cdep -iinclude.txt -fsources.txt
/temp/test/lib1/widget.h

/temp/test/lib2/container.h
    /temp/test/lib1/Widget.h <--- lib1: errore!

/temp/test/lib2/widget.h

Molteplici solo le soluzioni al problema:

Un ultimo problema l'ho riscontrato con le direttive di inclusione indentate: cdep sembra ignorarle; conviene quindi assicurarsi di specificarle sempre a inizio riga.

Riferimenti

  1. Gumz, M. "idep". google code. <http://code.google.com/p/idep/>. Visitato l'11 Gennaio 2012.
  2. Lakos, J. Large-Scale C++ Software Design. Addison-Wesley Professional, 1996.

Pagina modificata il 12/01/2012