> 백엔드 개발 > C++ > 본문

C 반사 매직: 임의 함수 인수 및 결과 인쇄를 위한 래퍼를 사용한 간단한 로깅

WBOY
풀어 주다: 2024-07-18 20:20:31
원래의
442명이 탐색했습니다.

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

이 기사는 임의의 C 함수의 인수와 결과를 자동으로 기록하는 도우미 래퍼 작성의 몇 가지 잠재적인 구현 측면을 다루는 연구 보고서입니다. 이는 C에서도 리플렉션이 유용한 이유 중 하나입니다. 구현은 Metac 프로젝트를 기반으로 합니다. 이 글에 그 내용이 소개되어 있습니다. 연구는 좋은 결과를 얻었지만 아직 진행 중입니다. 어떻게 하면 더 나은 방법으로 할 수 있는지에 대한 의견을 보내주시면 감사하겠습니다.

로깅은 디버깅의 중요한 방법 중 하나입니다. 디버거를 사용하지 않고 잠재적으로 무엇이 잘못되었는지 이해하려면 적절한 로깅을 수행하는 것이 중요합니다. 하지만 각 함수의 모든 인수와 그 결과를 인쇄하는 것은 귀찮습니다. DWARF에서 제공하는 디버깅 정보에는 각 인수 유형에 대한 모든 데이터가 있기 때문에 Metac을 사용한 C 리플렉션에는 잠재적으로 이 작업을 수행할 수 있는 기능이 있을 수 있습니다. 확인해 보세요. 테스트 애플리케이션은 다음과 같습니다.

#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;
}
로그인 후 복사

test_function1_with_args의 인수를 인쇄하기 위해 일종의 래퍼를 만들고 싶습니다. Metac은 METAC_GSYM_LINK(test_function1_with_args) 이후 반사 정보를 생성합니다. 코드에 있습니다. 단순화를 위해 int 및 short 인수 유형이 선택됩니다. 래퍼를 만드는 첫 번째 아이디어는 매크로를 만드는 것입니다.

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;
}
로그인 후 복사

이 래퍼는 지금까지 인수만 처리했지만 첫 번째 단계에서는 괜찮습니다. print_args를 구현해 보겠습니다. 첫 번째 순진한 시도는 다음과 같습니다.

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;
}
로그인 후 복사

실행하면 다음이 표시됩니다.

% ./c_print_args
test_function1_with_args(a: 1, b: 2)
fn returned: 9
로그인 후 복사

효과가 있어요! 그러나 기본 유형만 처리합니다. 그리고 우리는 그것이 보편적이기를 바랍니다.

여기서 주요 과제는 다음 라인입니다.

 _type_ val = va_arg(args, _type_); 
로그인 후 복사

C의 va_arg 매크로에서는 컴파일 타임에 알려진 인수 유형이 필요합니다. 그러나 리플렉션 정보는 런타임에만 유형 이름을 제공합니다. 속일 수 있나요? va_arg는 내장 함수를 다루는 매크로입니다. 두 번째 매개변수는 유형(매우 일반적이지 않은 것)입니다. 그런데 왜 이런 유형이 필요한가요? 대답은 크기를 이해하고 스택에서 가져올 수 있다는 것입니다. 가능한 모든 크기를 다루고 다음 인수에 대한 포인터를 가져와야 합니다. Metac 측에서는 인수의 크기를 알고 있습니다. 다음 스니펫을 사용하여 이를 얻을 수 있습니다.

        metac_size_t param_byte_sz = 0;
        if (metac_entry_byte_size(p_param_type_entry, &param_byte_sz) != 0) {
            // something is wrong
            break;
        }
로그인 후 복사

다음 아이디어로 1개의 크기를 처리하는 매크로를 만들고 올바르게 처리하는지 확인하겠습니다.

        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_

로그인 후 복사

