Need to use third-party libraries that depend on spdlog in your Unreal project? Here’s how to simulate the spdlog API using Unreal’s native UE_LOG system, to let you avoid adding extra dependencies while keeping your code clean.

We’ll create a logging utility class that matches the spdlog interface, handling type conversions to FString and formatting along the way.

Implementation

Here’s the code that bridges spdlog and Unreal’s logging system:

#pragma once

#include "Containers/UnrealString.h"
#include "Internationalization/Text.h"
#include "Templates/EnableIf.h"
#include "Templates/IsIntegral.h"
#include "Templates/IsFloatingPoint.h"
#include "Logging/LogMacros.h"
#include <string>

DEFINE_LOG_CATEGORY_STATIC(LogPiperLibrary, Log, All);

/**
 * Class to simulate the spdlog API in Unreal Engine
 * spdlog is used in the original piper 
 */
class spdlog
{
private:
	// Template function for integral types
	template <typename T, typename TEnableIf<TIsIntegral<T>::Value, bool>::Type = 0>
	static FString ToFString(T value)
	{
		// Most integral types won't be larger than signed 64-bit integer
		// TODO: Add support for larger integral types supported by UE (uint64)
		return FString::Printf(TEXT("%lld"), static_cast<int64>(value));
	}

	// Template function for floating-point types
	template <typename T, typename TEnableIf<TIsFloatingPoint<T>::Value, bool>::Type = 0>
	static FString ToFString(T value)
	{
		return FString::Printf(TEXT("%f"), static_cast<double>(value));
	}

	// Specialize for FString
	static FString ToFString(const FString& Value)
	{
		return Value;
	}

	// Specialize for std::string
	static FString ToFString(const std::string& Value)
	{
		return StringCast<TCHAR>(Value.c_str(), Value.size()).Get();
	}

	// Convert the format string to UTF-16 for Unreal Engine
	static FText ConvertFormatString(const char* FormatString)
	{
		// Convert the format string to FText
		return FText::FromString(StringCast<TCHAR>(FormatString).Get());
	}

	// Convert a single argument to FFormatArgumentValue
	template <typename T>
	static FFormatArgumentValue ToFormatArgumentValue(const T& value)
	{
		return FFormatArgumentValue(FText::FromString(ToFString(value)));
	}

	// Specialize for FString
	static FFormatArgumentValue ToFormatArgumentValue(const FString& value)
	{
		return FFormatArgumentValue(FText::FromString(value));
	}

	// Specialize for std::string
	static FFormatArgumentValue ToFormatArgumentValue(const std::string& value)
	{
		return FFormatArgumentValue(FText::FromString(StringCast<TCHAR>(value.c_str(), value.size()).Get()));
	}

	/**
	 * Replace {} placeholders with {0}, {1}, etc
	 * This is necessary because FText::Format does not support {} placeholders
	 * @param FormatString The format string to convert
	 * @return The converted format string
	 */
	static FString ConvertBracedFormatString(const FString& FormatString)
	{
		FString ConvertedFormatString = FormatString;
		int32 PlaceholderIndex = 0;

		while (ConvertedFormatString.Contains(TEXT("{}")))
		{
			FStringBuilderBase Builder;
			Builder = ConvertedFormatString;
			Builder.ReplaceAt(ConvertedFormatString.Find(TEXT("{}")), 2,
			                  FString::Printf(TEXT("{%d}"), PlaceholderIndex++));
			ConvertedFormatString = Builder.ToString();
		}

		return ConvertedFormatString;
	}

	// Format log message with provided arguments
	template <typename... Args>
	static FString FormatLogMessage(const char* FormatString, Args... args)
	{
		// Convert the format string to FText and replace placeholders
		FText FormatText = ConvertFormatString(FormatString);
		FText ConvertedFormatText = FText::FromString(ConvertBracedFormatString(FormatText.ToString()));

		// Create an array to hold the arguments for formatting
		TArray<FFormatArgumentValue> FormatArgs;
		(FormatArgs.Add(ToFormatArgumentValue(args)), ...);

		// Use FText to format the string with arguments
		FText FormattedText = FText::Format(ConvertedFormatText, FormatArgs);
		return FormattedText.ToString();
	}

public:
	//~ Functions for simulating the spdlog API
	template <typename... Args>
	static void error(const char* FormatString, Args... args)
	{
		FString LogMessage = FormatLogMessage(FormatString, args...);
		UE_LOG(LogPiperLibrary, Error, TEXT("%s"), *LogMessage);
	}

	template <typename... Args>
	static void warn(const char* FormatString, Args... args)
	{
		FString LogMessage = FormatLogMessage(FormatString, args...);
		UE_LOG(LogPiperLibrary, Warning, TEXT("%s"), *LogMessage);
	}

	template <typename... Args>
	static void debug(const char* FormatString, Args... args)
	{
		FString LogMessage = FormatLogMessage(FormatString, args...);
		UE_LOG(LogPiperLibrary, Log, TEXT("%s"), *LogMessage);
	}

	template <typename... Args>
	static void info(const char* FormatString, Args... args)
	{
		FString LogMessage = FormatLogMessage(FormatString, args...);
		UE_LOG(LogPiperLibrary, Log, TEXT("%s"), *LogMessage);
	}

	// @formatter:off

	// Dummy functionality to simulate the spdlog API
	struct level { constexpr static bool debug = false; };
	static bool should_log(bool) { return true; }
	// Add more functions as needed

	// @formatter:on
};

Usage

The class lets you use familiar spdlog calls like spdlog::info, spdlog::debug, spdlog::warn, and spdlog::error in your code. Behind the scenes, it routes everything through Unreal’s logging system, so you can avoid adding the spdlog dependency to your project.