1

---- Question ----

I'm attempting to convert the file name "/path/to/Module.cpp" into a const char* holding the value "Module" compile-time. This helps me print ergonomic logging in a microcontroller. I use GCC 8.3.

There are lots of fine examples on how to strip only the path component, e.g. an adaptation of this answer

constexpr const char* filename(std::string_view path) {
    return path.substr(path.find_last_of('/') + 1).data();
}

static const char* TAG = filename(__FILE__);

This results in TAG holding "Module.cpp". That's a good start, but I'd like to strip the ".cpp" as well. Compile-time, naturally. Any ideas?

---- Answer ----

Thanks to @KamilCuk I was able to come up with the following header file which, when included by a cpp file, creates a nice tag for ESP IDF logging macros:

#ifndef _LOG_HPP_
#define _LOG_HPP_

#include "esp_log.h"
#include <string_view>

// Note: path processing naïvely assumes a valid Unix file path containing
// directories and an extension.

/**
 * Find the length of stem in a file name
 * @param path A file name with '/' as path separator and '.' as extension separator
 * @return Number of characters in file stem excluding terminating zero
 */
constexpr size_t stemNameLen(const std::string_view& path) {
    return path.find_last_of('.') - path.find_last_of('/') - 1;
}

// Rudimentary unit test
static_assert(stemNameLen(std::string_view("../foo/bar/MyModule.cpp")) == 8);

/**
 * Get the stem in a file name
 * @param path A file name with '/' as path separator and '.' as extension separator
 * @return A string_view holding the stem of the input file name
 */
constexpr std::string_view stemName(const std::string_view& path) {
    return path.substr(path.find_last_of('/') + 1, stemNameLen(path));
}

// Rudimentary unit test
static_assert(stemName(std::string_view("../foo/bar/MyModule.cpp")) == "MyModule");

/// Helper class for creating a C-style zero-terminated string from a string_view
template <size_t N>
class TerminatedString {
public:
    constexpr TerminatedString(const std::string_view& path) {
        size_t i = 0;
        for (auto it = path.cbegin(); i + 1 < sizeof(_str) && it != path.cend(); i++, it++) {
            _str[i] = *it;
        }
    }
    constexpr const char *str() const {
        return _str;
    }
private:
    char _str[N] {'\0', };
};

/// Module name from the file which includes this header
static constexpr std::string_view moduleName = stemName(__BASE_FILE__);
/// A zero-terminated log prefix from module name, initialized compile-time
static constexpr TerminatedString<moduleName.length() + 1> logPrefix{moduleName};

// Sanity check, assumes all file stems in project are less than 100 chars
static_assert(moduleName.length() < 100);

#define err(args...) ESP_LOGE(logPrefix.str(), args)
#define warn(args...) ESP_LOGW(logPrefix.str(), args)
#define info(args...) ESP_LOGI(logPrefix.str(), args)
#define debug(args...) ESP_LOGD(logPrefix.str(), args)
#define trace(args...) ESP_LOGV(logPrefix.str(), args)

#endif // _LOG_HPP_
Tarmo
  • 3,728
  • 1
  • 8
  • 25
  • Not sure that's something the compiler can do. That's pretty involved function call that requires dynamic allocation. Also how long-lived would that temporary be, anyway? – tadman Feb 09 '21 at 20:39
  • 1
    Yeah, I have to admit it's a bit of a challenge, that's why I'm turning to SO :) Anyway, the resulting variable would live for the duration of the program's execution (i.e. forever) - hopefully in the code section, not any of the data sections (to save RAM). – Tarmo Feb 09 '21 at 20:41
  • Well, this is less a challenge and more like trying to achieve cold fusion using paperclips and glue. It's really not going to happen. – tadman Feb 09 '21 at 20:42
  • Maybe. But I see that the compiler is happy to produce any sub-string (including cuts from the right side) from a `string_view` object compile-time. Hopefully I'm just too dumb to convert this into a zero terminated C string. – Tarmo Feb 09 '21 at 20:54

1 Answers1

3

The following "works", but it's not perfectly clean. Cleaning it up is left as an exercise. Anyway, it may show you how to do it:

#include <cstdio>

constexpr unsigned long filename_we_size(const char *path) {
    // I know - some C pointer stuff. I don't know which C++ functions are 
    // constexpr which are not, and I am too lazy to check, so I used C pointers. 
    // Preferably rewrite it in more C++-ish style.
    auto i = path;
    while (*i) ++i;
    auto end = i;
    while (*i != '.' && i != path) --i;
    const auto ext_len = end - i;
    while (*i != '/' && i != path) --i;
    const auto filename_len = end - i;
    return filename_len - ext_len;
}

constexpr const char *filename_we(const char *path, char *out) {
    auto i = path;
    while (*i) ++i;
    while (*i != '/' && i != path) --i;
    if (*i) ++i;
    auto r = out;
    while (*i != '.' && *i) *r++ = *i++;
    *r = 0;
    return r;
}

// A structure. *Something* has to store the memory.
template <size_t N>
struct Tag {
    char mem[N]{};
    constexpr Tag(const char *path) {
        filename_we(path, mem);
    }
    constexpr const char *str() const {
        return mem;
    }
    constexpr operator const char *() const{
        return mem;
    }    
    constexpr char operator[](size_t i) const {
        return mem[i];
    }
};

static constexpr Tag<filename_we_size(__FILE__)> TAG{__FILE__};

int main() {
    printf("%s\n", TAG.str());
}
KamilCuk
  • 120,984
  • 8
  • 59
  • 111
  • That this works is both terrifying and cool at the same time. – tadman Feb 09 '21 at 21:06
  • Cool. Definitely works as specified. The immediate problem here is that the code for `Tag::str()` gets duplicated for every new value of N (checked with nm), which is a bummer if you're short on Flash. Otherwise very cool. – Tarmo Feb 09 '21 at 21:21
  • `that the code for Tag::str()` ? What code? It should get optimized by the compiler. `Tag::str()` is just returning an address to the object, I believe it should get optimized away with even the most simple optimization. – KamilCuk Feb 09 '21 at 21:23
  • Oh, didn't turn on optimization. Maybe I should name all my files in the 8.3 format :) – Tarmo Feb 09 '21 at 21:26
  • This is a good solution, I adapted it for my conditions and it seems to work perfectly. No code in sight in the elf file, just the tag variables. Updated the question with my adaptation. Thank you! – Tarmo Feb 10 '21 at 11:11