Home > OS >  Compiling out strings used in print statements in multi-level log - C
Compiling out strings used in print statements in multi-level log - C

Time:12-02

I'm looking for a way to compile print strings out of my binary if a specific macro-based condition is met.

here, _dLvl can be conditionally set equal or lower than the maximum allowed level.


enum DEBUG_LEVELS : int
{
    DEBUG_NONE,
    DEBUG_ERRORS,
    DEBUG_WARN,
    DEBUG_INFO,
    DEBUG_VERBOSE
};

#define MAX_LEVEL  DEBUG_WARN

int _dLvl = DEBUG_ERRORS;

template <typename... Args> void E(const char * _f, Args... args){
    if (_dLvl >= DEBUG_ERRORS){
        printf(_f, args...);
    }
}
template <typename... Args> void W(const char * _f, Args... args){
    if (_dLvl >= DEBUG_WARN){
        printf(_f, args...);
    }
}
template <typename... Args> void I(const char * _f, Args... args){
    if (_dLvl >= DEBUG_INFO){
        printf(_f, args...);
    }
}
template <typename... Args> void V(const char * _f, Args... args){
    if (_dLvl >= DEBUG_VERBOSE){
        printf(_f, args...);
    }
}

int main(){
    E("This will print\n");
    W("This might be printed based on _dLvl, existence of this string is ok.\n");
    I("This wont print ever, the existence of this string is memory waste\n");
    V("Same as I\n");
}

What adds to the challenge that I've multiple instances of a logger class, where each instance would have a different MAX level, see this question for a more clear example of multi-instances.

Here's a solution for my situation (but an ugly and unmanageable onewherein it requires a special macro per instance to be used differently within the source code):

#if (WIFI_LOG_MAX_LEVEL >= 1)
#define w_log_E(f_, ...) logger.E((f_), ##__VA_ARGS__)
#else
#define w_log_E(f_, ...)
#endif

#if (WIFI_LOG_MAX_LEVEL >= 2)
#define w_log_W(f_, ...) logger.W((f_), ##__VA_ARGS__)
#else
#define w_log_W(f_, ...)
#endif

#if (WIFI_LOG_MAX_LEVEL >= 3)
#define w_log_I(f_, ...) logger.I((f_), ##__VA_ARGS__)
#else
#define w_log_I(f_, ...)
#endif

#if (WIFI_LOG_MAX_LEVEL >= 4)
#define w_log_V(f_, ...) logger.V((f_), ##__VA_ARGS__)
#else
#define w_log_V(f_, ...)
#endif

Is there any trick to solve it?

CodePudding user response:

Implement your logging functions in a conditionally compiled block, e.g. constexpr if would be a modern way:

// Sorry just a habit to order severity the other way around
enum DEBUG_LEVELS : int
{
    DEBUG_VERBOSE = 0,
    DEBUG_INFO = 1,
    DEBUG_WARN = 2,
    DEBUG_ERRORS = 3,
    DEBUG_NONE = 4
};

constexpr DEBUG_LEVELS kLevel = DEBUG_LEVELS::DEBUG_WARN;
// ^^^^^^^^^^^^^^^^^^^^^^^^^^ Log level

template <class... Args>
void warning([[maybe_unused]] const char *msg, Args&&... args)
{
    if constexpr (kLevel <= DEBUG_WARN) {
        printf(msg, std::forward<Args>(args)...);
    }
}

template <class... Args>
void info([[maybe_unused]] const char *msg, Args&&... args)
{
    if constexpr (kLevel <= DEBUG_INFO) {
        printf(msg, std::forward<Args>(args)...);
    }
}

int main()
{
    warning("Ruuuun %i", 2);
    info("Fuuuun %i", 2);
}

Demo : Shows that the string "Fuuuuuun" is nowhere to be found, since the info level is lower than the WARN level we chose to compile with. Compiler detects an "empty" function body and optimizes out the call:

unsigned __int64 `__local_stdio_printf_options'::`2'::_OptionsStorage DQ 01H DUP (?) ; `__local_stdio_printf_options'::`2'::_OptionsStorage
`string' DB 'Ruuuun %i', 00H     ; `string'

msg$ = 8
<args_0>$ = 16
void warning<int>(char const *,int &&) PROC                  ; warning<int>, COMDAT
        mov     edx, DWORD PTR [rdx]
        jmp     printf
void warning<int>(char const *,int &&) ENDP                  ; warning<int>

msg$ = 8
<args_0>$ = 16
void info<int>(char const *,int &&) PROC               ; info<int>, COMDAT
        ret     0
void info<int>(char const *,int &&) ENDP               ; info<int>

main    PROC                                            ; COMDAT
$LN6:
        sub     rsp, 40                             ; 00000028H
        mov     edx, 2
        lea     rcx, OFFSET FLAT:`string'
        call    printf
        xor     eax, eax
        add     rsp, 40                             ; 00000028H
        ret     0
main    ENDP

CodePudding user response:

Here's a complete answer, it's based on Nikos Athanasiou's answer (Thanks Nikos).

What's added is a templated class per DEBUG_LEVELS enum, which defines the MAX_LEVEL, which would be used in constexpr if statement at compile time, to compile out unused strings.

#include <utility>
#include <cstdio>

enum DEBUG_LEVELS : int
{
    DEBUG_NONE,
    DEBUG_ERRORS,
    DEBUG_WARN,
    DEBUG_INFO,
    DEBUG_VERBOSE
};

template <int MAX_LEVEL>
class Logger {
    DEBUG_LEVELS dLvl;
public:
    void set_level(DEBUG_LEVELS level) {
        if (level > MAX_LEVEL)
            dLvl = static_cast<DEBUG_LEVELS>(MAX_LEVEL);
        else
            dLvl = level;
    }
    Logger(DEBUG_LEVELS defaultLvl) {
        if (defaultLvl > MAX_LEVEL)
            dLvl = static_cast<DEBUG_LEVELS>(MAX_LEVEL);
        else
            dLvl = defaultLvl;
    }
    template <class... Args>
    void warning([[maybe_unused]] const char *msg, Args&&... args)
    {
        if constexpr (MAX_LEVEL >= DEBUG_WARN) {
            if (dLvl >= DEBUG_WARN)
                printf(msg, std::forward<Args>(args)...);
        }
    }

    template <class... Args>
    void info([[maybe_unused]] const char *msg, Args&&... args)
    {
        if constexpr (MAX_LEVEL >= DEBUG_INFO) {
            if (dLvl >= DEBUG_INFO)
                printf(msg, std::forward<Args>(args)...);
        }
    }
};

Logger<DEBUG_WARN> logger(DEBUG_WARN);
Logger<DEBUG_INFO> logger_2(DEBUG_INFO);
int main()
{
    logger.warning("Ruuuun %i\n", 2);
    logger.info("Fuuuun %i\n", 2);
    logger_2.info("Hello\n");
    logger_2.set_level(DEBUG_NONE);
    logger_2.info("Doesn't print\n");  // Dynamically set (But the whole call and related string are optimised by the compiler..)
}
  • Related