UE 中 Log 实现代码分析

在 UE 中,经常需要在 C++ 代码中打印日志,UE 也提供了方法来创建出可以通过 UE_LOG 打印日志的宏。
首先,先来看一下 UE_LOG 是什么,它是个宏,使用的方法:

1
UE_LOG(LogTemp,Log,TEXT(""));

它被定义在Core/Public/Logging/LogMacros.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
/** 
* A macro that outputs a formatted message to log if a given logging category is active at a given verbosity level
* @param CategoryName name of the logging category
* @param Verbosity, verbosity level to test against
* @param Format, format text
***/
#define UE_LOG(CategoryName, Verbosity, Format, ...) \
{ \
static_assert(TIsArrayOrRefOfType<decltype(Format), TCHAR>::Value, "Formatting string must be a TCHAR array."); \
static_assert((ELogVerbosity::Verbosity & ELogVerbosity::VerbosityMask) < ELogVerbosity::NumVerbosity && ELogVerbosity::Verbosity > 0, "Verbosity must be constant and in range."); \
CA_CONSTANT_IF((ELogVerbosity::Verbosity & ELogVerbosity::VerbosityMask) <= ELogVerbosity::COMPILED_IN_MINIMUM_VERBOSITY && (ELogVerbosity::Warning & ELogVerbosity::VerbosityMask) <= FLogCategory###CategoryName::CompileTimeVerbosity) \
{ \
UE_LOG_EXPAND_IS_FATAL(Verbosity, PREPROCESSOR_NOTHING, if (!CategoryName.IsSuppressed(ELogVerbosity::Verbosity))) \
{ \
auto UE_LOG_noinline_lambda = [](const auto& LCategoryName, const auto& LFormat, const auto&... UE_LOG_Args) FORCENOINLINE \
{ \
TRACE_LOG_MESSAGE(LCategoryName, Verbosity, LFormat, UE_LOG_Args...) \
UE_LOG_EXPAND_IS_FATAL(Verbosity, \
{ \
FMsg::Logf_Internal(UE_LOG_SOURCE_FILE(__FILE__), __LINE__, LCategoryName.GetCategoryName(), ELogVerbosity::Verbosity, LFormat, UE_LOG_Args...); \
_DebugBreakAndPromptForRemote(); \
FDebug::ProcessFatalError(); \
}, \
{ \
FMsg::Logf_Internal(nullptr, 0, LCategoryName.GetCategoryName(), ELogVerbosity::Verbosity, LFormat, UE_LOG_Args...); \
} \
) \
}; \
UE_LOG_noinline_lambda(CategoryName, Format, ###__VA_ARGS__); \
UE_LOG_EXPAND_IS_FATAL(Verbosity, CA_ASSUME(false);, PREPROCESSOR_NOTHING) \
} \
} \
}

可以看到,使用 UE_LOG 时传入的第一个参数,是一个对象,后面的参数则是一个枚举值以及输出的 Formater,以及更多的参数(用于匹配 Formater 中的占位符)。

UE 提供了几种方法来创建 Log 的 Category:

1
2
3
DECLARE_LOG_CATEGORY_EXTERN(LogCategoryName,All,All);
DECLARE_LOG_CATEGORY_CLASS(LogCategoryName2,All,All);
DECLARE_LOG_CATEGORY_EXTERN_HELPER(LogCategoryName3,All,All);

挨个来看一下它们的定义,其实他们都是定义了一个类,并需要创建出一个对象,可以用来传递给 UE_LOG 的第一个参数,而后两个参数则都是 ELogVerbosity 的枚举值,用于给当前的 LogCategory 指定运行时和编译时的日志等级。
看一下这个枚举的定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
/** 
* Enum that defines the verbosity levels of the logging system.
* Also defines some non-verbosity levels that are hacks that allow
* breaking on a given log line or setting the color.
**/
namespace ELogVerbosity
{
enum Type : uint8
{
/** Not used */
NoLogging = 0,

/** Always prints a fatal error to console (and log file) and crashes (even if logging is disabled) */
Fatal,

/**
* Prints an error to console (and log file).
* Commandlets and the editor collect and report errors. Error messages result in commandlet failure.
*/
Error,

/**
* Prints a warning to console (and log file).
* Commandlets and the editor collect and report warnings. Warnings can be treated as an error.
*/
Warning,

/** Prints a message to console (and log file) */
Display,

/** Prints a message to a log file (does not print to console) */
Log,

/**
* Prints a verbose message to a log file (if Verbose logging is enabled for the given category,
* usually used for detailed logging)
*/
Verbose,

/**
* Prints a verbose message to a log file (if VeryVerbose logging is enabled,
* usually used for detailed logging that would otherwise spam output)
*/
VeryVerbose,

// Log masks and special Enum values

All = VeryVerbose,
NumVerbosity,
VerbosityMask = 0xf,
SetColor = 0x40, // not actually a verbosity, used to set the color of an output device
BreakOnLog = 0x80
};
}

可以根据自己的需求来指定不同的日志等级。

DECLARE_LOG_CATEGORY_EXTERN

1
2
3
4
5
6
7
8
9
10
11
/** 
* A macro to declare a logging category as a C++ "extern", usually declared in the header and paired with DEFINE_LOG_CATEGORY in the source. Accessible by all files that include the header.
* @param CategoryName, category to declare
* @param DefaultVerbosity, default run time verbosity
* @param CompileTimeVerbosity, maximum verbosity to compile into the code
**/
#define DECLARE_LOG_CATEGORY_EXTERN(CategoryName, DefaultVerbosity, CompileTimeVerbosity) \
extern struct FLogCategory###CategoryName : public FLogCategory<ELogVerbosity::DefaultVerbosity, ELogVerbosity::CompileTimeVerbosity> \
{ \
FORCEINLINE FLogCategory###CategoryName() : FLogCategory(TEXT(#CategoryName)) {} \
} CategoryName;

如果有以下声明:

1
DECLARE_LOG_CATEGORY_EXTERN(LogCategoryName,All,All);

宏展开之后就为:

1
2
3
4
extern struct FLogCategoryLogCategoryName : public FLogCategory<ELogVerbosity::All, ELogVerbosity::All>
{
FORCEINLINE FLogCategoryLogCategoryName() : FLogCategory(TEXT("LogCategoryName")) {}
} LogCategoryName;

其实就是继承自 FLogCategory 的一个类定义,并且 声明 了一个 LogCategoryName 的对象。
注意,这里只是声明,还需要定义,不然在编译时会有未定义错误,所以就需要在 cpp 里写代码进行定义,UE 也提供了一个宏:

1
DEFINE_LOG_CATEGORY(LogCategoryName);

它的定义就很简单了,只是定义一个对象而已:

1
2
3
4
5
/** 
* A macro to define a logging category, usually paired with DECLARE_LOG_CATEGORY_EXTERN from the header.
* @param CategoryName, category to define
**/
#define DEFINE_LOG_CATEGORY(CategoryName) FLogCategory###CategoryName CategoryName;

对象经过定义之后就可以在 UE_LOG 使用了。
这种分离声明和定义的方式可以用在暴露给外部使用的情况,别的文件或者模块只需要包含具有声明的头文件即可。

DECLARE_LOG_CATEGORY_CLASS

DECLARE_LOG_CATEGORY_CLASS宏的实现就比 DECLARE_LOG_CATEGORY_EXTERN 多做了一些操作,它定义了一个结构并 创建出一个 static 对象 ,不需要自己再使用DEFINE_LOG_CATEGRORY 进行定义。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/** 
* A macro to define a logging category as a C++ "static". This should ONLY be declared in a source file. Only accessible in that single file.
* @param CategoryName, category to declare
* @param DefaultVerbosity, default run time verbosity
* @param CompileTimeVerbosity, maximum verbosity to compile into the code
**/
#define DEFINE_LOG_CATEGORY_STATIC(CategoryName, DefaultVerbosity, CompileTimeVerbosity) \
static struct FLogCategory###CategoryName : public FLogCategory<ELogVerbosity::DefaultVerbosity, ELogVerbosity::CompileTimeVerbosity> \
{ \
FORCEINLINE FLogCategory###CategoryName() : FLogCategory(TEXT(#CategoryName)) {} \
} CategoryName;

/**
* A macro to declare a logging category as a C++ "class static"
* @param CategoryName, category to declare
* @param DefaultVerbosity, default run time verbosity
* @param CompileTimeVerbosity, maximum verbosity to compile into the code
**/
#define DECLARE_LOG_CATEGORY_CLASS(CategoryName, DefaultVerbosity, CompileTimeVerbosity) \
DEFINE_LOG_CATEGORY_STATIC(CategoryName, DefaultVerbosity, CompileTimeVerbosity)

它的声明会展开为:

1
2
3
4
static struct FLogCategoryLogCategoryName : public FLogCategory<ELogVerbosity::All, ELogVerbosity::All>
{
FORCEINLINE FLogCategoryLogCategoryName() : FLogCategory(TEXT("LogCategoryName")) {}
} LogCategoryName;

可以看到,和 DECLARE_LOG_CATEGORY_EXTERN 的区别在于:

  1. 去掉了 extern 修饰符
  2. 增加了 static 修饰符,定义对象

使用这个宏的用途一般直接写在.cpp 文件中,只供当前的翻译单元使用。

DECLARE_LOG_CATEGORY_EXTERN_HELPER

DECLARE_LOG_CATEGORY_EXTERN_HELPER这个宏只是 DECLARE_LOG_CATEGORY_EXTERN 的封装,并没有自己做什么特别的事情,和 DECLARE_LOG_CATEGORY_EXTERN 的用法完全一致。

1
2
3
// Platform specific logs, set here to make it easier to use them from anywhere
// need another layer of macro to help using a define in a define
#define DECLARE_LOG_CATEGORY_EXTERN_HELPER(A,B,C) DECLARE_LOG_CATEGORY_EXTERN(A,B,C)

后记

DECLARE_LOG_CATEGORY_EXTERN 也可以通过 XXXX_API 的方式修饰并导出符号,使其可以在外部模块中使用。