jeudi 3 octobre 2019

Writing custom [s]printf using variadic template in C++ for Arduino

Generally, I would like to implement a SerialLog class containing a collection of methods that format strings using a printf-style API and output the compiled message to the Arduino serial console using the Serial.print() method contained within the basic arduino library.

Such code would enable much cleaner serial console log invocations within my code (the below begins by showing the required nested function calls of the core Arduino c++ library, whereas the latter two calls show the API I would like to implement):

# normal debug logging for Arduino using the provided functions
Serial.log(sprintf("A log message format string: %d/%f", 123, 456.78));

# the call I would like to use for string formatting
String message = SerialLog.sprintf("A log message format string: %d/%f", 123, 456.78);

# the call I would like to use to output a formatted string
SerialLog.printf("A log message format string: %d/%f", 123, 456.78);

As can be seen in the above examples, my intention is to create a set of class methods for serial console output with arguments that mirror the normal C++ printf function.

I've attempted to implement such a printf-style API using simple variadic definitions like myVariadicPrintfFunction(const char * format, ...) but such a function definition appears to require that all arguments are of type const char *. This is not the behavior I want. As such, my current implementation uses templates to enable arguments of any type (obviously the type must be ultimately acceptable to the C++ core printf function, though).

My implementation includes the following public methods within a SerialLog class:

  • SerialLog::sprint (String sprint(const char * format)): Accepts a const char * argument. Returns the string as the Arduino String object.

  • SerialLog::sprintf (template <typename ...Args> String sprintf(const char * format, Args ...args)): Accepts a const char * argument as the format string and any number of additional arguments (of various types) which will be substituted within the format string. Returns the string as the Arduino String object.

  • SerialLog::print (SerialLog& print(const char * format)): Same as SerialLog::sprint to output the string to the serial console using Serial.print() instead of simply returning it.

  • SerialLog::printf (template <typename ...Args> SerialLog& printf(const char * format, Args ...args)): Uses the return value of SerialLog::sprintf to output the string to the serial console using Serial.print() instead of simply returning it.

As with the normal C++ printf function, both SerialLog::sprintf and SerialLog::printf must accept a format string as the first argument followed by any number of acceptable argument of any acceptable type which are used as the substitution values for the provided format string.

For example, a format of "This %s contains %d substituted %s such as this float: %d." with additional arguments of string (as a char *), 4 (as an int), "values" (as a char *), and 123.45 (as a float) would result in the following compiled string: "This string contains 4 substituted values such as this float: 123.45.".

I have been unable to achieve the described behavior using the following code:

debug.h

#include <stdio.h>
#include <Arduino.h>

namespace Debug
{
    class SerialLog
    {
        public:

            String sprint(const char * format);

            template <typename ...Args>
            String sprintf(const char * format, Args ...args);

            SerialLog& print(const char * format);

            template <typename ...Args>
            SerialLog& printf(const char * format, Args ...args);

    } /* END class SerialLog */

} /* END namespace Debug */

debug.cpp

#include <debug.h>

namespace Debug
{
    String SerialLog::sprint(const char * format)
    {
        return String(format);
    }

    template <typename ...Args>
    String SerialLog::sprintf(const char * format, Args ...args)
    {
        char buffer[256];

        snprintf(buffer, 256, format, args...);

        return String(buffer);
    }

    SerialLog& SerialLog::print(const char * format)
    {
        Serial.print(format);

        return *this;
    }

    template <typename ...Args>
    SerialLog& SerialLog::printf(const char * format, Args ...args)
    {
        Serial.print(this->sprintf(format, args...));

        return *this;
    }

} /* END namespace Debug */

At this time, the follow errors occur during compilation:

C:\Temp\ccz35B6U.ltrans0.ltrans.o: In function `setup':
c:\arduino-app/src/main.cpp:18: undefined reference to `String RT::Debug::SerialLog::sprintf<char const*>(char const*, char const*)'
c:\arduino-app/src/main.cpp:22: undefined reference to `RT::Debug::SerialLog& RT::Debug::SerialLog::printf<char const*>(char const*, char const*)'        
c:\arduino-app/src/main.cpp:26: undefined reference to `RT::Debug::SerialLog& RT::Debug::SerialLog::printf<char const*>(char const*, char const*)'
c:\arduino-app/src/main.cpp:29: undefined reference to `RT::Debug::SerialLog& RT::Debug::SerialLog::printf<char const*>(char const*, char const*)'        
c:\arduino-app/src/main.cpp:30: undefined reference to `RT::Debug::SerialLog& RT::Debug::SerialLog::printf<char const*, int, double>(char const*, char const*, int, double)'
collect2.exe: error: ld returned 1 exit status
*** [.pio\build\debug\firmware.elf] Error 1

Note: The above code is extracted from a larger Debug namespace and an expanded SerialLog class that contains additional methods, so the following error message line numbers will not correctly represent the example code shown.

The full VSCode build log (using the PlatformIO extension) can be located as a Gist at gist.github.com/robfrawley/7ccbdeffa064ee522a18512b77d7f6f9. Moreover, the entire project codebase can be referenced at github.com/src-run/raspetub-arduino-app, with the relevant projects for this question located at lib/Debug/Debug.h and lib/Debug/Debug.cpp.

Lastly, while I am proficient in many other languages like Python, PHP, Ruby, and others, this is the first C++ project! I am learning the C++ language through this application's implementation and am aware that many suboptimal choices exist within the codebase; different aspects of this application will be amended and improved as my knowledge of C++ evolves. As such, I am not particularly interested in comments regarding deficiencies in my implementation or verbose opinion pieces explaining the shortcomings in my understanding of C++. Please keep any discussion focused on the singular question outlined above.

Thanks for taking the time to read through this entire question and I greatly appreciate any assistance provided!

Aucun commentaire:

Enregistrer un commentaire