Last active
November 30, 2025 14:42
-
-
Save Solessfir/eb0df57297f8a61f0c598629b0a78865 to your computer and use it in GitHub Desktop.
C++23 std::print-style Single Header Logging library for Unreal Engine 5.2+
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| /** | |
| * Copyright (c) Solessfir under MIT license | |
| * | |
| * #define EASY_LOG_CATEGORY LogMyGame // Optional (must be defined before include) | |
| * #include "EasyLog.h" | |
| * | |
| * Usage Examples: | |
| * | |
| * // Basic | |
| * LOG_DISPLAY("Value is {}", MyInt); | |
| * | |
| * // Extended (Key, Duration) | |
| * LOG_DISPLAY_EX(-1, 5.f, "Value is {}", MyInt); | |
| * | |
| * // Positional args | |
| * LOG_DISPLAY("Value is {1}, expected {0}", Foo, Bar); // Value is Bar, expected Foo | |
| * | |
| * // Formatting Specifiers | |
| * LOG_DISPLAY("Int: {:03}", 7); // Output: Int: 007 | |
| * LOG_DISPLAY("Float: {:.2}", 3.14159); // Output: Float: 3.14 | |
| * | |
| * // Hex and Binary | |
| * int32 Val = 255; | |
| * LOG_DISPLAY("Hex: {:#x}", Val); // Output: Hex: 0xff | |
| * LOG_DISPLAY("Bin: {:#b}", Val); // Output: Bin: 0b11111111 | |
| * | |
| * // Pointer Address | |
| * LOG_DISPLAY("Ptr: {:#x}", this); // Output: Ptr: 0x00... | |
| * | |
| * // Container Support (TArray, TSet) | |
| * TArray<float> Values = {1.11f, 2.22f}; | |
| * LOG_DISPLAY("Values: {:.1}", Values); // Output: Values: [1.1, 2.2] | |
| * | |
| * // UEnum and FGameplayTag Support | |
| * LOG_DISPLAY("State: {}", EMyEnum::Walking); | |
| * LOG_DISPLAY("Tag: {}", MyTag); | |
| * | |
| * // Conditional Logging | |
| * CLOG_ERROR(Health <= 0, "Actor {} died!", this); | |
| */ | |
| #pragma once | |
| #include "CoreMinimal.h" | |
| #include "Engine/Engine.h" | |
| #include "Logging/StructuredLog.h" | |
| #include <source_location> | |
| #include <concepts> | |
| #include <cstring> | |
| #include <cstdarg> | |
| #ifndef EASY_LOG_CATEGORY | |
| #define EASY_LOG_CATEGORY EasyLog | |
| #endif | |
| // Helper to expand macro before definition | |
| #define EASY_LOG_DEFINE_CATEGORY_INTERNAL(Category) DEFINE_LOG_CATEGORY_STATIC(Category, Log, All) | |
| EASY_LOG_DEFINE_CATEGORY_INTERNAL(EASY_LOG_CATEGORY); | |
| #undef EASY_LOG_DEFINE_CATEGORY_INTERNAL | |
| #if !(UE_BUILD_SHIPPING || UE_BUILD_TEST) | |
| #define LOG_DISPLAY(Format, ...) \ | |
| Easy::Log<ELogVerbosity::Display>(Easy::Loc(), 10.f, Format, ##__VA_ARGS__) | |
| #define LOG_WARNING(Format, ...) \ | |
| Easy::Log<ELogVerbosity::Warning>(Easy::Loc(), 10.f, Format, ##__VA_ARGS__) | |
| #define LOG_ERROR(Format, ...) \ | |
| Easy::Log<ELogVerbosity::Error>(Easy::Loc(), 10.f, Format, ##__VA_ARGS__) | |
| #define CLOG_DISPLAY(Condition, Format, ...) \ | |
| if (Condition) Easy::Log<ELogVerbosity::Display>(Easy::Loc(), 10.f, Format, ##__VA_ARGS__) | |
| #define CLOG_WARNING(Condition, Format, ...) \ | |
| if (Condition) Easy::Log<ELogVerbosity::Warning>(Easy::Loc(), 10.f, Format, ##__VA_ARGS__) | |
| #define CLOG_ERROR(Condition, Format, ...) \ | |
| if (Condition) Easy::Log<ELogVerbosity::Error>(Easy::Loc(), 10.f, Format, ##__VA_ARGS__) | |
| #define LOG_DISPLAY_EX(Key, Duration, Format, ...) \ | |
| Easy::Log<ELogVerbosity::Display>(Key, Duration, Easy::Loc(), Format, ##__VA_ARGS__) | |
| #define LOG_WARNING_EX(Key, Duration, Format, ...) \ | |
| Easy::Log<ELogVerbosity::Warning>(Key, Duration, Easy::Loc(), Format, ##__VA_ARGS__) | |
| #define LOG_ERROR_EX(Key, Duration, Format, ...) \ | |
| Easy::Log<ELogVerbosity::Error>(Key, Duration, Easy::Loc(), Format, ##__VA_ARGS__) | |
| #else | |
| #define LOG_DISPLAY(...) | |
| #define LOG_WARNING(...) | |
| #define LOG_ERROR(...) | |
| #define CLOG_DISPLAY(...) | |
| #define CLOG_WARNING(...) | |
| #define CLOG_ERROR(...) | |
| #define LOG_DISPLAY_EX(...) | |
| #define LOG_WARNING_EX(...) | |
| #define LOG_ERROR_EX(...) | |
| #endif | |
| namespace Easy | |
| { | |
| // Concepts | |
| template<typename ArgType> concept CIsStringArg = std::constructible_from<FStringFormatArg, ArgType>; | |
| template<typename ArgType> concept CHasToString = requires(ArgType t) { { t.ToString() } -> std::convertible_to<FString>; }; | |
| template<typename ArgType> concept CIsActor = std::derived_from<std::remove_pointer_t<std::remove_cvref_t<ArgType>>, AActor>; | |
| template<typename ArgType> concept CIsUObject = std::derived_from<std::remove_pointer_t<std::remove_cvref_t<ArgType>>, UObject>; | |
| template<typename ArgType> concept CIsUEnum = std::is_enum_v<std::remove_cvref_t<ArgType>> && requires { StaticEnum<std::remove_cvref_t<ArgType>>(); }; | |
| template<typename ArgType> concept CIsRawEnum = std::is_enum_v<std::remove_cvref_t<ArgType>> && !CIsUEnum<ArgType>; | |
| template<typename ArgType> concept CIsContainer = requires(ArgType t) { std::begin(t); std::end(t); } && !CIsStringArg<ArgType> && !std::is_same_v<ArgType, FString>; | |
| struct FParsedFormat | |
| { | |
| FString FixedString; | |
| TArray<FString> Specifiers; | |
| }; | |
| struct FormatStringHelper | |
| { | |
| static FParsedFormat Parse(const FString& InFormat) | |
| { | |
| if (!InFormat.Contains(TEXT("{"))) | |
| { | |
| return { InFormat, {} }; | |
| } | |
| FParsedFormat Result; | |
| Result.FixedString.Reserve(InFormat.Len() + 16); | |
| int32 ArgIndex = 0; | |
| const int32 Len = InFormat.Len(); | |
| for (int32 i = 0; i < Len; ++i) | |
| { | |
| const TCHAR Char = InFormat[i]; | |
| if (Char == '{') | |
| { | |
| if (i + 1 < Len && InFormat[i + 1] == '{') | |
| { | |
| Result.FixedString.AppendChar('{'); Result.FixedString.AppendChar('{'); i++; | |
| continue; | |
| } | |
| int32 CloseIdx = -1; | |
| for (int32 j = i + 1; j < Len; ++j) | |
| { | |
| if (InFormat[j] == '}') | |
| { | |
| CloseIdx = j; | |
| break; | |
| } | |
| } | |
| if (CloseIdx != -1) | |
| { | |
| FString Content = InFormat.Mid(i + 1, CloseIdx - (i + 1)); | |
| FString Specifier; | |
| int32 ColonIdx; | |
| // Extract specifier (e.g. "02" from "{:02}" or "{0:02}") | |
| if (Content.FindChar(TEXT(':'), ColonIdx)) | |
| { | |
| Specifier = Content.RightChop(ColonIdx + 1); | |
| Specifier.TrimStartAndEndInline(); | |
| Content = Content.Left(ColonIdx); | |
| } | |
| if (Content.IsEmpty()) | |
| { | |
| // Case: {} or {:spec} -> Auto-Increment Index | |
| Result.FixedString.AppendChar('{'); Result.FixedString.AppendInt(ArgIndex); Result.FixedString.AppendChar('}'); | |
| if (Result.Specifiers.Num() <= ArgIndex) Result.Specifiers.SetNum(ArgIndex + 1); | |
| Result.Specifiers[ArgIndex] = Specifier; | |
| ArgIndex++; | |
| } | |
| else if (Content.IsNumeric()) | |
| { | |
| // Case: {0} or {0:spec} -> Explicit Index | |
| Result.FixedString.AppendChar('{'); Result.FixedString += Content; Result.FixedString.AppendChar('}'); | |
| const int32 ExplicitIdx = FCString::Atoi(*Content); | |
| if (Result.Specifiers.Num() <= ExplicitIdx) Result.Specifiers.SetNum(ExplicitIdx + 1); | |
| Result.Specifiers[ExplicitIdx] = Specifier; | |
| } | |
| else | |
| { | |
| // Unsupported Case: {Name} -> Treat as literal text | |
| // Append the entire original block including braces | |
| Result.FixedString.AppendChars(&InFormat[i], CloseIdx - i + 1); | |
| } | |
| i = CloseIdx; | |
| continue; | |
| } | |
| } | |
| Result.FixedString.AppendChar(Char); | |
| } | |
| return Result; | |
| } | |
| }; | |
| struct SafeFormatter | |
| { | |
| static FString RunFormat(const TCHAR* Fmt, ...) | |
| { | |
| TCHAR Buffer[128]; | |
| va_list Ap; | |
| va_start(Ap, Fmt); | |
| FCString::GetVarArgs(Buffer, 128, Fmt, Ap); | |
| va_end(Ap); | |
| return FString(Buffer); | |
| } | |
| static FString ToBinary(const int64 Value, const bool bUsePrefix) | |
| { | |
| FString Result; | |
| uint64 UVal = static_cast<uint64>(Value); | |
| if (UVal == 0) | |
| { | |
| Result = TEXT("0"); | |
| } | |
| else | |
| { | |
| while (UVal > 0) | |
| { | |
| Result = (UVal & 1 ? TEXT("1") : TEXT("0")) + Result; | |
| UVal >>= 1; | |
| } | |
| } | |
| return bUsePrefix ? TEXT("0b") + Result : Result; | |
| } | |
| static FString FormatInt(const int64 Value, const FString& Spec) | |
| { | |
| if (Spec.EndsWith(TEXT("b"), ESearchCase::IgnoreCase)) | |
| { | |
| return ToBinary(Value, Spec.Contains(TEXT("#"))); | |
| } | |
| if (Spec.EndsWith(TEXT("x"), ESearchCase::IgnoreCase)) | |
| { | |
| FString HexSpec = Spec; | |
| if (HexSpec.Contains(TEXT("x"))) HexSpec.ReplaceInline(TEXT("x"), TEXT("llx")); | |
| else if (HexSpec.Contains(TEXT("X"))) HexSpec.ReplaceInline(TEXT("X"), TEXT("llX")); | |
| const FString Fmt = TEXT("%") + HexSpec; | |
| return RunFormat(*Fmt, Value); | |
| } | |
| const FString Fmt = TEXT("%") + Spec + TEXT("lld"); | |
| return RunFormat(*Fmt, Value); | |
| } | |
| static FString FormatFloat(const double Value, const FString& Spec) | |
| { | |
| const FString Fmt = TEXT("%") + Spec + TEXT("f"); | |
| return RunFormat(*Fmt, Value); | |
| } | |
| }; | |
| // Argument Formatting | |
| template <typename ArgType> | |
| FString ElementToString(const ArgType& Value, const FString& Spec = FString()) | |
| { | |
| using RawType = std::remove_cvref_t<ArgType>; | |
| // Literal nullptr | |
| if constexpr (std::is_same_v<RawType, std::nullptr_t>) return TEXT("nullptr"); | |
| // Pointers (Check for address formatting request) | |
| else if constexpr (std::is_pointer_v<RawType>) | |
| { | |
| // If format string provided (e.g. {:#x}), treat pointer address as int64 and format it | |
| if (!Spec.IsEmpty()) | |
| { | |
| return SafeFormatter::FormatInt(reinterpret_cast<int64>(Value), Spec); | |
| } | |
| // Otherwise standard pointer handling | |
| if constexpr (CIsUObject<RawType>) | |
| { | |
| if (IsInGameThread() && IsValid(Value)) | |
| { | |
| if constexpr (CIsActor<RawType>) return Value->GetActorNameOrLabel(); | |
| return Value->GetName(); | |
| } | |
| return IsValid(Value) ? TEXT("[AsyncUObject]") : TEXT("None"); | |
| } | |
| else if constexpr (std::is_same_v<std::remove_pointer_t<RawType>, TCHAR>) | |
| { | |
| return Value ? FString(Value) : TEXT("(NullString)"); | |
| } | |
| else | |
| { | |
| // Generic pointer default format | |
| return FString::Printf(TEXT("0x%p"), Value); | |
| } | |
| } | |
| // Float | |
| else if constexpr (std::is_floating_point_v<RawType>) | |
| { | |
| if (!Spec.IsEmpty()) | |
| { | |
| return SafeFormatter::FormatFloat(static_cast<double>(Value), Spec); | |
| } | |
| return FString::SanitizeFloat(Value); | |
| } | |
| // Integer and Enum (Raw) | |
| else if constexpr (std::is_integral_v<RawType> && !std::is_same_v<RawType, bool>) | |
| { | |
| if (!Spec.IsEmpty()) | |
| { | |
| return SafeFormatter::FormatInt(static_cast<int64>(Value), Spec); | |
| } | |
| return LexToString(Value); | |
| } | |
| // UEnum | |
| else if constexpr (CIsUEnum<RawType>) | |
| { | |
| if (const UEnum* EnumPtr = StaticEnum<RawType>()) | |
| { | |
| return EnumPtr->GetNameStringByValue(static_cast<int64>(Value)); | |
| } | |
| return LexToString(static_cast<int64>(Value)); | |
| } | |
| // Standard | |
| else if constexpr (CIsStringArg<ArgType>) return LexToString(Value); | |
| else if constexpr (CHasToString<ArgType>) return Value.ToString(); | |
| else | |
| { | |
| return TEXT("[?]"); | |
| } | |
| } | |
| template <typename ArgType> | |
| FString ContainerToString(const ArgType& Container, const FString& Spec) | |
| { | |
| FString Result = TEXT("["); | |
| int32 Count = 0; | |
| constexpr int32 MaxElements = 15; | |
| for (const auto& Elem : Container) | |
| { | |
| if (Count > 0) Result += TEXT(", "); | |
| if (Count >= MaxElements) { Result += TEXT("..."); break; } | |
| Result += ElementToString(Elem, Spec); | |
| Count++; | |
| } | |
| return Result + TEXT("]"); | |
| } | |
| struct FSourceLoc | |
| { | |
| FString Function; | |
| int32 Hash; | |
| }; | |
| consteval std::source_location Loc(const std::source_location& Location = std::source_location::current()) | |
| { | |
| return Location; | |
| } | |
| inline FSourceLoc ProcessLocation(const std::source_location& Location) | |
| { | |
| const char* FnAnsi = Location.function_name(); | |
| uint32 Hash = FCrc::MemCrc32(FnAnsi, std::strlen(FnAnsi)); | |
| Hash = (Hash ^ (Location.line() << 15)) & 0x7FFFFFFF; | |
| FString FnName(ANSI_TO_TCHAR(FnAnsi)); | |
| int32 Index; | |
| if (FnName.FindLastChar(TEXT(' '), Index)) FnName = FnName.RightChop(Index + 1); | |
| if (FnName.FindChar(TEXT('('), Index)) FnName = FnName.Left(Index); | |
| return { FString::Printf(TEXT("%s:%d"), *FnName, Location.line()), static_cast<int32>(Hash) }; | |
| } | |
| // Argument Processor | |
| inline void FillArgs(FStringFormatOrderedArguments&, const TArray<FString>&, int32) {} | |
| template<typename FirstArgType, typename... RestArgsType> | |
| void FillArgs(FStringFormatOrderedArguments& Args, const TArray<FString>& Specs, const int32 Index, FirstArgType&& First, RestArgsType&&... Rest) | |
| { | |
| FString CurrentSpec = (Specs.IsValidIndex(Index)) ? Specs[Index] : FString(); | |
| if constexpr (CIsContainer<std::remove_cvref_t<FirstArgType>>) | |
| { | |
| Args.Emplace(ContainerToString(First, CurrentSpec)); | |
| } | |
| else | |
| { | |
| Args.Emplace(ElementToString(First, CurrentSpec)); | |
| } | |
| FillArgs(Args, Specs, Index + 1, std::forward<RestArgsType>(Rest)...); | |
| } | |
| // Helper for Output Log | |
| template <ELogVerbosity::Type Verbosity> | |
| void LogToConsole(const FString& Message) | |
| { | |
| if constexpr (Verbosity == ELogVerbosity::Fatal) UE_LOGFMT(EASY_LOG_CATEGORY, Fatal, "{0}", Message); | |
| else if constexpr (Verbosity == ELogVerbosity::Error) UE_LOGFMT(EASY_LOG_CATEGORY, Error, "{0}", Message); | |
| else if constexpr (Verbosity == ELogVerbosity::Warning) UE_LOGFMT(EASY_LOG_CATEGORY, Warning, "{0}", Message); | |
| else if constexpr (Verbosity == ELogVerbosity::Display) UE_LOGFMT(EASY_LOG_CATEGORY, Display, "{0}", Message); | |
| else if constexpr (Verbosity == ELogVerbosity::Verbose) UE_LOGFMT(EASY_LOG_CATEGORY, Verbose, "{0}", Message); | |
| else UE_LOGFMT(EASY_LOG_CATEGORY, Log, "{0}", Message); | |
| } | |
| // Main Log Functions | |
| template <ELogVerbosity::Type Verbosity, typename... ArgsType> | |
| void Log(const std::source_location& SourceLocation, const float Duration, const FString& Format, ArgsType&&... Arguments) | |
| { | |
| const auto [Function, Hash] = ProcessLocation(SourceLocation); | |
| const auto [FixedString, Specifiers] = FormatStringHelper::Parse(Format); | |
| FStringFormatOrderedArguments OrderedArgs; | |
| FillArgs(OrderedArgs, Specifiers, 0, std::forward<ArgsType>(Arguments)...); | |
| const FString UserMessage = FString::Format(*FixedString, MoveTemp(OrderedArgs)); | |
| const FString FullMessage = FString::Printf(TEXT("%s | %s"), *UserMessage, *Function); | |
| LogToConsole<Verbosity>(FullMessage); | |
| if (GEngine && Duration > 0.f) | |
| { | |
| const FColor Color = Verbosity == ELogVerbosity::Error ? FColor::Red : Verbosity == ELogVerbosity::Warning ? FColor::Orange : FColor::White; | |
| GEngine->AddOnScreenDebugMessage(Hash, Duration, Color, UserMessage); | |
| } | |
| } | |
| template <ELogVerbosity::Type Verbosity, typename... ArgsType> | |
| void Log(const int32 Key, const float Duration, const std::source_location& SourceLocation, const FString& Format, ArgsType&&... Arguments) | |
| { | |
| const auto [Function, Hash] = ProcessLocation(SourceLocation); | |
| const auto [FixedString, Specifiers] = FormatStringHelper::Parse(Format); | |
| FStringFormatOrderedArguments OrderedArgs; | |
| FillArgs(OrderedArgs, Specifiers, 0, std::forward<ArgsType>(Arguments)...); | |
| const FString UserMessage = FString::Format(*FixedString, MoveTemp(OrderedArgs)); | |
| const FString FullMessage = FString::Printf(TEXT("%s | %s"), *UserMessage, *Function); | |
| LogToConsole<Verbosity>(FullMessage); | |
| if (GEngine && Duration > 0.f) | |
| { | |
| const FColor Color = Verbosity == ELogVerbosity::Error ? FColor::Red : Verbosity == ELogVerbosity::Warning ? FColor::Orange : FColor::White; | |
| GEngine->AddOnScreenDebugMessage(Key, Duration, Color, UserMessage); | |
| } | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment