Heim  >  Artikel  >  Backend-Entwicklung  >  C Reflection Magic: Einfache Protokollierung mit einem Wrapper zum Drucken beliebiger Funktionsargumente und Ergebnisse

C Reflection Magic: Einfache Protokollierung mit einem Wrapper zum Drucken beliebiger Funktionsargumente und Ergebnisse

WBOY
WBOYOriginal
2024-07-18 20:20:31448Durchsuche

C Reflection Magic: Simple Logging with A Wrapper for Printing Arbitrary Functions Arguments and Results

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, &param_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, &param_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:

  1. clang stellt keine Debug-Informationen zu externen Funktionen wie printf zur Verfügung. Das bedeutet, dass unser Wrapper damit so wie es ist nicht funktionieren wird. Möglicherweise müssen wir einige zusätzliche Tricks einführen.
  2. Funktionen mit nicht spezifizierten Argumenten ... zeigen solche Argumente nicht an. Es gibt keine generische Möglichkeit, aber möglicherweise möchten wir möglicherweise eine Möglichkeit bieten, einen Rückruf bereitzustellen, um Informationen für solche Fälle zu extrahieren.
  3. Es gibt (noch?) keine Unterstützung für die Fälle verknüpfter Argumente, z.B. wenn wir Zeiger und Länge als zwei separate, aber logisch verbundene Argumente übergeben.

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!

Stellungnahme:
Der Inhalt dieses Artikels wird freiwillig von Internetnutzern beigesteuert und das Urheberrecht liegt beim ursprünglichen Autor. Diese Website übernimmt keine entsprechende rechtliche Verantwortung. Wenn Sie Inhalte finden, bei denen der Verdacht eines Plagiats oder einer Rechtsverletzung besteht, wenden Sie sich bitte an admin@php.cn