C++ Interface for Logging

Published by

on

Knowing what’s happening inside of my pmo DLL is going to be pretty critical to getting things working in UE5. With that in mind I needed a decent logging solution. I found a really nice framework called spdlog which handles all the usual logging stuff you’d want (writing to files etc) and a nice API. The nice API turns out to be because it’s built off of fmtlib the formatting library I was already using!

One problem, you can’t link spdlog in UE5 because pesky macros break everything everywhere all the time. Trying the workaround:

#pragma push_macro("check")
#undef check

#include <fmt/format.h>

#pragma pop_macro("check")

Unfortunately did not work as I started getting other errors as spdlog linked to some data. Why does this matter you ask? Because UE5 wants to delay-load DLLs. Note the first constraint:

  • Imports of data can’t be supported. A workaround is to explicitly handle the data import yourself by using LoadLibrary (or by using GetModuleHandle after you know the delay-load helper has loaded the DLL) and GetProcAddress.

I then realized UE5 doesn’t even need to call spdlog, I just need to use (ha ha macros…) the UE_LOG logging macros.

What I want is an interface class that I can pass to all my objects in my shared library. That would allow me to have my pmoclient/pmoserver executables use a class that implements the interface with spdlog. I’d then have another implementation that was only in UE5 that implements the interface that calls UE_LOG.

There are some other things I want, I want it to be able to take variadac arguments, this way I can pass things like Logger.Info("some format thing num:{} char:{} string:{}", 1 'a', "ABC"); To do this in C++ you use a parameter pack. My first attempt was thus:

template<typename... Args>
class Logger
{
public:
	virtual void Debug(const char* fmt, const Args&... Args) = 0;
	virtual void Info(const char* fmt, const Args&... Args) = 0;
	virtual void Warn(const char* fmt, const Args&... Args) = 0;
	virtual void Error(const char* fmt, const Args&... Args) = 0;
}

The idea being my real implementations would just implement these virtual methods.

If you try to implement this, you’ll get an error such as: use of class template requires template argument list even if calling with the additional <>. After searching around and thinking, I’m not even sure this is possible because the compiler would need to know every possible type in any possible order when creating this type of class. As a shared library, it won’t even have access to all the ways it could be called during runtime!

One thing I’ve realized working on this project is understanding the requirements and constraints for compile time vs runtime. There is some code (particularly with templates) you just can’t do because the compiler can’t know what to expect during runtime.

I started looking around for what I wanted, but it’s not exactly clear what I needed to search for. This medium(ugh) article was close, but not exactly what I wanted in that it wasn’t allowing for parameter pack arguments which I need. Somehow, I then stumbled upon this implementation that was close to what I needed. A stack overflow post aptly titled C++ creating an interface that has variadic template methods.

The first answer had almost exactly what I wanted, except the body was missing, containing only…

// Your homework assignment goes here

For some reason my dumb lizard brain didn’t realize I just needed to format the string and all would be well. Finally, I searched for formatting code and came across this as an option:

