1

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?

Hamza Hajeir
  • 119
  • 1
  • 8
  • 1
    I see `printf` and macros as C++ "solutions", I weep... – DevSolar Nov 19 '22 at 19:33
  • Surround the `printf` calls with `#if`s based on the log level. The printf call won't make it into the binary and the the optimizer should take care of the calls to that function (since it does nothing without the `printf`.) – 3Dave Nov 19 '22 at 19:35
  • @DevSolar for embedded code, `printf()` is available and usual, iostream isn't. I'm happy to have any solutions other than macros. – Hamza Hajeir Nov 19 '22 at 19:35
  • I see your point @3Dave, but unfortunately the printf() is shared across all instances, one instance could be using VERBOSE while the other's maximum is ERRORS. – Hamza Hajeir Nov 19 '22 at 19:37
  • @HamzaHajeir There is nothing in your question indicating your environment being limited that way. Do you have access to `std::format` at least? Nothing good comes from mixing C (`printf`, `char *`, varargs) and C++... – DevSolar Nov 19 '22 at 20:57
  • Can you explain how your single instance solution works? There's nothing prohibit from setting `_dLvl` to `DEBUG_VERBOSE` despite you have `#define MAX_LEVEL DEBUG_WARN`. – Ranoiaetep Nov 19 '22 at 21:41
  • And your second solution, seems like now all the `w_log_X` functions will only work on a single logger named `logger`. I don't see how that helps you when you created multiple instances of `Logger` class. – Ranoiaetep Nov 19 '22 at 21:47
  • @DevSolar, It'd be a toolchain issue, wherein I can't take C++20 as granted, the default is C++11 now. – Hamza Hajeir Nov 20 '22 at 09:43
  • @Ranoiaetep, there's a check at setter against the MAX. `Logger` class is used inside other objects, so each object's `logger` is different. I hope it's clear now. – Hamza Hajeir Nov 20 '22 at 09:45

2 Answers2

1

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
Nikos Athanasiou
  • 29,616
  • 15
  • 87
  • 153
  • Thanks, that's a step forward, I'm wondering what's required standard version? – Hamza Hajeir Nov 20 '22 at 09:48
  • An issue there is that kLevel is changeable at runtime, this could be solved by replacing it with the aforementioned `MAX_LEVEL`, and adding another condition inside the `if` statement carries the dynamic `_dLvl`. But an issue would arise then, wherein the templates there at logger class can't know the compile-time value of `MAX_LEVEL` of an instance except at constructor (I think this is a flaw in the software design), so there _might_ be a second template level to achieve that ...? – Hamza Hajeir Nov 20 '22 at 09:55
  • @HamzaHajeir "Compile out" does not happen at runtime. – Nikos Athanasiou Nov 20 '22 at 10:14
  • 1
    @HamzaHajeir `if constexpr` is c++17 https://stackoverflow.com/questions/43434491/difference-between-if-constexpr-vs-if – Nikos Athanasiou Nov 20 '22 at 10:15
  • correct, that's why the ugly proposed solution using macros will compile strings out, but I smell a better solution extended from the answered one. A hypothetical straight solution would be if a class accepts a constexpr value and considers that in inner templates. – Hamza Hajeir Nov 20 '22 at 10:55
  • Is it possible to template the `Logger` class itself by the maximum level, alongwith your provided solution? if so, does the generated classes split out so static members and methods would differ? – Hamza Hajeir Nov 21 '22 at 16:43
  • @HamzaHajeir Can you link an example of how the suggested solution (the one with the preprocessor #ifs) excludes the strings from the compiled binary? Preprocessing happens even before compiling, so if a function/class depends in any way on runtime information, the compiler cannot declare the body of that function empty and optimize it away - it simply doesn't know whether at runtime a variable affecting that code is going to have a value that enables the if. So, yes all what you ask is possible, but it won't compile out a string from a binary. – Nikos Athanasiou Nov 21 '22 at 20:09
  • @HamzaHajeir I have worked in severely restricted environments with esp-idf, and there you can segregate the loggers depending on the phase of the program you run. E.g. when printing at startup (so no user code can tweak the log level and it remains what was specified at compile time) you can compile out strings. On the other hand, runtime logs have a different macro (`ESP_LOG_LEVEL`). Check [this header](https://github.com/espressif/esp-idf/blob/master/components/log/include/esp_log.h) for reference. Again the runtime log commands don't compile the strings out of the binary . – Nikos Athanasiou Nov 21 '22 at 20:15
  • you're correct, the ugly solution provided at the question excludes the strings by macros, so the function `#define w_log_V(f_, ...)` is either empty (replaced by an empty line), or `logger.V((f_), ##__VA_ARGS__)` based on the debug level, the comparison goes totally at compile time, where `WIFI_LOG_MAX_LEVEL` is a compile-time macro. So the usage would be by the controlled macro `w_log_V()`. there's no magic, it's old ugly solution. – Hamza Hajeir Nov 22 '22 at 16:49
  • For ESP-IDF, I'm aware of that, but I'm heading towards achieving dynamically changed logging. where I can set one instance to not be logging, while logging all about other instance. I will try to expand your solution and check what I get. – Hamza Hajeir Nov 22 '22 at 16:52
  • As I told you, I was smelling a solution based on your one, check it out [here](https://godbolt.org/z/1oconM9zM). – Hamza Hajeir Nov 23 '22 at 19:36
  • Because my source has static members, what would happen to them? would they still be shared across all loggers? or splitted based on the ? – Hamza Hajeir Nov 23 '22 at 19:43
  • @HamzaHajeir There is no run-time tweaking in the solution you link. All decisions are made at compile time. Choosing `Logger<2>` over `Logger<3>` would be similar to calling `.info` over `.warning`, all compile-time decisions. I get that maybe you wanted to have types that can be independently tweaked but beware that effects may be confusing. Setting log level to DEBUG (as in the example) and having an `.info('Hello')` being printed is not the most intuitive. – Nikos Athanasiou Nov 23 '22 at 21:02
  • @HamzaHajeir As per your question, methods are not shared across types, meaning `Logger<2>::info` generates a symbol distinct from `Logger<3>::info`. Sizeof `logger` and `logger_2` are not affected by the amount of (non-virutal) methods you declare in their type, but the generated code has to contain symbols for all distinct instantiations. This means that template code can potentially bloat the binary but in your case I don't think it's the case (you have a single template parameter and there's no type combinations happening) – Nikos Athanasiou Nov 23 '22 at 21:12
  • Here's a [complete example](https://godbolt.org/z/dGbnnEezx) with adding the utility of changing debug levels. For static members, I know that C++ would instantiate a different class per different parameter, but does that really happen with different `` value? I'm not experienced with this, but I really doubt; because there's no difference in the type. – Hamza Hajeir Nov 25 '22 at 19:24
  • This [example](https://godbolt.org/z/aePnoGdPc) demonstrates you're correct regarding static members, there's different classes instantiated per '`. – Hamza Hajeir Nov 25 '22 at 19:31
  • There's an issue dealing with godbolt, where it eliminates the string "Doesn't print\n" with preceeding the line with the dynamic 'set_level(DEBUG_NONE)' call ... – Hamza Hajeir Nov 25 '22 at 20:35
  • I've solved the static members issue by using a singleton class, now everything goes as expected and desired! – Hamza Hajeir Nov 28 '22 at 17:10
  • Perhaps you paste the complete example, so I can select the answer! Thank you @Nikos-athanasiou. – Hamza Hajeir Nov 28 '22 at 17:10
0

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..)
}
Hamza Hajeir
  • 119
  • 1
  • 8