本文是一份研究報告,涵蓋了編寫輔助包裝器的一些潛在實現方面,該包裝器將自動記錄任意 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中文網其他相關文章!