namespace cpp_string
{
template<typename ...Args>
inline std::string format(const char *format, Args ...args)
{
    size_t size = snprintf(nullptr, 0, format, args...) + 1; // Extra space for '\0'
    std::unique_ptr<char[]> buf(new char[size]);
    snprintf(buf.get(), size, format, args...);
    return std::string(buf.get(), buf.get() + size - 1); // We don't want the '\0' inside
}

I looked it over for a bit and thought… wait a second this is going to call snprintf twice, and allocate memory? Yeash, no thanks.

Finally, I realized I could just use fmt:format. So I built out the logger.h class:

class Logger
{
public:
	template<typename ...Args>
	void Debug(const char* Format, Args && ...AArgs)
	{
		LogDebug(LogFormat(Format, std::forward<Args>(AArgs)...));
	}
	template<typename ...Args>
	void Info(const char* Format, Args && ...AArgs)
	{
		LogInfo(LogFormat(Format, std::forward<Args>(AArgs)...));
	}
	template<typename ...Args>
	void Warn(const char* Format, Args && ...AArgs)
	{
		LogWarn(LogFormat(Format, std::forward<Args>(AArgs)...));
	}
	template<typename ...Args>
	void Error(const char* Format, Args && ...AArgs)
	{
		LogError(LogFormat(Format, std::forward<Args>(AArgs)...));
	}
private:
	template<typename ...Args>
	inline std::string LogFormat(const char *Format, Args && ...AArgs)
	{
		return std::string(fmt::format(Format, std::forward<Args>(AArgs)...));
	}
	virtual void LogDebug(const std::string &) = 0;
	virtual void LogInfo(const std::string &) = 0;
	virtual void LogWarn(const std::string &) = 0;
	virtual void LogError(const std::string &) = 0;
};

Now my implementations just need to override the LogDebug, LogInfo, LogWarn, LogError private methods and we’d be good to go!

Unfortunately this didn’t work, fmt::format has some constraints requiring format strings being a constant expression. Doing this is also apparently an error in std::format:

As of P2216R3, it is an error if the format string is not a constant expression. std::vformat can be used in this case

OK well fmt exposes a fmt::vformat, maybe that’d work! Sure enough, I just needed to change to vformat and call fmt::make_format_args to get everything working, my updated LogFormat method now looks like:

template<typename ...Args>
inline std::string LogFormat(const char *Format, Args && ...AArgs)
{
	return std::string(fmt::vformat(Format, fmt::make_format_args(std::forward<Args>(AArgs)...)));
}

With this logger base class in place, I can now create my implementations. I created a new sub-CMake project called logging which will create an implement called SpdLogger. This will be linked into my pmoclient/pmoserver executables.

The CMake file just links to fmt and spdlog: target_link_libraries(logger PUBLIC spdlog fmt)

Here’s my SpdLogger class:

// spdlogger.h
class SpdLogger : public Logging::Logger
{
public:
    SpdLogger(std::string LogName, std::string LogFilePath);

private:
    virtual void LogDebug(const std::string &Msg) override;
    
    virtual void LogInfo(const std::string &Msg) override;
    
    virtual void LogWarn(const std::string &Msg) override;

    virtual void LogError(const std::string &Msg) override;

    std::shared_ptr<spdlog::logger> Logger;
};

// spdlogger.cpp
SpdLogger::SpdLogger(std::string LogName, std::string LogFilePath)
{
    Logger = spdlog::basic_logger_mt(LogName, LogFilePath);
    spdlog::set_default_logger(Logger);
};

void SpdLogger::LogDebug(const std::string &Msg)
{
    Logger->debug(Msg);
}

void SpdLogger::LogInfo(const std::string &Msg)
{
    Logger->info(Msg);
}

void SpdLogger::LogWarn(const std::string &Msg) 
{
    Logger->warn(Msg);
}

void SpdLogger::LogError(const std::string &Msg)
{
    Logger->error(Msg);
}

Pretty simple implementation, and now I can call it in pmoclient and pass it to my client/servers:

    auto Log = SpdLogger::SpdLogger("test", "client.log");
    Log.Info("some args {}", 1);
    // pass to client thread code...
    auto ClientThread = net::Client(InQueue, OutQueue, Crypto, ServerSocket, Log);
    // pass to ECS module:
    GameWorld.get_mut<player::Player>()->SetLogger(&Log);

As for UE5, I created a class inside of the PMOModule:

#pragma once

#include "Core.h"
#include "logger/logger.h"

class PMO_API UPMOLogger : public Logging::Logger
{
public:
    UPMOLogger() {};

private:
    virtual void LogDebug(const std::string &Msg) override 
    {
        FString LogMsg(Msg.c_str());
        UE_LOG(LogTemp, Verbose, TEXT("%s"), *LogMsg);
    }; 
    
    virtual void LogInfo(const std::string &Msg) override 
    {
        FString LogMsg(Msg.c_str());
        UE_LOG(LogTemp, Log, TEXT("%s"), *LogMsg);
    };
    
    virtual void LogWarn(const std::string &Msg) override 
    {
        FString LogMsg(Msg.c_str());
        UE_LOG(LogTemp, Warning, TEXT("%s"), *LogMsg);
    };

    virtual void LogError(const std::string &Msg) override 
    {
        FString LogMsg(Msg.c_str());
        UE_LOG(LogTemp, Error, TEXT("%s"), *LogMsg);
    };
};

Some things to note about this class, obviously I inherit from my Logging::Logger from my pmo DLL. But I don’t inherit from UObject, meaning I don’t have this class managed by UE5’s memory manager. (I realize now I should remove the U part of the class name, as U is reserved for UObject managed objects!).

So there we have it, we can now log directly in our UE console from our pmo DLL because we pass in this derived logger class! Horray!


All of the Info level messages are coming from my DLL. This is a success I really need because this project is really throwing a lot of roadblocks my way.

Maybe some day I’ll actually be able to work on the game code.