Maison >développement back-end >C++ >C Reflection Magic : journalisation simple avec un wrapper pour imprimer des arguments et des résultats de fonctions arbitraires
Cet article est un rapport de recherche qui couvre certains aspects potentiels de l'implémentation de l'écriture d'un wrapper d'assistance qui enregistrera automatiquement les arguments et les résultats de la fonction C arbitraire. C'est l'un des exemples pour lesquels la réflexion peut être utile même en C. L'implémentation est basée sur le projet Metac. L’introduction en a été donnée dans cet article. La recherche donne de bons résultats, mais elle est toujours en cours. Les commentaires sur la façon dont cela pourrait être amélioré sont appréciés.
La journalisation est l'un des moyens importants de débogage. Effectuer une journalisation appropriée est la clé pour comprendre ce qui a potentiellement mal tourné sans utiliser de débogueur. Mais c’est ennuyeux d’imprimer tous les arguments de chaque fonction et son résultat. La réflexion C avec Metac pourrait potentiellement avoir la capacité de le faire, car les informations de débogage fournies par DWARF contiennent toutes les données sur le type de chaque argument. Vérifiez-le. Voici l'application de test :
#include <stdio.h> #include <stdarg.h> #include <stdlib.h> #include <string.h> #include "metac/reflect.h" int test_function1_with_args(int a, short b) { return a + b + 6; } METAC_GSYM_LINK(test_function1_with_args); int main() { printf("fn returned: %i\n", test_function1_with_args(1, 2)); return 0; }
Nous voulons créer une sorte de wrapper pour imprimer les arguments de test_function1_with_args. Metac générera ses informations de réflexion depuis METAC_GSYM_LINK(test_function1_with_args); est dans le code. Pour plus de simplicité, les types d’arguments int et courts sont sélectionnés. La première idée pour créer un wrapper est de créer une macro :
void print_args(metac_entry_t *p_entry, ...) { // use va_args and debug information about types to print value of each argument } #define METAC_WRAP_FN(_fn_, _args_...) ({ \ print_args(METAC_GSYM_LINK_ENTRY(_fn_), _args_); \ _fn_(_args_); \ }) int main() { // use wrapper instead of printf("fn returned: %i\n", test_function1_with_args(1, 2)); printf("fn returned: %i\n", METAC_WRAP_FN(test_function1_with_args, 1, 2)); return 0; }
Ce wrapper ne gère jusqu'à présent que les arguments, mais c'est ok pour la première étape. Essayons d'implémenter print_args. Voici la première tentative naïve :
void print_args(metac_entry_t *p_entry, ...) { if (p_entry == NULL || metac_entry_has_paremeter(p_entry) == 0) { return; } va_list args; va_start(args, p_entry); printf("%s(", metac_entry_name(p_entry)); // output each argument for (int i = 0; i < metac_entry_paremeter_count(p_entry); ++i) { if (i > 0) { printf(", "); } // get i-th arg metac_entry_t * p_param_entry = metac_entry_by_paremeter_id(p_entry, i); if (metac_entry_is_parameter(p_param_entry) == 0) { // something is wrong break; } // if it’s … argument just print … - there is no way so far to handle that if (metac_entry_is_unspecified_parameter(p_param_entry) != 0) { // we don't support printing va_args... there is no generic way printf("..."); break; } // get arg name and info about arg type metac_name_t param_name = metac_entry_name(p_param_entry); metac_entry_t * p_param_type_entry = metac_entry_parameter_entry(p_param_entry); if (param_name == NULL || param_name == NULL) { // something is wrong break; } // lets handle only base_types for now if (metac_entry_is_base_type(p_param_type_entry) != 0) { // take what type of base type it is. It can be char, unsigned char.. etc metac_name_t param_base_type_name = metac_entry_base_type_name(p_param_type_entry); // if _type_ is matching with param_base_type_name, get data using va_arg and print it. #define _base_type_arg_(_type_, _pseudoname_) \ do { \ if (strcmp(param_base_type_name, #_pseudoname_) == 0) { \ _type_ val = va_arg(args, _type_); \ metac_value_t * p_val = metac_new_value(p_param_type_entry, &val); \ if (p_val == NULL) { \ break; \ } \ char * s = metac_value_string(p_val); \ if (s == NULL) { \ metac_value_delete(p_val); \ break; \ } \ printf("%s: %s", param_name, s); \ free(s); \ metac_value_delete(p_val); \ } \ } while(0) // handle all known base types _base_type_arg_(char, char); _base_type_arg_(unsigned char, unsigned char); _base_type_arg_(short, short int); _base_type_arg_(unsigned short, unsigned short int); _base_type_arg_(int, int); _base_type_arg_(unsigned int, unsigned int); _base_type_arg_(long, long int); _base_type_arg_(unsigned long, unsigned long int); _base_type_arg_(long long, long long int); _base_type_arg_(unsigned long long, unsigned long long int); _base_type_arg_(bool, _Bool); _base_type_arg_(float, float); _base_type_arg_(double, double); _base_type_arg_(long double, long double); _base_type_arg_(float complex, complex); _base_type_arg_(double complex, complex); _base_type_arg_(long double complex, complex); #undef _base_type_arg_ } } printf(")\n"); va_end(args); return; }
Si nous l'exécutons, nous verrons :
% ./c_print_args test_function1_with_args(a: 1, b: 2) fn returned: 9
Ça marche ! Mais il ne gère que les types de base. Et nous voulons que ce soit universel.
Le principal défi ici est avec cette ligne :
_type_ val = va_arg(args, _type_);
La macro va_arg de C nécessite que le type de l'argument soit connu au moment de la compilation. Toutefois, les informations de réflexion fournissent uniquement les noms de types au moment de l'exécution. Pouvons-nous le tromper ? va_arg est une macro qui couvre une fonction intégrée. Le deuxième paramètre est un type (chose très atypique). Mais pourquoi cette chose a-t-elle besoin du type ? La réponse est : comprendre la taille et être capable de la sortir de la pile. Nous devons couvrir toutes les tailles possibles et obtenir un pointeur vers l’argument suivant. Du côté de Metac, nous connaissons la taille de l'argument - nous pouvons utiliser cet extrait pour l'obtenir :
metac_size_t param_byte_sz = 0; if (metac_entry_byte_size(p_param_type_entry, ¶m_byte_sz) != 0) { // something is wrong break; }
Comme idée suivante, créons la macro qui couvrira 1 taille et assurons-nous de la gérer correctement :
char buf[32]; int handled = 0; #define _handle_sz_(_sz_) \ do { \ if (param_byte_sz == _sz_) { \ char *x = va_arg(args, char[_sz_]); \ memcpy(buf, x, _sz_); \ handled = 1; \ } \ } while(0) _handle_sz_(1); _handle_sz_(2); _handle_sz_(3); _handle_sz_(4); // and so on ... _handle_sz_(32); #undef _handle_sz_
Avec cette approche, nous avons couvert différentes tailles de 1 à 32. Nous pourrions générer un code et couvrir des arguments dimensionnés jusqu'à n'importe quel nombre arbitraire, mais dans la plupart des cas, les gens utilisent des pointeurs plutôt que de transmettre directement des tableaux/structures. Pour notre exemple, nous en garderons 32.
Refactorisons notre fonction pour la rendre plus réutilisable, divisons-la en 2 vprint_args et print_args de la même manière que « vprtintf » et printf :
void vprint_args(metac_tag_map_t * p_tag_map, metac_entry_t *p_entry, va_list args) { if (p_entry == NULL || metac_entry_has_paremeter(p_entry) == 0) { return; } printf("%s(", metac_entry_name(p_entry)); for (int i = 0; i < metac_entry_paremeter_count(p_entry); ++i) { if (i > 0) { printf(", "); } metac_entry_t * p_param_entry = metac_entry_by_paremeter_id(p_entry, i); if (metac_entry_is_parameter(p_param_entry) == 0) { // something is wrong break; } if (metac_entry_is_unspecified_parameter(p_param_entry) != 0) { // we don't support printing va_args... there is no generic way printf("..."); break; } metac_name_t param_name = metac_entry_name(p_param_entry); metac_entry_t * p_param_type_entry = metac_entry_parameter_entry(p_param_entry); if (param_name == NULL || p_param_type_entry == NULL) { // something is wrong break; } metac_size_t param_byte_sz = 0; if (metac_entry_byte_size(p_param_type_entry, ¶m_byte_sz) != 0) { // something is wrong break; } char buf[32]; int handled = 0; #define _handle_sz_(_sz_) \ do { \ if (param_byte_sz == _sz_) { \ char *x = va_arg(args, char[_sz_]); \ memcpy(buf, x, _sz_); \ handled = 1; \ } \ } while(0) _handle_sz_(1); _handle_sz_(2); //... _handle_sz_(32); #undef _handle_sz_ if (handled == 0) { break; } metac_value_t * p_val = metac_new_value(p_param_type_entry, &buf); if (p_val == NULL) { break; } char * v = metac_value_string_ex(p_val, METAC_WMODE_deep, p_tag_map); if (v == NULL) { metac_value_delete(p_val); break; } char * arg_decl = metac_entry_cdecl(p_param_type_entry); if (arg_decl == NULL) { free(v); metac_value_delete(p_val); break; } printf(arg_decl, param_name); printf(" = %s", v); free(arg_decl); free(v); metac_value_delete(p_val); } printf(")"); } void print_args(metac_tag_map_t * p_tag_map, metac_entry_t *p_entry, ...) { va_list args; va_start(args, p_entry); vprint_args(p_tag_map, p_entry, args); va_end(args); return; }
Le lecteur remarquera peut-être que nous avons ajouté p_tag_map comme premier argument. Ceci est pour des recherches plus approfondies - ce n'est pas utilisé dans cet article.
Essayons maintenant de créer une partie qui gère le résultat. Malheureusement, typeof n'est pas pris en charge avant C23 (extension gcc en option, mais cela ne fonctionnera pas avec clang) et nous avons un dilemme : voulons-nous conserver notre notation METAC_WRAP_FN telle quelle, ou pouvons-nous lui transmettre un argument supplémentaire - type du résultat de la fonction à utiliser comme tampon. Nous pourrions probablement utiliser libffi pour gérer cela de manière universelle - Metac connaît le type, mais il n'est pas clair comment placer les données renvoyées dans un tampon de taille appropriée. Pour plus de simplicité changeons notre macro :
#define METAC_WRAP_FN_RES(_type_, _fn_, _args_...) ({ \ printf("calling "); \ print_args(NULL, METAC_GSYM_LINK_ENTRY(_fn_), _args_); \ printf("\n"); \ WITH_METAC_DECLLOC(loc, _type_ res = _fn_(_args_)); \ print_args_and_res(NULL, METAC_GSYM_LINK_ENTRY(_fn_), METAC_VALUE_FROM_DECLLOC(loc, res), _args_); \ res; \ })
Maintenant, nous passons _type_ comme premier argument pour stocker le résultat. Si nous passons un type ou des arguments incorrects, le compilateur se plaindra de ce _type_ res = _fn_(_args_). C'est bien.
Imprimer le résultat est une tâche triviale, nous l'avons déjà fait dans le premier article. Mettons également à jour nos fonctions de test pour accepter différents types de paramètres.
Voici le dernier exemple de code.
Si nous l'exécutons, nous obtiendrons les commentaires :
% ./c_print_args # show args of base type arg function calling test_function1_with_args(int a = 10, short int b = 22) fn returned: 38 # show args if the first arg is a pointer calling test_function2_with_args(int * a = (int []){689,}, short int b = 22) fn returned: 1710 # using METAC_WRAP_FN_RES which will print the result. using pointer to list calling test_function3_with_args(list_t * p_list = (list_t []){{.x = 42.420000, .p_next = (struct list_s []){{.x = 45.400000, .p_next = NULL,},},},}) fn returned: 87.820000 # another example of METAC_WRAP_FN_RES with int * as a first arg calling test_function2_with_args(int * a = (int []){689,}, short int b = 22) test_function2_with_args(int * a = (int []){689,}, short int b = 22) returned 1710 # the log where 1 func with wrapper calls another func with wrapper calling test_function4_with_args(list_t * p_list = (list_t []){{.x = 42.420000, .p_next = (struct list_s []){{.x = 45.400000, .p_next = NULL,},},},}) calling test_function3_with_args(list_t * p_list = (list_t []){{.x = 42.420000, .p_next = (struct list_s []){{.x = 45.400000, .p_next = NULL,},},},}) test_function3_with_args(list_t * p_list = (list_t []){{.x = 42.420000, .p_next = (struct list_s []){{.x = 45.400000, .p_next = NULL,},},},}) returned 87.820000 test_function4_with_args(list_t * p_list = (list_t []){{.x = 42.420000, .p_next = (struct list_s []){{.x = 45.400000, .p_next = NULL,},},},}) returned -912.180000
On voit que Metac nous imprime la représentation profonde des arguments ainsi que des résultats. En général, cela fonctionne, bien qu'il y ait quelques défauts comme la nécessité de gérer chaque taille d'argument séparément.
Voici quelques limitations supplémentaires :
Si vous avez des suggestions sur la façon dont cela pourrait être plus générique, veuillez commenter. Merci d'avoir lu !
Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!