Предисловие
Привет Хабр!
В этой статье я хочу немного разобрать как работают UHT и рефлексия в Unreal Engine.
Точнее:
Как формируются .generated.h и .gen.cpp.
Как подставляются макросы, кои являются основой для работы Рефлексии.
Как UHT генерирует эти файлы, откуда берутся модули, пакеты, файлы в них.
Какие зависимости, классы и структуры позволяют рефлексии работать.
Немного затронем виртуальную машину.
Новичкам, полагаю, эта статья мало будет интересна, т.к будет много анализа исходного кода самого движка и его составляющих.
Однако, основная цель этой статьи это помочь всем желающим получше разобраться в движке и смотивировать покопаться в нем лично. Поэтому настоятельно рекомендую разобраться в этом самим, обращая особое внимание на те вещи, которые я буду здесь упоминать.
Также отмечу, что я лишь средний разработчик, не имеющий достаточно много опыта в движке, его модификации и пр. Я лишь пытаюсь разобраться в технологии. Точно так же, как если бы попытались вы. Следовательно, как бы я не пытался, мог допустить множество логических ошибок в процессе анализа.
Посему, если вы найдете в статье какие бы то ни было несостыковки, опечатки, пробелы или откровенный бред: обязательно пишите об этом в комментариях. Я исправлю материал так быстро, как только смогу.
Отдельное спасибо:
Тимофей Белов
За помощь в подготовке, проверки статьи и консультации.
habr.com/en/users/Signer/
Александр Шумейко
За консультации в процессе написания и проверке.
www.linkedin.com/in/forrgit/
Приятного ознакомления!
Основная часть.
Что есть сам UHT?
Unreal Header Tool - парсер, который поддерживает работу UObject системы, позволяя сущестовать такой вещи, как рефлексия в Unreal Engine.
Суть его проста: перед компиляцией вашего кода и генерации библиотек запускается UHT, который проходится по всем вашим файлам, в которых есть необходимые макросы, и генерирует новые: .generated.h
и .gen.cpp
.
Если это класс, то там перегружаются конструкторы с новыми параметрами. Конструкторы копирования и с move-семантикой добавляются в private поля. При помощи макроса DECLARE_CLASS
создаются новые static функции, перегружаются операторы присваивания и пр.
Если это структура, то кода для нее генерируется сильно меньше; StaticStruct
, определения для параметров и пр.
И после этого весь этот код вставляется на место GENERATED_BODY
в ваших сущностях.
Глава 1. Зависимости.
Взглянем сначала на те файлы, которые подключаются к нашим сгенерированным файлам.
Создадим простую структуру c минимальным функционалом:
#pragma once
#include "TestStruct.generated.h"
USTRUCT()
struct FMyStruct
{
GENERATED_BODY()
};
После компиляции сгенерируется два файла .generated.h и .gen.cpp
//TestStruct.generated.h
// Copyright Epic Games, Inc. All Rights Reserved.
/*===========================================================================
Generated c/Teode exported from UnrealHeaderTool.
DO NOT modify this manually! Edit the corresponding .h files instead!
===========================================================================*/
#include "UObject/ObjectMacros.h"
#include "UObject/ScriptMacros.h"
PRAGMA_DISABLE_DEPRECATION_WARNINGS
#ifdef MYPROJECT_TestStruct_generated_h
#error "TestStruct.generated.h already included, missing '#pragma once' in TestStruct.h"
#endif
#define MYPROJECT_TestStruct_generated_h
#define FID_MyProject_Source_MyProject_TestStruct_h_7_GENERATED_BODY \
friend struct Z_Construct_UScriptStruct_FMyStruct_Statics; \
MYPROJECT_API static class UScriptStruct* StaticStruct();
template<> MYPROJECT_API UScriptStruct* StaticStruct<struct FMyStruct>();
#undef CURRENT_FILE_ID
#define CURRENT_FILE_ID FID_MyProject_Source_MyProject_TestStruct_h
PRAGMA_ENABLE_DEPRECATION_WARNINGS
Включаемые хеддеры это "UObject/ObjectMacros.h"
и "UObject/ScriptMacros.h"
1.1 ObjectMacros.h
Внутри него можно найти множество интересных сущностей. Например, пространство имен UF, в котором содержатся модификаторы, которые вписываются в макрос UFUNCTION()
namespace UF
{
// valid keywords for the UFUNCTION and UDELEGATE macros
enum
{
/// This function is designed to be overridden by a blueprint. Do not provide a body for this function;
/// the autogenerated code will include a thunk that calls ProcessEvent to execute the overridden body.
BlueprintImplementableEvent,
/// This function is designed to be overridden by a blueprint, but also has a native implementation.
/// Provide a body named [FunctionName]_Implementation instead of [FunctionName]; the autogenerated
/// code will include a thunk that calls the implementation method when necessary.
BlueprintNativeEvent,
/// This function is sealed and cannot be overridden in subclasses.
/// It is only a valid keyword for events; declare other methods as static or final to indicate that they are sealed.
SealedEvent,
/// This function is executable from the command line.
Exec,
/// This function is replicated, and executed on servers. Provide a body named [FunctionName]_Implementation instead of [FunctionName];
/// the autogenerated code will include a thunk that calls the implementation method when necessary.
Server,
/// This function is replicated, and executed on clients. Provide a body named [FunctionName]_Implementation instead of [FunctionName];
/// the autogenerated code will include a thunk that calls the implementation method when necessary.
Client,
/// This function is both executed locally on the server and replicated to all clients, regardless of the Actor's NetOwner
NetMulticast,
/// Replication of calls to this function should be done on a reliable channel.
/// Only valid when used in conjunction with Client or Server
Reliable,
/// Replication of calls to this function can be done on an unreliable channel.
/// Only valid when used in conjunction with Client or Server
Unreliable,
...
}
Кроме него так же есть UP, где описаны модификаторы для UPROPERTY():
namespace UP
{
// valid keywords for the UPROPERTY macro
enum
{
/// This property is const and should be exported as const.
Const,
/// Property should be loaded/saved to ini file as permanent profile.
Config,
/// Same as above but load config from base class, not subclass.
GlobalConfig,
/// Property should be loaded as localizable text. Implies ReadOnly.
Localized,
/// Property is transient: shouldn't be saved, zero-filled at load time.
Transient,
/// Property should always be reset to the default value during any type of duplication (copy/paste, binary duplication, etc.)
DuplicateTransient,
/// Property should always be reset to the default value unless it's being duplicated for a PIE session - deprecated, use NonPIEDuplicateTransient instead
NonPIETransient,
/// Property should always be reset to the default value unless it's being duplicated for a PIE session
NonPIEDuplicateTransient,
/// Value is copied out after function call. Only valid on function param declaration.
Ref,
/// Object property can be exported with it's owner.
Export,
/// Hide clear (and browse) button in the editor.
NoClear,
...
};
Все я перечислять не буду, однако отмечу, что кроме них есть еще:
namespace US // USTRUCT()
namespace UM // Metadata для разных USTRUCT,UPROPERTY и пр
namespace UI // UINTERFACE()
namespace UC // UCLASS()
Так же там есть ENUM'ы для различных флагов, которые используется в редакторе.
Например флаги для переменных, используемых в панели Details
под полем Defined Property Flags:
Добавление новых свойств на панели или в коде будет сопровождаться появлением соответствующих флагов.
Сами флаги учавствуют во многих частях движка . Так, например, флаг CPF_SaveGame используется в методе для сериализации данных:
bool FProperty::ShouldSerializeValue( FArchive& Ar ) const
{
if (Ar.ShouldSkipProperty(this))
{
return false;
}
//Через побитовые операции проверяется текущее состояние PropertyFlags.
if (!(PropertyFlags & CPF_SaveGame) && Ar.IsSaveGame())
{
return false;
}
const uint64 SkipFlags = CPF_Transient | CPF_DuplicateTransient | CPF_NonPIEDuplicateTransient | CPF_NonTransactional | CPF_Deprecated | CPF_DevelopmentAssets | CPF_SkipSerialization;
if (!(PropertyFlags & SkipFlags))
{
return true;
}
bool Skip =
((PropertyFlags & CPF_Transient) && Ar.IsPersistent() && !Ar.IsSerializingDefaults())
|| ((PropertyFlags & CPF_DuplicateTransient) && (Ar.GetPortFlags() & PPF_Duplicate))
|| ((PropertyFlags & CPF_NonPIEDuplicateTransient) && !(Ar.GetPortFlags() & PPF_DuplicateForPIE) && (Ar.GetPortFlags() & PPF_Duplicate))
|| ((PropertyFlags & CPF_NonTransactional) && Ar.IsTransacting())
|| ((PropertyFlags & CPF_Deprecated) && !Ar.HasAllPortFlags(PPF_UseDeprecatedProperties) && (Ar.IsSaving() || Ar.IsTransacting() || Ar.WantBinaryPropertySerialization()))
|| ((PropertyFlags & CPF_SkipSerialization) && (Ar.WantBinaryPropertySerialization() || !Ar.HasAllPortFlags(PPF_ForceTaggedSerialization)))
|| (IsEditorOnlyProperty() && Ar.IsFilterEditorOnly());
return !Skip;
}
Настоятельно рекомендую запомнить структуру FProperty
. Мы к ней еще вернемся.
Есть еще пара примеров работы с этими флагами:
В UHT парсере есть функция, возвращающая тип переменной через out-параметр.
void FHeaderParser::GetVarType(
FScope* Scope,
EGetVarTypeOptions Options,
FPropertyBase& VarProperty,
EPropertyFlags Disallow,
EUHTPropertyType OuterPropertyType,
EPropertyFlags OuterPropertyFlags,
EPropertyDeclarationStyle::Type PropertyDeclarationStyle,
EVariableCategory VariableCategory,
FIndexRange* ParsedVarIndexRange,
ELayoutMacroType* OutLayoutMacroType
)
Эта функция используется внутри самой себя, рекурсивно, и в нескольких других функциях, название которых говорит само за себя:
FUnrealFunctionDefinitionInfo& FHeaderParser::CompileDelegateDeclaration(const FStringView& DelegateIdentifier, EDelegateSpecifierAction::Type SpecifierAction)
void FHeaderParser::ParseParameterList(FUnrealFunctionDefinitionInfo& FunctionDef, bool bExpectCommaBeforeName, TMap<FName, FString>* MetaData, EGetVarTypeOptions Options)
FUnrealFunctionDefinitionInfo& FHeaderParser::CompileFunctionDeclaration()
void FHeaderParser::CompileVariableDeclaration(FUnrealStructDefinitionInfo& StructDef)
Если взглянуть в саму функцию GetVarType
, то обнаружим несколько интересных для нас моментов.
Если тип переменной TMap, то мы смотрим тип ее ключа, рекурсивно вызывая GetVarType()
, если тип - контейнер, то выбрасываем ошибку, т.к вложенные контейнеры не поддерживаются движком, то же самое и для UINTERFACE
, TEXT
, и если мы пытаемся контейнер реплицировать.
Важно замечание: везде тут используются флаги из приведенного выше ENUM'a
: CPT_Text,CPF_Net,CPT_Interface
...
//Если TMap
else if ( VarType.IsValue(TEXT("TMap"), ESearchCase::CaseSensitive) )
{
RequireSymbol( TEXT('<'), TEXT("'tmap'") );
FPropertyBase MapKeyType(CPT_None);
//Рекурсивно смотрим флаги уже TMap
GetVarType(Scope, Options, MapKeyType, Disallow, EUHTPropertyType::Map, Flags, EPropertyDeclarationStyle::None, VariableCategory);
if (MapKeyType.IsContainer()) // если ключ является контейнером, выбрасываем Error.
{
Throwf(TEXT("Nested containers are not supported.") );
}
// TODO: Prevent sparse delegate types from being used in a container
//Если тип ключа не контейнер, но интерфейс - выбрасываем Error
if (MapKeyType.Type == CPT_Interface)
{
Throwf(TEXT("UINTERFACEs are not currently supported as key types."));
}
if (MapKeyType.Type == CPT_Text)
{
Throwf(TEXT("FText is not currently supported as a key type."));
}
//И если TMap пытаемся реплицировать, выбрасываем Error.
if (EnumHasAnyFlags(Flags, CPF_Net))
{
LogError(TEXT("Replicated maps are not supported."));
}
...
Сами флаги ставятся по достаточно простой логике.
Например, если парсер обнаружил ключевое слово const у поля, то во флаг добавляем его в переменную через побитовое ИЛИ:
...
// const before the variable type support (only for params)
if (MatchIdentifier(TEXT("const"), ESearchCase::CaseSensitive))
{
Flags |= CPF_ConstParm;
bNativeConst = true;
}
...
Интересно, кстати, что сам тип переменной Flags
определен через макрос ENUM_CLASS_FLAGS(EPropertyFlags)
где и можно посмотреть перегруженный оператор |=
, используемый в выражении Flags |= CPF_ConstParm;
// Defines all bitwise operators for enum classes so it can be (mostly) used as a regular flags enum
#define ENUM_CLASS_FLAGS(Enum) \
inline Enum& operator|=(Enum& Lhs, Enum Rhs) { return Lhs = (Enum)((__underlying_type(Enum))Lhs | (__underlying_type(Enum))Rhs); } \
inline Enum& operator&=(Enum& Lhs, Enum Rhs) { return Lhs = (Enum)((__underlying_type(Enum))Lhs & (__underlying_type(Enum))Rhs); } \
inline Enum& operator^=(Enum& Lhs, Enum Rhs) { return Lhs = (Enum)((__underlying_type(Enum))Lhs ^ (__underlying_type(Enum))Rhs); } \
inline constexpr Enum operator| (Enum Lhs, Enum Rhs) { return (Enum)((__underlying_type(Enum))Lhs | (__underlying_type(Enum))Rhs); } \
inline constexpr Enum operator& (Enum Lhs, Enum Rhs) { return (Enum)((__underlying_type(Enum))Lhs & (__underlying_type(Enum))Rhs); } \
inline constexpr Enum operator^ (Enum Lhs, Enum Rhs) { return (Enum)((__underlying_type(Enum))Lhs ^ (__underlying_type(Enum))Rhs); } \
inline constexpr bool operator! (Enum E) { return !(__underlying_type(Enum))E; } \
inline constexpr Enum operator~ (Enum E) { return (Enum)~(__underlying_type(Enum))E; }
Таким образом в переменную Flags через побитовый оператор собираются всевозможные состояние, которые только смог обнаружить парсер. Весь поиск выполняется рекурсивно.
И, наконец, так же можем обнаружить там объявления макросов UFUNCTION
,UPROPERTY
и п.р.
// These macros wrap metadata parsed by the Unreal Header Tool, and are otherwise
// ignored when code containing them is compiled by the C++ compiler
#define UPROPERTY(...)
#define UFUNCTION(...)
#define USTRUCT(...)
#define UMETA(...)
#define UPARAM(...)
#define UENUM(...)
#define UDELEGATE(...)
#define RIGVM_METHOD(...)
1.2 ScriptMacros.h
Следующий хеддер это "UObject/ScriptMacros.h"
.
Вверху хеддера сразу дают понять что это такое:
/*=============================================================================
ScriptMacros.h: Kismet VM execution engine.
=============================================================================*/
...
По факту, в этом хеддере собраны макросы для работы виртуальной машины.
...
#define P_GET_PROPERTY_REF(PropertyType, ParamName) \
PropertyType::TCppType ParamName##Temp = PropertyType::GetDefaultPropertyValue(); \
PropertyType::TCppType& ParamName = Stack.StepCompiledInRef<PropertyType, PropertyType::TCppType>(&ParamName##Temp);
#define P_GET_UBOOL(ParamName) uint32 ParamName##32 = 0; bool ParamName=false; Stack.StepCompiledIn<FBoolProperty>(&ParamName##32); ParamName = !!ParamName##32; // translate the bitfield into a bool type for non-intel platforms
#define P_GET_UBOOL8(ParamName) uint32 ParamName##32 = 0; uint8 ParamName=0; Stack.StepCompiledIn<FBoolProperty>(&ParamName##32); ParamName = ParamName##32 ? 1 : 0; // translate the bitfield into a bool type for non-intel platforms
#define P_GET_UBOOL16(ParamName) uint32 ParamName##32 = 0; uint16 ParamName=0; Stack.StepCompiledIn<FBoolProperty>(&ParamName##32); ParamName = ParamName##32 ? 1 : 0; // translate the bitfield into a bool type for non-intel platforms
#define P_GET_UBOOL32(ParamName) uint32 ParamName=0; Stack.StepCompiledIn<FBoolProperty>(&ParamName); ParamName = ParamName ? 1 : 0; // translate the bitfield into a bool type for non-intel platforms
#define P_GET_UBOOL64(ParamName) uint64 ParamName=0; Stack.StepCompiledIn<FBoolProperty>(&ParamName); ParamName = ParamName ? 1 : 0; // translate the bitfield into a bool type for non-intel platforms
#define P_GET_UBOOL_REF(ParamName) PARAM_PASSED_BY_REF_ZEROED(ParamName, FBoolProperty, bool)
#define P_GET_STRUCT(StructType,ParamName) PARAM_PASSED_BY_VAL(ParamName, FStructProperty, PREPROCESSOR_COMMA_SEPARATED(StructType))
#define P_GET_STRUCT_REF(StructType,ParamName) PARAM_PASSED_BY_REF(ParamName, FStructProperty, PREPROCESSOR_COMMA_SEPARATED(StructType))
#define P_GET_OBJECT(ObjectType,ParamName) PARAM_PASSED_BY_VAL_ZEROED(ParamName, FObjectPropertyBase, ObjectType*)
#define P_GET_OBJECT_REF(ObjectType,ParamName) PARAM_PASSED_BY_REF_ZEROED(ParamName, FObjectPropertyBase, ObjectType*)
...
Важной для нас является другая вещь, немного лучше раскрывающая работу VM, это переменная Stack, которая часто используется даже в самых простых макросах вроде:
#define P_GET_UBOOL(ParamName) uint32 ParamName##32 = 0; bool ParamName=false; Stack.StepCompiledIn<FBoolProperty>(&ParamName##32); ParamName = !!ParamName##32; // translate the bitfield into a bool type for non-intel platforms
В процессе анализа можем наткнуться на такую структуру как
struct FFrame : public FOutputDevice
в Stack.h
struct FFrame : public FOutputDevice
{
public:
// Variables.
UFunction* Node;
UObject* Object;
uint8* Code;
uint8* Locals;
FProperty* MostRecentProperty;
uint8* MostRecentPropertyAddress;
/** The execution flow stack for compiled Kismet code */
FlowStackType FlowStack;
/** Previous frame on the stack */
FFrame* PreviousFrame;
/** contains information on any out parameters */
FOutParmRec* OutParms;
/** If a class is compiled in then this is set to the property chain for compiled-in functions. In that case, we follow the links to setup the args instead of executing by code. */
FField* PropertyChainForCompiledIn;
/** Currently executed native function */
UFunction* CurrentNativeFunction;
bool bArrayContextFailed;
#if PER_FUNCTION_SCRIPT_STATS
/** Increment for each PreviousFrame on the stack (Max 255) */
uint8 DepthCounter;
#endif
public:
...
Как ясно из полей, и как мы увидим далее, эта структура используется как execution-frame:
Стэк объектов, хранящих текущую исполняемую функцию, локальные переменные, код, контекст, т.е объект на котором эта функция вызывается, цепочку из свойств, т.е аргументов фукнции и пр.
К примеру, один из макросов возвращает свойство из этого стэка кадров:
#define P_GET_PROPERTY(PropertyType, ParamName) \
PropertyType::TCppType ParamName = PropertyType::GetDefaultPropertyValue(); \
Stack.StepCompiledIn<PropertyType>(&ParamName);
1.3 Сущности.
1.3.1. UFunction.
Посмотрим поподробнее на объект UFunction
:
//
// Reflection data for a replicated or Kismet callable function.
//
class COREUOBJECT_API UFunction : public UStruct
{
DECLARE_CASTED_CLASS_INTRINSIC(UFunction, UStruct, 0, TEXT("/Script/CoreUObject"), CASTCLASS_UFunction)
DECLARE_WITHIN(UClass)
public:
// Persistent variables.
/** EFunctionFlags set defined for this function */
EFunctionFlags FunctionFlags;
// Variables in memory only.
/** Number of parameters total */
uint8 NumParms;
/** Total size of parameters in memory */
uint16 ParmsSize;
/** Memory offset of return value property */
uint16 ReturnValueOffset;
/** Id of this RPC function call (must be FUNC_Net & (FUNC_NetService|FUNC_NetResponse)) */
uint16 RPCId;
/** Id of the corresponding response call (must be FUNC_Net & FUNC_NetService) */
uint16 RPCResponseId;
/** pointer to first local struct property in this UFunction that contains defaults */
FProperty* FirstPropertyToInit;
#if UE_BLUEPRINT_EVENTGRAPH_FASTCALLS
/** The event graph this function calls in to (persistent) */
UFunction* EventGraphFunction;
/** The state offset inside of the event graph (persistent) */
int32 EventGraphCallOffset;
#endif
#if WITH_LIVE_CODING
/** Pointer to the cached singleton pointer to this instance */
UFunction** SingletonPtr;
#endif
private:
/** C++ function this is bound to */
FNativeFuncPtr Func;
...
В нем видим понятные свойства: кол-во параметров NumParms,
размер всех параметров в памяти ParmsSize.
Интересно, что у нее также есть SetNativeFunc
;
/**
* Sets the native func pointer.
*
* @param InFunc - The new function pointer.
*/
FORCEINLINE void SetNativeFunc(FNativeFuncPtr InFunc)
{
Func = InFunc;
}
Которое явно говорит, что UFunction это объект, который позволяет контролировать процесс репликации или вызова из Blueprint, и он всего навсего хранит указатель на нативную функцию, которая должна быть в итоге вызвана. Есть так же функциональности для работы в Графе самого движка.
И последнее что нас тут интересует, это функция Invoke
, которая вызывается на переданном объекте, принимая аргументом выше озвученный стэк и out параметр объявленный через макрос RESULT_DECL.
/**
* Invokes the UFunction on a UObject.
*
* @param Obj - The object to invoke the function on.
* @param Stack - The parameter stack for the function call.
* @param Result - The result of the function.
*/
void Invoke(UObject* Obj, FFrame& Stack, RESULT_DECL);
Внутрии нее сначала берется объект, на котором нужно вызвать фунцию, после чего производится сам вызов с обращением к нативной функции, а точнее, к ее wraper'у:
void UFunction::Invoke(UObject* Obj, FFrame& Stack, RESULT_DECL)
{
checkSlow(Func);
UClass* OuterClass = (UClass*)GetOuter();
if (OuterClass->IsChildOf(UInterface::StaticClass()))
{
Obj = (UObject*)Obj->GetInterfaceAddress(OuterClass);
}
TGuardValue<UFunction*> NativeFuncGuard(Stack.CurrentNativeFunction, this);
return (*Func)(Obj, Stack, RESULT_PARAM);
}
/** The type of a native function callable by script */
typedef void (*FNativeFuncPtr)(UObject* Context, FFrame& TheStack, RESULT_DECL);
1.3.2. UField.
//
// Base class of reflection data objects.
//
class COREUOBJECT_API UField : public UObject
{
DECLARE_CASTED_CLASS_INTRINSIC(UField, UObject, CLASS_Abstract, TEXT("/Script/CoreUObject"), CASTCLASS_UField)
typedef UField BaseFieldClass;
typedef UClass FieldTypeClass;
/** Next Field in the linked list */
UField* Next;
// Constructors.
UField(EStaticConstructor, EObjectFlags InFlags);
// UObject interface.
virtual void Serialize( FArchive& Ar ) override;
virtual void PostLoad() override;
virtual bool NeedsLoadForClient() const override;
virtual bool NeedsLoadForServer() const override;
...
};
Репрезентация Поля так такового в движке. Сам по себе, класс представляет forward list, содержащий указатель на следующее поле:
/** Next Field in the linked list */
UField* Next;
UField
, к примеру, умеет выводить локализованный текст, сериализироваться, и имеет интерфейс для наследников:
...
FText UField::GetDisplayNameText() const
{
FText LocalizedDisplayName;
static const FString Namespace = TEXT("UObjectDisplayNames");
static const FName NAME_DisplayName(TEXT("DisplayName"));
const FString Key = GetFullGroupName(false);
FString NativeDisplayName = GetMetaData(NAME_DisplayName);
if (NativeDisplayName.IsEmpty())
{
NativeDisplayName = FName::NameToDisplayString(FDisplayNameHelper::Get(*this), false);
}
if ( !( FText::FindText( Namespace, Key, /*OUT*/LocalizedDisplayName, &NativeDisplayName ) ) )
{
LocalizedDisplayName = FText::FromString(NativeDisplayName );
}
return LocalizedDisplayName;
}
...
// UField interface.
virtual void AddCppProperty(FProperty* Property);
virtual void Bind();
...
//У поля свойств нет.
void UField::AddCppProperty(FProperty* Property)
{
UE_LOG(LogClass, Fatal,TEXT("UField::AddCppProperty"));
}
1.3.3 UStruct
В первую очередь, это UField
, который содержит свои собственные (X)Field
.
Полей в данном случае пара:
Первый это указатель на UFiled
и второй - указатель на FField
.
UField
сам по себе, это легаси, которое еще требуется для загрузки старых проектов, и содержит базовые функциональности, которые я указал выше.
/** Pointer to start of linked list of child fields */
UField* Children;
/** Pointer to start of linked list of child fields */
FField* ChildProperties;
Сам FField
может быть создан на основе существующего UField
:
// Constructors.
FField(EInternal InInernal, FFieldClass* InClass);
FField(FFieldVariant InOwner, const FName& InName, EObjectFlags InObjectFlags);
/** Creates a new FField from existing UField */
static FField* CreateFromUField(UField* InField);
Кроме того, у UStruct
есть Свойства (FProperty)
Представлены четырьмя полями:
/** In memory only: Linked list of properties from most-derived to base */
FProperty* PropertyLink;
/** In memory only: Linked list of object reference properties from most-derived to base */
FProperty* RefLink;
/** In memory only: Linked list of properties requiring destruction. Note this does not include things that will be destroyed byt he native destructor */
FProperty* DestructorLink;
/** In memory only: Linked list of properties requiring post constructor initialization */
FProperty* PostConstructLink;
Каждое поле несет свою специфическую функциональность, как например одно из них, это исключительно свойства структуры, которые требуют инициализации после вызова конструктора:
/** In memory only: Linked list of properties requiring post constructor initialization */
FProperty* PostConstructLink;
Или, требующие вызова деструктора:
/** In memory only: Linked list of properties requiring destruction. Note this does not include things that will be destroyed byt he native destructor */
FProperty* DestructorLink;
Или вовсе список абсолютно всех свойств:
/** In memory only: Linked list of properties from most-derived to base */
FProperty* PropertyLink;
Структура также имеет перегруженную функцию с добавлением свойства:
// UField interface.
virtual void AddCppProperty(FProperty* Property) override;
Где раскрывается несложный механизм работы добавления новых FProperty
в обычный список PropertyLink.
void UStruct::AddCppProperty(FProperty* Property)
{
Property->Next = ChildProperties;
ChildProperties = Property;
}
И, как полагается, у всех структур есть свои собственные флаги:'//
enum EStructFlags
{
// State flags.
STRUCT_NoFlags = 0x00000000,
STRUCT_Native = 0x00000001,
/** If set, this struct will be compared using native code */
STRUCT_IdenticalNative = 0x00000002,
STRUCT_HasInstancedReference= 0x00000004,
STRUCT_NoExport = 0x00000008,
/** Indicates that this struct should always be serialized as a single unit */
STRUCT_Atomic = 0x00000010,
/** Indicates that this struct uses binary serialization; it is unsafe to add/remove members from this struct without incrementing the package version */
STRUCT_Immutable = 0x00000020,
/** If set, native code needs to be run to find referenced objects */
STRUCT_AddStructReferencedObjects = 0x00000040,
...
};'
1.3.4 UScriptStruct
/**
* Reflection data for a standalone structure declared in a header or as a user defined struct
*/
class UScriptStruct : public UStruct
{
public:
/** Interface to template to manage dynamic access to C++ struct construction and destruction **/
struct COREUOBJECT_API ICppStructOps
{
/**
* Constructor
* @param InSize: sizeof() of the structure
*/
ICppStructOps(int32 InSize, int32 InAlignment)
: Size(InSize)
, Alignment(InAlignment)
{
}
virtual ~ICppStructOps() {}
/** return true if this class has a no-op constructor and takes EForceInit to init **/
virtual bool HasNoopConstructor() = 0;
/** return true if memset can be used instead of the constructor **/
virtual bool HasZeroConstructor() = 0;
/** Call the C++ constructor **/
virtual void Construct(void *Dest) = 0;
/** Call the C++ constructor without value-init (new T instead of new T()) **/
virtual void ConstructForTests(void* Dest) = 0;
/** return false if this destructor can be skipped **/
virtual bool HasDestructor() = 0;
/** Call the C++ destructor **/
virtual void Destruct(void *Dest) = 0;
/** return the sizeof() of this structure **/
FORCEINLINE int32 GetSize()
{
return Size;
}
/** return the alignof() of this structure **/
FORCEINLINE int32 GetAlignment()
{
return Alignment;
}
...
};
Весь этот класс поддерживает работу той сущности, которую вы используете как поле в другой структуре, или классе. Однако, а что если мы создаем структуру в движке?
И у этих “Стуктур
” есть свои “Родители
”.
Этим классом для структур, на основе которых создаются наши Blueprint структуры, выступает UUserDefinedStruct
, который является наследником вышеуказанного класса UScriptStruct
и поддерживает ее работу.
UCLASS()
class ENGINE_API UUserDefinedStruct : public UScriptStruct
{
GENERATED_UCLASS_BODY()
public:
#if WITH_EDITORONLY_DATA
/** The original struct, when current struct isn't a temporary duplicate, the field should be null */
UPROPERTY(Transient)
TWeakObjectPtr<UUserDefinedStruct> PrimaryStruct;
UPROPERTY()
FString ErrorMessage;
UPROPERTY()
TObjectPtr<UObject> EditorData;
#endif // WITH_EDITORONLY_DATA
/** Status of this struct, outside of the editor it is assumed to always be UpToDate */
UPROPERTY()
TEnumAsByte<enum EUserDefinedStructureStatus> Status;
/** Uniquely identifies this specific user struct */
UPROPERTY()
FGuid Guid;
protected:
/** Default instance of this struct with default values filled in, used to initialize structure */
FUserStructOnScopeIgnoreDefaults DefaultStructInstance;
/** Bool to indicate we want to initialize a version of this struct without defaults, this is set while allocating the DefaultStructInstance itself */
bool bIgnoreStructDefaults;
…
2. Рефлексия.
2.1 UStruct
Вернемся к написанной в первой главе структуре.
#pragma once
#include "TestStruct.generated.h"
USTRUCT()
struct FMyStruct
{
GENERATED_BODY()
};
UHT генерирует для нас следующий файл:
#include "UObject/ObjectMacros.h"
#include "UObject/ScriptMacros.h"
PRAGMA_DISABLE_DEPRECATION_WARNINGS
#ifdef MYPROJECT_TestStruct_generated_h
#error "TestStruct.generated.h already included, missing '#pragma once' in TestStruct.h"
#endif
#define MYPROJECT_TestStruct_generated_h
//Важное объявления макроса.
#define FID_MyProject_Source_MyProject_TestStruct_h_7_GENERATED_BODY \
friend struct Z_Construct_UScriptStruct_FMyStruct_Statics; \
MYPROJECT_API static class UScriptStruct* StaticStruct();
template<> MYPROJECT_API UScriptStruct* StaticStruct<struct FMyStruct>();
#undef CURRENT_FILE_ID
#define CURRENT_FILE_ID FID_MyProject_Source_MyProject_TestStruct_h
PRAGMA_ENABLE_DEPRECATION_WARNINGS
Файл этот сильно меньше, чем его коллега с UCLASS(). Теперь подробнее:
После include’ов объявляем макрос, отключающий предупреждения об устаревших полях/функциях, такой же макрос используется в конце файла для их включения.
Строки относятся к файлу GenericPlatformCompilerPreSetup.h где объявлены макросы для deprecation warning’ов
...
#ifndef DISABLE_DEPRECATION
#define DEPRECATED(VERSION, MESSAGE) DEPRECATED_MACRO(4.22, "The DEPRECATED macro has been deprecated in favor of UE_DEPRECATED().") __declspec(deprecated(MESSAGE " Please update your code to the new API before upgrading to the next release, otherwise your project will no longer compile."))
#define PRAGMA_DISABLE_DEPRECATION_WARNINGS \
__pragma (warning(push)) \
__pragma (warning(disable: 4995)) /* 'function': name was marked as #pragma deprecated */ \
__pragma (warning(disable: 4996)) /* The compiler encountered a deprecated declaration. */
#define PRAGMA_ENABLE_DEPRECATION_WARNINGS \
__pragma (warning(pop))
#endif // DISABLE_DEPRECATION
...
Далее идет проверка на включение #pragma once
#ifdef MYPROJECT_TestStruct_generated_h
#error "TestStruct.generated.h already included, missing '#pragma once' in TestStruct.h"
#endif
Генерируется макрос FID_Fighting_Source_Fighting_TestStruct_h_7_GENERATED_BODY
После чего идет объявление friend структуры Z_Construct_UScriptStruct_FMyStruct_Statics
И, наконец, объявление static
функции, возвращающей UScriptStruct* StaticStruct()
Это так называемый CDO (Class default Object).
Вы часто могли ее наблюдать, когда хотели передать “Тип” вашей структуры. Для создание новой такой же, или для проверки IsA()
, к примеру: FMyStruct::StaticStruct();
Как видно, создан этот класс чтобы быстро можно было получить класс или структуру по-умолчанию,не создавая новую, для вышеописанных целей.
Хотя по сути, это лишь перегруженная версия шаблонной функции из файла ReflectedTypeAccessors.h :
// Copyright Epic Games, Inc. All Rights Reserved.
#pragma once
class UClass;
class UScriptStruct;
class UEnum;
/*-----------------------------------------------------------------------------
C++ templated Static(Class/Struct/Enum) retrieval function prototypes.
-----------------------------------------------------------------------------*/
template<typename ClassType> UClass* StaticClass();
template<typename StructType> UScriptStruct* StaticStruct();
template<typename EnumType> UEnum* StaticEnum();
Где мы лишь указываем свой конкретный тип.
Далее, лишь определение конкретного ID для текущего сгенерированного файла, и доступа к нему.
#undef CURRENT_FILE_ID
#define CURRENT_FILE_ID FID_MyProject_Source_MyProject_TestStruct_h
Для полного понимания можно обратиться к const FString& FUnrealSourceFile::GetFileId() const
Файла UnrealSourceFile.cpp
После того как эти строки с объявлением функции, возвращающей CDO, сгенерированы, их необходимо куда то вставить, верно?
Этим и занимается макрос GENERATED_BODY().
Работа этого макроса еще лучше будет раскрываться при обзоре сгенерированного файла для класса.
В данном же случае, все сгенерированные объявления будут вставлены там же, где окажется макрос FID_MyProject_Source_MyProject_TestStruct_h_7_GENERATED_BODY.
Взглянем на макрос GENERATED_BODY().
Сам он, объявлен следующим образом:
#define GENERATED_BODY(...) BODY_MACRO_COMBINE(CURRENT_FILE_ID,_,__LINE__,_GENERATED_BODY);
Сама же цепочка BODY_MACRO_
весьма банально соединяет все переданные аргументы при помощи токена ##
.
// This pair of macros is used to help implement GENERATED_BODY() and GENERATED_USTRUCT_BODY()
#define BODY_MACRO_COMBINE_INNER(A,B,C,D) A##B##C##D
#define BODY_MACRO_COMBINE(A,B,C,D) BODY_MACRO_COMBINE_INNER(A,B,C,D)
И вместо макроса GENERATED_BODY
, сначала, вставляется макрос, где прописаны CURRENT_FILE_ID
+ _
+__LINE__
+ _GENERATED BODY
;
И в конце для каждого отдельного .generated.h
файла мы получим что-то наподобии:
FID_MyProject_Source_MyProject_TestStruct_h_7_GENERATED_BODY
Где:
FID_MyProject_Source_MyProject_TestStruct_h
- Файл ID
_7
- строка, на которой объявлен макрос GENERATED_BODY()
Перейдем теперь к реализации. В файл .gen.cpp
этой структуры.
Кода там явно больше, поэтому обойдемся отрывками.
В первую очередь, видим новый .h файл: #include "UObject/GeneratedCppIncludes.h"
Хранящий другие .h файлы:
#include "CoreMinimal.h"
#include "UObject/Object.h"
#include "UObject/UObjectGlobals.h"
#include "UObject/CoreNative.h"
#include "UObject/Class.h"
#include "UObject/Package.h"
#include "UObject/MetaData.h"
#include "UObject/UnrealType.h"
#include "UObject/EnumProperty.h"
#include "UObject/TextProperty.h"
#include "UObject/FieldPathProperty.h"
Заострять внимание на них не будем. Отмечу лишь, что здесь есть хеддеры, которые мы видели ранее.
e.g #include "UObject/Class.h"
Ниэе уже идет определение функции ::StaticStruct для нашей структуры:
class UScriptStruct* FMyStruct::StaticStruct()
{
if (!Z_Registration_Info_UScriptStruct_MyStruct.OuterSingleton)
{
Z_Registration_Info_UScriptStruct_MyStruct.OuterSingleton = GetStaticStruct(Z_Construct_UScriptStruct_FMyStruct, Z_Construct_UPackage__Script_MyProject(), TEXT("MyStruct"));
}
return Z_Registration_Info_UScriptStruct_MyStruct.OuterSingleton;
}
И вызов этой функции в шаблонной функции StaticStruct<>
template<> MYPROJECT_API UScriptStruct* StaticStruct<FMyStruct>()
{
return FMyStruct::StaticStruct();
}
Далее, объявленные в теле структуры поля получают свою реализацию.
Для полностью пустой USTRUCT()
UHT генерирует следующую структуру:
struct Z_Construct_UScriptStruct_FMyStruct_Statics
{
#if WITH_METADATA
static const UECodeGen_Private::FMetaDataPairParam Struct_MetaDataParams[];
#endif
static void* NewStructOps();
static const UECodeGen_Private::FStructParams ReturnStructParams;
};
Функция NewStructOps()
возвращает указатель на void
, однако по факту идет обращение к классу UScriptStruct
что я обозначал ранее, и его вложенной структуре TCppStructOps
что по факту является наследником интерфейса, что предоставляет доступ к конструктору и деструктору из c++.
/** Template to manage dynamic access to C++ struct construction and destruction **/
template<class CPPSTRUCT>
struct TCppStructOps : public ICppStructOps
{
typedef TStructOpsTypeTraits<CPPSTRUCT> TTraits;
TCppStructOps()
: ICppStructOps(sizeof(CPPSTRUCT), alignof(CPPSTRUCT))
{
}
...
Так что в действительности. Эта функция возвращает указатель на экземпляр шаблонной структуры UScriptStruct::TCppStructOps, где параметром является наша структура.
void* Z_Construct_UScriptStruct_FMyStruct_Statics::NewStructOps()
{
return (UScriptStruct::ICppStructOps*)new UScriptStruct::TCppStructOps<FMyStruct>();
}
Далее идет определение static
поля UECodeGen_Private::FStructParams.
const UECodeGen_Private::FStructParams Z_Construct_UScriptStruct_FMyStruct_Statics::ReturnStructParams = {
(UObject* (*)())Z_Construct_UPackage__Script_MyProject,
nullptr,
&NewStructOps,
"MyStruct",
sizeof(FMyStruct),
alignof(FMyStruct),
nullptr,
0,
RF_Public|RF_Transient|RF_MarkAsNative,
EStructFlags(0x00000001),
METADATA_PARAMS(Z_Construct_UScriptStruct_FMyStruct_Statics::Struct_MetaDataParams, UE_ARRAY_COUNT(Z_Construct_UScriptStruct_FMyStruct_Statics::Struct_MetaDataParams))
};
По сути, тут возвращаются параметры, описывающие нашу структуру.
Сама FStructParams
описывает следующее:
struct FStructParams
{
UObject* (*OuterFunc)();
UScriptStruct* (*SuperFunc)();
void* (*StructOpsFunc)(); // really returns UScriptStruct::ICppStructOps*
const char* NameUTF8;
SIZE_T SizeOf;
SIZE_T AlignOf;
const FPropertyParamsBase* const* PropertyArray;
int32 NumProperties;
EObjectFlags ObjectFlags;
uint32 StructFlags; // EStructFlags
#if WITH_METADATA
const FMetaDataPairParam* MetaDataArray;
int32 NumMetaData;
#endif
};
Некоторые вещи нам уже встречались.
Как например void* (*StructOpsFunc)();
Или uint32 StructFlags;
Тут еще важно упомянуть enum EObjectFlags
, содержащий поля, маркирующие наши объекты. Именно объекты, которые могут быть сериализованы или собраны при помощи GC.
/**
* Flags describing an object instance
*/
enum EObjectFlags
{
// Do not add new flags unless they truly belong here. There are alternatives.
// if you change any the bit of any of the RF_Load flags, then you will need legacy serialization
RF_NoFlags = 0x00000000, ///< No flags, used to avoid a cast
// This first group of flags mostly has to do with what kind of object it is. Other than transient, these are the persistent object flags.
// The garbage collector also tends to look at these.
RF_Public =0x00000001, ///< Object is visible outside its package.
RF_Standalone =0x00000002, ///< Keep object around for editing even if unreferenced.
RF_MarkAsNative =0x00000004, ///< Object (UField) will be marked as native on construction (DO NOT USE THIS FLAG in HasAnyFlags() etc)
RF_Transactional =0x00000008, ///< Object is transactional.
RF_ClassDefaultObject =0x00000010, ///< This object is its class's default object
RF_ArchetypeObject =0x00000020, ///< This object is a template for another object - treat like a class default object
RF_Transient =0x00000040, ///< Don't save object.
...
RF_FinishDestroyed =0x00010000, ///< FinishDestroy has been called on the object.
// Misc. Flags
RF_BeingRegenerated =0x00020000, ///< Flagged on UObjects that are used to create UClasses (e.g. Blueprints) while they are regenerating their UClass on load (See FLinkerLoad::CreateExport()), as well as UClass objects in the midst of being created
RF_DefaultSubObject =0x00040000, ///< Flagged on subobjects that are defaults
RF_WasLoaded =0x00080000, ///< Flagged on UObjects that were loaded
...
RF_PendingKill UE_DEPRECATED(5.0, "RF_PendingKill should not be used directly. Make sure references to objects are released using one of the existing engine callbacks or use weak object pointers.") = 0x20000000, ///< Objects that are pending destruction (invalid for gameplay but valid objects). This flag is mirrored in EInternalObjectFlags as PendingKill for performance
RF_Garbage UE_DEPRECATED(5.0, "RF_Garbage should not be used directly. Use MarkAsGarbage and ClearGarbage instead.") =0x40000000, ///< Garbage from logical point of view and should not be referenced. This flag is mirrored in EInternalObjectFlags as Garbage for performance
RF_AllocatedInSharedPage =0x80000000, ///< Allocated from a ref-counted page shared with other UObjects
};
Тут же передается простой набор параметров, представленных как 2 операции побитового ИЛИ: RF_Public|RF_Transient|RF_MarkAsNative
У сгенерированной структуры есть еще поле: static const UECodeGen_Private::FMetaDataPairParam Struct_MetaDataParams[];
Т.е метаданные. По умолчанию указываются ["BlueprintType", "true"]
; и относительный путь внутри директории с исходным кодом ["ModuleRelativePath", "TestStruct.h"]
- т.к я не поместил их в другую папку.
#if WITH_METADATA
const UECodeGen_Private::FMetaDataPairParam Z_Construct_UScriptStruct_FMyStruct_Statics::Struct_MetaDataParams[] = {
{ "BlueprintType", "true" },
{ "ModuleRelativePath", "TestStruct.h" },
};
#endif
Давайте теперь попробуем добавить в структуру пару полей:
struct FMyStruct
{
GENERATED_BODY()
UPROPERTY(BlueprintReadWrite,Category="TestCategory")
float TestFloat;
private:
UPROPERTY(BlueprintReadOnly,Category="TestCategory",meta = (AllowPrivateAccess = "true"))
FString TestString;
};
Теперь структура сильно расширилась:
struct Z_Construct_UScriptStruct_FMyStruct_Statics
{
#if WITH_METADATA
static const UECodeGen_Private::FMetaDataPairParam Struct_MetaDataParams[];
#endif
static void* NewStructOps();
#if WITH_METADATA
static const UECodeGen_Private::FMetaDataPairParam NewProp_TestFloat_MetaData[];
#endif
static const UECodeGen_Private::FFloatPropertyParams NewProp_TestFloat;
#if WITH_METADATA
static const UECodeGen_Private::FMetaDataPairParam NewProp_TestString_MetaData[];
#endif
static const UECodeGen_Private::FStrPropertyParams NewProp_TestString;
static const UECodeGen_Private::FPropertyParamsBase* const PropPointers[];
static const UECodeGen_Private::FStructParams ReturnStructParams;
};
Помимо сгенерированных полей для функции, появились следующие:
Для TestFloat:
#if WITH_METADATA
static const UECodeGen_Private::FMetaDataPairParam NewProp_TestFloat_MetaData[];
#endif
static const UECodeGen_Private::FFloatPropertyParams NewProp_TestFloat;
Для TestString:
#if WITH_METADATA
static const UECodeGen_Private::FMetaDataPairParam NewProp_TestString_MetaData[];
#endif
static const UECodeGen_Private::FStrPropertyParams NewProp_TestString;
И для обоих:
static const UECodeGen_Private::FPropertyParamsBase* const PropPointers[];
Для наших переменных генерируется дополнительная мета информация, служащая для хранения всех тех свойств, которые мы задали в макросе UPROPERTY().
Важно обратить внимание также на макрос WITH_METADATA
, и что FMetaDataPairParam
объявляется лишь с этим макросом.Если порыться в исходниках, то можно обнаружить:данные, завернутые в этот макрос, используются движком. Определение static
переменной это подтверждает, указывая параметры, которые использоваться могут исключительно в движке, и никак не быть задействованными в игре.
Ведь зачем нам знать в билде во время игры, что поле Health в Blueprint исключительно ReadOnly?
Так, например, поле TestString
для репрезентации в движке после генерации получит следующую реализацию:
const UECodeGen_Private::FMetaDataPairParam Z_Construct_UScriptStruct_FMyStruct_Statics::NewProp_TestString_MetaData[] = {
{ "AllowPrivateAccess", "true" },
{ "Category", "TestCategory" },
{ "ModuleRelativePath", "TestStruct.h" },
};
Основные же параметры, которые генерируются в любом случае, следующие:
const UECodeGen_Private::FStrPropertyParams Z_Construct_UScriptStruct_FMyStruct_Statics::NewProp_TestString = { "TestString", nullptr, (EPropertyFlags)0x0040000000000014, UECodeGen_Private::EPropertyGenFlags::Str, RF_Public|RF_Transient|RF_MarkAsNative, 1, STRUCT_OFFSET(FMyStruct, TestString), METADATA_PARAMS(Z_Construct_UScriptStruct_FMyStruct_Statics::NewProp_TestString_MetaData, UE_ARRAY_COUNT(Z_Construct_UScriptStruct_FMyStruct_Statics::NewProp_TestString_MetaData)) };
Для полного понимания обратимся к структуре, отвечающей за хранение данных о полях, в нашем случае FStrPropertyParams
.
Хотя разница не большая:
// These property types don't add new any construction parameters to their base property
typedef FGenericPropertyParams FInt8PropertyParams;
typedef FGenericPropertyParams FInt16PropertyParams;
typedef FGenericPropertyParams FIntPropertyParams;
typedef FGenericPropertyParams FInt64PropertyParams;
typedef FGenericPropertyParams FFInt16PropertyParams;
typedef FGenericPropertyParams FUInt32PropertyParams;
typedef FGenericPropertyParams FFInt64PropertyParams;
typedef FGenericPropertyParams FUnsizedIntPropertyParams;
typedef FGenericPropertyParams FUnsizedFIntPropertyParams;
typedef FGenericPropertyParams FFloatPropertyParams;
typedef FGenericPropertyParams FDoublePropertyParams;
typedef FGenericPropertyParams FLargeWorldCoordinatesRealPropertyParams;
typedef FGenericPropertyParams FNamePropertyParams;
typedef FGenericPropertyParams FStrPropertyParams;
typedef FGenericPropertyParams FSetPropertyParams;
typedef FGenericPropertyParams FTextPropertyParams;
typedef FObjectPropertyParams FWeakObjectPropertyParams;
typedef FObjectPropertyParams FLazyObjectPropertyParams;
typedef FObjectPropertyParams FObjectPtrPropertyParams;
typedef FClassPropertyParams FClassPtrPropertyParams;
typedef FObjectPropertyParams FSoftObjectPropertyParams;
В структуре FGenericPropertyParams
находится Имя переменной, ее RepNotify функция, EPropertyFlags
которые мы уже видели, EPropertyGenFlags
указывающая тип данных через enum:
enum class EPropertyGenFlags : uint32
{
None = 0x00,
// First 6 bits are the property type
Byte = 0x00,
Int8 = 0x01,
Int16 = 0x02,
Int = 0x03,
Int64 = 0x04,
UInt16 = 0x05,
UInt32 = 0x06,
UInt64 = 0x07,
UnsizedInt = 0x08,
...
};
Ну и последнее интересующее нас: EObjectFlags
которую мы так уже видели.
Все остальное это детали хранения в памяти, выравнивание в памяти и очередные Meta данные для движка.
2.2 UClass
Теперь можно взглянуть на сгенерированные файлы под UCLASS()
.
Испытуемый:
#pragma once
#include "TestClass.generated.h"
UCLASS()
class UTestClass : public UObject
{
GENERATED_BODY()
};
В данном случае .generated.h
файл получился сильно больше, чем предыдущий (93 строки).
// Copyright Epic Games, Inc. All Rights Reserved.
/*===========================================================================
Generated code exported from UnrealHeaderTool.
DO NOT modify this manually! Edit the corresponding .h files instead!
===========================================================================*/
#include "UObject/ObjectMacros.h"
#include "UObject/ScriptMacros.h"
...
#define FID_MyProject_Source_MyProject_TestClass_h_8_SPARSE_DATA
#define FID_MyProject_Source_MyProject_TestClass_h_8_RPC_WRAPPERS
#define FID_MyProject_Source_MyProject_TestClass_h_8_RPC_WRAPPERS_NO_PURE_DECLS
#define FID_MyProject_Source_MyProject_TestClass_h_8_INCLASS_NO_PURE_DECLS \
private: \
static void StaticRegisterNativesUTestClass(); \
friend struct Z_Construct_UClass_UTestClass_Statics; \
public: \
DECLARE_CLASS(UTestClass, UObject, COMPILED_IN_FLAGS(0), CASTCLASS_None, TEXT("/Script/MyProject"), NO_API) \
DECLARE_SERIALIZER(UTestClass)
...
#define FID_MyProject_Source_MyProject_TestClass_h_7_STANDARD_CONSTRUCTORS \
/** Standard constructor, called after all reflected properties have been initialized */ \
NO_API UTestClass(const FObjectInitializer& ObjectInitializer = FObjectInitializer::Get()); \
DEFINE_DEFAULT_OBJECT_INITIALIZER_CONSTRUCTOR_CALL(UTestClass) \
DECLARE_VTABLE_PTR_HELPER_CTOR(NO_API, UTestClass); \
DEFINE_VTABLE_PTR_HELPER_CTOR_CALLER(UTestClass); \
...
#define FID_MyProject_Source_MyProject_TestClass_h_7_ENHANCED_CONSTRUCTORS \
/** Standard constructor, called after all reflected properties have been initialized */ \
NO_API UTestClass(const FObjectInitializer& ObjectInitializer = FObjectInitializer::Get()) : Super(ObjectInitializer) { }; \
private: \
/** Private move- and copy-constructors, should never be used */ \
NO_API UTestClass(UTestClass&&); \
NO_API UTestClass(const UTestClass&); \
public: \
DECLARE_VTABLE_PTR_HELPER_CTOR(NO_API, UTestClass); \
DEFINE_VTABLE_PTR_HELPER_CTOR_CALLER(UTestClass); \
DEFINE_DEFAULT_OBJECT_INITIALIZER_CONSTRUCTOR_CALL(UTestClass)
...
#define FID_MyProject_Source_MyProject_TestClass_h_7_GENERATED_BODY \
PRAGMA_DISABLE_DEPRECATION_WARNINGS \
public: \
FID_MyProject_Source_MyProject_TestClass_h_7_SPARSE_DATA \
FID_MyProject_Source_MyProject_TestClass_h_7_RPC_WRAPPERS_NO_PURE_DECLS \
FID_MyProject_Source_MyProject_TestClass_h_7_INCLASS_NO_PURE_DECLS \
FID_MyProject_Source_MyProject_TestClass_h_7_ENHANCED_CONSTRUCTORS \
private: \
PRAGMA_ENABLE_DEPRECATION_WARNINGS
...
В данном случае UHT сгенерировал достаточно много пустых макросов, которые пока что не несут никакой цели.
Кроме них, были сгенерированные дополнительные конструкторы, принимающиеFObjectInitializer
параметром. Конструкторы копирования помещены в private поле.
Обратим внимание на макрос DECLARE CLASS()
куда передается наш класс, несколько параметров и, немаловажно путь /Script/MyProject
- так называемый Package, который в третьей главе мы еще увидим.
#define DECLARE_CLASS( TClass, TSuperClass, TStaticFlags, TStaticCastFlags, TPackage, TRequiredAPI ) \
private: \
TClass& operator=(TClass&&); \
TClass& operator=(const TClass&); \
TRequiredAPI static UClass* GetPrivateStaticClass(); \
public: \
/** Bitwise union of #EClassFlags pertaining to this class.*/ \
enum {StaticClassFlags=TStaticFlags}; \
/** Typedef for the base class ({{ typedef-type }}) */ \
typedef TSuperClass Super;\
/** Typedef for {{ typedef-type }}. */ \
typedef TClass ThisClass;\
/** Returns a UClass object representing this class at runtime */ \
inline static UClass* StaticClass() \
{ \
return GetPrivateStaticClass(); \
} \
/** Returns the package this class belongs in */ \
inline static const TCHAR* StaticPackage() \
{ \
return TPackage; \
} \
/** Returns the static cast flags for this class */ \
inline static EClassCastFlags StaticClassCastFlags() \
{ \
return TStaticCastFlags; \
} \
/** For internal use only; use StaticConstructObject() to create new objects. */ \
inline void* operator new(const size_t InSize, EInternal InInternalOnly, UObject* InOuter = (UObject*)GetTransientPackage(), FName InName = NAME_None, EObjectFlags InSetFlags = RF_NoFlags) \
{ \
return StaticAllocateObject(StaticClass(), InOuter, InName, InSetFlags); \
} \
/** For internal use only; use StaticConstructObject() to create new objects. */ \
inline void* operator new( const size_t InSize, EInternal* InMem ) \
{ \
return (void*)InMem; \
} \
/* Eliminate V1062 warning from PVS-Studio while keeping MSVC and Clang happy. */ \
inline void operator delete(void* InMem) \
{ \
::operator delete(InMem); \
}
Как видно, этот макрос генерирует большое количество полей, type alias'ов, методов, в том числе и CDO helper'ов, для нашего класса который передали параметром.
И все это в конце вставляется сначала в макрос FID_MyProject_Source_MyProject_TestClass_h_7_GENERATED_BODY,
а после и он попадает на место нашего GENERATED_BODY().
#define FID_MyProject_Source_MyProject_TestClass_h_7_GENERATED_BODY \
PRAGMA_DISABLE_DEPRECATION_WARNINGS \
public: \
FID_MyProject_Source_MyProject_TestClass_h_7_SPARSE_DATA \
FID_MyProject_Source_MyProject_TestClass_h_7_RPC_WRAPPERS_NO_PURE_DECLS \
FID_MyProject_Source_MyProject_TestClass_h_7_INCLASS_NO_PURE_DECLS \
FID_MyProject_Source_MyProject_TestClass_h_7_ENHANCED_CONSTRUCTORS \
private: \
PRAGMA_ENABLE_DEPRECATION_WARNINGS
Создадим теперь несколько функций:
...
UFUNCTION()
void SimpleFun();
UFUNCTION(BlueprintCallable)
void BPCallableFun(int32 Param);
UFUNCTION(BlueprintCallable,BlueprintPure)
FString BPCallablePure(const FString& Param);
protected:
UFUNCTION(BlueprintImplementableEvent,BlueprintCallable)
int32 BPImplFun();
...
И сразу заметим изменения. UHT сгенерировал для нас несколько объявлений в ранее пустых макросах. Например: FID_MyProject_Source_MyProject_TestClass_h_10_RPC_WRAPPERS
.
Сам макрос DECLARE_FUNCTION
- объявляет наши функции с префиксом exec:
#define FID_MyProject_Source_MyProject_TestClass_h_10_RPC_WRAPPERS \
\
DECLARE_FUNCTION(execBPCallablePure); \
DECLARE_FUNCTION(execBPCallableFun); \
DECLARE_FUNCTION(execSimpleFun);
Сам макрос достаточно прост:
// This macro is used to declare a thunk function in autogenerated boilerplate code
#define DECLARE_FUNCTION(func) static void func( UObject* Context, FFrame& Stack, RESULT_DECL )
Объявляется static
функция, в которую передается контекст, стэк функций, и константный указатель, т.е, возвращаемое нашей функцией значение.
#define RESULT_DECL void*const RESULT_PARAM
#define RESULT_PARAM Z_Param__Result
И мы все это уже видели, когда я говорил о функции UFunction::Invoke();
Именно таким образом объявляется Нативная функция,которая после вызывается классом UFunction
.
Так же UHT генерирует для нас структуры, содержащие передаваемые параметры, и макрос FID_MyProject_Source_MyProject_TestClass_h_10_EVENT_PARMS
для них.
#define FID_MyProject_Source_MyProject_TestClass_h_10_EVENT_PARMS \
struct TestClass_eventBPImplFun_Parms \
{ \
int32 ReturnValue; \
\
/** Constructor, initializes return property only **/ \
TestClass_eventBPImplFun_Parms() \
: ReturnValue(0) \
{ \
} \
};
Интересно, что здесь так же указано значение по-умолчанию ReturnValue(0)
. В Blueprint при вызове вы это значение и увидите.
Перейдем теперь к реализации этих функций.
Определяются функции банально. При помощи соседнего с DECLARE_FUNCTION
DEFINE_FUNCTION
.
// This macro is used to define a thunk function in autogenerated boilerplate code
#define DEFINE_FUNCTION(func) void func( UObject* Context, FFrame& Stack, RESULT_DECL )
DEFINE_FUNCTION(UTestClass::execBPCallablePure)
{
P_GET_PROPERTY(FStrProperty,Z_Param_Param);
P_FINISH;
P_NATIVE_BEGIN;
*(FString*)Z_Param__Result=P_THIS->BPCallablePure(Z_Param_Param);
P_NATIVE_END;
}
Далее необходимо найти тот параметр, который мы решили передать, т.е const FString& Param
UHT сразу понял что это за параметр, и для его описания использовалась структура FStrProperty
, а имя этого параметра Z_Param_Param
.
Сам поиск представляет из себя обращение к виртуальной машине - стэке объектов, тому самому FFrame. Выше мы уже обсуждали его, как и следующий макрос:
P_GET_PROPERTY(FStrProperty,Z_Param_Param);
#define P_GET_PROPERTY(PropertyType, ParamName) \
PropertyType::TCppType ParamName = PropertyType::GetDefaultPropertyValue(); \
Stack.StepCompiledIn<PropertyType>(&ParamName);
Тут достается значение нашей переменной по-умолчанию, и out параметром передается в Stack, где этот параметр или уже находится, или создается и записывается:
FORCEINLINE_DEBUGGABLE void FFrame::StepCompiledIn(void* Result, const FFieldClass* ExpectedPropertyType)
{
if (Code)
{
Step(Object, Result);
}
else
{
checkSlow(ExpectedPropertyType && ExpectedPropertyType->IsChildOf(FProperty::StaticClass()));
checkSlow(PropertyChainForCompiledIn && PropertyChainForCompiledIn->IsA(ExpectedPropertyType));
FProperty* Property = (FProperty*)PropertyChainForCompiledIn;
PropertyChainForCompiledIn = Property->Next;
StepExplicitProperty(Result, Property);
}
}
Также обращу ваше внимание на тип FProperty
,который я уже упоминал выше, когда приводил в пример сериализацию при помощи флага CPF_SaveGame
, т.е на тип данных, который представляет “Переменную” внутри движка, содержащую флаги, RepNotify
функцию и пр.
После того как параметр найден, указатель на Code
в переменной Stack смещается до тех пор, пока не будет указывать на nullptr
:
#define P_FINISH Stack.Code += !!Stack.Code; /* increment the code ptr unless it is null */
Далее идет вызов макросов:
#define P_NATIVE_BEGIN { SCOPED_SCRIPT_NATIVE_TIMER(ScopedNativeCallTimer);
#define P_NATIVE_END }
Меж которыми заключен вызов к нашей родной функции.
Обращу еще раз внимание на RESULT_DECL
, передаваемый в DEFINE_FUNCTION
,
Т.е параметр Z_Param__Result
, возвращаемое значение, это константный указатель на void
, который необходимо скастить к нашему типу FString
.
По этой причине вызов к нашей функции имеет такой странный на первый взгляд синтаксис:
*(FString*)Z_Param__Result=P_THIS->BPCallablePure(Z_Param_Param);
У каждого UClass’а
есть своя таблица имен нативных cpp функций, называемая NativeFunctionLookupTable
. Все объявленные нами функции должны быть туда записаны.
Следующее выражение именно эту функцию и выполняет:
void UTestClass::StaticRegisterNativesUTestClass()
{
UClass* Class = UTestClass::StaticClass();
static const FNameNativePtrPair Funcs[] = {
{ "BPCallableFun", &UTestClass::execBPCallableFun },
{ "BPCallablePure", &UTestClass::execBPCallablePure },
{ "SimpleFun", &UTestClass::execSimpleFun },
};
FNativeFunctionRegistrar::RegisterFunctions(Class, Funcs, UE_ARRAY_COUNT(Funcs));
}
Туда записаны все функции, кроме BPImplFun
, которая cpp
имплементации не имеет.
Вместо этого UHT сгенерировал следующее для нее определение вместе со статическим полем, в котором прописано имя этой функции:
static FName NAME_UTestClass_BPImplFun = FName(TEXT("BPImplFun"));
int32 UTestClass::BPImplFun()
{
TestClass_eventBPImplFun_Parms Parms;
ProcessEvent(FindFunctionChecked(NAME_UTestClass_BPImplFun),&Parms);
return Parms.ReturnValue;
}
По сути функция объявлена, только не нами :)
Структура TestClass_eventBPImplFun_Parms
, напомню, определена еще в .generated.h
файле.
Что касается ProcessEvent
.
Эта функция, объявленная в классе UObject, где явно сказано, что она поддерживает работу виртуальной машины и исполняет UFunction’ы.
/*-----------------------------
Virtual Machine
-----------------------------*/
/** Called by VM to execute a UFunction with a filled in UStruct of parameters */
virtual void ProcessEvent( UFunction* Function, void* Parms );
Однако, перед ней этот UFunction
требуется найти. Этим занимается пара следующих функций, также объявленных в UObject’е
.
/** Returns a UFunction with the specified name, wrapper for UClass::FindFunctionByName() */
UFunction* FindFunction( FName InName ) const;
/** Version of FindFunction() that will assert if the function was not found */
UFunction* FindFunctionChecked( FName InName ) const;
Таким образом, через обращение к нативному объекту UClass
’, являющимся для UObject’а
репрезентацией всех тех полей, функций и прочих объектов получается желаемая нами функция:
UFunction* UObject::FindFunction( FName InName ) const
{
return GetClass()->FindFunctionByName(InName);
}
...
/** Class the object belongs to. */
UClass* ClassPrivate;
Сам поиск осуществляется через попытки найти фукнцию в разных “Местах”.Сначала ищем в текущей UClass’е
, т.е фактическом классе UObject’а
. Если функции там нет, обращаемся к родителю текущего класса. Если найденный родитель валиден, ищем в его таблице имен функций. Если не валиден, то смотрим в интерфейсах,которые этот класс реализует.
UFunction* UClass::FindFunctionByName(FName InName, EIncludeSuperFlag::Type IncludeSuper) const
{
LLM_SCOPE(ELLMTag::UObject);
UFunction* Result = FuncMap.FindRef(InName);
if (Result == nullptr && IncludeSuper == EIncludeSuperFlag::IncludeSuper)
{
UClass* SuperClass = GetSuperClass();
if (SuperClass || Interfaces.Num() > 0)
{
...
};
После чего найденая функция передается в UObject::ProcessEvent.
Вся функция состоит из следующих основных пунктов:
Сначала проверяется попытка вызова функции удаленно, если та, разумеется, имеет соответствующие флаги.
...
if ((Function->FunctionFlags & FUNC_Native) != 0)
{
int32 FunctionCallspace = GetFunctionCallspace(Function, NULL);
if (FunctionCallspace & FunctionCallspace::Remote)
{
CallRemoteFunction(Function, Parms, NULL, NULL);
}
if ((FunctionCallspace & FunctionCallspace::Local) == 0)
{
return;
}
}
else if (Function->Script.Num() == 0)
{
return;
}
...
Т.к UObject’ы
не должны вызывать удаленные функции, GetFunctionCallspace()
всегда вернет FunctionCallspace::Local
А CallRemoteFunction()
так вообще возвращает всегда false.
/**
* Call the actor's function remotely
*
* @param Function function to call
* @param Parameters arguments to the function call
* @param Stack stack frame for the function call
*/
virtual bool CallRemoteFunction( UFunction* Function, void* Parms, struct FOutParmRec* OutParms, FFrame* Stack )
{
return false;
}
У того же AActor’а эти функции перегружены, и там опять же, проверяются флаги у переданной UFunction.
//
// Return whether a function should be executed remotely.
//
int32 AActor::GetFunctionCallspace( UFunction* Function, FFrame* Stack )
{
if (GAllowActorScriptExecutionInEditor)
{
// Call local, this global is only true when we know it's being called on an editor-placed object
DEBUG_CALLSPACE(TEXT("GetFunctionCallspace ScriptExecutionInEditor: %s"), *Function->GetName());
return FunctionCallspace::Local;
}
if ((Function->FunctionFlags & FUNC_Static) || (GetWorld() == nullptr))
{
// Use the same logic as function libraries for static/CDO called functions, will try to use the global context to check authority only/cosmetic
DEBUG_CALLSPACE(TEXT("GetFunctionCallspace Static: %s"), *Function->GetName());
return GEngine->GetGlobalFunctionCallspace(Function, this, Stack);
}
...
}
Если вызова функции удаленно так и не произошло.
После этого проверяем, если функция вызывается из Blueprint, то заполняем текущий ее фрейм переданными параметрами:
...
if (Function->HasAnyFunctionFlags(FUNC_UbergraphFunction))
{
Frame = Function->GetOuterUClassUnchecked()->GetPersistentUberGraphFrame(this, Function);
}
#endif
const bool bUsePersistentFrame = (NULL != Frame);
if (!bUsePersistentFrame)
{
Frame = (uint8*)FMemory_Alloca_Aligned(Function->PropertiesSize, Function->GetMinAlignment());
// zero the local property memory
FMemory::Memzero(Frame + Function->ParmsSize, Function->PropertiesSize - Function->ParmsSize);
}
// initialize the parameter properties
FMemory::Memcpy(Frame, Parms, Function->ParmsSize);
...
Как только это сделано, проверяем на наличие у функции флага FUNC_HasOutParms
. Если таковой находим,смотрим все FProperty
функции и проверяем есть ли флаг CPF_OutParm
, и если есть, то выделяем память под структуру, хранящую информацию об out-параметре,и передаем туда найденный FProperty
, и помещаем этот параметр в стэк FFrame
.
И, наконец, вызываем UFunction::Invoke(), передавая туда текущий объект, стэк, и адрес возвращаемого значения.
Function->Invoke(this, NewStack, ReturnValueAddress);
2.3 Ошибка:
"#include found after .generated.h file - the .generated.h file should always be the last #include in a header"
Почему .generated.h файл должен быть последним?
Обратимся для этого к функции SimplifiedClassParse()
в файле HeaderParser.cpp
.В ней выполняется парсинг .h файлов. Часть из этой логики разберем ниже.
Цикл в котором идет парсинг:
...
for (const TCHAR* StartOfLine = Buffer; FParse::Line(&Buffer, StrLine, true); StartOfLine = Buffer)
{
CurrentLine++;
const TCHAR* Str = *StrLine;
bool bProcess = !bInComment; // for skipping nested multi-line comments
int32 BraceCount = 0;
if( bProcess && FParse::Command(&Str,TEXT("#if")) )
{
}
else if ( bProcess && FParse::Command(&Str,TEXT("#include")) )
{
// Handle #include directives as if they were 'dependson' keywords.
const FString& DependsOnHeaderName = Str;
if (DependsOnHeaderName != TEXT("\"UObject/DefineUPropertyMacros.h\"") && DependsOnHeaderName != TEXT("\"UObject/UndefineUPropertyMacros.h\""))
{
if (bFoundGeneratedInclude)
{
FUHTMessage(SourceFile, CurrentLine).Throwf(TEXT("#include found after .generated.h file - the .generated.h file should always be the last #include in a header"));
}
bFoundGeneratedInclude = DependsOnHeaderName.Contains(TEXT(".generated.h"));
if (!bFoundGeneratedInclude && DependsOnHeaderName.Len())
{
bool bIsQuotedInclude = DependsOnHeaderName[0] == '\"';
int32 HeaderFilenameEnd = DependsOnHeaderName.Find(bIsQuotedInclude ? TEXT("\"") : TEXT(">"), ESearchCase::CaseSensitive, ESearchDir::FromStart, 1);
if (HeaderFilenameEnd != INDEX_NONE)
{
// Include the extension in the name so that we later know where this entry came from.
SourceFile.GetIncludes().AddUnique(FHeaderProvider(EHeaderProviderSourceType::FileName, FPaths::GetCleanFilename(DependsOnHeaderName.Mid(1, HeaderFilenameEnd - 1))));
}
}
}
}
...
Нас интересует переменная DependsOnHeaderName
, которая проверяется на несовпадение с include’ом
файлов UObject/DefineUPropertyMacros.h
или UObject/UndefineUPropertyMacros.h
Если таковой файл не находим, то проверяем переменную bFoundGeneratedInclude
, которая должна была быть вставлена еще на предыдущем шаге. Проще говоря, цикл на момент парсинга .generated.h
уже должен заканчиваться.
Взглянем на эти depend-файлы DefineUPropertyMacros.h
и UndefineUPropertyMacros.h
.:
Вот в принципе и ответ на вопрос, почему он должен быть последним:
DefineUPropertyMacros.h:
// Copyright Epic Games, Inc. All Rights Reserved.
/*=============================================================================
DefineUPropertyMacros.h: macro'ed typedefs for FProperties
=============================================================================*/
#ifndef UProperty
#define UProperty DEPRECATED_MACRO(4.25, "UProperty has been renamed to FProperty") FProperty
#endif
#ifndef UNumericProperty
#define UNumericProperty DEPRECATED_MACRO(4.25, "UNumericProperty has been renamed to FNumericProperty") FNumericProperty
#endif
#ifndef UByteProperty
#define UByteProperty DEPRECATED_MACRO(4.25, "UByteProperty has been renamed to FByteProperty") FByteProperty
#endif
...
UndefineUPropertyMacros.h:
// Copyright Epic Games, Inc. All Rights Reserved.
/*=============================================================================
UndefineUPropertyMacros.h
=============================================================================*/
#ifdef UProperty
#undef UProperty
#endif
#ifdef UNumericProperty
#undef UNumericProperty
#endif
#ifdef UByteProperty
#undef UByteProperty
#endif
#ifdef UInt8Property
#undef UInt8Property
#endif
#ifdef UInt16Property
#undef UInt16Property
#endif
...
Если взглянуть на файлы где они используются, то можно заметить, что они создают некий скоуп, где все вышеперечисленные макросы будут гарантированно отключены.
FieldPathProperty.cpp:
// Copyright Epic Games, Inc. All Rights Reserved.
#include "UObject/FieldPathProperty.h"
#include "UObject/PropertyPortFlags.h"
#include "UObject/Package.h"
#include "UObject/UnrealTypePrivate.h"
#include "UObject/LinkerLoad.h"
#include "UObject/ConstructorHelpers.h"
#include "Misc/Parse.h"
#include "UObject/PropertyHelper.h"
// WARNING: This should always be the last include in any file that needs it (except .generated.h)
#include "UObject/UndefineUPropertyMacros.h"
…
…
bool FFieldPathProperty::SupportsNetSharedSerialization() const
{
return false;
}
#include "UObject/DefineUPropertyMacros.h"
//Конец файла.
Т.е чтобы гарантировать, что данные макросы будут включены и выключены везде, где это необходимо,требуется этот зависимый .generated
.h файл включать последним, т.к цепочка include’ов
в этом файле может привести к этим объявлениям.
Хотя, если верить комментариям разработчиков, эта функциональность уже устарела. Так что быть может в будущем нас избавят от нужды включать этот файл последним.
3. Unreal Header Tool.
Теперь взглянем на сам UHT непосредственно. Продукт парсинга мы уже разобрали, и немного затронули процесс. Коснемся его вскользь, т.к обзорная статья может превратиться в огромный учебник на тысячу страниц.
Все начинается с очень неприветливого макроса INT32_MAIN_INT32_ARGC_TCHAR_ARGV
,
объявленного в файле Platform.h
, и вызываемого в файле UnrealHeadderToolMain
.
/**
* Application entry point
*
* @param ArgC Command-line argument count
* @param ArgV Argument strings
*/
INT32_MAIN_INT32_ARGC_TCHAR_ARGV()
{
FTaskTagScope Scope(ETaskTag::EGameThread);
FString CmdLine;
for (int32 Arg = 0; Arg < ArgC; Arg++)
{
FString LocalArg = ArgV[Arg];
if (LocalArg.Contains(TEXT(" "), ESearchCase::CaseSensitive))
{
CmdLine += TEXT("\"");
CmdLine += LocalArg;
CmdLine += TEXT("\"");
...
В процессе выполнения парсится имя игры и файла, в котором содержится информация обо всех модулях, для который UHT должен сгенерировать код.
...
// Parse the game name or project filename. UHT reads the list of plugins from there in case one of the plugins is UHT plugin.
FString GameName = FParse::Token(CmdLinePtr, false);
// This parameter is the absolute path to the file which contains information about the modules
// that UHT needs to generate code for.
ModuleInfoFilename = FParse::Token(CmdLinePtr, false );
...
В конце этой main функции вызывается то, что нас и интересует.
extern ECompilationResult::Type UnrealHeaderTool_Main(const FString& ModuleInfoFilename);
ECompilationResult::Type Result = UnrealHeaderTool_Main(ModuleInfoFilename);
return Result;
Перейдем к реализации UnrealHeaderTool_Main:
Сначала ставится таймер, отсчитывающий время парсинга модулей, после чего подгружается путь к модулям, и вызывается FManifest::LoadFromFile
ECompilationResult::Type UnrealHeaderTool_Main(const FString& ModuleInfoFilename)
{
double MainTime = 0.0;
FDurationTimer MainTimer(MainTime);
MainTimer.Start();
check(GIsUCCMakeStandaloneHeaderGenerator);
FString ModuleInfoPath = FPaths::GetPath(ModuleInfoFilename);
// The meta data keywords must be initialized prior to going wide
FBaseParser::InitMetadataKeywords();
// Load the manifest file, giving a list of all modules to be processed, pre-sorted by dependency ordering
FResults::Try([&ModuleInfoFilename]() { GManifest = FManifest::LoadFromFile(ModuleInfoFilename); });
TArray<FUnrealSourceFile*> OrderedSourceFiles;
TArray<FUnrealPackageDefinitionInfo*> PackageDefs;
PackageDefs.Reserve(GManifest.Modules.Num());
double TotalPrepareModuleTime = FResults::TimedTry([&PackageDefs, &ModuleInfoPath]() { PrepareModules(PackageDefs, ModuleInfoPath); });
...
FManifest
- Структура, содержащая информацию о всех модулях в порядке, учитывающим зависимости, т.е наиболее зависимые модули будут парсится первыми. Также там содержатся имена и пути root директории.
struct FManifest
{
bool IsGameTarget;
FString RootLocalPath;
FString RootBuildPath;
FString TargetName;
FString ExternalDependenciesFile;
/** Ordered list of modules that define UObjects or UStructs, which we may need to generate
code for. The list is in module dependency order, such that most dependent modules appear first. */
TArray<FManifestModule> Modules;
/**
* Loads a *.uhtmanifest from the specified filename.
*
* @param Filename The filename of the manifest to load.
* @return The loaded module info.
*/
static FManifest LoadFromFile(const FString& Filename);
friend FArchive& operator<<(FArchive& Ar, FManifest& Manifest)
{
Ar << Manifest.IsGameTarget;
Ar << Manifest.RootLocalPath;
Ar << Manifest.RootBuildPath;
Ar << Manifest.TargetName;
Ar << Manifest.Modules;
return Ar;
}
};
В самой структуре, описывающей Модуль так таковой мы можем найти следующее:
Имя модуля.
Тип модуля.
Дополнительные переопределенные данные модуля.
Имя пакета.
Списки public-header и private-header файлов с UObject классами.
Внутренние public-header файлы и пр.
struct FManifestModule
{
/** The name of the module */
FString Name;
/** Module type */
EBuildModuleType::Type ModuleType;
/** Overridden package settings to add additional flags that can help with organization */
EPackageOverrideType::Type OverrideModuleType;
/** Long package name for this module's UObject class */
FString LongPackageName;
/** Base directory of this module on disk */
FString BaseDirectory;
/** The directory to which #includes from this module should be relative */
FString IncludeBase;
/** Directory where generated include files should go */
FString GeneratedIncludeDirectory;
...
Интересно, какого вида бывают модули:
struct EBuildModuleType
{
enum Type
{
Program,
EngineRuntime,
EngineUncooked,
EngineDeveloper,
EngineEditor,
EngineThirdParty,
GameRuntime,
GameUncooked,
GameDeveloper,
GameEditor,
GameThirdParty,
// NOTE: If you add a new value, make sure to update the ToString() method below!
Max
};
...
Т.е модули могут иметь разную конфигурацию. Какие то собираются исключительно для движка в рантайме,какие то для самой игры (билда разных конфигураций).
После загрузки Manifest
файла начинается подгрузка так называемых модулей и сам парсинг. В коде это выглядит как последовательный вызов к нескольким функциям, где каждая вызывается через лямбду внутри функции TimedTry
. Используется это для фиксации времени выполнения функции выполнения try-catch
блока.
/**
* Invoke the given lambda in a try block catching all supported exception types.
* @param InLambda The code to be executed in the try block
* @return The time in seconds it took to execute the lambda
*/
template<typename Lambda>
static double TimedTry(Lambda&& InLambda)
{
double DeltaTime = 0.0;
{
FScopedDurationTimer Timer(DeltaTime);
Try(InLambda);
}
return DeltaTime;
}
...
void PrepareModules(TArray<FUnrealPackageDefinitionInfo*>& PackageDefs, const FString& ModuleInfoPath)
...
В PrepareModules()
из Manifest файла извлекается информация о модулях (FManifestModule) из модулей извлекается информация о Package’ах (FUnrealPackageDefinitionInfo).
Сначала формируется сам пакет из модуля. У каждого пакета есть свой определенный __API макрос.
// Create the package definition
TSharedRef<FUnrealPackageDefinitionInfo> PackageDefRef = MakeShared<FUnrealPackageDefinitionInfo>(Module, Package);
FUnrealPackageDefinitionInfo& PackageDef = *PackageDefRef;
GTypeDefinitionInfoMap.AddNameLookup(PackageDef);
PackageDefs.Add(&PackageDef)
Так что по сути. В пакете хранится исключительно информация об исходных файлах и классах подлежащих парсингу. Ну и еще некая meta информация:
//FUnrealPackageDefinitionInfo
private:
const FManifestModule& Module;
TArray<TSharedRef<FUnrealSourceFile>> AllSourceFiles;
TArray<TSharedRef<FUnrealTypeDefinitionInfo>> AllClasses;
FString SingletonName;
FString SingletonNameChopped;
FString ExternDecl;
FString ShortUpperName;
FString API;
bool bWriteClassesH = false;
};
После создания пакета его необходимо заполнить информацией о текущих исходниках.
Сначала цикл проходится по PublicClassesHeaders
потом PublicHeaders
и в конце InternalHeaders
.
На каждом этапе берутся все Module.PublicUObjectClassesHeaders
Module.PublicUObjectHeaders
и Module.InternalUObjectHeaders
, из них формируются структуры FUnrealSourceFile
и после записываются в AllSourceFiles
для каждого отдельного пакета.
Кроме того выполняются некоторые проверки на дублирование хеддеров:
if (NormalizedFullFilename != NormalizedExistingFilename)
{
FUHTMessage(*UnrealSourceFile).LogError(TEXT("Duplicate leaf header name found: %s (original: %s)"), *NormalizedFullFilename, *NormalizedExistingFilename);
}
Как только пакеты сформированы, они возвращаются out параметром и попадают во все следующие функции:
double TotalPreparseTime = FResults::TimedTry([&PackageDefs, &ModuleInfoPath]() { PreparseSources(PackageDefs, ModuleInfoPath); });
double TotalDefineTypesTime = FResults::TimedTry([&PackageDefs]() { DefineTypes(PackageDefs); });
double TotalResolveParentsTime = FResults::TimedTry([&PackageDefs]() { ResolveParents(PackageDefs); });
double TotalPrepareTypesForParsingTime = FResults::TimedTry([&PackageDefs]() { PrepareTypesForParsing(PackageDefs); });
double TotalTopologicalSortTime = FResults::TimedTry([&OrderedSourceFiles]() { TopologicalSort(OrderedSourceFiles); });
double TotalParseTime = FResults::TimedTry([&OrderedSourceFiles]() { ParseSourceFiles(OrderedSourceFiles); });
double TotalPostParseFinalizeTime = FResults::TimedTry([&PackageDefs]() { PostParseFinalize(PackageDefs); });
TotalTopologicalSortTime += FResults::TimedTry([&OrderedSourceFiles]() { TopologicalSort(OrderedSourceFiles); }); // Sort again to include new dependencies
double TotalCodeGenTime = FResults::TimedTry([&PackageDefs, &OrderedSourceFiles]() { Export(PackageDefs, OrderedSourceFiles); });
double TotalCheckForScriptPluginsTime = FResults::TimedTry([&ScriptPlugins]() { GetScriptPlugins(ScriptPlugins); });
double TotalCreateEngineTypesTime = ScriptPlugins.IsEmpty() ? 0.0 : FResults::TimedTry([&PackageDefs]() { CreateEngineTypes(PackageDefs); });
double TotalPluginTime = ScriptPlugins.IsEmpty() ? 0.0 : FResults::TimedTry([&ScriptPlugins, &PackageDefs, &ExternalDependencies]() { ExportToScriptPlugins(ScriptPlugins, PackageDefs, ExternalDependencies); });
double TotalWriteExternalDependenciesTime = FResults::TimedTry([&ExternalDependencies]() { WriteExternalDependencies(ExternalDependencies); });
double TotalSummaryTime = FResults::TimedTry([&PackageDefs]() { GenerateSummary(PackageDefs); });
Сначала начинается так называемый PreparseSources()
.
В нем:
Загружается файл для парсинга.
Выполняется упрощенный его парсинг.
Функция, выполняющая парсинг:
void PreparseSources(TArray<FUnrealPackageDefinitionInfo*>& PackageDefs, const FString& ModuleInfoPath)
{
#if UHT_ENABLE_CONCURRENT_PREPARSING
FGraphEventArray LoadTasks;
LoadTasks.Reserve(1024); // Fairly arbitrary number
for (FUnrealPackageDefinitionInfo* PackageDef : PackageDefs)
{
for (TSharedRef<FUnrealSourceFile>& SourceFile : PackageDef->GetAllSourceFiles())
{
// Phase #1: Load the file
auto LoadLambda = [&SourceFile = *SourceFile, &ModuleInfoPath]()
{
LoadSource(SourceFile, ModuleInfoPath);
};
// Phase #2: Perform simplified class parse (can run concurrenrtly)
auto PreProcessLambda = [&SourceFile = *SourceFile]()
{
PreparseSource(SourceFile);
};
...
};
В ней вызывается FHeaderParser::SimplifiedClassParse()
и результат записывается в SourceFile.
...
// Parse the header to extract the information needed
FUHTStringBuilder ClassHeaderTextStrippedOfCppText;
FHeaderParser::SimplifiedClassParse(SourceFile, *SourceFile.GetContent(), /*out*/ ClassHeaderTextStrippedOfCppText);
SourceFile.SetContent(MoveTemp(ClassHeaderTextStrippedOfCppText));
...
И в SimplifiedClassParse()
производится предварительное извлечение полезной информации из хедера.
Сначала файл проверяется на наличие #else-#if
деректив, и выбрасываются нужные исключения:
...
bool bIsDirective = false;
if( FParse::Command(&Str,TEXT("#endif")) )
{
...
else if( FParse::Command(&Str,TEXT("#if")) || FParse::Command(&Str,TEXT("#ifdef")) || FParse::Command(&Str,TEXT("#ifndef")) )
{
...
FUHTMessage(SourceFile, CurrentLine).Throwf(
TEXT("Mixing %s with %s in an #elif preprocessor block is not supported"),
GetBlockDirectiveTypeString(OldDirective),
GetBlockDirectiveTypeString(NewDirective)
);
...
const TCHAR* FoundSubstr = nullptr;
if (FindInitialStr(FoundSubstr, TrimmedStrLine, TEXT("UPROPERTY"))
|| FindInitialStr(FoundSubstr, TrimmedStrLine, TEXT("UCLASS"))
|| FindInitialStr(FoundSubstr, TrimmedStrLine, TEXT("USTRUCT"))
|| FindInitialStr(FoundSubstr, TrimmedStrLine, TEXT("UENUM"))
|| FindInitialStr(FoundSubstr, TrimmedStrLine, TEXT("UINTERFACE"))
|| FindInitialStr(FoundSubstr, TrimmedStrLine, TEXT("UDELEGATE"))
|| FindInitialStr(FoundSubstr, TrimmedStrLine, TEXT("UFUNCTION")))
{
FUHTMessage(SourceFile, CurrentLine).Throwf(TEXT("%s must not be inside preprocessor blocks, except for WITH_EDITORONLY_DATA"), FoundSubstr);
}
После этого начинается блок функции, который мы уже ранее видели:
проверка на включенные в файл хедеров:
...
for (const TCHAR* StartOfLine = Buffer; FParse::Line(&Buffer, StrLine, true); StartOfLine = Buffer)
{
CurrentLine++;
const TCHAR* Str = *StrLine;
bool bProcess = !bInComment; // for skipping nested multi-line comments
int32 BraceCount = 0;
if( bProcess && FParse::Command(&Str,TEXT("#if")) )
{
}
else if ( bProcess && FParse::Command(&Str,TEXT("#include")) )
{
// Handle #include directives as if they were 'dependson' keywords.
const FString& DependsOnHeaderName = Str;
if (DependsOnHeaderName != TEXT("\"UObject/DefineUPropertyMacros.h\"") && DependsOnHeaderName != TEXT("\"UObject/UndefineUPropertyMacros.h\""))
{
if (bFoundGeneratedInclude)
{
FUHTMessage(SourceFile, CurrentLine).Throwf(TEXT("#include found after .generated.h file - the .generated.h file should always be the last #include in a header"));
}
...
Так же есть проверки на комментарии в коде, дабы не генерировать для комментариев код:
// look for a / * ... * / block, ignoring anything inside literal strings
Pos = StrLine.Find(TEXT("/*"), ESearchCase::CaseSensitive, ESearchDir::FromStart, Pos);
EndPos = StrLine.Find(TEXT("*/"), ESearchCase::CaseSensitive, ESearchDir::FromStart, FMath::Max(0, Pos - 1));
И завершается функция блоком из 5-ти блоков if-else:
В первом пытаемся найти:UINTERFACE
// Get class or interface name
if (const TCHAR* UInterfaceMacroDecl = FCString::Strfind(Str, TEXT("UINTERFACE")))
{
if (UInterfaceMacroDecl == FCString::Strspn(Str, TEXT("\t ")) + Str)
{
if (UInterfaceMacroDecl[10] != TEXT('('))
{
FUHTMessage(SourceFile, CurrentLine).Throwf(TEXT("Missing open parenthesis after UINTERFACE"));
}
TSharedRef<FUnrealTypeDefinitionInfo> ClassDecl = Parser.ParseClassDeclaration(StartOfLine + (UInterfaceMacroDecl - Str), CurrentLine, true, TEXT("UINTERFACE"));
bGeneratedIncludeRequired |= !ClassDecl->AsClassChecked().HasAnyClassFlags(CLASS_NoExport);
SourceFile.AddDefinedClass(MoveTemp(ClassDecl));
}
}
Во втором:UCLASS
else if (const TCHAR* UClassMacroDecl = FCString::Strfind(Str, TEXT("UCLASS")))
{
if (UClassMacroDecl == FCString::Strspn(Str, TEXT("\t ")) + Str)
{
if (UClassMacroDecl[6] != TEXT('('))
{
FUHTMessage(SourceFile, CurrentLine).Throwf(TEXT("Missing open parenthesis after UCLASS"));
}
TSharedRef<FUnrealTypeDefinitionInfo> ClassDecl = Parser.ParseClassDeclaration(StartOfLine + (UClassMacroDecl - Str), CurrentLine, false, TEXT("UCLASS"));
bGeneratedIncludeRequired |= !ClassDecl->AsClassChecked().HasAnyClassFlags(CLASS_NoExport);
SourceFile.AddDefinedClass(MoveTemp(ClassDecl));
}
}
И т.д
И в каждом подобном вызове имеется своя функция для парсинга конкретной сущности.
Для класса и интерфейса вызывается ParseClassDeclaration
Для енама ParseEnumDeclaration
и т.п
Разберем для примера, как происходит парсинг класса.
TSharedRef<FUnrealTypeDefinitionInfo> FHeaderPreParser::ParseClassDeclaration(const TCHAR* InputText, int32 InLineNumber, bool bClassIsAnInterface, const TCHAR* StartingMatchID)
{
const TCHAR* ErrorMsg = TEXT("Class declaration");
ResetParser(InputText, InLineNumber);
// Require 'UCLASS' or 'UINTERFACE'
RequireIdentifier(StartingMatchID, ESearchCase::CaseSensitive, ErrorMsg);
...
В ResetParser()
сбрасывается вся информация в парсере. Предыдущая позиция, ее норме и пр. Остается лишь информация о текущей строке, т.е буфер.
Осуществляется проверка токенов:
// Require 'UCLASS' or 'UINTERFACE'
RequireIdentifier(StartingMatchID, ESearchCase::CaseSensitive, ErrorMsg);
И, наконец, сам парсинг всех UPROPERTY
и UFUNCTION
внутри класса в функции ReadSpecifierSetInsideMacro().
// Reads a set of specifiers (with optional values) inside the () of a new-style metadata macro like UPROPERTY or UFUNCTION
void FBaseParser::ReadSpecifierSetInsideMacro(TArray<FPropertySpecifier>& SpecifiersFound, const TCHAR* TypeOfSpecifier, TMap<FName, FString>& MetaData)
{
int32 FoundSpecifierCount = 0;
auto ErrorMessageGetter = [TypeOfSpecifier]() { return FString::Printf(TEXT("%s declaration specifier"), TypeOfSpecifier); };
RequireSymbol(TEXT('('), ErrorMessageGetter);
...
В ней на каждом этапе проверяется корректность синтаксиса, и если она выполняется, то метаданные записываются в MetaData
при помощи функции ApplyToMetadata()
.
После этого:
Выполняется проверка на очередной идентификатор:
// Require 'class'
RequireIdentifier(TEXT("class"), ESearchCase::CaseSensitive, ErrorMsg);
Производится парсинг на наличие API в определении класса:
// Read the class name
FString RequiredAPIMacroIfPresent;
FString ClassName;
ParseNameWithPotentialAPIMacroPrefix(/*out*/ ClassName, /*out*/ RequiredAPIMacroIfPresent, StartingMatchID);
Парсится информация о родителе класса.
Все вышеперечисленное записывается в переменную ClassDef
и возвращается:
TSharedRef<FUnrealTypeDefinitionInfo> ClassDecl = Parser.ParseClassDeclaration(StartOfLine + (UClassMacroDecl - Str), CurrentLine, false, TEXT("UCLASS"));
Сама информация о классе записывается в SourceFile
и парсинг продолжается.Все исходные файлы после этого записываются в сформированный ранее список пакетов.
Как только сам парсинг выполняется, следующая функция отвечает за определение типов в этом файле. Т.е если синтаксис прошел проверку, то теперь надо проверить семантику.
Внутри нее есть последовательный вызов “Process_X” функций для всех содержащихся в SourceFile классов,енамов,структур и пр.
double TotalDefineTypesTime = FResults::TimedTry([&PackageDefs]() { DefineTypes(PackageDefs); });
Внутри нее есть последовательный вызов “Process_X” функций для всех содержащихся в SourceFile
классов,енамов,структур и пр.
Для классов:
for (TSharedRef<FUnrealTypeDefinitionInfo>& TypeDef : SourceFile.GetDefinedClasses())
{
FUnrealClassDefinitionInfo& ClassDef = TypeDef->AsClassChecked();
ProcessParsedClass(ClassDef);
GTypeDefinitionInfoMap.AddNameLookup(UHTCastChecked<FUnrealObjectDefinitionInfo>(TypeDef));
AllClasses.Add(TypeDef);
}
…
Для структур:
...
for (TSharedRef<FUnrealTypeDefinitionInfo>& TypeDef : SourceFile.GetDefinedStructs())
{
FUnrealScriptStructDefinitionInfo& ScriptStructDef = TypeDef->AsScriptStructChecked();
ProcessParsedStruct(ScriptStructDef);
GTypeDefinitionInfoMap.AddNameLookup(UHTCastChecked<FUnrealObjectDefinitionInfo>(TypeDef));
}
...
Рассмотрим для примера ProcessParsedClass()
:
Внутри банальные проверки на префикс:
// All classes must start with a valid unreal prefix
if (!FHeaderParser::ClassNameHasValidPrefix(ClassName, ClassNameStripped))
{
ClassDef.Throwf(TEXT("Invalid class name '%s'. The class name must have an appropriate prefix added (A for Actors, U for other classes)."), *ClassName);
}
На имя класса:
if(FHeaderParser::IsReservedTypeName(ClassNameStripped))
{
ClassDef.Throwf(TEXT("Invalid class name '%s'. Cannot use a reserved name ('%s')."), *ClassName, *ClassNameStripped);
}
То что класс наследуется от UObject’а:
if (BaseClassName.IsEmpty() && (ClassName != TEXT("UObject")))
{
ClassDef.Throwf(TEXT("Class '%s' must inherit UObject or a UObject-derived class"), *ClassName);
}
Даже на наследование от самого себя...
if (ClassName == BaseClassName)
{
ClassDef.Throwf(TEXT("Class '%s' cannot inherit from itself"), *ClassName);
}
Ближе к концу функции имеется интересный вызов, в котором производится поиск UClass’а
:
UClass* ResultClass = FEngineAPI::FindObject<UClass>(ANY_PACKAGE,*ClassNameStripped);
Результат поиска записывается в ClassDef.
ClassDef.InitializeFromExistingUObject(ResultClass);
ClassDef.SetObject(ResultClass);
Далее в ResolveParents
производится поиск родителей для всех классов из PackageDefs
.
Отвечает за это функция ResolveSuperClasses.Внутри ищется определение для базового класса из ClassDef.После чего выставляются флаги и сам родитель.
SuperClassInfo.Struct = FoundBaseClassDef;
ClassDef.SetClassCastFlags(FoundBaseClassDef->GetClassCastFlags());
if (Class != nullptr)
{
Class->SetSuperStruct(FoundBaseClassDef->GetClass());
}
В случае, если поиск не увенчался успехом, выбрасывается исключение:
if (FoundBaseClassDef == nullptr)
{
// Don't know its parent class. Raise error.
ClassDef.Throwf(TEXT("Couldn't find parent type for '%s' named '%s' in current module (Package: %s) or any other module parsed so far."), *ClassDef.GetName(), *BaseClassName, PackageName);
}
После поиска родителей имеется вызов к PrepareTypesForParsing()
.
Однако…
void PrepareTypesForParsing(TArray<FUnrealPackageDefinitionInfo*>& PackageDefs)
{
// Does nothing now
}
После производится топологическая сортировка функцией TopologicalSort
, в которой выясняются зависимости включаемых файлов, формируется упорядоченный список из этих файлов.
Сначала из не отсортированных файлов формируется OrderedSourceFiles
,передавая туда размер не отсортированного списка, и помечая все файлы из него как Unmarked
.
...
const TArray<FUnrealSourceFile*>& UnorderedSourceFiles = GUnrealSourceFilesMap.GetAllSourceFiles();
OrderedSourceFiles.Reset(UnorderedSourceFiles.Num());
for (FUnrealSourceFile* SourceFile : UnorderedSourceFiles)
{
SourceFile->SetTopologicalState(ETopologicalState::Unmarked);
}
...
Затем для каждого файла вызывается TopologicalVisit
, куда передается out параметр OrderedSourceFiles
и текущий SourceFile, для которого выявляются включенные уже в него файлы, и для всех них так же вызывается TopologicalVisit
.
В процессе файлы маркируются, если вызов прошел успешно, будет выставлено Permanent
и файл будет добавлен в OrderedSourceFiles
.
Сам процесс сильно напоминает обход графа в глубину.
На каждом этапе разрешения зависимости производится проверка: если разрешить зависимость не удалось, то может быть выброшена ошибка, которую вы множество раз видели: "Circular dependency detected".
...
if (FUnrealSourceFile* Recusion = TopologicalVisit(OrderedSourceFiles, *SourceFile))
{
UE_LOG(LogCompile, Error, TEXT("Circular dependency detected:"));
TopologicalRecursion(*Recusion, *Recusion);
FResults::SetResult(ECompilationResult::OtherCompilationError);
return;
}
...
Отсортированный топологической сортировкой список передается в ParseSourceFiles
.
Там формируется массив TArray<FParseCPP> ParsedCPPs
;, в нем резервируется место:
ParsedCPPs.Reserve(OrderedSourceFiles.Num());
И инициализируется, где для каждого элемента имеется пара: Пакет и файл:
FParseCPP(FUnrealPackageDefinitionInfo& InPackageDef, FUnrealSourceFile&)
После этого создается массив Тасок - объектов, отвечающих за выполнение функций в асинхронном порядке, их тип: FGraphEvent
.
В структуре FParseCPP
даже имеется третье поле для этой задачи:
struct FParseCPP
{
FParseCPP(FUnrealPackageDefinitionInfo& InPackageDef, FUnrealSourceFile& InSourceFile)
: PackageDef(InPackageDef)
, SourceFile(InSourceFile)
{}
/**
* The package definition being exported
*/
FUnrealPackageDefinitionInfo& PackageDef;
/**
* The source file being exported
*/
FUnrealSourceFile& SourceFile;
/**
* This task represents the task that parses the source
*/
FGraphEventRef ParseTaskRef;
};
Далее цикл проходится по каждому объекту в ParsedCPPs вызывая для него;
FHeaderParser::Parse(ParsedCPP.PackageDef, ParsedCPP.SourceFile);
Где ParsedCPP
- элмент этого массива.
Внутри инициализируется структура FHeaderParser
и вызывается ParseHeader()
Там отдельно парсятся делегаты функцией FixupDelegateProperties()
, проверяется вложенность:
...
// Make sure the compilation ended with valid nesting.
if (bEncounteredNewStyleClass_UnmatchedBrackets)
{
Throwf(TEXT("Missing } at end of class") );
}
if (NestLevel == 1)
{
Throwf(TEXT("Internal nest inconsistency") );
}
else if (NestLevel > 2)
{
Throwf(TEXT("Unexpected end of script in '%s' block"), NestTypeName(TopNest->NestType) );
}
...
И в конце для каждого FUnrealClassDefinitionInfo
вызывается PostParsingClassSetup()
, где для каждого класса выставляются соответствующие флаги:
...
// Set the class config flag if any properties have config
for (TSharedRef<FUnrealPropertyDefinitionInfo> PropertyDef : ClassDef.GetProperties())
{
if (PropertyDef->HasAnyPropertyFlags(CPF_Config))
{
ClassDef.SetClassFlags(CLASS_Config);
break;
}
}
...
Так же есть проверки на наличие .generated.h
файла:
if (!bSpottedAutogeneratedHeaderInclude && !bEmptyFile && !bNoExportClassesOnly)
{
const FString& ExpectedHeaderName = SourceFile.GetGeneratedHeaderFilename();
Throwf(TEXT("Expected an include at the top of the header: '#include \"%s\"'"), *ExpectedHeaderName);
}
На следующем этапе производится финализация парсинга, вызывая PostParseFinalize()
.
В нем создается еще один пакет - не имеющего исходников, но в котором все еще есть типы, для которых требуется вызывать финализацию.
TArray<FUnrealTypeDefinitionInfo*> NoSourceTypeDefs;
GTypeDefinitionInfoMap.ForAllTypesByName([&NoSourceTypeDefs](FUnrealTypeDefinitionInfo& TypeDef)
{
if (!TypeDef.HasSource())
{
NoSourceTypeDefs.Add(&TypeDef);
}
});
Далее для них вызывается сама финализация - PostParseFinalizeInternal
, состоящая из двух этапов,нас интересует второй. В нем формируется название пакета и записывается в переменные:
...
case EPostParseFinalizePhase::Phase2:
{
UPackage* Package = GetPackage();
FString PackageName = Package->GetName();
PackageName.ReplaceInline(TEXT("/"), TEXT("_"), ESearchCase::CaseSensitive);
SingletonName.Appendf(TEXT("Z_Construct_UPackage_%s()"), *PackageName);
SingletonNameChopped = SingletonName.LeftChop(2);
ExternDecl.Appendf(TEXT("\tUPackage* %s;\r\n"), *SingletonName);
break;
}
...
После чего из пакета достаются все файлы, содержащие распарсенные типы, и уже для них вызывается PostParseFinalize().
...
for (TSharedRef<FUnrealSourceFile>& LocalSourceFile : GetAllSourceFiles())
{
for (TSharedRef<FUnrealTypeDefinitionInfo>& TypeDef : LocalSourceFile->GetDefinedTypes())
{
TypeDef->PostParseFinalize(Phase);
}
}
...
После завершения финализации вновь вызывается топологическая сортировка для включения новых зависимостей и, наконец:
void Export(TArray<FUnrealPackageDefinitionInfo*>& PackageDefs, TArray<FUnrealSourceFile*>& OrderedSourceFiles).
Внутри нее производится генерация всех файлов с помощью функций
FNativeClassHeaderGenerator::GenerateSourceFiles(GeneratedCPPs)
Куда передаются упорядоченные исходные файлы из OrderedSourceFiles
.
И
FNativeClassHeaderGenerator::Generate(*PackageDef, GeneratedCPPs);
Которая вызывается в цикле с конкретным пакетом, уже заполненными GeneratedCPPs
.
for (FUnrealPackageDefinitionInfo* PackageDef : PackageDefs)
{
FResults::Try([&GeneratedCPPs, PackageDef]() { FNativeClassHeaderGenerator::Generate(*PackageDef, GeneratedCPPs); });
}
В итоге, в первой функции GenerateSourceFiles()
генерируется сам .h файл
.
Внутри можем обнаружить то, что генерируется в самом верху нашего .h файла:
GeneratedHeaderText.Logf(
TEXT("#ifdef %s" LINE_TERMINATOR_ANSI
"#error \"%s.generated.h already included, missing '#pragma once' in %s.h\"" LINE_TERMINATOR_ANSI
"#endif" LINE_TERMINATOR_ANSI
"#define %s" LINE_TERMINATOR_ANSI
LINE_TERMINATOR_ANSI),
*FileDefineName, *StrippedFilename, *StrippedFilename, *FileDefineName);
Именно он генерирует строки для нашего класса:
#ifdef MYPROJECT_TestClass_generated_h
#error "TestClass.generated.h already included, missing '#pragma once' in TestClass.h"
#endif
#define MYPROJECT_TestClass_generated_h
После чего цикл проходится по всем собранным типам нашего класса и для каждого индивидуально вызывает:
Generator.ExportGeneratedEnumInitCode()
для enum’а
Generator.ExportGeneratedStructBodyMacros()
для структур
Для делегатов:
Generator.ExportDelegateDeclaration()
Generator.ExportDelegateDefinition()
И, наконец, для класса:
Generator.ExportClassFromSourceFileInner
...
for (FUnrealFieldDefinitionInfo* FieldDef : Types)
{
if (FUnrealEnumDefinitionInfo* EnumDef = UHTCast<FUnrealEnumDefinitionInfo>(FieldDef))
{
// Is this ever not the case?
if (EnumDef->GetOuter()->GetObject()->IsA(UPackage::StaticClass()))
{
GeneratedFunctionDeclarations.Log(EnumDef->GetExternDecl(true));
Generator.ExportGeneratedEnumInitCode(GeneratedCPPText, ReferenceGatherers, SourceFile, *EnumDef);
EnumRegs.Add(EnumDef);
}
}
else if (FUnrealScriptStructDefinitionInfo* ScriptStructDef = UHTCast<FUnrealScriptStructDefinitionInfo>(FieldDef))
{
...
После генерации объявлений и определений файл закрывается следующими макросами:
GeneratedHeaderText.Log(TEXT("#undef CURRENT_FILE_ID\r\n"));
GeneratedHeaderText.Logf(TEXT("#define CURRENT_FILE_ID %s\r\n\r\n\r\n"), *SourceFile.GetFileId());
В классе это выглядит следующим образом:
#undef CURRENT_FILE_ID
#define CURRENT_FILE_ID FID_MyProject_Source_MyProject_TestClass_h
После этой логики есть также дополнительная, где цикл проверяет все типы вновь, и генерирует дополнительный текст для перечислений:
for (FUnrealFieldDefinitionInfo* FieldDef : Types)
{
if (FUnrealEnumDefinitionInfo* EnumDef = UHTCast<FUnrealEnumDefinitionInfo>(FieldDef))
{
Generator.ExportEnum(GeneratedHeaderText, *EnumDef);
}
}
И заканчивается функция так называемой Registration info
:
Дополнительным шаблонным кодом для .cpp файла.
...
// Generate the single registration method
if (!EnumRegs.IsEmpty() || !ScriptStructRegs.IsEmpty() || !ClassRegs.IsEmpty())
{
static const TCHAR* Prefix = TEXT("Z_CompiledInDeferFile_");
FString StaticsName = FString::Printf(TEXT("%s%s_Statics"), Prefix, *SourceFile.GetFileId());
GeneratedCPPText.Logf(TEXT("\tstruct %s\r\n"), *StaticsName);
GeneratedCPPText.Log(TEXT("\t{\r\n"));
...
После генерации текста и записи его в GeneratedCPP.
В WriteHeader()
и WriteSource()
происходит создание файлов и записи текста в них в функции:
void FNativeClassHeaderGenerator::WriteSourceFile(FGeneratedCPP& GeneratedCPP)
bool bHasChanged = WriteHeader(GeneratedCPP.Header, GeneratedHeaderText, AdditionalHeaders, GeneratedCPP.ForwardDeclarations);
WriteSource(Module, GeneratedCPP.Source, GeneratedCPPText, &SourceFile, GeneratedCPP.CrossModuleReferences);
В WriteHeader()
и WriteSource()
есть таке же дополнительная логика для записи тех самых #include’ов
необходимых зависимостей, который не были записаны на предыдущем шаге.
Для хедера:
FUHTStringBuilder GeneratedHeaderTextWithCopyright;
GeneratedHeaderTextWithCopyright.Log(HeaderCopyright);
GeneratedHeaderTextWithCopyright.Log(TEXT("#include \"UObject/ObjectMacros.h\"\r\n"));
GeneratedHeaderTextWithCopyright.Log(TEXT("#include \"UObject/ScriptMacros.h\"\r\n"));
И для cpp:
FUHTStringBuilder FileText;
FileText.Log(HeaderCopyright);
FileText.Log(RequiredCPPIncludes);
Интересно, что только для cpp файла были созданы глобальные переменные…
const TCHAR* HeaderCopyright =
TEXT("// Copyright Epic Games, Inc. All Rights Reserved.\r\n"
"/*===========================================================================\r\n"
"\tGenerated code exported from UnrealHeaderTool.\r\n"
"\tDO NOT modify this manually! Edit the corresponding .h files instead!\r\n"
"===========================================================================*/\r\n" LINE_TERMINATOR_ANSI);
const TCHAR* RequiredCPPIncludes = TEXT("#include \"UObject/GeneratedCppIncludes.h\"" LINE_TERMINATOR_ANSI);
Есть также функция
FNativeClassHeaderGenerator::Generate(
FUnrealPackageDefinitionInfo& PackageDef,
TArray<FGeneratedCPP>& GeneratedCPPs
Которая вызывается сразу после GenerateSourceFiles()
, однако, как написали разработчики, она генерирует дополнительные выходные файлы, создает отсортированный список сгенерированных файлов, записывает префиксы, pragma once’ы и т.д.
...
// Write the classes and enums header prefixes.
FUHTStringBuilder ClassesHText;
ClassesHText.Log(HeaderCopyright);
ClassesHText.Log(TEXT("#pragma once\r\n"));
ClassesHText.Log(TEXT("\r\n"));
ClassesHText.Log(TEXT("\r\n"));
// Fill with the rest source files from this package.
TSet<FUnrealSourceFile*> PublicHeaderGroupIncludes;
for (FGeneratedCPP* GeneratedCPP : ExportedSorted)
{
if (GeneratedCPP->SourceFile.IsPublic())
{
PublicHeaderGroupIncludes.Add(&GeneratedCPP->SourceFile);
}
}
for (TSharedRef<FUnrealSourceFile>& SourceFile : PackageDef.GetAllSourceFiles())
{
if (SourceFile->IsPublic())
{
PublicHeaderGroupIncludes.Add(&*SourceFile);
}
}
...
Предлагаю самостоятельно ознакомится с деталями ее реализации.
После генерации и записи .generated.h и .cpp файлов вызывается:
double TotalCheckForScriptPluginsTime = FResults::TimedTry([&ScriptPlugins]() { GetScriptPlugins(ScriptPlugins); });
GetScriptPlugins()
функция производит генерацию и инициализацию уже для плагинов, так же беря их из GManifest.
Следующая cоздает так называемые типы данных самого движка, т.е берет все файлы из пакетов, из файлов уже достает типы и инициализирует их нужными параметрами.
void CreateEngineTypes(TArray<FUnrealPackageDefinitionInfo*>& PackageDefs)
Для Enum’а
инициализация выглядит следующим образом:
FUnrealFieldDefinitionInfo::CreateUObjectEngineTypesInternal(Phase);
...
switch (Phase)
{
case ECreateEngineTypesPhase::Phase1:
{
UPackage* Package = GetPackageDef().GetPackage();
const FString& EnumName = GetNameCPP();
// Create enum definition.
UEnum* Enum = new(EC_InternalUseOnlyConstructor, Package, FName(EnumName), RF_Public) UEnum(FObjectInitializer());
Enum->SetEnums(Names, CppForm, EnumFlags, false);
Enum->CppType = CppType;
Enum->GetPackage()->GetMetaData()->SetObjectValues(Enum, GetMetaDataMap());
SetObject(Enum);
break;
}
case ECreateEngineTypesPhase::Phase2:
break;
case ECreateEngineTypesPhase::Phase3:
break;
}
...
Где, достается пакет, имя перечисления и создается его определение, где, в основном, выставляются различные meta данные.
/**
* Set the key/value pair in the Property's metadata
* @param Object the object to set the metadata for
* @Values The metadata key/value pairs
*/
void SetObjectValues(const UObject* Object, const TMap<FName, FString>& Values);
13-ый вызов приурочен к экспорту плагинов, загрузке классов из них в дерево классов FClassTree
и вызову к функциям для каждого отдельного плагина:
ExportClassToScriptPlugins(SourceFileLookup, ClassTree.GetClass(), Module, *Plugin);
ExportClassTreeToScriptPlugins(SourceFileLookup, &ClassTree, Module, *Plugin);
14-ый вызов сохраняет все внешние зависимости сохраняя их в файл:
void WriteExternalDependencies(const FString& ExternalDependencies)
{
FFileHelper::SaveStringToFile(ExternalDependencies, *GManifest.ExternalDependenciesFile);
}
И, наконец, последний вызов собирает всю информацию об сгенерированных файлах, обработанных строках, общем времени и т.д, и выводит это в логи:
void GenerateSummary(TArray<FUnrealPackageDefinitionInfo*>& PackageDefs)
{
for (FUnrealPackageDefinitionInfo* PackageDef : PackageDefs)
{
const FManifestModule& Module = PackageDef->GetModule();
double TotalTimes[int32(ESourceFileTime::Count)] = { 0.0 };
int32 LinesParsed = 0;
int32 StatementsParsed = 0;
int32 SourceCount = 0;
TArray<TSharedRef<FUnrealSourceFile>>& SourceFiles = PackageDef->GetAllSourceFiles();
for (TSharedRef<FUnrealSourceFile>& SourceFile : SourceFiles)
{
for (int32 Index = 0; Index < int32(ESourceFileTime::Count); ++Index)
{
TotalTimes[int32(Index)] += SourceFile->GetTime(ESourceFileTime(Index));
}
LinesParsed += SourceFile->GetLinesParsed();
StatementsParsed += SourceFile->GetStatementsParsed();
}
UE_LOG(LogCompile, Log, TEXT("Success: Module %s parsed %d sources(s), %d line(s), %d statement(s). Times(secs) Load: %.3f, PreParse: %.3f, Parse: %.3f, Generate: %.3f."), *Module.Name,
SourceFiles.Num(), LinesParsed, StatementsParsed, TotalTimes[int32(ESourceFileTime::Load)], TotalTimes[int32(ESourceFileTime::PreParse)], TotalTimes[int32(ESourceFileTime::Parse)], TotalTimes[int32(ESourceFileTime::Generate)]);
}
}
И заканчивается парсинг финализацией всех асинхронных задач, о которых я выше не упоминал дабы не усложнять эту статью еще сильнее, выводом дополнительных логов о модулях, исходных файлах, затраченном времени и т.п
// Count the number of sources
int NumSources = 0;
for (const FManifestModule& Module : GManifest.Modules)
{
NumSources += Module.PublicUObjectClassesHeaders.Num() + Module.PublicUObjectHeaders.Num() + Module.InternalUObjectHeaders.Num() + Module.PrivateUObjectHeaders.Num();
}
UE_LOG(LogCompile, Log, TEXT("Preparing %d modules took %.3f seconds"), GManifest.Modules.Num(), TotalPrepareModuleTime);
UE_LOG(LogCompile, Log, TEXT("Preparsing %d sources took %.3f seconds"), NumSources, TotalPreparseTime);
UE_LOG(LogCompile, Log, TEXT("Defining types took %.3f seconds"), TotalDefineTypesTime);
UE_LOG(LogCompile, Log, TEXT("Resolving type parents took %.3f seconds"), TotalResolveParentsTime);
UE_LOG(LogCompile, Log, TEXT("Preparing types for parsing took %.3f seconds"), TotalPrepareTypesForParsingTime);
...
Где в самом конце вызывается:
RequestEngineExit(TEXT("UnrealHeaderTool finished"));
return FResults::GetOverallResults();
4. Заключение.
Вот в принципе и все! Спасибо что читали, и особое спасибо тем, кто дочитал до конца, и проанализоровал все самостоятельно.
Части кода были намеренно указаны для большей понятности происходящего, однако, это не гарант того, что весь описываемый мною контекст был до конца понятен всем. Поэтому для глубого понимания, опять же, советую проделать все те же действия, следуя вниз по статье.
Пишите, что бы вам хотелось разобрать еще и комментируйте!
Спасибо!
Starzo
Спасибо за годный материал!