risorse | idep
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.
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:
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 è 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.
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.
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:
Più NCCD è prossimo all'unità, più vicina all'ottimo risulta l'organizzazione del software.
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
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.
Pagina modificata il 12/01/2012