Last active
August 23, 2026 16:26
-
-
Save Solessfir/eb0df57297f8a61f0c598629b0a78865 to your computer and use it in GitHub Desktop.
C++23 std::print-style Single Header Logging library for Unreal Engine 4 and 5
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); | |
| * | |
| * For UE4/UE5.0/UE5.1 - add these lines to your .Target.cs: | |
| * bOverrideBuildEnvironment = true; | |
| * CppStandard = CppStandardVersion.Cpp17; | |
| * | |
| * UE5.2+ uses the C++20 implementation when the target enables std::source_location. | |
| * C++17 targets automatically use the legacy path. | |
| */ | |
| #pragma once | |
| #include "CoreMinimal.h" | |
| #include "Engine/Engine.h" | |
| #include "Templates/IsUEnumClass.h" | |
| #include <cstdio> | |
| #include <iterator> | |
| #include <type_traits> | |
| #include <utility> | |
| // ------------------------------------------------------------------------- | |
| // Version detection | |
| // ------------------------------------------------------------------------- | |
| // EASY_LOG_MODERN = 1 : UE5.2+ with C++20 source_location (UE_LOGFMT) | |
| // EASY_LOG_MODERN = 0 : UE4+ with C++17 (UE_LOG, __FUNCTION__) | |
| // ------------------------------------------------------------------------- | |
| #if ENGINE_MAJOR_VERSION > 5 || (ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 2) | |
| #if defined(__has_include) | |
| #if __has_include(<version>) | |
| #include <version> | |
| #endif | |
| #endif | |
| #endif | |
| #if (ENGINE_MAJOR_VERSION > 5 || (ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 2)) && defined(__cpp_lib_source_location) && (__cpp_lib_source_location >= 201907L) | |
| #define EASY_LOG_MODERN 1 | |
| #else | |
| #define EASY_LOG_MODERN 0 | |
| #endif | |
| #if EASY_LOG_MODERN | |
| #include "Logging/StructuredLog.h" | |
| #include <source_location> | |
| #include <cstring> | |
| #endif | |
| #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 | |
| // ------------------------------------------------------------------------ | |
| // Public macros | |
| // ------------------------------------------------------------------------ | |
| #if !(UE_BUILD_SHIPPING || UE_BUILD_TEST) | |
| #if EASY_LOG_MODERN | |
| #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__) | |
| // do { } while (0) prevents dangling-else when used as a bare if/else body | |
| #define CLOG_DISPLAY(Condition, Format, ...) do { if (Condition) LOG_DISPLAY(Format, ##__VA_ARGS__); } while (0) | |
| #define CLOG_WARNING(Condition, Format, ...) do { if (Condition) LOG_WARNING(Format, ##__VA_ARGS__); } while (0) | |
| #define CLOG_ERROR(Condition, Format, ...) do { if (Condition) LOG_ERROR(Format, ##__VA_ARGS__); } while (0) | |
| #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 // Legacy (UE4 / UE5.0 / UE5.1) | |
| #if defined(_MSC_VER) | |
| #define EASY_FUNC_SIG __FUNCTION__ | |
| #else | |
| #define EASY_FUNC_SIG __PRETTY_FUNCTION__ // Clang/GCC (Linux) | |
| #endif | |
| #define GET_LOG_LOC Easy::FormatLocation(EASY_FUNC_SIG, __LINE__) | |
| #define LOG_KEY_HASH static_cast<int32>((FCrc::MemCrc32(__FUNCTION__, sizeof(__FUNCTION__) - 1) ^ (static_cast<uint32>(__LINE__) << 15)) & 0x7FFFFFFF) | |
| #define LOG_DISPLAY(Fmt, ...) Easy::Dispatch<ELogVerbosity::Display>(LOG_KEY_HASH, 10.f, GET_LOG_LOC, Fmt, ##__VA_ARGS__) | |
| #define LOG_WARNING(Fmt, ...) Easy::Dispatch<ELogVerbosity::Warning>(LOG_KEY_HASH, 10.f, GET_LOG_LOC, Fmt, ##__VA_ARGS__) | |
| #define LOG_ERROR(Fmt, ...) Easy::Dispatch<ELogVerbosity::Error>(LOG_KEY_HASH, 10.f, GET_LOG_LOC, Fmt, ##__VA_ARGS__) | |
| // do { } while (0) prevents dangling-else when used as a bare if/else body | |
| #define CLOG_DISPLAY(Cond, Fmt, ...) do { if (Cond) LOG_DISPLAY(Fmt, ##__VA_ARGS__); } while (0) | |
| #define CLOG_WARNING(Cond, Fmt, ...) do { if (Cond) LOG_WARNING(Fmt, ##__VA_ARGS__); } while (0) | |
| #define CLOG_ERROR(Cond, Fmt, ...) do { if (Cond) LOG_ERROR(Fmt, ##__VA_ARGS__); } while (0) | |
| #define LOG_DISPLAY_EX(Key, Duration, Format, ...) Easy::Dispatch<ELogVerbosity::Display>(Key, Duration, GET_LOG_LOC, Format, ##__VA_ARGS__) | |
| #define LOG_WARNING_EX(Key, Duration, Format, ...) Easy::Dispatch<ELogVerbosity::Warning>(Key, Duration, GET_LOG_LOC, Format, ##__VA_ARGS__) | |
| #define LOG_ERROR_EX(Key, Duration, Format, ...) Easy::Dispatch<ELogVerbosity::Error>(Key, Duration, GET_LOG_LOC, Format, ##__VA_ARGS__) | |
| #endif // EASY_LOG_MODERN | |
| #else // UE_BUILD_SHIPPING || UE_BUILD_TEST | |
| #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 | |
| { | |
| // ------------------------------------------------------------------------ | |
| // Shared parsing and type conversion. Both paths require only C++17. | |
| // ------------------------------------------------------------------------ | |
| struct FFormatField | |
| { | |
| int32 ArgumentIndex = INDEX_NONE; | |
| FString Specifier; | |
| }; | |
| struct FParsedFormat | |
| { | |
| // Literals.Num() is always Fields.Num() + 1 for a valid parse. | |
| TArray<FString> Literals; | |
| TArray<FFormatField> Fields; | |
| FString Error; | |
| bool bIsValid = true; | |
| }; | |
| struct FormatStringHelper | |
| { | |
| static bool TryParseIndex(const FString& Text, int32& OutIndex) | |
| { | |
| if (Text.IsEmpty()) return false; | |
| int32 Value = 0; | |
| for (const TCHAR Char : Text) | |
| { | |
| if (!TChar<TCHAR>::IsDigit(Char)) return false; | |
| const int32 Digit = Char - TEXT('0'); | |
| if (Value > (MAX_int32 - Digit) / 10) return false; | |
| Value = Value * 10 + Digit; | |
| } | |
| OutIndex = Value; | |
| return true; | |
| } | |
| static bool LooksLikeInvalidIndex(const FString& Text) | |
| { | |
| if (Text.IsEmpty()) return false; | |
| const TCHAR First = Text[0]; | |
| return TChar<TCHAR>::IsDigit(First) || First == TEXT('+') || First == TEXT('-') || First == TEXT('.'); | |
| } | |
| static FParsedFormat Parse(const FString& InFormat, const int32 ArgumentCount) | |
| { | |
| FParsedFormat Result; | |
| FString CurrentLiteral; | |
| CurrentLiteral.Reserve(InFormat.Len()); | |
| int32 NextAutomaticIndex = 0; | |
| const int32 Len = InFormat.Len(); | |
| for (int32 i = 0; i < Len; ++i) | |
| { | |
| const TCHAR Char = InFormat[i]; | |
| if (Char == TEXT('{')) | |
| { | |
| // EasyLog uses the conventional doubled-brace escape syntax. | |
| if (i + 1 < Len && InFormat[i + 1] == TEXT('{')) | |
| { | |
| CurrentLiteral.AppendChar(TEXT('{')); | |
| ++i; | |
| continue; | |
| } | |
| int32 CloseIndex = INDEX_NONE; | |
| for (int32 j = i + 1; j < Len; ++j) | |
| { | |
| if (InFormat[j] == TEXT('}')) | |
| { | |
| CloseIndex = j; | |
| break; | |
| } | |
| } | |
| // An unmatched opening brace is harmless literal log text. | |
| if (CloseIndex == INDEX_NONE) | |
| { | |
| CurrentLiteral.AppendChar(Char); | |
| continue; | |
| } | |
| FString Content = InFormat.Mid(i + 1, CloseIndex - i - 1); | |
| FString Specifier; | |
| int32 ColonIndex = INDEX_NONE; | |
| if (Content.FindChar(TEXT(':'), ColonIndex)) | |
| { | |
| Specifier = Content.RightChop(ColonIndex + 1); | |
| Specifier.TrimStartAndEndInline(); | |
| Content = Content.Left(ColonIndex); | |
| } | |
| int32 ArgumentIndex = INDEX_NONE; | |
| bool bIsPlaceholder = false; | |
| if (Content.IsEmpty()) | |
| { | |
| ArgumentIndex = NextAutomaticIndex++; | |
| bIsPlaceholder = true; | |
| } | |
| else if (TryParseIndex(Content, ArgumentIndex)) | |
| { | |
| bIsPlaceholder = true; | |
| } | |
| else if (LooksLikeInvalidIndex(Content)) | |
| { | |
| Result.bIsValid = false; | |
| Result.Error = TEXT("invalid argument index '") + Content + TEXT("'"); | |
| return Result; | |
| } | |
| if (bIsPlaceholder) | |
| { | |
| if (ArgumentIndex < 0 || ArgumentIndex >= ArgumentCount) | |
| { | |
| Result.bIsValid = false; | |
| Result.Error = TEXT("argument index ") + FString::FromInt(ArgumentIndex) + TEXT(" is out of range for ") + FString::FromInt(ArgumentCount) + TEXT(" argument(s)"); | |
| return Result; | |
| } | |
| Result.Literals.Add(MoveTemp(CurrentLiteral)); | |
| CurrentLiteral.Reset(); | |
| FFormatField Field; | |
| Field.ArgumentIndex = ArgumentIndex; | |
| Field.Specifier = MoveTemp(Specifier); | |
| Result.Fields.Add(MoveTemp(Field)); | |
| } | |
| else | |
| { | |
| // Named fields are unsupported, so preserve them literally. | |
| CurrentLiteral.AppendChars(&InFormat[i], CloseIndex - i + 1); | |
| } | |
| i = CloseIndex; | |
| continue; | |
| } | |
| if (Char == TEXT('}') && i + 1 < Len && InFormat[i + 1] == TEXT('}')) | |
| { | |
| CurrentLiteral.AppendChar(TEXT('}')); | |
| ++i; | |
| continue; | |
| } | |
| CurrentLiteral.AppendChar(Char); | |
| } | |
| Result.Literals.Add(MoveTemp(CurrentLiteral)); | |
| return Result; | |
| } | |
| }; | |
| enum class ENumberPresentation : uint8 | |
| { | |
| Decimal, | |
| HexLower, | |
| HexUpper, | |
| BinaryLower, | |
| BinaryUpper, | |
| Float, | |
| FloatUpper | |
| }; | |
| struct FNumericFormatSpec | |
| { | |
| ENumberPresentation Presentation = ENumberPresentation::Decimal; | |
| int32 Width = 0; | |
| int32 Precision = 0; | |
| bool bHasPrecision = false; | |
| bool bAlternate = false; | |
| bool bZeroPad = false; | |
| bool bLeftAlign = false; | |
| bool bAlwaysSign = false; | |
| bool bSpaceSign = false; | |
| FString MakePrintfBody() const | |
| { | |
| FString Result; | |
| if (bLeftAlign) Result.AppendChar(TEXT('-')); | |
| if (bAlwaysSign) Result.AppendChar(TEXT('+')); | |
| else if (bSpaceSign) Result.AppendChar(TEXT(' ')); | |
| if (bAlternate) Result.AppendChar(TEXT('#')); | |
| if (bZeroPad) Result.AppendChar(TEXT('0')); | |
| if (Width > 0) Result.AppendInt(Width); | |
| if (bHasPrecision) | |
| { | |
| Result.AppendChar(TEXT('.')); | |
| Result.AppendInt(Precision); | |
| } | |
| return Result; | |
| } | |
| }; | |
| struct SafeFormatter | |
| { | |
| static constexpr int32 MaxWidthOrPrecision = 4096; | |
| static constexpr int32 MaxFormattedChars = 16384; | |
| static int32 RequiredBufferSize(const FNumericFormatSpec& Spec, const int32 BaseCharacters) | |
| { | |
| const int32 PrecisionCharacters = Spec.bHasPrecision ? Spec.Precision + BaseCharacters : BaseCharacters; | |
| return FMath::Min(MaxFormattedChars, FMath::Max(PrecisionCharacters, Spec.Width + BaseCharacters)); | |
| } | |
| template <typename ValueType> | |
| static FString RunFormat(const int32 BufferSize, const FString& Fmt, const ValueType Value) | |
| { | |
| TArray<ANSICHAR> Buffer; | |
| Buffer.SetNumUninitialized(FMath::Clamp(BufferSize, 2, MaxFormattedChars)); | |
| Buffer[0] = '\0'; | |
| const FTCHARToUTF8 ConvertedFormat(*Fmt); | |
| const int32 Written = std::snprintf(Buffer.GetData(), Buffer.Num(), ConvertedFormat.Get(), Value); | |
| return Written >= 0 && Written < Buffer.Num() ? FString(UTF8_TO_TCHAR(Buffer.GetData())) : FString(TEXT("[FormattedValueTooLong]")); | |
| } | |
| static bool ParseBoundedNumber(const FString& Spec, int32& Index, int32& OutValue) | |
| { | |
| const int32 StartIndex = Index; | |
| int32 Value = 0; | |
| while (Index < Spec.Len() && TChar<TCHAR>::IsDigit(Spec[Index])) | |
| { | |
| const int32 Digit = Spec[Index] - TEXT('0'); | |
| if (Value > (MaxWidthOrPrecision - Digit) / 10) return false; | |
| Value = Value * 10 + Digit; | |
| ++Index; | |
| } | |
| if (Index == StartIndex) return false; | |
| OutValue = Value; | |
| return true; | |
| } | |
| static bool ParseNumericSpec(const FString& Spec, const bool bFloatingPoint, FNumericFormatSpec& Out) | |
| { | |
| Out = FNumericFormatSpec(); | |
| int32 Index = 0; | |
| while (Index < Spec.Len()) | |
| { | |
| const TCHAR Char = Spec[Index]; | |
| if (Char == TEXT('#')) Out.bAlternate = true; | |
| else if (Char == TEXT('0')) Out.bZeroPad = true; | |
| else if (Char == TEXT('-')) Out.bLeftAlign = true; | |
| else if (Char == TEXT('+')) Out.bAlwaysSign = true; | |
| else if (Char == TEXT(' ')) Out.bSpaceSign = true; | |
| else break; | |
| ++Index; | |
| } | |
| if (Index < Spec.Len() && TChar<TCHAR>::IsDigit(Spec[Index])) | |
| { | |
| if (!ParseBoundedNumber(Spec, Index, Out.Width)) return false; | |
| } | |
| if (Index < Spec.Len() && Spec[Index] == TEXT('.')) | |
| { | |
| ++Index; | |
| Out.bHasPrecision = true; | |
| if (!ParseBoundedNumber(Spec, Index, Out.Precision)) return false; | |
| } | |
| TCHAR Type = TEXT('\0'); | |
| if (Index < Spec.Len()) | |
| { | |
| if (Index + 1 != Spec.Len()) return false; | |
| Type = Spec[Index]; | |
| ++Index; | |
| } | |
| if (Index != Spec.Len()) return false; | |
| if (bFloatingPoint) | |
| { | |
| if (Type != TEXT('\0') && Type != TEXT('f') && Type != TEXT('F')) return false; | |
| Out.Presentation = Type == TEXT('F') ? ENumberPresentation::FloatUpper : ENumberPresentation::Float; | |
| return true; | |
| } | |
| switch (Type) | |
| { | |
| case TEXT('\0'): | |
| case TEXT('d'): | |
| case TEXT('i'): Out.Presentation = ENumberPresentation::Decimal; return true; | |
| case TEXT('x'): Out.Presentation = ENumberPresentation::HexLower; return true; | |
| case TEXT('X'): Out.Presentation = ENumberPresentation::HexUpper; return true; | |
| case TEXT('b'): Out.Presentation = ENumberPresentation::BinaryLower; return true; | |
| case TEXT('B'): Out.Presentation = ENumberPresentation::BinaryUpper; return true; | |
| default: return false; | |
| } | |
| } | |
| static FString InvalidSpec(const FString& Spec) | |
| { | |
| return TEXT("[InvalidFormatSpec:") + Spec + TEXT("]"); | |
| } | |
| static FString FormatBinary(const uint64 Value, const FNumericFormatSpec& Spec) | |
| { | |
| FString Digits; | |
| uint64 Remaining = Value; | |
| if (Remaining == 0) | |
| { | |
| Digits = TEXT("0"); | |
| } | |
| else | |
| { | |
| while (Remaining > 0) | |
| { | |
| Digits.InsertAt(0, (Remaining & 1) ? TEXT('1') : TEXT('0')); | |
| Remaining >>= 1; | |
| } | |
| } | |
| while (Spec.bHasPrecision && Digits.Len() < Spec.Precision) | |
| { | |
| Digits.InsertAt(0, TEXT('0')); | |
| } | |
| FString Prefix; | |
| if (Spec.bAlternate) | |
| { | |
| Prefix = Spec.Presentation == ENumberPresentation::BinaryUpper ? TEXT("0B") : TEXT("0b"); | |
| } | |
| const int32 PaddingCount = FMath::Max(0, Spec.Width - Prefix.Len() - Digits.Len()); | |
| FString Padding; | |
| Padding.Reserve(PaddingCount); | |
| for (int32 i = 0; i < PaddingCount; ++i) Padding.AppendChar(TEXT(' ')); | |
| if (Spec.bLeftAlign) return Prefix + Digits + Padding; | |
| if (Spec.bZeroPad && !Spec.bHasPrecision) | |
| { | |
| FString ZeroPadding; | |
| ZeroPadding.Reserve(PaddingCount); | |
| for (int32 i = 0; i < PaddingCount; ++i) ZeroPadding.AppendChar(TEXT('0')); | |
| return Prefix + ZeroPadding + Digits; | |
| } | |
| return Padding + Prefix + Digits; | |
| } | |
| static FString FormatSigned(const int64 Value, const FString& Spec) | |
| { | |
| FNumericFormatSpec Parsed; | |
| if (!ParseNumericSpec(Spec, false, Parsed)) return InvalidSpec(Spec); | |
| if (Parsed.Presentation == ENumberPresentation::BinaryLower || Parsed.Presentation == ENumberPresentation::BinaryUpper) | |
| { | |
| return FormatBinary(static_cast<uint64>(Value), Parsed); | |
| } | |
| FString Fmt = TEXT("%") + Parsed.MakePrintfBody(); | |
| if (Parsed.Presentation == ENumberPresentation::HexLower) | |
| { | |
| Fmt += TEXT("llx"); | |
| return RunFormat(RequiredBufferSize(Parsed, 128), Fmt, static_cast<unsigned long long>(Value)); | |
| } | |
| if (Parsed.Presentation == ENumberPresentation::HexUpper) | |
| { | |
| Fmt += TEXT("llX"); | |
| return RunFormat(RequiredBufferSize(Parsed, 128), Fmt, static_cast<unsigned long long>(Value)); | |
| } | |
| Fmt += TEXT("lld"); | |
| return RunFormat(RequiredBufferSize(Parsed, 128), Fmt, static_cast<long long>(Value)); | |
| } | |
| static FString FormatUnsigned(const uint64 Value, const FString& Spec) | |
| { | |
| FNumericFormatSpec Parsed; | |
| if (!ParseNumericSpec(Spec, false, Parsed)) return InvalidSpec(Spec); | |
| if (Parsed.Presentation == ENumberPresentation::BinaryLower || Parsed.Presentation == ENumberPresentation::BinaryUpper) | |
| { | |
| return FormatBinary(Value, Parsed); | |
| } | |
| FString Fmt = TEXT("%") + Parsed.MakePrintfBody(); | |
| if (Parsed.Presentation == ENumberPresentation::HexLower) Fmt += TEXT("llx"); | |
| else if (Parsed.Presentation == ENumberPresentation::HexUpper) Fmt += TEXT("llX"); | |
| else Fmt += TEXT("llu"); | |
| return RunFormat(RequiredBufferSize(Parsed, 128), Fmt, static_cast<unsigned long long>(Value)); | |
| } | |
| static FString FormatFloat(const double Value, const FString& Spec) | |
| { | |
| FNumericFormatSpec Parsed; | |
| if (!ParseNumericSpec(Spec, true, Parsed)) return InvalidSpec(Spec); | |
| const FString Fmt = TEXT("%") + Parsed.MakePrintfBody() + (Parsed.Presentation == ENumberPresentation::FloatUpper ? TEXT("F") : TEXT("f")); | |
| return RunFormat(RequiredBufferSize(Parsed, 512), Fmt, Value); | |
| } | |
| }; | |
| template <typename T> | |
| using Decayed = typename std::decay<T>::type; | |
| template <typename T, typename = void> | |
| struct THasToString : std::false_type {}; | |
| template <typename T> | |
| struct THasToString<T, std::void_t<decltype(std::declval<const Decayed<T>&>().ToString())>> | |
| : std::is_convertible<decltype(std::declval<const Decayed<T>&>().ToString()), FString> {}; | |
| template <typename T, typename = void> | |
| struct TIsContainer : std::false_type {}; | |
| template <typename T> | |
| struct TIsContainer<T, std::void_t<decltype(std::begin(std::declval<const Decayed<T>&>())), decltype(std::end(std::declval<const Decayed<T>&>()))>> : std::true_type {}; | |
| template <typename T> | |
| struct TIsStringValue : std::is_constructible<FString, Decayed<T>> {}; | |
| template <typename T> | |
| struct TIsCharacterType : std::integral_constant<bool, std::is_same<typename std::remove_cv<T>::type, TCHAR>::value || std::is_same<typename std::remove_cv<T>::type, ANSICHAR>::value || std::is_same<typename std::remove_cv<T>::type, WIDECHAR>::value || std::is_same<typename std::remove_cv<T>::type, UCS2CHAR>::value || std::is_same<typename std::remove_cv<T>::type, UTF8CHAR>::value> {}; | |
| template <typename T> | |
| struct TIsCharacterPointer : std::integral_constant<bool, std::is_pointer<Decayed<T>>::value && TIsCharacterType<typename std::remove_pointer<Decayed<T>>::type>::value> {}; | |
| template <typename T> | |
| constexpr bool IsContainerArgument = TIsContainer<T>::value && !TIsStringValue<T>::value; | |
| template <typename T> | |
| FString ElementToString(const T& Value, const FString& Spec = FString()); | |
| template <typename T> | |
| FString ContainerToString(const T& Container, const FString& Spec) | |
| { | |
| FString Result = TEXT("["); | |
| int32 Count = 0; | |
| constexpr int32 MaxElements = 15; | |
| for (const auto& Element : Container) | |
| { | |
| if (Count > 0) Result += TEXT(", "); | |
| if (Count >= MaxElements) | |
| { | |
| Result += TEXT("..."); | |
| break; | |
| } | |
| Result += ElementToString(Element, Spec); | |
| ++Count; | |
| } | |
| Result += TEXT("]"); | |
| return Result; | |
| } | |
| template <typename T> | |
| FString ElementToString(const T& Value, const FString& Spec) | |
| { | |
| using RawType = Decayed<T>; | |
| if constexpr (std::is_same<RawType, std::nullptr_t>::value) | |
| { | |
| return Spec.IsEmpty() ? TEXT("nullptr") : SafeFormatter::InvalidSpec(Spec); | |
| } | |
| else if constexpr (std::is_pointer<RawType>::value) | |
| { | |
| if (!Spec.IsEmpty()) | |
| { | |
| return SafeFormatter::FormatUnsigned(static_cast<uint64>(reinterpret_cast<UPTRINT>(Value)), Spec); | |
| } | |
| if constexpr (TIsCharacterPointer<RawType>::value) | |
| { | |
| return Value ? FString(Value) : TEXT("(NullString)"); | |
| } | |
| else if constexpr (std::is_convertible<RawType, const UObject*>::value) | |
| { | |
| // Avoid dereferencing UObjects from worker threads. | |
| if (!IsInGameThread()) return TEXT("[AsyncUObject]"); | |
| if (!IsValid(Value)) return TEXT("None"); | |
| if constexpr (std::is_convertible<RawType, const AActor*>::value) | |
| { | |
| return Value->GetActorNameOrLabel(); | |
| } | |
| return Value->GetName(); | |
| } | |
| else | |
| { | |
| return SafeFormatter::FormatUnsigned(static_cast<uint64>(reinterpret_cast<UPTRINT>(Value)), TEXT("#x")); | |
| } | |
| } | |
| else if constexpr (IsContainerArgument<RawType>) | |
| { | |
| return ContainerToString(Value, Spec); | |
| } | |
| else if constexpr (std::is_same<RawType, bool>::value) | |
| { | |
| if (!Spec.IsEmpty()) | |
| { | |
| return SafeFormatter::FormatUnsigned(Value ? 1u : 0u, Spec); | |
| } | |
| return Value ? TEXT("true") : TEXT("false"); | |
| } | |
| else if constexpr (std::is_floating_point<RawType>::value) | |
| { | |
| if (!Spec.IsEmpty()) return SafeFormatter::FormatFloat(static_cast<double>(Value), Spec); | |
| return FString::SanitizeFloat(Value); | |
| } | |
| else if constexpr (std::is_integral<RawType>::value) | |
| { | |
| if (Spec.IsEmpty()) return LexToString(Value); | |
| if constexpr (std::is_signed<RawType>::value) | |
| { | |
| return SafeFormatter::FormatSigned(static_cast<int64>(Value), Spec); | |
| } | |
| else | |
| { | |
| return SafeFormatter::FormatUnsigned(static_cast<uint64>(Value), Spec); | |
| } | |
| } | |
| else if constexpr (std::is_enum<RawType>::value) | |
| { | |
| if constexpr (TIsUEnumClass<RawType>::Value) | |
| { | |
| if (Spec.IsEmpty()) | |
| { | |
| if (const UEnum* Enum = StaticEnum<RawType>()) | |
| { | |
| const FString EnumName = Enum->GetNameStringByValue(static_cast<int64>(Value)); | |
| if (!EnumName.IsEmpty()) return EnumName; | |
| } | |
| } | |
| } | |
| using UnderlyingType = typename std::underlying_type<RawType>::type; | |
| if constexpr (std::is_signed<UnderlyingType>::value) | |
| { | |
| const int64 NumericValue = static_cast<int64>(static_cast<UnderlyingType>(Value)); | |
| return Spec.IsEmpty() ? LexToString(NumericValue) : SafeFormatter::FormatSigned(NumericValue, Spec); | |
| } | |
| else | |
| { | |
| const uint64 NumericValue = static_cast<uint64>(static_cast<UnderlyingType>(Value)); | |
| return Spec.IsEmpty() ? LexToString(NumericValue) : SafeFormatter::FormatUnsigned(NumericValue, Spec); | |
| } | |
| } | |
| else if constexpr (THasToString<RawType>::value) | |
| { | |
| return Spec.IsEmpty() ? Value.ToString() : SafeFormatter::InvalidSpec(Spec); | |
| } | |
| else if constexpr (TIsStringValue<RawType>::value) | |
| { | |
| return Spec.IsEmpty() ? FString(Value) : SafeFormatter::InvalidSpec(Spec); | |
| } | |
| else | |
| { | |
| return TEXT("[?]"); | |
| } | |
| } | |
| inline bool TryFormatArgumentAt(const int32, const FString&, FString&) | |
| { | |
| return false; | |
| } | |
| template <typename FirstType, typename... RestTypes> | |
| bool TryFormatArgumentAt(const int32 Index, const FString& Spec, FString& Out, const FirstType& First, const RestTypes&... Rest) | |
| { | |
| if (Index == 0) | |
| { | |
| Out = ElementToString(First, Spec); | |
| return true; | |
| } | |
| if (Index < 0) return false; | |
| return TryFormatArgumentAt(Index - 1, Spec, Out, Rest...); | |
| } | |
| template <typename... ArgsTypes> | |
| FString BuildUserMessage(const FString& Format, const ArgsTypes&... Arguments) | |
| { | |
| const FParsedFormat Parsed = FormatStringHelper::Parse(Format, sizeof...(Arguments)); | |
| if (!Parsed.bIsValid) | |
| { | |
| return TEXT("[EasyLog format error: ") + Parsed.Error + TEXT("] ") + Format; | |
| } | |
| FString Result = Parsed.Literals[0]; | |
| Result.Reserve(Format.Len() + 32); | |
| for (int32 i = 0; i < Parsed.Fields.Num(); ++i) | |
| { | |
| FString FormattedArgument; | |
| const FFormatField& Field = Parsed.Fields[i]; | |
| if (!TryFormatArgumentAt(Field.ArgumentIndex, Field.Specifier, FormattedArgument, Arguments...)) | |
| { | |
| return TEXT("[EasyLog format error: argument lookup failed] ") + Format; | |
| } | |
| Result += FormattedArgument; | |
| Result += Parsed.Literals[i + 1]; | |
| } | |
| return Result; | |
| } | |
| #if EASY_LOG_MODERN | |
| // ------------------------------------------------------------------------ | |
| // Modern path: UE5.2+ with C++20 source_location and UE_LOGFMT | |
| // ------------------------------------------------------------------------ | |
| struct FSourceLoc | |
| { | |
| FString Function; | |
| int32 Hash; | |
| }; | |
| // The default argument is evaluated at the macro call site. | |
| constexpr std::source_location Loc(const std::source_location& Location = std::source_location::current()) noexcept | |
| { | |
| return Location; | |
| } | |
| inline FSourceLoc ProcessLocation(const std::source_location& Location) | |
| { | |
| const char* FnAnsi = Location.function_name(); | |
| const int32 Line = static_cast<int32>(Location.line()); | |
| uint32 Hash = FCrc::MemCrc32(FnAnsi, std::strlen(FnAnsi)); | |
| Hash = (Hash ^ (static_cast<uint32>(Line) << 15)) & 0x7FFFFFFF; | |
| FString FnName(ANSI_TO_TCHAR(FnAnsi)); | |
| int32 Index; | |
| // Strips return type prefix (Clang/GCC on Linux return the full signature) | |
| if (FnName.FindChar(TEXT('('), Index)) FnName = FnName.Left(Index); | |
| if (FnName.FindLastChar(TEXT(' '), Index)) FnName = FnName.RightChop(Index + 1); | |
| return { FString::Printf(TEXT("%s:%d"), *FnName, Line), static_cast<int32>(Hash) }; | |
| } | |
| 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); | |
| } | |
| // Standard LOG_* — key derived from source location hash | |
| template <ELogVerbosity::Type Verbosity, typename... ArgsType> | |
| void Log(const std::source_location& SourceLocation, const float Duration, const FString& Format, ArgsType&&... Arguments) | |
| { | |
| const FSourceLoc Location = ProcessLocation(SourceLocation); | |
| const FString UserMessage = BuildUserMessage(Format, Arguments...); | |
| LogToConsole<Verbosity>(FString::Printf(TEXT("%s | %s"), *UserMessage, *Location.Function)); | |
| if (GEngine && Duration > 0.f && IsInGameThread()) | |
| { | |
| const FColor Color = Verbosity == ELogVerbosity::Error ? FColor::Red : Verbosity == ELogVerbosity::Warning ? FColor::Orange : FColor::White; | |
| GEngine->AddOnScreenDebugMessage(Location.Hash, Duration, Color, UserMessage); | |
| } | |
| } | |
| // LOG_*_EX — explicit key | |
| 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 FSourceLoc Location = ProcessLocation(SourceLocation); | |
| const FString UserMessage = BuildUserMessage(Format, Arguments...); | |
| LogToConsole<Verbosity>(FString::Printf(TEXT("%s | %s"), *UserMessage, *Location.Function)); | |
| if (GEngine && Duration > 0.f && IsInGameThread()) | |
| { | |
| const FColor Color = Verbosity == ELogVerbosity::Error ? FColor::Red : Verbosity == ELogVerbosity::Warning ? FColor::Orange : FColor::White; | |
| GEngine->AddOnScreenDebugMessage(Key, Duration, Color, UserMessage); | |
| } | |
| } | |
| #else | |
| // ------------------------------------------------------------------------ | |
| // Legacy path: UE4+ C++17 with UE_LOG | |
| // ------------------------------------------------------------------------ | |
| #define EASY_UE_LOG_EXPAND(Category, Verbosity, Format, ...) UE_LOG(Category, Verbosity, Format, ##__VA_ARGS__) | |
| inline FString FormatLocation(const char* InFunc, const int32 Line) | |
| { | |
| FString Result(InFunc); | |
| // Strips parameter list and return type prefix (relevant for __PRETTY_FUNCTION__) | |
| if (Result.Contains(TEXT("("))) Result = Result.Left(Result.Find(TEXT("("))); | |
| int32 SpaceIdx = -1; | |
| if (Result.FindLastChar(TEXT(' '), SpaceIdx)) Result = Result.RightChop(SpaceIdx + 1); | |
| return FString::Printf(TEXT("%s:%d"), *Result, Line); | |
| } | |
| template <ELogVerbosity::Type Verbosity, typename... ArgsType> | |
| void Dispatch(const int32 Key, const float Duration, const FString& Location, const FString& Format, ArgsType&&... Arguments) | |
| { | |
| const FString UserMessage = BuildUserMessage(Format, Arguments...); | |
| const FString FullMessage = FString::Printf(TEXT("%s | %s"), *UserMessage, *Location); | |
| switch (Verbosity) | |
| { | |
| case ELogVerbosity::Fatal: EASY_UE_LOG_EXPAND(EASY_LOG_CATEGORY, Fatal, TEXT("%s"), *FullMessage); break; | |
| case ELogVerbosity::Error: EASY_UE_LOG_EXPAND(EASY_LOG_CATEGORY, Error, TEXT("%s"), *FullMessage); break; | |
| case ELogVerbosity::Warning: EASY_UE_LOG_EXPAND(EASY_LOG_CATEGORY, Warning, TEXT("%s"), *FullMessage); break; | |
| case ELogVerbosity::Display: EASY_UE_LOG_EXPAND(EASY_LOG_CATEGORY, Display, TEXT("%s"), *FullMessage); break; | |
| case ELogVerbosity::Verbose: EASY_UE_LOG_EXPAND(EASY_LOG_CATEGORY, Verbose, TEXT("%s"), *FullMessage); break; | |
| default: EASY_UE_LOG_EXPAND(EASY_LOG_CATEGORY, Log, TEXT("%s"), *FullMessage); break; | |
| } | |
| if (GEngine && Duration > 0.f && IsInGameThread()) | |
| { | |
| const FColor Color = Verbosity == ELogVerbosity::Error ? FColor::Red : Verbosity == ELogVerbosity::Warning ? FColor::Orange : FColor::White; | |
| GEngine->AddOnScreenDebugMessage(Key, Duration, Color, UserMessage); | |
| } | |
| } | |
| #endif // EASY_LOG_MODERN | |
| } // namespace Easy |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment