本文是一份研究报告,涵盖了编写辅助包装器的一些潜在实现方面,该包装器将自动记录任意 C 函数的参数和结果。这是反射在 C 语言中也可能有用的示例之一。该实现基于 Metac 项目。本文对此进行了介绍。该研究已经取得了一些不错的成果,但仍在进行中。对于如何以更好的方式完成它的评论表示赞赏。
日志记录是调试的重要方式之一。进行正确的日志记录是在不使用调试器的情况下了解可能出现问题的关键。但是打印出每个函数的所有参数及其结果是很烦人的。使用 Metac 的 C 反射可能有能力做到这一点,因为 DWARF 提供的调试信息包含有关每个参数类型的所有数据。一探究竟。这是测试应用程序:
#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 和短参数类型。我们如何创建包装器的第一个想法是 - 创建一个宏:
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。
让我们重构我们的函数,使其更可重用,将其分为 2 个 vprint_args 和 print_args,类似于“vprtintf”和 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; }
读者可能会注意到我们添加了 p_tag_map 作为第一个参数。这是为了进一步研究 - 本文中未使用它。
现在让我们尝试创建一个处理结果的部件。不幸的是,直到 C23 才支持 typeof(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 Reflection Magic:使用包装器进行简单记录,用于打印任意函数参数和结果的详细内容。更多信息请关注PHP中文网其他相关文章!