이 기사는 임의의 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, ¶m_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, ¶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; }
독자는 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은 주장과 결과에 대한 심층적인 표현을 인쇄하는 것으로 나타났습니다. 일반적으로 작동하지만 각 크기의 인수를 별도로 처리해야 하는 등 몇 가지 결함이 있습니다.
몇 가지 추가 제한사항은 다음과 같습니다.
더 일반적인 방법에 대한 제안 사항이 있으면 댓글을 달아주세요. 읽어주셔서 감사합니다!
위 내용은 C 반사 매직: 임의 함수 인수 및 결과 인쇄를 위한 래퍼를 사용한 간단한 로깅의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!