ホームページ  >  記事  >  バックエンド開発  >  C Reflection Magic: 任意の関数の引数と結果を出力するためのラッパーを使用した簡単なロギング

C Reflection Magic: 任意の関数の引数と結果を出力するためのラッパーを使用した簡単なロギング

WBOY
WBOYオリジナル
2024-07-18 20:20:31447ブラウズ

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

この記事は、任意の C 関数の引数と結果を自動的にログに記録するヘルパー ラッパーの作成に関する潜在的な実装の側面をいくつか取り上げた研究レポートです。これは、C でもリフレクションが役立つ例の 1 つです。実装は Metac プロジェクトに基づいています。その紹介はこちらの記事でさせていただきました。研究はいくつかの良い結果をもたらしましたが、まだ進行中です。より良い方法で実行できる方法についてのコメントは大歓迎です。

ログはデバッグの重要な方法の 1 つです。適切なログを作成することは、デバッガーを使用せずに潜在的に問題が発生した原因を理解するための鍵となります。しかし、各関数のすべての引数とその結果を出力するのは面倒です。 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_GSYM_LINK(test_function1_with_args); 以来、Metac はリフレクション情報を生成します。コードの中にあります。わかりやすくするために、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 は組み込み関数をカバーするマクロです。 2 番目のパラメータは型です (非常に一般的ではありません)。しかし、そもそもなぜこれに型が必要なのでしょうか?答えは、サイズを理解し、スタックからそれを取得できることです。考えられるすべてのサイズをカバーし、次の引数へのポインターを取得する必要があります。 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 表記をそのまま維持するか、それとももう 1 つの引数を渡しても問題ありません。 - バッファとして使用される関数結果のタイプ。おそらく 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 Reflection Magic: 任意の関数の引数と結果を出力するためのラッパーを使用した簡単なロギングの詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。

声明:
この記事の内容はネチズンが自主的に寄稿したものであり、著作権は原著者に帰属します。このサイトは、それに相当する法的責任を負いません。盗作または侵害の疑いのあるコンテンツを見つけた場合は、admin@php.cn までご連絡ください。