Heim >Backend-Entwicklung >C++ >C Reflection Magic: Einfache Protokollierung mit einem Wrapper zum Drucken beliebiger Funktionsargumente und Ergebnisse
Bei diesem Artikel handelt es sich um einen Forschungsbericht, der einige mögliche Implementierungsaspekte beim Schreiben eines Hilfs-Wrappers behandelt, der Argumente und Ergebnisse der beliebigen C-Funktion automatisch protokolliert. Dies ist eines der Beispiele, warum Reflexion auch in C nützlich sein kann. Die Implementierung basiert auf dem Metac-Projekt. Die Einführung dazu wurde in diesem Artikel gegeben. Die Forschung hat einige gute Ergebnisse gebracht, ist aber noch im Gange. Wir freuen uns über Kommentare, wie es besser gemacht werden könnte.
Die Protokollierung ist eine der wichtigen Methoden zum Debuggen. Eine ordnungsgemäße Protokollierung ist ein Schlüssel zum Verständnis, was möglicherweise schief gelaufen ist, ohne einen Debugger zu verwenden. Aber es ist nervig, alle Argumente jeder Funktion und ihr Ergebnis auszudrucken. Die C-Reflektion mit Metac könnte möglicherweise dazu in der Lage sein, da die von DWARF bereitgestellten Debug-Informationen alle Daten über den Typ jedes Arguments enthalten. Hör zu. Hier ist die Testanwendung:
#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; }
Wir möchten eine Art Wrapper erstellen, um Argumente von test_function1_with_args auszugeben. Metac generiert seine Reflexionsinformationen, da METAC_GSYM_LINK(test_function1_with_args); steht im Code. Der Einfachheit halber werden die Argumenttypen int und short ausgewählt. Die erste Idee, wie wir einen Wrapper erstellen könnten, ist: Erstellen Sie ein Makro:
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; }
Dieser Wrapper verarbeitet bisher nur Argumente, ist aber für den ersten Schritt in Ordnung. Versuchen wir, print_args zu implementieren. Hier ist der erste naive Versuch:
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; }
Wenn wir es ausführen, sehen wir:
% ./c_print_args test_function1_with_args(a: 1, b: 2) fn returned: 9
Es funktioniert! Es werden jedoch nur Basistypen verarbeitet. Und wir wollen, dass es universell ist.
Die größte Herausforderung besteht hier in dieser Zeile:
_type_ val = va_arg(args, _type_);
Das va_arg-Makro von C erfordert, dass der Typ des Arguments zur Kompilierungszeit bekannt ist. Allerdings stellen Reflektionsinformationen zur Laufzeit nur Typnamen bereit. Können wir es austricksen? va_arg ist ein Makro, das eine integrierte Funktion abdeckt. Der zweite Parameter ist ein Typ (sehr untypisch). Aber warum braucht dieses Ding überhaupt den Typ? Die Antwort lautet: Die Größe verstehen und in der Lage sein, sie vom Stapel zu nehmen. Wir müssen alle möglichen Größen abdecken und einen Hinweis auf das nächste Argument erhalten. Auf Metac-Seite kennen wir die Größe des Arguments – wir können dieses Snippet verwenden, um es zu erhalten:
metac_size_t param_byte_sz = 0; if (metac_entry_byte_size(p_param_type_entry, ¶m_byte_sz) != 0) { // something is wrong break; }
Als nächste Idee erstellen wir das Makro, das eine Größe abdeckt, und stellen sicher, dass wir es richtig handhaben:
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_
Mit diesem Ansatz haben wir verschiedene Größen von 1 bis 32 abgedeckt. Wir könnten einen Code generieren und Argumente mit einer Größe bis zu einer beliebigen Zahl abdecken, aber in den meisten Fällen werden Zeiger verwendet, anstatt Arrays/Strukturen direkt zu übergeben. Für unser Beispiel behalten wir 32.
Lassen Sie uns unsere Funktion umgestalten, um sie wiederverwendbar zu machen. Teilen Sie sie in zwei vprint_args und print_args auf, ähnlich wie „vprtintf“ und 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; }
Dem Leser fällt vielleicht auf, dass wir p_tag_map als erstes Argument hinzugefügt haben. Dies dient der weiteren Recherche – es wird in diesem Artikel nicht verwendet.
Versuchen wir nun, einen Teil zu erstellen, der das Ergebnis verarbeitet. Leider wird typeof bis C23 nicht unterstützt (gcc-Erweiterung als Option, funktioniert aber nicht mit clang) und wir stehen vor einem Dilemma: Wollen wir unsere METAC_WRAP_FN-Notation so beibehalten, wie sie ist, oder ist es in Ordnung, ihr ein weiteres Argument zu übergeben? - Typ des Funktionsergebnisses, das als Puffer verwendet werden soll. Wahrscheinlich könnten wir libffi verwenden, um dies auf universelle Weise zu handhaben – Metac kennt den Typ, aber es ist nicht klar, wie die zurückgegebenen Daten in den Puffer mit der richtigen Größe eingefügt werden sollen. Der Einfachheit halber ändern wir unser Makro:
#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; \ })
Jetzt übergeben wir _type_ als erstes Argument, um das Ergebnis zu speichern. Wenn wir falsche type oder Argumente übergeben, wird sich der Compiler darüber beschweren _type_ res = _fn_(_args_). Das ist gut.
Das Ergebnis auszudrucken ist eine triviale Aufgabe, das haben wir bereits im ersten Artikel gemacht. Aktualisieren wir auch unsere Testfunktionen, um einige verschiedene Parametertypen zu akzeptieren.
Hier ist der letzte Beispielcode.
Wenn wir es ausführen, erhalten wir folgende Kommentare:
% ./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
Es hat sich gezeigt, dass Metac für uns die tiefgründige Darstellung der Argumente und Ergebnisse ausdruckt. Im Allgemeinen funktioniert es, obwohl es einige Mängel gibt, z. B. die Notwendigkeit, jede Argumentgröße separat zu behandeln.
Hier sind einige zusätzliche Einschränkungen:
Wenn Sie einen Vorschlag haben, wie es allgemeiner sein könnte, kommentieren Sie ihn bitte. Danke fürs Lesen!
Das obige ist der detaillierte Inhalt vonC Reflection Magic: Einfache Protokollierung mit einem Wrapper zum Drucken beliebiger Funktionsargumente und Ergebnisse. Für weitere Informationen folgen Sie bitte anderen verwandten Artikeln auf der PHP chinesischen Website!