Extraction des fonctions d’un fichier C

Publié par cpb
Oct 20 2014

Extraction des fonctions d'un fichier CJ’ai eu besoin il y a quelques jours de comparer deux versions d’un même fichier C après qu’il a subi de nombreuses modifications. La structure du fichier et le contenu ayant beaucoup bougé, les outils habituels (éditeurs, diff, meld, etc.) ne pouvaient me venir en aide. J’avais besoin d’extraire chaque fonction du programme pour qu’elle se trouve dans un fichier séparé. Cela m’a amené à écrire un petit script shell qui pourra peut-être s’avérer utile à d’autres…

Le fichier en question provenait d’un projet client ancien, dont le code avait été écrit de manière incrémentale, avait servi pour du débogage, de la mise au point d’algorithmes, etc. La première version du fichier devenait illisible et impossible à maintenir sereinement. Il a donc été décidé de procéder à un réusinage pour le rendre plus conforme aux standards logiciels actuels.

Cette phase de refactoring a consisté en plusieurs opérations. Entre autres

  • suppression de code en commentaire ou dans des portions inaccessibles (comme #ifdef 0 / #endif)
  • suppression de code mort (routines jamais appelées)
  • suppression des variables globales (plusieurs centaines !) inutilisées (plusieurs dizaines !)
  • réintégration du maximum de variables globales en variables statiques ou automatiques
  • renommage de variables et de constantes
  • réorganisation globale du fichier pour contenir des blocs cohérents (définitions des macros, des types, des fonctions privées, des variables globales, implémentations des fonctions publiques puis des fonctions privées)
  • réécriture du code pour correspondre à des normes de lisibilité actuelles. Personnellement j’ai adopté le standard de Busybox (voir la description dans son fichier docs/style-guide.txt).

Durant les opérations manuelles de ce refactoring (qui comprenait plusieurs dizaines de fichiers, chacun de plusieurs milliers de lignes de code), une erreur a été commise dont le débogage s’avérait complexe. J’ai donc décidé de comparer visuellement toutes les modifications, fonction par fonction du module incriminé.

Il existe un outil très agréable pour comparer deux versions d’un même fichier : il s’agit de Meld.Toutefois, lorsque la structure complète du fichier a été modifiée, il devient difficilement utilisable. La solution la plus simple pour moi consistait à exploser mon programme source en autant de fichiers qu’il y avait de fonctions (une petite centaine). Et de les comparer une-à-une (d’abord avec diff en console pour voir si  quelque chose me sautait aux yeux, puis avec Meld pour les fonctions les plus longues).

Il existe un utilitaire nommé csplit qui permet de découper en fichier en utilisant des expressions régulières pour décrire le contexte (le « c » de csplit signifie context, pas « langage C » !). Malheureusement, dans les blocs de code commentés, il se trouvait des déclarations de fonctions avortées, et du code invalide. La description sous forme d’expression régulière semblait impossible.

J’ai commencé par demander à ctags d’extraire la liste des fonctions présentes, et de me la présenter de manière « humainement lisible ».

Je prends en exemple le fichier main.c se trouvant dans le répertoire init/ des sources du noyau Linux.

$ ctags -x -f - --c-kinds=f main.c 
boot_cpu_init    function    431 main.c           static void __init boot_cpu_init(void)
debug_kernel     function    195 main.c           static int __init debug_kernel(char *str)
do_basic_setup   function    777 main.c           static void __init do_basic_setup(void)
do_ctors         function    647 main.c           static void __init do_ctors(void)
do_early_param   function    389 main.c           static int __init do_early_param(char *param, char *val, const char *unused)
do_initcall_level function    746 main.c           static void __init do_initcall_level(int level)
do_initcalls     function    762 main.c           static void __init do_initcalls(void)
do_one_initcall  function    680 main.c           int __init_or_module do_one_initcall(initcall_t fn)
do_one_initcall_debug function    662 main.c           static int __init_or_module do_one_initcall_debug(initcall_t fn)
do_pre_smp_initcalls function    789 main.c           static void __init do_pre_smp_initcalls(void)
init_setup       function    291 main.c           static int __init init_setup(char *str)
kernel_init      function    807 main.c           static int __ref kernel_init(void *unused)
kernel_init_freeable function    848 main.c           static noinline void __init kernel_init_freeable(void)
loglevel         function    210 main.c           static int __init loglevel(char *str)
mark_rodata_ro   function     92 main.c           static inline void mark_rodata_ro(void) { }
mm_init          function    454 main.c           static void __init mm_init(void)
obsolete_checksetup function    158 main.c           static int __init obsolete_checksetup(char *line)
parse_early_options function    407 main.c           void __init parse_early_options(char *cmdline)
parse_early_param function    413 main.c           void __init parse_early_param(void)
quiet_kernel     function    201 main.c           static int __init quiet_kernel(char *str)
rdinit_setup     function    308 main.c           static int __init rdinit_setup(char *str)
repair_env_string function    230 main.c           static int __init repair_env_string(char *param, char *val, const char *unused)
rest_init        function    360 main.c           static noinline void __init_refok rest_init(void)
run_init_process function    797 main.c           static int run_init_process(const char *init_filename)
set_reset_devices function    144 main.c           static int __init set_reset_devices(char *str)
setup_command_line function    341 main.c           static void __init setup_command_line(char *command_line)
setup_nr_cpu_ids function    331 main.c           static inline void setup_nr_cpu_ids(void) { }
smp_init         function    323 main.c           static void __init smp_init(void)
smp_prepare_cpus function    332 main.c           static inline void smp_prepare_cpus(unsigned int maxcpus) { }
smp_setup_processor_id function    441 main.c           void __init __weak smp_setup_processor_id(void)
start_kernel     function    468 main.c           asmlinkage void __init start_kernel(void)
thread_info_cache_init function    446 main.c           void __init __weak thread_info_cache_init(void)
unknown_bootoption function    250 main.c           static int __init unknown_bootoption(char *param, char *val, const char *unused)

Nous voyons que chaque ligne correspond à une fonction du fichier. Le premier champ est le nom de la fonction, il est suivi par le type d’item (il s’agit toujours de « function » car c’est ce que j’ai demandé à ctags). Le troisième champ est le numéro de la première ligne, le quatrième le nom du fichier et enfin nous trouvons le prototype de la fonction.

Je souhaite extraire chaque fonction séparément, il me suffit donc de parcourir cette liste, et de créer un fichier à chaque ligne. L’implémentation de chaque fonction commence – dans le fichier original – à la ligne dont le numéro est indiqué en troisième colonne. Comme je ne sais pas où la fonction se termine, il me suffit de prendre tout le contenu jusqu’à la fonction suivante.

Pour cela il faut ordonner la liste ci-dessus. Utilisons sort.

$ ctags -x -f - --c-kinds=f main.c | sort -nk 3
mark_rodata_ro   function     92 main.c           static inline void mark_rodata_ro(void) { }
set_reset_devices function    144 main.c           static int __init set_reset_devices(char *str)
obsolete_checksetup function    158 main.c           static int __init obsolete_checksetup(char *line)
debug_kernel     function    195 main.c           static int __init debug_kernel(char *str)
quiet_kernel     function    201 main.c           static int __init quiet_kernel(char *str)
loglevel         function    210 main.c           static int __init loglevel(char *str)
repair_env_string function    230 main.c           static int __init repair_env_string(char *param, char *val, const char *unused)
unknown_bootoption function    250 main.c           static int __init unknown_bootoption(char *param, char *val, const char *unused)
init_setup       function    291 main.c           static int __init init_setup(char *str)
rdinit_setup     function    308 main.c           static int __init rdinit_setup(char *str)
smp_init         function    323 main.c           static void __init smp_init(void)
setup_nr_cpu_ids function    331 main.c           static inline void setup_nr_cpu_ids(void) { }
smp_prepare_cpus function    332 main.c           static inline void smp_prepare_cpus(unsigned int maxcpus) { }
setup_command_line function    341 main.c           static void __init setup_command_line(char *command_line)
rest_init        function    360 main.c           static noinline void __init_refok rest_init(void)
do_early_param   function    389 main.c           static int __init do_early_param(char *param, char *val, const char *unused)
parse_early_options function    407 main.c           void __init parse_early_options(char *cmdline)
parse_early_param function    413 main.c           void __init parse_early_param(void)
boot_cpu_init    function    431 main.c           static void __init boot_cpu_init(void)
smp_setup_processor_id function    441 main.c           void __init __weak smp_setup_processor_id(void)
thread_info_cache_init function    446 main.c           void __init __weak thread_info_cache_init(void)
mm_init          function    454 main.c           static void __init mm_init(void)
start_kernel     function    468 main.c           asmlinkage void __init start_kernel(void)
do_ctors         function    647 main.c           static void __init do_ctors(void)
do_one_initcall_debug function    662 main.c           static int __init_or_module do_one_initcall_debug(initcall_t fn)
do_one_initcall  function    680 main.c           int __init_or_module do_one_initcall(initcall_t fn)
do_initcall_level function    746 main.c           static void __init do_initcall_level(int level)
do_initcalls     function    762 main.c           static void __init do_initcalls(void)
do_basic_setup   function    777 main.c           static void __init do_basic_setup(void)
do_pre_smp_initcalls function    789 main.c           static void __init do_pre_smp_initcalls(void)
run_init_process function    797 main.c           static int run_init_process(const char *init_filename)
kernel_init      function    807 main.c           static int __ref kernel_init(void *unused)
kernel_init_freeable function    848 main.c           static noinline void __init kernel_init_freeable(void)

Une simple boucle while read du shell nous donnera :

$ ctags -x -f - --c-kinds=f main.c | sort -nk 3 | while read name unused line rest; do echo "${name}:  ${line}"; done
mark_rodata_ro:  92
set_reset_devices:  144
obsolete_checksetup:  158
debug_kernel:  195
quiet_kernel:  201
loglevel:  210
repair_env_string:  230
unknown_bootoption:  250
init_setup:  291
rdinit_setup:  308
smp_init:  323
setup_nr_cpu_ids:  331
smp_prepare_cpus:  332
setup_command_line:  341
rest_init:  360
do_early_param:  389
parse_early_options:  407
parse_early_param:  413
boot_cpu_init:  431
smp_setup_processor_id:  441
thread_info_cache_init:  446
mm_init:  454
start_kernel:  468
do_ctors:  647
do_one_initcall_debug:  662
do_one_initcall:  680
do_initcall_level:  746
do_initcalls:  762
do_basic_setup:  777
do_pre_smp_initcalls:  789
run_init_process:  797
kernel_init:  807
kernel_init_freeable:  848

Il nous faut maintenant extraire du fichier original la portion contenue entre le numéro indiqué sur chaque ligne et celui de la ligne suivante.

Une solution simple est d’utiliser sed. Une commande comme

$ sed -ne '123,456p' < fic-entree > fic-sortie

copiera (commande p pour print) dans le fichier de sortie le contenu du fichier d’entrée depuis la ligne 123 jusqu’à la ligne 456. De même

$ sed -ne '2014,$p' <  fic-entree > fic-sortie

fera la copie depuis la ligne 2014 jusqu’à la fin du fichier. Attention à bien mettre des quotes simples pour encadrer la commande afin que le shell n’interprète pas le $p comme un nom de variable.

Prenons les dernières lignes affichées ci-dessus. Lorsque nous traitons la ligne

kernel_init:  807

nous ne savons pas encore où la fonction kernel_init se terminera. Il nous faut donc mémoriser son nom et le numéro 807, pour qu’une fois lue la ligne

kernel_init_freeable:  848

nous puissions extraire la portion 807 – (848-1) du fichier original pour le stocker dans un nouveau fichier nommé function-kernel_init par exemple.

Pour pouvoir traiter la dernière fonction de la liste, il faut que notre boucle while read soit appelée une fois de plus, aussi devons nous ajouter une ligne supplémentaire ainsi :

$ (ctags -x -f - --c-kinds=f main.c | sort -nk 3 ; echo) | while read name unused line rest; do echo "${name}:  ${line}"; done
mark_rodata_ro:  92
set_reset_devices:  144
obsolete_checksetup:  158
debug_kernel:  195
quiet_kernel:  201
loglevel:  210
repair_env_string:  230
unknown_bootoption:  250
init_setup:  291
rdinit_setup:  308
smp_init:  323
setup_nr_cpu_ids:  331
smp_prepare_cpus:  332
setup_command_line:  341
rest_init:  360
do_early_param:  389
parse_early_options:  407
parse_early_param:  413
boot_cpu_init:  431
smp_setup_processor_id:  441
thread_info_cache_init:  446
mm_init:  454
start_kernel:  468
do_ctors:  647
do_one_initcall_debug:  662
do_one_initcall:  680
do_initcall_level:  746
do_initcalls:  762
do_basic_setup:  777
do_pre_smp_initcalls:  789
run_init_process:  797
kernel_init:  807
kernel_init_freeable:  848
:  

lors de la dernière itération tous les champs sont vides, ce qui conduit à la ligne contenant seulement le caractère deux-points.

Nous pouvons maintenant regrouper tout ceci dans un petit script qui prendra autant de fichiers d’entrée qu’on le souhaite.

extract-functions.sh:
#! /bin/sh

if [ $# -eq 0 ]
then
  echo "usage: $0 C-file..." >&2
  exit 1
fi

for file in "$@"
do
  prev_line=""
  prev_name=""
  (ctags -x -f - --c-kinds=f "${file}" | \
   sort -n -k 3                        ;
   echo)                               | \
  while read name unused line rest
  do
    if [ "$prev_name" != "" ] # avoid first line
    then
      if [ "${name}" != "" ]
      then
        # normal line
        sed -ne "${prev_line},$((line - 1))p" < "${file}" > "function-${prev_name}.c"
      else
        # last line
        sed -ne "${prev_line},\$p" < "${file}" > "function-${prev_name}.c"
      fi
    fi
    prev_line="${line}"
    prev_name="${name}"
  done
done

En voici un exemple d’utilisation.

$ ls
main.c
$ extract-functions.sh  main.c 
$ ls
function-boot_cpu_init.c      function-do_one_initcall.c        function-mark_rodata_ro.c       function-repair_env_string.c   function-smp_prepare_cpus.c
function-debug_kernel.c       function-do_one_initcall_debug.c  function-mm_init.c              function-rest_init.c           function-smp_setup_processor_id.c
function-do_basic_setup.c     function-do_pre_smp_initcalls.c   function-obsolete_checksetup.c  function-run_init_process.c    function-start_kernel.c
function-do_ctors.c           function-init_setup.c             function-parse_early_options.c  function-set_reset_devices.c   function-thread_info_cache_init.c
function-do_early_param.c     function-kernel_init.c            function-parse_early_param.c    function-setup_command_line.c  function-unknown_bootoption.c
function-do_initcall_level.c  function-kernel_init_freeable.c   function-quiet_kernel.c         function-setup_nr_cpu_ids.c    main.c
function-do_initcalls.c       function-loglevel.c               function-rdinit_setup.c         function-smp_init.c
$

En conclusion, ce petit script est loin d’être parfait, il n’évite pas les lignes blanches en fin de fichier par exemple, mais sa simplicité le rend facile à modifier pour l’adapter à d’autres situations où il pourra être utile.

URL de trackback pour cette page