이 접근 방식을 통해 우리는 1부터 32까지 다양한 크기를 다루었습니다. 코드를 생성하고 임의의 숫자까지 크기가 지정된 인수를 다룰 수 있었지만 대부분의 경우 사람들은 배열/구조체를 직접 전달하는 대신 포인터를 사용합니다. 예시를 위해 32를 유지하겠습니다.
더 재사용 가능하도록 함수를 리팩토링하여 'vprtintf' 및 printf:
와 유사하게 2개의 vprint_args 및 print_args로 분할하겠습니다.

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;
}
로그인 후 복사

독자는 p_tag_map을 첫 번째 인수로 추가했음을 알 수 있습니다. 이는 추가 연구를 위한 것이며 이 기사에서는 사용되지 않습니다.

이제 결과를 처리하는 부분을 만들어 보겠습니다. 불행하게도 typeof는 C23까지 지원되지 않으며(gcc 확장자는 옵션이지만 clang에서는 작동하지 않습니다) 딜레마에 빠졌습니다. METAC_WRAP_FN 표기법을 그대로 유지하고 싶은지, 아니면 인수를 하나 더 전달해도 괜찮습니까? - 버퍼로 사용될 함수 결과의 유형입니다. 아마도 우리는 이것을 보편적인 방식으로 처리하기 위해 libffi를 사용할 수 있을 것입니다. Metac은 유형을 알고 있지만 반환된 데이터를 적절한 크기의 버퍼에 넣는 방법은 명확하지 않습니다. 단순화를 위해 매크로를 변경해 보겠습니다.

#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; \
    })
로그인 후 복사

이제 결과를 저장하기 위해 _type_을 첫 번째 인수로 전달합니다. 잘못된 type 또는 인수를 전달하면 컴파일러는 이 _type_ res = _fn_(_args_)에 대해 불평합니다. 좋네요.
결과를 인쇄하는 것은 간단한 작업입니다. 첫 번째 기사에서 이미 그 작업을 수행했습니다. 또한 다양한 유형의 매개변수를 허용하도록 테스트 기능을 업데이트해 보겠습니다.
최종 예제 코드는 다음과 같습니다.

실행하면 다음과 같은 댓글이 표시됩니다.

% ./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
로그인 후 복사

Metac은 주장과 결과에 대한 심층적인 표현을 인쇄하는 것으로 나타났습니다. 일반적으로 작동하지만 각 크기의 인수를 별도로 처리해야 하는 등 몇 가지 결함이 있습니다.

몇 가지 추가 제한사항은 다음과 같습니다.

  1. clang은 printf와 같은 외부 함수에 대한 디버그 정보를 노출하지 않습니다. 즉, 우리 래퍼는 있는 그대로 작동하지 않습니다. 몇 가지 추가 트릭을 도입해야 할 수도 있습니다.
  2. 지정되지 않은 인수가 있는 함수는 ... 해당 인수를 표시하지 않습니다. 일반적인 방법은 없지만 잠재적으로 그러한 경우에 대한 정보를 추출하기 위해 콜백을 제공하는 방법을 제공할 수 있습니다.
  3. 연결된 인수의 경우에는 (아직?) 지원이 없습니다. 포인터와 길이를 2개의 별도이지만 논리적으로 연결된 인수로 전달할 때.

더 일반적인 방법에 대한 제안 사항이 있으면 댓글을 달아주세요. 읽어주셔서 감사합니다!

위 내용은 C 반사 매직: 임의 함수 인수 및 결과 인쇄를 위한 래퍼를 사용한 간단한 로깅의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!

원천:dev.to
본 웹사이트의 성명
본 글의 내용은 네티즌들의 자발적인 기여로 작성되었으며, 저작권은 원저작자에게 있습니다. 본 사이트는 이에 상응하는 법적 책임을 지지 않습니다. 표절이나 침해가 의심되는 콘텐츠를 발견한 경우 admin@php.cn으로 문의하세요.
인기 튜토리얼
더>
최신 다운로드
더>
웹 효과
웹사이트 소스 코드
웹사이트 자료
프론트엔드 템플릿
회사 소개 부인 성명 Sitemap
PHP 중국어 웹사이트:공공복지 온라인 PHP 교육,PHP 학습자의 빠른 성장을 도와주세요!