6

I have a standard logging API built into my C code, a nice simple logF(const char *pFormat, ...) thing which has always, so far, been mapped to vprintf(), i.e.:

void logF(const char *pFormat, ...)
{
    va_list args;
    va_start(args, pFormat);
    vprintf(pFormat, args);
    va_end(args);
}

The C code above this API must work on multiple embedded platforms. I've just reached a platform (Nordic NRF52840) where the underlying logging interface I have to work with is presented as a variadic macro of the form NRF_LOG_INFO(...).

QUESTION: how do I correctly pass fn(const char *pFormat, ...) into a BLAH(...) macro? My brain hurts....

This is with GCC 4.9.3, though it would be nice to have a solution that is relatively relaxed about C compiler version.

EDIT 1: noted that I could redefine my logF() function to be a variadic macro and map it there, the issue is that I would then have a platform-specific header file rather than a generic one, I would have to move it down into the platform code and have one for each. Not impossible but more messy.

EDIT 2: I was asked for the trail of how NRF_LOG_INFO() expands. Here's the relevant output of the pre-processor:

#define NRF_LOG_INFO(...) NRF_LOG_INTERNAL_INFO( __VA_ARGS__)

#define NRF_LOG_INTERNAL_INFO(...) NRF_LOG_INTERNAL_MODULE(NRF_LOG_SEVERITY_INFO, NRF_LOG_SEVERITY_INFO, __VA_ARGS__)

#define NRF_LOG_INTERNAL_MODULE(level,level_id,...) if (NRF_LOG_ENABLED && (NRF_LOG_LEVEL >= level) && (level <= NRF_LOG_DEFAULT_LEVEL)) { if (NRF_LOG_FILTER >= level) { LOG_INTERNAL(LOG_SEVERITY_MOD_ID(level_id), __VA_ARGS__); } }

#define LOG_INTERNAL(type,...) LOG_INTERNAL_X(NUM_VA_ARGS_LESS_1( __VA_ARGS__), type, __VA_ARGS__)

#define LOG_INTERNAL_X(N,...) CONCAT_2(LOG_INTERNAL_, N) (__VA_ARGS__)

Then depending on number of args, anything up to:

#define LOG_INTERNAL_6(type,str,arg0,arg1,arg2,arg3,arg4,arg5) nrf_log_frontend_std_6(type, str, (uint32_t)(arg0), (uint32_t)(arg1), (uint32_t)(arg2), (uint32_t)(arg3), (uint32_t)(arg4), (uint32_t)(arg5))

void nrf_log_frontend_std_6(uint32_t severity_mid,
                            char const * const p_str,
                            uint32_t val0,
                            uint32_t val1,
                            uint32_t val2,
                            uint32_t val3,
                            uint32_t val4,
                            uint32_t val5);
Rob
  • 865
  • 1
  • 8
  • 21
  • 1
    You could rename your variadic function and use a variadic wrapper macro which calls either your own function or the platform specific macro. – Bodo Mar 25 '20 at 14:07
  • @MOehm: it's the other way around, I'm inside a function which has received `const char pFormat, ...`, how do I shove that down `BLAH(...)`. – Rob Mar 25 '20 at 14:09
  • Have you looked at what the variadic macro does inside? Can you post that? – John Zwinck Mar 25 '20 at 14:10
  • What does `NRF_LOG_INFO` expand to? – vgru Mar 25 '20 at 14:10
  • @Bodo, agreed, I was trying to avoid that though as the header file is part of the API and should be platform independent. To do that I'd need to move the header down into the platform code and have one for each platform. Not impossible, just more messy. – Rob Mar 25 '20 at 14:10
  • @Groo, @John Zwinck: ah, well, they like their abstractions over in the NRF5 SDK. First it becomes `NRF_LOG_INTERNAL_INFO( __VA_ARGS__)`, will dig to see where it goes from there... – Rob Mar 25 '20 at 14:12
  • @Rob: No need to dig if you don't want to, you can just preprocess the source code and see what it expands to for a few sample usages. With GCC the option to preprocess without compiling is `-E`. – John Zwinck Mar 25 '20 at 14:13
  • @Rob Please [edit] your question and add all relevant information there. (e.g. what you replied to my comment) – Bodo Mar 25 '20 at 14:16
  • If you are asking how a function with variable arguments can pass its arguments to a macro with variable arguments, this is fundamentally impossible. The mechanism for processing arguments of a function with variable arguments, with the `va_list` defined in ``, operates when the program is executing. The mechanism for passing arguments to a macro operates during program translation (compilation). The run-time information is simply not available at translation time. A workaround is to parse the format string yourself and use invocations of the macro with one value at a time. – Eric Postpischil Mar 25 '20 at 14:19
  • @Bodo, and John Zwinck see edits. – Rob Mar 25 '20 at 14:24
  • @EricPostpischil: I was afraid someone would say that. Looks like I'll have to present my `logF()` as a variadic macro and have a header file down in each platform directory. – Rob Mar 25 '20 at 14:26

1 Answers1

4

It is not possible to pass the arguments from a variadic function to a variadic macro.

As you want to hide the platform specific macro call from the API header you can process the function arguments with vsnprintf instead of vprintf and call the logging macro with format "%s" and the resulting string buffer.

void logF(const char *pFormat, ...)
{
    va_list args;
    /* Choose a reasonable size or replace with dynamic allocation based on the return value of vsnprintf */
    /* This could also be a static variable or a global variable to avoid allocation of a big buffer on the stack. */
    char buffer[1024];

    va_start(args, pFormat);
    vsnprintf(buffer, sizeof(buffer), pFormat, args);

    NRF_LOG_INFO("%s", buffer);

    va_end(args);
}

Note that you may have to call NRF_LOG_FLUSH before the buffer goes out of scope or gets overwritten. See https://devzone.nordicsemi.com/f/nordic-q-a/22647/nrf_log_info-how-to-print-log-with-string-parameter

Bodo
  • 9,287
  • 1
  • 13
  • 29
  • Good post. Only thing I have to ask myself now is if I can afford the buffer space. Probably will avoid `malloc()` as I don't have much memory to play with and it might fragment. – Rob Mar 25 '20 at 14:27